Skip to content

perf(backend): parallelize retained PATCH copies - #821

Merged
qiffang merged 2 commits into
mainfrom
dat9-dev2/task-202-patch-copy-concurrency
Aug 2, 2026
Merged

perf(backend): parallelize retained PATCH copies#821
qiffang merged 2 commits into
mainfrom
dat9-dev2/task-202-patch-copy-concurrency

Conversation

@qiffang

@qiffang qiffang commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Summary

  • replace the serial retained-part UploadPartCopy loop with a request-scoped, bounded 8-worker pool
  • cancel remaining work on the first copy failure or request cancellation, wait for all workers, then abort the multipart upload exactly once with a detached bounded cleanup context
  • preserve part-number ordering and completion semantics while adding deterministic unit, race, integration, and benchmark coverage

Correctness

  • the worker pool only schedules retained copy tasks and does not change the PATCH session state machine
  • CopiedParts is assembled from the original ordered task list, not completion order
  • multipart completion still uses S3 ListParts, whose implementations return part-number-sorted ETag mappings
  • copy failure/cancellation aborts before reservation or upload metadata is written
  • AWS SDK standard retry behavior remains the single retry layer for retryable 503 / SlowDown responses

Validation

  • go test ./pkg/backend/internal/patchcopy -count=100
  • go test -race ./pkg/backend/internal/patchcopy -count=20
  • go test ./pkg/backend/internal/patchcopy -run '^$' -bench '^BenchmarkCopy$' -benchtime=10x -count=1
    • 36 parts, concurrency 1: 46.23ms/op
    • 36 parts, concurrency 8: 6.44ms/op (~7.2x)
  • go test -c ./pkg/backend
  • go test -c ./pkg/server
  • go vet ./pkg/backend/internal/patchcopy ./pkg/backend ./pkg/server
  • git diff --check

The two DB-backed LocalS3 integration regressions compile locally. Runtime execution is deferred to hosted CI because this machine has no rootless Docker and pkg/backend TestMain requires testcontainers MySQL.

Fixes #820

Summary by CodeRabbit

  • New Features

    • Retained file parts are now copied concurrently with bounded parallelism during patch uploads.
    • Patch uploads preserve modified data while efficiently reusing unchanged parts.
  • Bug Fixes

    • Improved error reporting for individual part-copy failures.
    • Failed or cancelled multipart uploads are cleaned up reliably, preventing incomplete uploads and related metadata from lingering.
  • Tests

    • Added coverage for concurrency, cancellation, cleanup, failure handling, data integrity, and performance.

@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 293ec654-a48a-4ff4-b7b3-e088e84d46a8

📥 Commits

Reviewing files that changed from the base of the PR and between 63fb4c2 and 89e37b2.

📒 Files selected for processing (1)
  • pkg/server/patch_copy_e2e_test.go

📝 Walkthrough

Walkthrough

Multipart PATCH uploads now batch retained-part copy tasks and execute them with bounded concurrency. Copy failures cancel remaining work and trigger detached, timeout-bound multipart cleanup. Tests cover unit, backend, and end-to-end success, failure, and cancellation paths.

Changes

Multipart patch copy

Layer / File(s) Summary
Bounded copy engine
pkg/backend/internal/patchcopy/copy.go, pkg/backend/internal/patchcopy/copy_test.go
Adds typed copy contracts, bounded workers, range handling, cancellation, first-part error retention, input validation, and benchmark coverage.
Copy failure cleanup integration
pkg/backend/internal/patchcopy/copy.go, pkg/backend/internal/patchcopy/copy_test.go, pkg/backend/patch.go
Adds detached timeout-bound abort handling, joins copy and abort errors, batches retained-part tasks, logs typed part failures, and routes cleanup paths through the shared abort helper.
Patch upload integration validation
pkg/backend/patch_test.go
Verifies concurrent retained-part copying, completion ordering, dirty-part preservation, failure handling, multipart cleanup, and active-upload removal.
End-to-end PATCH validation
pkg/server/patch_copy_e2e_test.go
Verifies bounded copy concurrency, byte ranges, patch-plan results, copy-failure cleanup, request cancellation, and preservation of the original file.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related issues

Possibly related PRs

  • mem9-ai/drive9#242 — Modifies multipart patch copying in pkg/backend/patch.go.
  • mem9-ai/drive9#548 — Relates to multipart patch initiation and abort handling in pkg/backend/patch.go.

Sequence Diagram(s)

sequenceDiagram
  participant PatchRequest
  participant patchcopy.CopyOrAbort
  participant S3
  participant MultipartUpload
  PatchRequest->>patchcopy.CopyOrAbort: retained part tasks and concurrency limit
  patchcopy.CopyOrAbort->>S3: bounded UploadPartCopy calls
  S3-->>patchcopy.CopyOrAbort: copy results or PartError
  patchcopy.CopyOrAbort->>MultipartUpload: abort after copy failure
  MultipartUpload-->>PatchRequest: cleanup result
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: parallelizing retained PATCH copies in the backend.
Linked Issues check ✅ Passed The implementation meets issue #820 requirements for bounded concurrency, cancellation, ordering, cleanup, retry preservation, and regression coverage.
Out of Scope Changes check ✅ Passed The code and tests remain focused on parallel retained-part copying, cancellation, multipart cleanup, and performance validation for issue #820.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch dat9-dev2/task-202-patch-copy-concurrency

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai 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.

🧹 Nitpick comments (2)
pkg/backend/internal/patchcopy/copy.go (1)

59-64: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Define package-level sentinels for the two static parameter-validation errors. Both checks build a fixed message with fmt.Errorf (no format verbs), so callers cannot match them with errors.Is, and per the guideline sentinel errors belong at package level via errors.New.

  • pkg/backend/internal/patchcopy/copy.go#L59-L64: return a package-level ErrInvalidConcurrency instead of fmt.Errorf("patch copy concurrency must be positive").
  • pkg/backend/internal/patchcopy/copy.go#L146-L154: return a package-level ErrInvalidAbortTimeout instead of fmt.Errorf("patch abort timeout must be positive") at Line 149.

As per coding guidelines, "Use the ErrFoo pattern for sentinel errors and define them at package level with errors.New".

♻️ Proposed sentinels
+// ErrInvalidConcurrency reports a non-positive copy concurrency limit.
+var ErrInvalidConcurrency = errors.New("patch copy concurrency must be positive")
+
+// ErrInvalidAbortTimeout reports a non-positive multipart abort timeout.
+var ErrInvalidAbortTimeout = errors.New("patch abort timeout must be positive")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/backend/internal/patchcopy/copy.go` around lines 59 - 64, Define
package-level ErrInvalidConcurrency and ErrInvalidAbortTimeout sentinels with
errors.New in pkg/backend/internal/patchcopy/copy.go, then return them directly
from the concurrency validation at lines 59-64 and abort-timeout validation at
lines 146-154 instead of constructing fixed fmt.Errorf messages.

Source: Coding guidelines

pkg/backend/patch.go (1)

338-338: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider logging discarded abort errors.

All five patchcopy.Abort call sites drop the error, so a failed cleanup leaves an orphaned multipart upload with no signal. A logger.Warn on non-nil abort error would make these leaks visible.

Also applies to: 352-352, 361-361, 407-407

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/backend/patch.go` at line 338, Update all five patchcopy.Abort call sites
in the relevant patch handling flows to capture the returned error and emit a
logger.Warn when cleanup fails, including enough context to identify the
multipart upload or S3 key. Preserve the existing abort behavior and avoid
logging when Abort succeeds.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@pkg/backend/internal/patchcopy/copy.go`:
- Around line 59-64: Define package-level ErrInvalidConcurrency and
ErrInvalidAbortTimeout sentinels with errors.New in
pkg/backend/internal/patchcopy/copy.go, then return them directly from the
concurrency validation at lines 59-64 and abort-timeout validation at lines
146-154 instead of constructing fixed fmt.Errorf messages.

In `@pkg/backend/patch.go`:
- Line 338: Update all five patchcopy.Abort call sites in the relevant patch
handling flows to capture the returned error and emit a logger.Warn when cleanup
fails, including enough context to identify the multipart upload or S3 key.
Preserve the existing abort behavior and avoid logging when Abort succeeds.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: eac74c60-d537-4b22-b576-d0a36ca09a7c

📥 Commits

Reviewing files that changed from the base of the PR and between 41503f7 and 63fb4c2.

📒 Files selected for processing (4)
  • pkg/backend/internal/patchcopy/copy.go
  • pkg/backend/internal/patchcopy/copy_test.go
  • pkg/backend/patch.go
  • pkg/backend/patch_test.go

@qiffang qiffang left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Dev-Lead review (dat9-dev1) — exact head 63fb4c28ef9863aff71e69f2b58fa16c8fdaba65

Reviewed the tip commit 63fb4c2 (4 files, +897/-14: new pkg/backend/internal/patchcopy/{copy.go,copy_test.go} + patch.go wiring + patch_test.go). All Option-A gates pass on my axis. GREEN.

✅ Bounded concurrency

patchcopy.Copy: workerCount = min(maxConcurrency, len(tasks)), unbuffered jobs channel, fixed worker pool. patch.go sets patchPartCopyConcurrency = 8. No unbounded fan-out.

✅ First-error cancel → drain → exactly-once abort

  • failureOnce sync.Once records only the FIRST PartError and calls cancel(); workers observe copyCtx.Err() and stop; the feed loop break feeds on copyCtx.Done(), then close(jobs) + workers.Wait()all workers drain before Copy returns.
  • CopyOrAbort runs Copy (fully drained) then Abort once → abort happens after every worker stopped, exactly once.
  • TestCopyOrAbortWaitsAndAbortsExactlyOnce / TestCopyOrAbortDoesNotAbortAfterSuccess cover both.

✅ Detached abort — ctx-cancel cannot skip cleanup

Abort uses context.WithoutCancel(ctx) + timeout, so caller cancellation still runs the AbortMultipartUpload. TestCopyOrAbortUsesDetachedContextAfterParentCancellation / TestAbortUsesDetachedContext cover it.

✅ Part/ETag ordering — safe by design (not fragile to out-of-order completion)

patchcopy.Copy discards the UploadPartCopy ETag, and the pre-#821 code did too — no contract change. Copied-part ETags are collected at confirm-time via ListParts (upload.go:951CompleteMultipartUpload at 1010), which returns parts by number from S3. So concurrent out-of-order copy completion cannot corrupt the final assembly — the ordering is decoupled to the ListParts step. TestPatchUploadCopiesRetainedPartsConcurrentlyAndCompletesInPartOrder asserts this.

✅ No metadata written before copy success

In patch.go, CopyOrAbort (line 307) returns an error before any reservation/metadata persist (return nil, "copy retained parts: %w" at 324); the plan/reservation is created only after copies succeed. On copy failure: abort inside CopyOrAbort + return, no metadata. TestPatchUploadCopyFailureWaitsThenAbortsWithoutMetadata covers it.

✅ Option B not mixed in

Only the 4 copy-path files; no session state-machine / async changes. Scope-clean per adversary-1's constraint.

Tests

pkg/backend/internal/patchcopy passes with -race locally (11 discriminating tests: bounded concurrency, first-failure cancel, parent-cancel, exactly-once abort, no-abort-after-success, detached-context, invalid-concurrency/timeout). The patch_test.go integration tests (...ConcurrentlyAndCompletesInPartOrder, ...CopyFailureWaitsThenAbortsWithoutMetadata) require testcontainers → run on hosted CI (locally they hit the known rootless Docker not found TestMain gate, unrelated). Please confirm hosted CI green before merge.

Net: my correctness axis on the parallelization is GREEN. Awaiting adversary-1/adversary-2 exact-head concurrence + hosted CI. drive9 is qiffang-owned → merge on qiffang's「合」.

@qiffang

qiffang commented Jul 31, 2026

Copy link
Copy Markdown
Contributor Author

adversary-2 — GREEN @ 63fb4c28 (all correctness gates pass, no defects)

Independent adversarial review (subagent + personal source-verify). This parallelizes the serial 36× UploadPartCopy (16s) into a bounded scheduler. It's a concurrency change on a data-integrity-critical multipart path; I scrutinized race/atomicity, not style. All 7 axes clean.

✅ Bounded concurrency (axis 1)

copy.go: workerCount := min(maxConcurrency, len(tasks)), workers.Add(workerCount), N goroutines ranging an unbuffered jobs channel → at most 8 (patchPartCopyConcurrency, hardcoded const) in flight. No per-part fan-out. maxConcurrency<=0 rejected.

✅ Exactly-once abort, no MPU leak, no abort-vs-inflight (axis 2 — load-bearing)

  • First erroring worker sets failure + cancel() inside failureOnce sync.Once → recorded exactly once; loser workers just return.
  • close(jobs); workers.Wait() — a worker returns only after its in-flight UploadPartCopy completes (synchronous loop body), so Copy cannot return while any copy is in flight.
  • CopyOrAbort calls Abort once, from the single caller goroutine, only after Copy fully drained → no double-abort (abort isn't in the worker path) and no abort-vs-inflight (drain guarantee).
  • Abort uses context.WithTimeout(context.WithoutCancel(ctx), timeout)detached, so request-ctx cancel can't skip cleanup; it's synchronous (error joined via errors.Join), not a fire-and-forget goroutine that could be lost. postS3UploadFinalizeTimeout=2m, timeout<=0 rejected.

✅ ETag↔partNumber assembly (axis 3 — the top silent-corruption risk) — N/A BY DESIGN

The scheduler discards the UploadPartCopy ETag (if _, err := copier.UploadPartCopy(...)) — exactly as the old serial code did. No CompletedPart list is built here. ETag↔partNumber assembly happens at confirm-time: ConfirmUpload (upload.go:862) → b.s3.ListPartss3PartMap[p.Number]=p.ETag → cross-validates client ETag against S3 ETag by part number (upload.go:874-882) → CompleteMultipartUpload. S3 is the source of truth for ETags + order, so out-of-order copy completion is structurally incapable of corrupting the assembly. Highest-severity axis is moot.

✅ No metadata before copies succeed (axis 4)

patch.go: CopyOrAbort runs before reserveUploadOnServer / DB inserts. On copy failure → abort + return nil, err → no reservation, no upload rows. Copies-first-persist-second ordering preserved from the serial version.

✅ ctx-cancel (axis 5)

Parent cancel → copyCtx.Done() → feed breaks, workers drain, Copy returns ctx.Err()CopyOrAbort sees non-nil → detached Abort still runs (MPU cleaned up, no leak). No deadlock (feed selects on copyCtx.Done(); close(jobs) unblocks idle workers).

✅ Extraction preserves semantics (axis 7)

Per-part range math (partStart/partEnd + origSize clamp) and the dirty/copied split (!dirtySet[p.Number] && p.Number<=origPartCount) are byte-identical, moved verbatim into copyTasks. The 4 other abort sites (presign/reserve/supersede/insert failure) were swapped to detached patchcopy.Abort — a strict improvement. No off-by-one.

✅ Tests discriminate (axis 6)

Bounded-≤N (mutex-counted max-active), first-error→cancel+abortCalls==1, ctx-cancel→abort+activeAtAbort==0, out-of-order→real ConfirmUpload byte-check, benchmark {1,8}×36. Fail on a naive impl (no Once/no drain → activeAtAbort>0 / abortCalls>1). patchcopy 11 tests -race-clean; scheduler holds no shared result collection (ETags discarded) so -race-clean by construction; integration tests via hosted CI (testcontainers).

Non-blocking observation: concurrency is a hardcoded const 8 — correct for this PR (no premature config knob; negative-optionality). Flag only if ops later need per-tenant tuning.

Verdict: GREEN. Clean, well-factored, correctness handled precisely (detached-abort + ListParts-decoupled ordering are the two things that make it safe). Scope stays on the copy path; Option B correctly not mixed in. Merge authority = qiffang (drive9 repo); pending hosted CI green + adversary-1 concurrence.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 63fb4c28ef

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

); err != nil {
failureOnce.Do(func() {
failure = &PartError{PartNumber: task.PartNumber, Err: err}
cancel()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Re-abort multipart uploads after canceling active copies

When one retained copy fails while other AWS UploadPartCopy requests are in flight, this cancellation can make their client calls return before S3 has necessarily stopped the server-side copies; workers.Wait therefore does not establish that those uploads have settled. CopyOrAbort subsequently aborts exactly once, but S3's AbortMultipartUpload contract warns that in-progress uploads may finish after an abort and that repeated aborts can be necessary, so this failure path can leave orphaned, billable multipart parts. Either let active copies settle without canceling their request contexts or verify and retry the abort cleanup.

Useful? React with 👍 / 👎.

@qiffang qiffang left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

GREEN at exact head 63fb4c2.

No blocking findings. I attempted a formal APPROVE, but GitHub rejected it because this connector is authenticated as the PR author; treating this comment as my review verdict.

Reviewed gates:

  • Scope stays on Option A only: bounded in-request retained-part copy; no PATCH session-state-machine/async Option B changes.
  • patchcopy.Copy uses a bounded worker pool (min(maxConcurrency, len(tasks)), wired as 8), records first failure with sync.Once, cancels queued work, and waits for workers before returning.
  • CopyOrAbort performs cleanup after copy workers drain; Abort uses context.WithoutCancel plus a bounded timeout, so caller cancellation does not skip MPU cleanup.
  • patch.go calls CopyOrAbort before reservation/upload metadata; failure returns before reserveUploadOnServer / InsertUpload.
  • Part/ETag ordering is safe: copied part ETags are not consumed for completion; confirm lists parts from S3 and completes from S3's part-number order. CopiedParts is assembled from original task order.
  • Existing later abort paths now use the same detached bounded abort helper, which is a correctness improvement.

Validation checked:

  • git diff --check 41503f74493abd4c48ffe3085d7890dcc31a056c...HEAD
  • gofmt check on changed Go files
  • go test ./pkg/backend/internal/patchcopy -count=100
  • go test -race ./pkg/backend/internal/patchcopy -count=20
  • go test ./pkg/backend/internal/patchcopy -run '^$' -bench '^BenchmarkCopy$' -benchtime=10x -count=1 (~6.7x local 36-part speedup at concurrency 8 vs 1)
  • go test -c ./pkg/backend
  • go test -c ./pkg/server
  • go vet ./pkg/backend/internal/patchcopy ./pkg/backend ./pkg/server
  • Hosted checks are green at this head: ci, drive9-server-local e2e smoke, CodeRabbit.

Local DB-backed pkg/backend runtime regressions remain blocked on this machine by the known rootless Docker/testcontainers requirement, but hosted e2e/ci covered the runtime gate.

@qiffang

qiffang commented Jul 31, 2026

Copy link
Copy Markdown
Contributor Author

Exact-head E2E evidence for 89e37b2f1b547e2bdfaabc78b7a92befba4b7bcc:

  • TestPatchCopyE2EConcurrentRetainedPartsPreserveWholeFile
    • 12 logical parts; dirty parts 2/5/9; 9 retained copies (> worker cap 8).
    • Deterministically observes max 8 concurrent copies and forces completion order 11/12/10/8/7/6/4/3/1.
    • Verifies copy calls are unique and exactly the retained set, the completion plan remains in original part order, all first/middle/final-part byte ranges (including a partial tail) are correct, and the final HTTP GET matches full bytes, length, and SHA-256.
  • TestPatchCopyE2ECopyFailureLeavesOriginalAndNoUpload
    • Injected part-2 copy failure returns HTTP 500.
    • Verifies active copies exit before exactly one abort, ListParts and CompleteMultipartUpload fail after abort, no initiated/uploading metadata row remains, and the original object bytes/SHA-256 are unchanged.
  • TestPatchCopyE2ERequestCancellationAbortsAfterCopiesExit
    • Cancels a real HTTP request with copies in flight.
    • Verifies context.Canceled, then the same abort-once, no-metadata, non-completable-MPU, and original-byte invariants.

The server E2E tests ran against a temporary isolated Homebrew MySQL 8.4 datadir/database via DRIVE9_TEST_MYSQL_DSN:

go test ./pkg/server -run '^TestPatchCopyE2E' -count=10             PASS
go test -race ./pkg/server -run '^TestPatchCopyE2E' -count=3       PASS
go test ./pkg/backend/internal/patchcopy -count=100                 PASS
go test -race ./pkg/backend/internal/patchcopy -count=20           PASS

Backend/server compile, server race compile, vet, gofmt, and git diff --check also pass. Hosted exact-head checks are now pending.

@qiffang

qiffang commented Jul 31, 2026

Copy link
Copy Markdown
Contributor Author

adversary-2 — GREEN @ 89e37b2f (e2e tests are genuinely discriminating; correctness gate satisfied)

Re-reviewed at the new exact head (my prior GREEN was limited to 63fb4c28, not auto-carried). Delta = test-only: single new file pkg/server/patch_copy_e2e_test.go (+624/-0), zero production-code change from 63fb4c28 → my prior correctness GREEN on copy.go + patch.go carries forward. This round I verified the three e2e tests genuinely discriminate (would catch a real parallel-copy regression), not happy-path theater. Subagent + personal source-verify.

✅ Full-file correctness = independent SHA256, non-vacuous

patchCopyE2EExpected synthesizes expected test-side (clone original, overwrite ONLY dirty ranges by a deterministic formula distinct from the original's) — NOT from any server checksum. Dirty parts are uploaded as expected[start:end]; retained parts are server-side-copied from the original (which equals expected on clean ranges). Final: real HTTP GET whole file → sha256(got)==sha256(expected) && bytes.Equal (+ length). A wrong retained range, wrong part mapping, or misplaced dirty bytes breaks it.

✅ Bounded 8 with copied_parts=9 (>8), observable

12 parts, dirty {2,5,9} → 9 retained > 8. Asserts callCountBeforeRelease==8 AND maxActiveBeforeRelease==8 (mutex-tracked in the gated mock) before any gate opens — the 9th part provably queues. An unbounded/wrong impl → maxActive==9 → fail.

✅ Forced out-of-order completion through the REAL scheduler

Per-part gated channels drive completion in scrambled {11,12,10,8,7,6,4,3,1} (asserted), and the final SHA256 still matches → order-independence proven through the real patchcopy.Copy worker pool. Correctly, the per-part range assertion (ranges[partNumber]=={start,end-1}) is the discriminator for the part-mapping axis; "assemble-by-completion-order" is impossible by construction (ETag discarded in Copy; assembly is S3-owned via ListParts+CompleteMultipartUpload sorted by part number) — so the test targets what CAN break (byte ranges), which is right.

✅ No-duplicate + exact-coverage (the one real conflict risk)

assertPatchCopyE2EPartSet on the recorder's ACTUAL UploadPartCopy calls uses set-count (counts[p]!=1 fatal) → a part copied twice (two workers → same part number) fails. assertPatchCopyE2EPlanCoverage requires parts 1..12 each appear exactly once across copied∪dirty. Duplicate part number = caught.

✅ Failure path discriminates (all four bugs)

failPart=2 → PATCH 500 + abortCalls==1 (double-abort caught) + activeAtAbort==0 (abort-while-in-flight caught) + ListParts/Complete-after-abort error (MPU gone) + uploads-table active count==0 & no leaked row (leftover-metadata caught) + original file sha256 unchanged (corruption caught).

✅ Cancel path exercises the REAL detached-abort

Uses http.NewRequestWithContext + cancel() on the request context (not a direct Abort) → propagates to Copy's child ctx → workers exit → Abort runs on context.WithoutCancel; test waits on recorder.aborted (proves detached abort fired despite the cancel), then abort-once/activeAtAbort==0/no-metadata/original-unchanged. A naive impl that skips cleanup on ctx-cancel → never fires aborted (timeout) or leaks.

Two non-blocking observations (not gaps): the seed helper doesn't read-back-verify (validated downstream by the SHA asserts); v1 confirm path so completion-order-assembly is untestable-by-construction (correct — order is S3-owned).

Local: pkg/server -run '^TestPatchCopyE2E' -count=10 PASS, -race -count=3 PASS; patchcopy -count=100 / -race -count=20 PASS. Harness shared counters are mutex-guarded → -race-clean by construction.

Verdict: GREEN @ 89e37b2f. e2e tests are load-bearing and cover qiffang's file-correctness requirement (whole-file SHA256 across concurrent out-of-order copy + failure/cancel atomicity). Production correctness carries from 63fb4c28. Merge authority = qiffang; pending hosted CI green + adversary-1 concurrence. COS live-perf → #820 follow-up.

@qiffang

qiffang commented Jul 31, 2026

Copy link
Copy Markdown
Contributor Author

GREEN from adversary-1 for exact head 89e37b2f1b547e2bdfaabc78b7a92befba4b7bcc.

Formal GitHub APPROVE failed because this auth is treated as the PR author, so this comment is my review verdict.

Scope checked: delta from prior reviewed head 63fb4c28 is test-only (pkg/server/patch_copy_e2e_test.go); production copy/patch code remains covered by my prior review.

New e2e coverage is sufficiently discriminating: it exercises retained parts > concurrency cap (12 total, 9 copied, 8 in-flight), forces out-of-order copy completion, verifies copied/dirty plan coverage exactly once, checks retained byte ranges including non-full tail, uploads non-contiguous dirty parts, completes the patch, then compares full file bytes and SHA-256 against an independently synthesized expected buffer. Failure and request-cancel tests assert exactly one abort after copy workers drain, no active upload metadata, MPU not completable after abort, and original file bytes unchanged.

Validation observed locally: git diff --check 41503f7...HEAD, gofmt -l pkg/server/patch_copy_e2e_test.go, go test ./pkg/server -run 'TestPatchCopyE2E' -count=10 with temp MySQL 8.4, go test -race ./pkg/server -run 'TestPatchCopyE2E' -count=1, go test ./pkg/backend/internal/patchcopy -count=100, go test -race ./pkg/backend/internal/patchcopy -count=20, go test -c ./pkg/server, go test -c ./pkg/backend, and go vet ./pkg/backend/internal/patchcopy ./pkg/backend ./pkg/server passed. Hosted ci, drive9-server-local e2e smoke, and CodeRabbit are green at this head.

No blockers found. COS live smoke remains tracked separately in #820 and is not a PR #821 merge blocker.

@srstack srstack left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Review at 89e37b2 — LGTM ✅

Reviewed the full diff, the new patchcopy package, all three test files, and the patch.go integration. Ran go test ./pkg/backend/internal/patchcopy -count=50, -race -count=10, go vet, go test -c on pkg/backend + pkg/server, gofmt -l, git diff --check — all clean. Also ran two independent adversarial passes (concurrency-correctness and test-quality).

Concurrency correctness — proven sound. The core safety invariant holds on every interleaving:

Copy returning nil ⟺ every task's UploadPartCopy succeeded.

A task can only be dropped when copyCtx is Done, which requires either the parent ctx was cancelled (→ ctx.Err() != nil, non-nil return) or a worker called cancel() inside failureOnce.Do (→ failure != nil first, non-nil return). So a nil return can never coexist with a dropped/failed task. No deadlock (feeder's select on copyCtx.Done() pairs with every early worker exit), no goroutine leak (close(jobs) unconditional, defer workers.Done() on all paths), no data race on failure (single writer via sync.Once, HB edge through workers.Wait()).

Design highlights worth calling out:

  • Abort detaching via context.WithoutCancel(ctx) — cleanup survives request cancellation, which is exactly the leak this PR set out to close. Directly asserted (abortCtxErr == nil after parent cancel).
  • activeAtAbort == 0 invariant proves all workers drain before abort touches the MPU, so abort never races an in-flight copy.
  • CopiedParts rebuilt from the ascending copyTasks slice after CopyOrAbort returns — ordering is structurally guaranteed by the caller, independent of completion order.

Tests — strong. Bounded concurrency is asserted in both directions (peak maxActive == N and a non-blocking select proving no extra worker started); the E2E test forces genuine out-of-order completion (part 1 gated to finish last, still lands first in CopiedParts) through the real HTTP→backend→LocalS3 path with full SHA-256 integrity verification; abort-exactly-once is counter-asserted; both failure and cancellation paths verify the original object is untouched. No time.Sleep/rand/t.Parallel flakiness. No tautological tests — each would fail against a plausibly broken impl.


Non-blocking follow-ups (none gate merge):

  • [MINOR] patch_test.go:117 TestPatchUploadCopiesRetainedPartsConcurrentlyAndCompletesInPartOrder — the name promises out-of-order completion, but it releases all workers simultaneously, so it doesn't actually force reorder; it's effectively a concurrency + part-set check. The E2E test covers real reordering, so coverage isn't lost — suggest renaming (or add gates) to match intent.
  • [MINOR] Coverage gap: the presign-failure abort path (patch.go:266-272, also routed through patchcopy.Abort) isn't exercised by the new tests — only copy-failure and cancel-failure abort paths are.
  • [NIT] copy.go:116 prefers parent ctx.Err() over a PartError after Wait(). If the parent is cancelled in the narrow window between Wait() returning and this check while all copies actually succeeded, Copy reports a spurious error. Harmless in practice — the request is already tearing down and CopyOrAbort correctly aborts the MPU — but a real copy PartError could likewise be masked by a concurrent deadline, costing diagnostic detail.
  • [NIT] Abort with abortTimeout <= 0 skips cleanup entirely (returns the guard error). Unreachable today (callers pass postS3UploadFinalizeTimeout = 2m), just noting the contract.

Clean, well-scoped, measurably faster (~7.2x at 36 parts), and the leak-safety story is airtight. Approving.

@qiffang
qiffang merged commit e9fae58 into main Aug 2, 2026
3 checks passed
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.

perf(fuse/backend): PATCH on large mostly-unchanged file is slow — parallelize sequential retained-part UploadPartCopy (16s → ~1-2s)

2 participants