Skip to content

feat(upload): Resumable Uploads - #6203

Open
jjbayer wants to merge 47 commits into
masterfrom
feat/tus-offset
Open

feat(upload): Resumable Uploads#6203
jjbayer wants to merge 47 commits into
masterfrom
feat/tus-offset

Conversation

@jjbayer

@jjbayer jjbayer commented Jul 9, 2026

Copy link
Copy Markdown
Member

Implement uploads that can be resumed from a given byte offset.

See core protocol of TUS: https://tus.io/protocols/resumable-upload#core-protocol

Design

We have to work around a few limitations:

  • We have to be able to map Upload-Offset to multipart part numbers and vice versa.
  • S3 requires every multipart part to be at least 5 MiB.
  • S3 allows only 10,000 parts per multipart upload.
  • Objectstore requires us to know the size of a part upfront (this is getting fixed, see issue).

This leads to the following design:

image

To prevent excessive buffering, we upload a part as soon as we hit the 5 MiB threshold. This creates a lot of requests and IMO contributes to the high failure rate.

Near-Future Work

Once the content_length requirement on multipart.put_stream has been lifted, we can simply put the stream through an async_compression zstd-encoder and forward it 1:1 to objectstore. We just need to make sure to upload full grains, such that the number of uploaded bytes divides the upload granularity cleanly. See INGEST-1030.

Closes INGEST-974 INGEST-975

@linear-code

linear-code Bot commented Jul 9, 2026

Copy link
Copy Markdown

INGEST-974

INGEST-975

@jjbayer
jjbayer force-pushed the feat/tus-offset branch from 2d8f441 to 8e078f2 Compare July 9, 2026 21:07
Comment thread relay-server/src/services/upload.rs
Comment thread relay-server/src/services/objectstore.rs Outdated
@jjbayer
jjbayer marked this pull request as draft July 17, 2026 22:00
Comment on lines +56 to +60
if signed_location.upload_id().is_some() {
// `SignedLocation<Final>` should never have an upload ID.
// NOTE: we could encode this into the `Final` type.
return Err(ProcessingError::InvalidAttachmentRef);
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Interesting

@jjbayer
jjbayer marked this pull request as ready for review July 18, 2026 21:08
Comment thread relay-server/src/services/objectstore.rs
Comment thread relay-server/src/endpoints/upload.rs
Comment thread relay-server/src/services/objectstore.rs
Comment thread relay-server/src/services/objectstore.rs
Comment thread relay-server/src/services/objectstore.rs
/// The minimum distance between two `Upload-Offset`s in bytes.
///
/// Every `Upload-Offset` must be 0 modulo `UPLOAD_GRANULARITY`.
const UPLOAD_GRANULARITY: NonZeroUsize = NonZeroUsize::new(1024 * 1024).unwrap(); // 1 MiB

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

TIL about NonZeroUsize

@tobias-wilfert tobias-wilfert left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Looks reasonable, before I deploy this, will make a DD notebook to monitor that this doesn't break something.

Self::InvalidUploadLength { .. } => "invalid_upload_length",
Self::RequestTooSmall { .. } => "request_too_small",
Self::UnalignedBody { .. } => "unaligned_body",
Self::UnknownKey { .. } => "invalid_length",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Bug: The ErrorKind::UnknownKey variant is incorrectly mapped to the metric tag "invalid_length", which belongs to InvalidUploadLength, causing metric mislabeling.
Severity: LOW

Suggested Fix

Change the string returned for the ErrorKind::UnknownKey variant in the as_str() method from "invalid_length" to "unknown_key" to match the variant name and align with the pattern used for other error kinds.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.

Location: relay-server/src/services/objectstore.rs#L334

Potential issue: In the `as_str()` implementation for `ErrorKind`, the `UnknownKey`
variant is incorrectly mapped to the string `"invalid_length"`. This string is already
used by the `InvalidUploadLength` variant. As a result, when an `UnknownKey` error
occurs during an attachment upload, the `RelayCounters::AttachmentUpload` metric will be
incorrectly tagged with `result="invalid_length"`. This misattribution will skew
monitoring and alerting, conflating two distinct error types and hindering
observability.

Comment thread relay-server/src/services/objectstore.rs
match self {
ErrorKind::InvalidOffset { .. }
| ErrorKind::InvalidUploadLength { .. }
| ErrorKind::UnalignedBody { .. } => true,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Client error misclassified as server

Medium Severity

RequestTooSmall is a client-retryable condition, but is_client_error does not include it. Failures then fall through to relay_log::error, so expected undersized compressed chunks are reported as server errors.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 3940d47. Configure here.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 2 potential issues.

There are 3 total unresolved issues (including 1 from previous review).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 6809aba. Configure here.

headers={"Location": DUMMY_UPLOAD_LOCATION},
headers={
"Location": DUMMY_UPLOAD_LOCATION,
"Upload-Offset": len(request.data),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Dummy upload offset uses compressed size

Medium Severity

The dummy_upload fixture sets Upload-Offset from len(request.data), but that body is zstd-compressed (Content-Encoding: zstd). TUS Upload-Offset is the uncompressed byte offset, so upstream proxy tests can observe the wrong offset via StreamResult::try_from_response.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 6809aba. Configure here.

(_, None) => {
if offset != 0 {
return Err(Error::OffsetWithoutLength);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Misleading resume error without multipart

Low Severity

OffsetWithoutLength is returned whenever upload_id is missing and offset != 0, including when the client did send Upload-Length but multipart was disabled. The error text blames Defer-Length: 1, which is incorrect in that case.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 6809aba. Configure here.

@tobias-wilfert

Copy link
Copy Markdown
Member

This is put on hold for now, we first need to figure out some stuff that came up.

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.

2 participants