Fix #432: fork stranded name #433

Merged
crueber merged 2 commits from fix/issue-432 into main 2026-09-13 03:47:17 +00:00
Owner

Fixes #432: a fork whose share succeeds but whose access bootstrap or provenance write fails stranded the target name (child manifest present, no fork.json — every retry 409s until manual bucket cleanup).

Choice: (1) reserve-then-commit with rollback. The share Create already IS the reservation (it arbitrates the name like repo create); on pre-commit failure runFork now calls ForkExecutor.RollbackShare, which deletes exactly the keys this attempt Created (child manifest.pb, checkpoint pair, bootstrapped access.json) after re-verifying ownership by fresh GET (Repo == child, Revision == 1 — any WAL advance aborts, nothing deleted).

Rejected: (2) adopt-or-fail — with no pre-provenance marker, an unprovenanced manifest is indistinguishable from a racing repo create (empty-parent case is byte-identical), so adopting risks hijacking a live repo prefix. (3) strand-and-report sweeper — same ownership-detection problem, delayed reuse, new background machinery.

Safety: failure-path only (zero store calls on success, law 6); never packs/parent keys/parent index; fork.json on the prefix downgrades to share-keys-only. GC coordination: a rolled-back child is never listed in any parent index, so the #424 fail-closed forknet walk cannot reference it (racing pass sees manifest-404 and skips). Companion: provenance-412 with own fork.json adopts and completes (stale reservation self-heals for its owner); foreign stays 409 with our share keys released. Known residual: process crash between share and rollback still strands (documented; retry 409s loudly, never hijacks).

Tests: new internal/pulls/fork432_test.go — rollback+retry reuses the name, advanced/foreign/corrupt manifests refuse without deleting, adopted shares never roll back, provenance arbitration (foreign 409+rollback / own adopt / unreadable fail-closed), executor prefix-scoping incl. disputed-prefix and empty-parent cases. Full pulls suite -race green, package cover 95.9% (>= 95% gate). No new deps, no docker/compose changes.

Fixes #432: a fork whose share succeeds but whose access bootstrap or provenance write fails stranded the target name (child manifest present, no fork.json — every retry 409s until manual bucket cleanup). Choice: (1) reserve-then-commit with rollback. The share Create already IS the reservation (it arbitrates the name like repo create); on pre-commit failure runFork now calls ForkExecutor.RollbackShare, which deletes exactly the keys this attempt Created (child manifest.pb, checkpoint pair, bootstrapped access.json) after re-verifying ownership by fresh GET (Repo == child, Revision == 1 — any WAL advance aborts, nothing deleted). Rejected: (2) adopt-or-fail — with no pre-provenance marker, an unprovenanced manifest is indistinguishable from a racing repo create (empty-parent case is byte-identical), so adopting risks hijacking a live repo prefix. (3) strand-and-report sweeper — same ownership-detection problem, delayed reuse, new background machinery. Safety: failure-path only (zero store calls on success, law 6); never packs/parent keys/parent index; fork.json on the prefix downgrades to share-keys-only. GC coordination: a rolled-back child is never listed in any parent index, so the #424 fail-closed forknet walk cannot reference it (racing pass sees manifest-404 and skips). Companion: provenance-412 with own fork.json adopts and completes (stale reservation self-heals for its owner); foreign stays 409 with our share keys released. Known residual: process crash between share and rollback still strands (documented; retry 409s loudly, never hijacks). Tests: new internal/pulls/fork432_test.go — rollback+retry reuses the name, advanced/foreign/corrupt manifests refuse without deleting, adopted shares never roll back, provenance arbitration (foreign 409+rollback / own adopt / unreadable fail-closed), executor prefix-scoping incl. disputed-prefix and empty-parent cases. Full pulls suite -race green, package cover 95.9% (>= 95% gate). No new deps, no docker/compose changes.
Implements docs/features/03_pull_requests.md §7 remediation (1):
when the fork share succeeds but access bootstrap or provenance write
fails, delete exactly the keys this attempt Created (child manifest.pb,
checkpoint pair, bootstrapped access.json) so the target name is
immediately reusable. Ownership re-verified by fresh GET (Repo+Revision);
adopt-or-fail rejected (unprovenanced manifest indistinguishable from a
racing repo create), sweeper rejected (same detection problem + delayed
reuse). Companion: provenance-412 with own fork.json adopts and completes.
Decision appended to 03 Decisions; GC coordination noted (rolled-back
child never indexed, racing GC sees manifest-404 and skips).
runFork's adoptedShare Root-backfill error path returned without
rollback. When THIS attempt won the share and then adopted a stale own
fork.json, a backfill CAS exhaustion stranded the reservation — the
exact #432 class on one remaining path. rollback() is a no-op for pure
adopts (sharedThisAttempt false), so wiring it here is safe.

Test: TestFork432BackfillFailureRollsBackShare (fails on unfixed code,
passes with fix).
Sign in to join this conversation.
No description provided.