Skip to content

feat(rs-client): Add automatic resumable uploads - #700

Draft
lcian wants to merge 5 commits into
mainfrom
lcian/feat/rust-automatic-resumable-uploads
Draft

lcian wants to merge 5 commits into
mainfrom
lcian/feat/rust-automatic-resumable-uploads

Conversation

@lcian

@lcian lcian commented Oct 21, 2026 •

Copy link
Copy Markdown
Member

Changelog Entry

  • Session::put now uses resumable uploads automatically for buffers and regular files of 256 MiB or more, and resends only what the server hasn't stored after a failed write
    • Change the threshold with Usecase::with_resumable_threshold_bytes or per request with .resumable_threshold_bytes(...); None disables automatic resumable uploads.
    • Set ClientBuilder::spool_dir to choose where payloads compressed on the fly are spooled (defaults to the system temp dir).
    • Terminal failures of an automatic resumable upload return the new Error::UploadFailed.
    • The resumable-upload-api feature only gates the manual resumable upload API. This new behavior is enabled regardless of that feature flag.

Details

Session::put now uses resumable uploads automatically for eligible uploads, matching the Python client (#667). An upload is eligible if:

  • It's a buffer, an open regular file, or a path to a regular file (put, put_file, put_path)
  • Its remaining input size, from the current position and before client compression, is at least the resumable threshold (default 256 MiB)
  • It's sent standalone. Uploads queued through many() keep their existing batch or direct routing and ignore the threshold.

Streams, generic readers, non-regular files and files of unknown size still use a direct upload.
The API and behavior mirrors the Python client:

let usecase = Usecase::new("debug_files")?
    .with_resumable_threshold_bytes(256 * 1024 * 1024); // `None` disables, `0` includes everything

session.put(payload).resumable_threshold_bytes(None).send().await?; // per-request override

ClientBuilder::new(url).spool_dir("/var/tmp/objectstore"); // optional

File reader lifetime. reqwest can keep polling an HTTP/2 request body after it has returned a response. To avoid an old request reading from the file after a retry seeks, each file request gets its own reader task (put/payload.rs). That task is stopped and joined before the next request starts.

For reviewers

  • Error gains UploadFailed, and ResumableUploadUnavailable, UploadOffsetMismatch and the chunk-granularity variant are no longer feature-gated. Since Error is #[non_exhaustive] (feat(rs-client)!: Mark Error as non-exhaustive #706), this is additive.
  • Auth tokens are now created after the payload is prepared, because preparing a large payload can take longer than a token is valid.
  • Most of the logic is in put.rs (routing), put/payload.rs (preparation and readers) and put/automatic.rs (recovery loop).

via @lcian.

--

View Junior Session [Sentry]

Resume eligible buffer and file uploads from authoritative server progress, with configurable thresholds and temporary-file compression. Keep automatic uploads available without the manual protocol feature.
@codecov

codecov Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 99.66887% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 92.23%. Comparing base (4dba0a1) to head (b06ec0b).
⚠️ Report is 9 commits behind head on main.

Files with missing lines Patch % Lines
clients/rust/src/put/payload.rs 99.45% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #700      +/-   ##
==========================================
+ Coverage   91.90%   92.23%   +0.33%     
==========================================
  Files         117      119       +2     
  Lines       24124    24436     +312     
==========================================
+ Hits        22170    22539     +369     
+ Misses       1954     1897      -57     
Components Coverage Δ
Rust Backend 95.03% <ø> (+0.11%) ⬆️
Rust Client 87.54% <99.66%> (+4.67%) ⬆️
Python Client 93.73% <ø> (ø)

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@lcian lcian changed the title feat(rust): Add automatic resumable uploads feat(rust-client): Add automatic resumable uploads Oct 21, 2026
lcian and others added 3 commits October 21, 2026 14:48
Separate payload preparation and reader lifetime from protocol recovery. Stop and join each request reader before returning its result, report local IO errors directly, and keep progress probes in a dedicated retry loop.
Unify direct fallbacks, make sizing optional, and limit resumables to standalone uploads.
# Conflicts:
#	Cargo.lock
#	clients/rust/src/error.rs

Co-Authored-By: Lorenzo Cian <17258265+lcian@users.noreply.github.com>
@sentry-junior sentry-junior Bot changed the title feat(rust-client): Add automatic resumable uploads feat(rs-client): Add automatic resumable uploads Oct 22, 2026
@github-actions

github-actions Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

Semver Impact of This PR

🟡 Minor (new features)

📋 Changelog Preview

This is how your changes will appear in the changelog.
Entries from this PR are highlighted with a left border (blockquote style).


Python Client

  • Split the signing key from the token policy: SecretKey(kid, secret_key) now only holds the EdDSA key, and TokenGenerator(key, expiry_seconds=60, permissions=None) wraps it with the token defaults and is what you pass to Client(token=...). The deprecated sign_for_scope() alias is removed. by sentry-junior in #721
    • Replace SecretKey(kid, secret_key, expiry_seconds, permissions) or TokenGenerator(kid, secret_key, ...) with TokenGenerator(SecretKey(kid, secret_key), expiry_seconds, permissions).
      # Before
      client = Client(url, token=SecretKey(kid, secret_key, expiry_seconds=300))
      
      # After
      key = SecretKey(kid, secret_key)
      client = Client(url, token=TokenGenerator(key, expiry_seconds=300))
    • Replace .sign_for_scope(...) with .token_for_scope(...).
  • Remove the deprecated compression argument from Session.put() by sentry-junior in #718
    • Replace session.put(..., compression=...) with session.put(..., compress=...).
  • Remove the deprecated propagate_traces argument from Client by sentry-junior in #714
    • Drop propagate_traces=... from Client(...) calls. Sentry's StdlibIntegration, enabled by default, already propagates traces on Objectstore requests.
  • Add a response_content_disposition option ("inline" or "attachment") to Session.object_url() and Session.presigned_object_url(). Pre-signed URLs include it in their signature. by jan-auer in #696

Rust Client

  • Session::put now uses resumable uploads automatically for buffers and regular files of 256 MiB or more, and resends only what the server hasn't stored after a failed write by lcian in #700
    • Change the threshold with Usecase::with_resumable_threshold_bytes or per request with .resumable_threshold_bytes(...); None disables automatic resumable uploads.
    • Set ClientBuilder::spool_dir to choose where payloads compressed on the fly are spooled (defaults to the system temp dir).
    • Terminal failures of an automatic resumable upload return the new Error::UploadFailed.
    • The resumable-upload-api feature only gates the manual resumable upload API. This new behavior is enabled regardless of that feature flag.
  • Remove the deprecated Session::mint_token() by sentry-junior in #717
    • Replace session.mint_token() with session.get_token().
  • Remove the deprecated TokenGenerator::sign() by sentry-junior in #716
    • Replace generator.sign(&scope) with generator.create_token(&scope).sign().
  • Remove the deprecated PutBuilder::compression() by sentry-junior in #715
    • Replace .compression(...) with .compress(...) on put builders.
  • Usecase::new now returns a Result, and the panicking constructor is now Usecase::from_static by sentry-junior in #710
    • Replace Usecase::new("name") with Usecase::from_static("name").
    • Replace Usecase::try_new(name) with Usecase::new(name).
  • Mark Error as #[non_exhaustive] so new error variants can be added without a breaking change by lcian in #706
    • Add a wildcard arm (_ => ...) to exhaustive match expressions on objectstore_client::Error.
  • Remove the multipart upload API (Session::initiate_multipart_upload(), Session::resume_multipart_upload()) and the multipart feature flag by lcian in #704
    • Use resumable uploads instead.
    • Remove match arms for the removed Error::InvalidPartNumber, Error::InvalidUploadId and Error::MultipartComplete variants.

🤖 This preview updates automatically when you update the PR.

…ated docs

Co-Authored-By: Lorenzo Cian <17258265+lcian@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.

1 participant