Skip to content

Support streaming put payloads - #817

Open
senzzzi wants to merge 2 commits into
apache:mainfrom
senzzzi:bruno.rodrigues/put-supports-streaming
Open

senzzzi wants to merge 2 commits into
apache:mainfrom
senzzzi:bruno.rodrigues/put-supports-streaming

Conversation

@senzzzi

@senzzzi senzzzi commented Jul 30, 2026

Copy link
Copy Markdown

Which issue does this PR close?

Closes #281.

Rationale for this change

PutPayload currently only supports collections of Bytes. As a result, uploading a file through put or put_opts requires loading the entire file into memory or using a multipart upload.

This change allows callers to stream a payload with a known content length through a single PUT request. File-backed payloads are a primary use case.

What changes are included in this PR?

  • Extends PutPayload to support replayable streaming bodies with a known content length.
  • Adds PutPayload::from_stream for constructing streaming payloads.
  • Adds PutPayload::from_file, which reads files in 16 KiB chunks by default.
  • Adds PutPayload::from_file_with_chunk_size for configuring the file chunk size.
  • Updates HTTP request bodies to poll payload streams asynchronously.
  • Recreates the stream for every request attempt so retryable uploads can resend the complete payload.
  • Updates S3 signing and checksum calculation to consume streaming payloads.
  • Updates the local filesystem and in-memory backends to accept streaming payloads.
  • Adds regression tests covering:
    • the default 16 KiB file chunk size;
    • replaying cloned streaming request bodies; and
    • retrying an upload using a file-backed streaming payload.

Validation performed:

  • cargo test
  • cargo test --all-features
  • cargo clippy --all-targets --all-features -- -D warnings
  • compilation with only the fs feature
  • compilation without default features

Are there any user-facing changes?

Yes. Callers can now upload a file without first loading the complete file into memory:

let payload = PutPayload::from_file(file_path).await?;
store.put(&location, payload).await?;

Custom streaming payloads can be created with PutPayload::from_stream. The stream factory must produce a new stream from the beginning for every invocation, and the
supplied content length must exactly match the number of bytes yielded.

Existing byte-backed PutPayload construction and iteration behavior is unchanged. Synchronous APIs such as iter, AsRef<[Bytes]>, and conversion into Bytes are not supported for streaming payloads; callers should use PutPayload::stream or PutPayload::bytes instead.

API compatibility consideration

PutPayload remains a public struct backed by a private enum so that existing byte-backed construction and usage remain source compatible:

enum PutPayloadInner {
    Bytes(Arc<[Bytes]>),
    Streaming {
        factory: StreamFactory,
        content_length: usize,
    },
}

There is an API-design limitation around the existing synchronous accessors. Methods and trait implementations such as PutPayload::iter, AsRef<[Bytes]>, IntoIterator, and conversion into Bytes cannot synchronously represent an asynchronous, fallible stream.

In this implementation, their existing behavior is unchanged for byte-backed payloads, but calling them with a streaming payload panics. Streaming-aware implementations should instead use PutPayload::stream or PutPayload::bytes.

Changing the existing synchronous APIs to return Option or Result would avoid the panic but would be a breaking public API change. Returning an empty iterator for streaming payloads was rejected because it could silently result in incomplete uploads.

Feedback from maintainers on the preferred long-term API shape would be appreciated. Possible alternatives include:

  • accepting the documented limitation for synchronous APIs;
  • changing these APIs in a future breaking release; or
  • introducing streaming uploads through a separate payload type or method.

@alamb

alamb commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

You can also use Mulit-part upload to achieve something similar (though most object stores would still require you to buffer the parts in 5mb chunks)

We haven't released these docs yet, btu here is an example of how to do it

Screenshot 2026-08-17 at 5 43 00 PM

@alamb

alamb commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

There is more rationale for non streaming APIs here:

TLDR is that streaming writes make a bunch of other operations (like retry , error handling, etc)

@alamb

alamb commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

I see you have talked about multi-part uploads here: #281 (comment)

@senzzzi

senzzzi commented Aug 17, 2026

Copy link
Copy Markdown
Author

You can also use Mulit-part upload to achieve something similar (though most object stores would still require you to buffer the parts in 5mb chunks)

We haven't released these docs yet, btu here is an example of how to do it

Screenshot 2026-08-17 at 5 43 00 PM

It's not the same because of costs. Uploading a 64Mb file in a single put request is still quite fast (depending on your needs) and you are charged for 1 request. If you upload 64 different 1Mb parts, you are charged by the initial request, 64 parts individually and 1 final requests. So it's 66 times more expensive. You can check the link in this comment: #281 (comment)

@tustvold tustvold left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the idea is a good one, but as Andrew says we need to think about how retries, etc... would work

Comment thread src/client/http/body.rs
fn clone(&self) -> Self {
Self {
payload: self.payload.clone(),
stream: None,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would likely be rather limiting in practice as it would break retries in client implementations

@alamb

alamb commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

There is a bunch more discussion on #281 now too

sao-coding added a commit to Limatura/tessera that referenced this pull request Oct 22, 2026
* refactor: 開源前清理註解:SQL migration、Cargo 設定檔,並移除本 repo 的 issue 編號

- migration SQL 的註解第一次整理:165 個檔、約 3100 行,AI 句型密度 104 → 19(每千行),破折號 175 → 0
- Cargo.toml、deny.toml、clippy.toml、rustfmt.toml、.cargo/config.toml 的註解一併整理,破折號 0
- 移除註解中所有指向本 repo 的 issue/PR 編號(開源會是新 repo,這些編號會失效或指錯);外部專案引用改成完整寫法 apache/arrow-rs-object-store#817
- apps/server/Cargo.toml 幾段疊在一起的說明拆回各自的依賴(takumi、html5ever、http-body-util)
- 過時說法:tests/shares.rs 寫「沒有東西會寫 disabled_at」,現在掃毒會寫
- tests/oauth.rs id_of 的 doc 被擠掉,補回
- #423 之後新增的 Rust 註解比照前幾輪整理
- 重新產生 openapi.json 與 contract 快照

* docs: 重新產生資料表文件(migration 註解變動)

---------

Co-authored-by: sao-coding <58339015+sao-coding@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

object_store: Support for streaming body in put/put_opts

3 participants