fix: hybrid passthrough for Scylla manifest mid-frame range tail#123
Conversation
2026.7.15 logged passthrough_blocked_reason=ranged_copy_segment_misaligned: manifest ranges (bytes=0-4999341931) end ~1.1MB into an 8.33MB encrypted frame. Copy all complete ciphertext segments server-side and re-encrypt only the trailing suffix instead of falling back to full streaming. Co-authored-by: Cursor <cursoragent@cursor.com>
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
|
Pushed What's new:
New tests ( Post-deploy check: kubectl logs -n s3proxy-python -l app.kubernetes.io/name=s3proxy-python --since=30m \
| rg 'UPLOAD_PART_COPY_KEEPALIVE|UPLOAD_PART_COPY_PASSTHROUGH_COMPLETE|UPLOAD_PART_COPY_CLIENT_GONE|FAILED_AFTER_200'Expect |
…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.
|
Pushed
Both reproduced locally against real MinIO (the reason they don't show in unit tests: the S3 mock doesn't validate part etags on complete) and verified fixed end-to-end. Regression tests added: |
Exhaustion-path tests hit all 25 retries with exponential sleep, which blocked the unit CI job until the 10-minute timeout cancelled it. Co-authored-by: Cursor <cursoragent@cursor.com>
Pass the new reservation argument to _stream_and_upload_framed in unit tests and bump the unit CI job timeout to 15 minutes. Co-authored-by: Cursor <cursoragent@cursor.com>
Summary
passthrough_blocked_reason=ranged_copy_segment_misaligned: Scylla sendsbytes=0-4999341931(~4767MB) but internal frames are ~8.33MB (50MB upload / 6), so the range ends mid-frame_split_plaintext_range_on_segments→ passthrough all complete ciphertext segments + re-encrypt only the trailing suffix (UPLOAD_PART_COPY_PASSTHROUGH_TAIL)streaming_tail_mbon passthrough events for diagnosisTests
test_split_plaintext_range_allows_hybrid_tail— split logictest_scylla_prod_shape_*— uses prod exact range end4999341931+ 8.33MB frames; expects 500+upload_part_copy+ 1upload_parttailPost-deploy
Expect
route=passthrough,passthrough_blocked_reason=null, andstreaming_tail_mb~1MB on.sm_*manifests.Made with Cursor