Problem
A PATCH /v1/fs/.../output/douyin/video_720.mp4 took duration_ms=16060 (~16s), status 202.
Log signal for that trace:
new_size≈308MB, total_parts=37, dirty_parts=1, copied_parts=36
- i.e. only one ~8MB part actually changed; the other 36 retained parts (~288MB) must be re-included in the new multipart object.
Root cause
In pkg/backend/patch.go, the plan-execution loop that re-assembles the retained parts is sequential:
for _, p := range newParts {
// unchanged part → server-side copy
_, err := b.s3.UploadPartCopy(ctx, newS3Key, mpu.UploadID, p.Number, sourceKey, partStart, partEnd)
...
}
No goroutine / errgroup / semaphore. So the 36 UploadPartCopy calls run one after another: 16060ms / 36 ≈ 446ms per copy (normal for an ~8MB S3 server-side part-copy) → 36 sequential = ~16s. The latency is the retained-part copies serialized, not the dirty-data transfer (only 1 part).
The get_upload_reservation / get_upload_by_path not_found warns on the same trace are benign first-patch misses (~60ms), not the latency source.
The 202 is written after InitiatePatchUploadIfRevision (server.go:2444 → 2496) completes the copies — it returns a PatchPlan (presigned dirty-part upload URL + read URLs); the client then uploads the dirty part and calls confirm (ConfirmUploadWithTags) which does CompleteMultipartUpload. So 202 is not a durability signal; there is already an upload+confirm step after it.
Fix — Option A (this issue): parallelize the in-request UploadPartCopy loop
Bounded-concurrency the copy loop. Contract unchanged, pure server-side. Expected 16s → ~1–2s.
Acceptance gates (from dat9-dev1 + adversary-1 + adversary-2):
Follow-up — Option B (separate issue, not this one): async copy
Since 202 returns a plan and a confirm step already follows, the retained-part copies only need to complete before CompleteMultipartUpload at confirm-time. So the handler could return the plan immediately, run copies in a background window, and join/wait at confirm — driving the handler sub-second without changing 202 semantics. More complex (copy-vs-confirm join + failure propagation); evaluate separately.
Owner: dat9-dev1 (Option A). Reviewers: adversary-1, adversary-2. drive9 is qiffang-owned → PR + regression, merge on qiffang's signal.
Problem
A
PATCH /v1/fs/.../output/douyin/video_720.mp4tookduration_ms=16060(~16s), status 202.Log signal for that trace:
new_size≈308MB, total_parts=37, dirty_parts=1, copied_parts=36Root cause
In
pkg/backend/patch.go, the plan-execution loop that re-assembles the retained parts is sequential:No goroutine / errgroup / semaphore. So the 36
UploadPartCopycalls run one after another:16060ms / 36 ≈ 446ms per copy(normal for an ~8MB S3 server-side part-copy) → 36 sequential = ~16s. The latency is the retained-part copies serialized, not the dirty-data transfer (only 1 part).The
get_upload_reservation/get_upload_by_pathnot_foundwarns on the same trace are benign first-patch misses (~60ms), not the latency source.The
202is written afterInitiatePatchUploadIfRevision(server.go:2444 → 2496) completes the copies — it returns aPatchPlan(presigned dirty-part upload URL + read URLs); the client then uploads the dirty part and calls confirm (ConfirmUploadWithTags) which doesCompleteMultipartUpload. So 202 is not a durability signal; there is already an upload+confirm step after it.Fix — Option A (this issue): parallelize the in-request UploadPartCopy loop
Bounded-concurrency the copy loop. Contract unchanged, pure server-side. Expected 16s → ~1–2s.
Acceptance gates (from dat9-dev1 + adversary-1 + adversary-2):
503 SlowDown.AbortMultipartUploadruns exactly once, no leaked parts / MPU.ctxcancellation stops subsequent copies.copied_partsproving the serial bottleneck is broken (assert concurrency, first-error abort, no leak).Follow-up — Option B (separate issue, not this one): async copy
Since 202 returns a plan and a confirm step already follows, the retained-part copies only need to complete before
CompleteMultipartUploadat confirm-time. So the handler could return the plan immediately, run copies in a background window, and join/wait at confirm — driving the handler sub-second without changing 202 semantics. More complex (copy-vs-confirm join + failure propagation); evaluate separately.Owner: dat9-dev1 (Option A). Reviewers: adversary-1, adversary-2. drive9 is qiffang-owned → PR + regression, merge on qiffang's signal.