Skip to content

fix: hybrid passthrough defer tail + UploadPartCopy keepalive (2026.7.17)#124

Merged
ServerSideHannes merged 9 commits into
mainfrom
fix/hybrid-passthrough-defer-tail
Jul 9, 2026
Merged

fix: hybrid passthrough defer tail + UploadPartCopy keepalive (2026.7.17)#124
ServerSideHannes merged 9 commits into
mainfrom
fix/hybrid-passthrough-defer-tail

Conversation

@ServerSideHannes

Copy link
Copy Markdown
Owner

Summary

  • Hybrid passthrough for Scylla manifest part 1: when the copy range ends mid internal encryption frame, passthrough aligned segments and handle the ~1MB tail without blocking on full streaming re-encrypt.
  • Defer sub-5MB hybrid tail: if part 1 is large (~4.7GB) and part 2 will follow, store the tail in upload state instead of uploading it as a non-final internal S3 part (< 5MB → EntityTooSmall on CompleteMultipartUpload). Part 2 prepends deferred bytes into streaming; complete flushes any leftover tail.
  • UploadPartCopy keepalive + parallel segment copies: fixes the original 30m rclone idle timeout (400 0 on PUT) seen on oceanio-dc2-scylladb-backups-v3.
  • Debug logs: HYBRID_TAIL_DEFER_DECISION, UPLOAD_PART_COPY_PASSTHROUGH_ALLOC, deferred-tail fields on passthrough/streaming complete, COMPLETE_MULTIPART_DEFERRED_TAIL_PENDING.

Test plan

  • CI unit: test_upload_part_copy_passthrough.py (fast two-part defer test + full Scylla prod-shape test)
  • CI unit: test_upload_part_copy_keepalive.py
  • Post-deploy on backup cluster: COMPLETE_MULTIPART_SUCCESS on .sm_ manifests, no fast POST EntityTooSmall/InvalidPart 400s, no new 30m PUT timeouts
  • Dedup % climbs without Failed tracking deduped bytes that then fail at complete

Made with Cursor

ServerSideHannes and others added 4 commits July 9, 2026 14:35
Add request-scoped context (path, uploadId, partNumber) to MEMORY_*,
REQUEST_* and S3_ERROR_RESPONSE logs so prod 4xx/5xx are diagnosable
without guessing. Harden the postgres-style UploadPart path: map
InvalidPart/EntityTooSmall ClientErrors to 400, reject internal part
ranges past S3's 10k ceiling, detect truncated upload bodies, retry
Redis WATCH conflicts with backoff (25 attempts), and raise S3
read_timeout to 300s for slow seaweed uploads.

Co-authored-by: Cursor <cursoragent@cursor.com>
…ents

A 4.7GB Scylla part copy sent zero response bytes until the copy finished,
so rclone's 5-minute idle timeout killed every attempt (~400s of work vs a
300s deadline) and the proxy kept the doomed copy running as a zombie while
scylla-manager retried forever.

- Commit to 200 OK and trickle whitespace while the copy runs (AWS S3 long-
  copy pattern); emit CopyPartResult or an <Error> document as the body
- Cancel in-flight backend work when the client disconnects (no zombies)
- Run passthrough segment copies concurrently (bounded, default 8) instead
  of ~570 sequential backend calls
- Overlap the synthetic-ETag MD5 source pass with the segment copies
  instead of serially re-reading the whole source after them
- Fix ruff check failures (F841, B039, import order) so CI lint passes
…ion failures)

test_copy_full_encrypted_object: the single-segment passthrough copy
returned a synthetic plaintext etag to the client but stored it as the
part etag too, so CompleteMultipartUpload forwarded an etag the backend
never issued -> InvalidPart. This was masked before ae39244: the proxy
turned the backend error into a 500, boto3 retried, and the retry
succeeded via state reconstruction (which reads real etags from
list_parts). Store the backend CopyPartResult etag in part state and
have _build_s3_parts use stored etags instead of the client echo.

test_part_number_out_of_range: rejecting UploadPart before reading its
body (new internal-part-range validation) left the body bytes on the
keep-alive connection, so the client's next request (the abort) hit a
raw uvicorn 400. Drain small bodies in the error handler; send
Connection: close for bodies too large to slurp.

Both reproduced locally against real MinIO and verified fixed.
…manifests

When part 1 hybrid passthrough ends with a sub-5MB mid-frame tail and part 2
follows, store the tail in upload state instead of uploading it as a non-final
internal S3 part. Part 2 prepends the deferred bytes into streaming; complete
flushes any leftover tail. Adds debug logs and regression tests.

Co-authored-by: Cursor <cursoragent@cursor.com>
@ServerSideHannes ServerSideHannes force-pushed the fix/hybrid-passthrough-defer-tail branch from 94d291b to c60697e Compare July 9, 2026 12:36
ServerSideHannes and others added 5 commits July 9, 2026 15:14
Use 8MB frames and 36MB part 1 so the copy hits passthrough (not simple
copy below STREAMING_THRESHOLD) and internal segments meet MIN_PART_SIZE.

Co-authored-by: Cursor <cursoragent@cursor.com>
Mark 500+ segment Scylla prod-shape unit tests as slow, exclude them
from parallel test-unit, and run them with -n0 in a separate CI step.

Co-authored-by: Cursor <cursoragent@cursor.com>
Streaming copy now calls take_deferred_copy_tail at part start; test
_Mgr/_TrackingMgr stubs were missing the method.

Co-authored-by: Cursor <cursoragent@cursor.com>
Move prod-scale unit tests out of the parallel unit job into unit-slow
with a longer timeout. Shrink part 2 in the two-part prod-shape test so
it still exercises streaming + deferred tail without ~1.3GB of work.

Co-authored-by: Cursor <cursoragent@cursor.com>
572-segment part 1 + part 2 streaming on a ~5GB mock source exhausts
runner memory. Coverage is split: prod part-1 passthrough (slow) and
two-part complete with deferred tail (fast).

Co-authored-by: Cursor <cursoragent@cursor.com>
@ServerSideHannes ServerSideHannes merged commit 9d6721d into main Jul 9, 2026
11 checks passed
@ServerSideHannes ServerSideHannes deleted the fix/hybrid-passthrough-defer-tail branch July 9, 2026 15:11
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