Repository navigation
Support non-contiguous put payloads / vectored writes (#5514) - #5538
Conversation
There was a problem hiding this comment.
Cleanup given SHA256 is the only checksum we support and are ever likely to
There was a problem hiding this comment.
(and to be clear for anyone else reading this PR -- this code is not exposed publicly so it can be changed in the future if needed as well)
There was a problem hiding this comment.
We defer setting the body until this point when we have a streaming body, as otherwise this would fail
3640a8c to
4e20f2d
Compare
4e20f2d to
70de4ee
Compare
There was a problem hiding this comment.
false is the default for a POST request
There was a problem hiding this comment.
false is the default
There was a problem hiding this comment.
This is moved into send above
There was a problem hiding this comment.
Setting this here sidesteps issues around clone-ability of stream based request bodies
There was a problem hiding this comment.
Maybe a function call like s.set_body(Some(x.body()) instead might be easier to understand the intent
There was a problem hiding this comment.
This is an upstream type unfortunately
| enum BufWriterState { | ||
| /// Buffer up to capacity bytes | ||
| Buffer(Path, Vec<u8>), | ||
| Buffer(Path, PutPayloadMut), |
There was a problem hiding this comment.
This change means that we no longer bump allocate 🎉
| upload, | ||
| buffer: Vec::with_capacity(chunk_size), | ||
| chunk_size, | ||
| buffer: PutPayloadMut::new(), |
There was a problem hiding this comment.
This change means we avoid bump allocating but also don't allocate an entire 10MB buffer up-front only to potentially use some fraction of it
|
|
||
| impl FromIterator<Bytes> for PutPayload { | ||
| fn from_iter<T: IntoIterator<Item = Bytes>>(iter: T) -> Self { | ||
| Self(iter.into_iter().collect()) |
There was a problem hiding this comment.
Use Arc::from_iter() directly to reduce one alloaction (after TrustedLen stablized in the future).
There was a problem hiding this comment.
I believe this does use Arc::from_iter?
There was a problem hiding this comment.
Arc::from_iter has special logic for TrustedLen that can avoid the collect()
There was a problem hiding this comment.
If you look at the definition of collect it just invokes from_iter for the collection being collected into, in this case Arc
fn collect<B: FromIterator<Self::Item>>(self) -> B
where
Self: Sized,
{
FromIterator::from_iter(self)
}
alamb
left a comment
There was a problem hiding this comment.
Thank yoU @tustvold -- I think the structure of this code / API is quite nice and elegant 👌 👏
I do think it is a non trivial API change (not complicated, but will require a bunch of mechanical changes) and so marked the PR as API change. To that end I suggest we add some better documentation to help users map types they are familiar with to the new PutPayload.
Here is a suggestion: tustvold#81
Other than that I had a few suggestions / nits, the only one I feel somewhat strongly about is adding a new put rather than updating the existing write API.
| config: &'a S3Config, | ||
| builder: RequestBuilder, | ||
| payload_sha256: Option<Vec<u8>>, | ||
| payload_sha256: Option<digest::Digest>, |
There was a problem hiding this comment.
These are unrelated cleanups right? Or is there a reason digest::Digest is preferred over Vec<u8>?
There was a problem hiding this comment.
It avoids an allocation, but yes an unrelated cleanup
There was a problem hiding this comment.
(and to be clear for anyone else reading this PR -- this code is not exposed publicly so it can be changed in the future if needed as well)
| /// should be able to observe a partially written object | ||
| async fn put(&self, location: &Path, bytes: Bytes) -> Result<PutResult> { | ||
| self.put_opts(location, bytes, PutOptions::default()).await | ||
| async fn put(&self, location: &Path, payload: PutPayload) -> Result<PutResult> { |
There was a problem hiding this comment.
For anyone else reviewing this PR. I think this is an (and the key) API change -- to take a slice of [Bytes] bytes rather than a Bytes directly
| } | ||
|
|
||
| /// Append a [`Bytes`] to this [`PutPayloadMut`] | ||
| pub fn push(&mut self, bytes: Bytes) { |
There was a problem hiding this comment.
Can you please document that this is a zero copy API -- specifically that the progress is closed off and the underlying data is not copied.
| if !self.in_progress.is_empty() { | ||
| let completed = std::mem::take(&mut self.in_progress).into(); | ||
| self.completed.push(completed); | ||
| } | ||
| PutPayload(self.completed.into()) |
There was a problem hiding this comment.
nit: I think you can save typing and allocating another Vec with something like
| if !self.in_progress.is_empty() { | |
| let completed = std::mem::take(&mut self.in_progress).into(); | |
| self.completed.push(completed); | |
| } | |
| PutPayload(self.completed.into()) | |
| let Self { in_progress, completed, ..} = self; | |
| if !in_progress.is_empty() { | |
| completed.push(in_progress); | |
| } | |
| PutPayload(completed.into()) |
There was a problem hiding this comment.
Vec::default doesn't allocate
| assert_eq!(chunks[3].len(), 23); | ||
| assert_eq!(chunks[4].len(), 20); | ||
| } | ||
| } |
There was a problem hiding this comment.
It would be good to add a test for push as well (esp to document the non copying behavior)
| /// Put a chunk of data into this [`WriteMultipart`] | ||
| /// | ||
| /// See [`Self::write`] for information on backpressure | ||
| pub fn put(&mut self, mut bytes: Bytes) { |
There was a problem hiding this comment.
If we are changing the API anyways, is there a reason to keep both write and put? As in why not change write to take a Bytes as one can could then call write via write(my_payload.into()) 🤔
There was a problem hiding this comment.
There are circumstances where having the writer perform the buffering might be advantageous, e.g. when integrating with the Write trait
There was a problem hiding this comment.
this would be great to include in the docs as I found it non obvious.
Update the docs were updated (thanks 🙏 )
/// Put a chunk of data into this [`WriteMultipart`] without copying
///
/// Data is buffered using [`PutPayloadMut::push`]. Implementations looking to
/// perform writes from non-owned buffers should prefer [`Self::write`] as this
/// will allow multiple calls to share the same underlying allocation.
There was a problem hiding this comment.
Maybe a function call like s.set_body(Some(x.body()) instead might be easier to understand the intent
| } | ||
|
|
||
| impl RetryableRequest { | ||
| /// Set whether this request is idempotent |
There was a problem hiding this comment.
Can we document what idempotent means in this context? Like it means the entire request can be retried?
| loop { | ||
| let s = req.try_clone().expect("request body must be cloneable"); | ||
| match client.execute(s).await { | ||
| let mut s = self |
There was a problem hiding this comment.
I know you like brevity but one character variable names that are different than the first letter of the things they are holding are pretty hard for me to grok. (I realize you can't use r b/c there is another variable below named r).
How about we rename s to request and x to payload?
…#5538) * Support non-contiguous put payloads (#5514) * Docs * Add more docs * Review feedback
Which issue does this PR close?
Closes apache/arrow-rs-object-store#81
Rationale for this change
Allowing non-contiguous payloads avoids needing to bump allocate buffers when writing data where the size isn't known up front. This can be significantly more efficient.
It will also open up interesting possibilities for the parquet writer, where the data is already buffered in chunks
What changes are included in this PR?
Are there any user-facing changes?
FYI @roeap @wjones127 @alamb