fix(uploads): unique write_atomic staging name to end routes_uploads flake (LC-208) #274

Merged
longjacksonle merged 1 commit from fix/lc-208-write-atomic-unique-staging into main 2026-05-30 04:02:16 +02:00

Investigated the long-standing routes_uploads flake (named in CLAUDE.md). Reproduced, diagnosed, fixed, validated against the reproduction recipe.

Phase 1 - reproduction

The flake is intra-binary, not cross-binary. Running the single routes_uploads binary in isolation:

Condition Result
--test-threads=8, isolated binary, x40 4 fail / 40 (~10%)
--test-threads=1 (serialized), x40 0 fail / 40

So concurrency within one binary is the trigger; no other binaries needed. The CLAUDE.md "concurrent-binary load" framing was imprecise. Every captured failure was a 500 Internal Server Error where 200 was expected (a handler-side error), including tiny_png tests - not a test-side assertion mismatch.

Phase 2 - root cause

The tests share a process-global upload dir (db::DATA_DIR is a OnceLock set once per process) and reuse identical fixture bytes, so concurrent uploads of the same bytes derive the same content-addressed final path. The dedup check in post_upload (metadata(final).is_err()) is TOCTOU: both writers see "absent" and both call write_atomic(final). write_atomic staged to a fixed {final}.partial, so both wrote the same staging file and both renamed it; whichever renamed second hit ENOENT after the first consumed the partial, and the upload returned 500. (The tiny_png failures rule out the medium_png remove_file path - the collision is the shared original write.)

This is a latent production race in write_atomic (two users uploading identical bytes simultaneously would hit it - one 500s and retries), amplified by the harness. So #[serial] would be the wrong fix: it would hide a real bug behind serialization.

Phase 3 - fix

The fix addresses the confirmed mechanism: a per-write unique staging name (a uuid nonce), so concurrent writers of the same final path never share a staging file. Both renames target the same identical-content final path, which is safe (rename is atomic, last writer wins with the same bytes). Fixes every write_atomic caller (uploads, admin regen, email attachments, bridge avatar), not just the test path.

Validation (same recipe used to find it)

Pre-fix Post-fix
routes_uploads isolated, --test-threads=8/=16 4/40 (10%) 0/160
Regression guard (uploads_write_atomic_concurrent) 20/20 fail 20/20 pass
Full suite, both modes - 129 binaries each, 0 failures

The regression guard drives 32 concurrent write_atomic calls to one path and asserts all succeed + no .partial leak. It fails deterministically (20/20) on the pre-fix code and passes post-fix, so it pins the property without depending on the flaky interleave.

CLAUDE.md note updated to record the real cause (intra-binary, write_atomic staging collision) and that LC-208 fixed it.

Scope

This is LC-208's fix, not a follow-up. The diagnosis landed on a small surgical production hardening (one function) plus one regression test, well within a single PR. No #[serial], no fixture rework, no separate production-bug ticket needed.

🤖 Generated with Claude Code

Investigated the long-standing `routes_uploads` flake (named in CLAUDE.md). Reproduced, diagnosed, fixed, validated against the reproduction recipe. ## Phase 1 - reproduction The flake is **intra-binary, not cross-binary**. Running the single `routes_uploads` binary in isolation: | Condition | Result | |---|---| | `--test-threads=8`, isolated binary, x40 | 4 fail / 40 (~10%) | | `--test-threads=1` (serialized), x40 | 0 fail / 40 | So concurrency *within one binary* is the trigger; no other binaries needed. The CLAUDE.md "concurrent-binary load" framing was imprecise. Every captured failure was a **500 Internal Server Error** where 200 was expected (a handler-side error), including `tiny_png` tests - not a test-side assertion mismatch. ## Phase 2 - root cause The tests share a **process-global** upload dir (`db::DATA_DIR` is a `OnceLock` set once per process) and reuse **identical fixture bytes**, so concurrent uploads of the same bytes derive the same content-addressed final path. The dedup check in `post_upload` (`metadata(final).is_err()`) is **TOCTOU**: both writers see "absent" and both call `write_atomic(final)`. `write_atomic` staged to a fixed `{final}.partial`, so both wrote the **same staging file** and both renamed it; whichever renamed second hit **ENOENT** after the first consumed the partial, and the upload returned 500. (The `tiny_png` failures rule out the `medium_png` `remove_file` path - the collision is the shared original write.) This is a **latent production race** in `write_atomic` (two users uploading identical bytes simultaneously would hit it - one 500s and retries), amplified by the harness. So `#[serial]` would be the wrong fix: it would hide a real bug behind serialization. ## Phase 3 - fix The fix addresses the confirmed mechanism: a **per-write unique staging name** (a uuid nonce), so concurrent writers of the same final path never share a staging file. Both renames target the same identical-content final path, which is safe (rename is atomic, last writer wins with the same bytes). Fixes every `write_atomic` caller (uploads, admin regen, email attachments, bridge avatar), not just the test path. ## Validation (same recipe used to find it) | | Pre-fix | Post-fix | |---|---|---| | `routes_uploads` isolated, `--test-threads=8`/`=16` | 4/40 (10%) | **0/160** | | Regression guard (`uploads_write_atomic_concurrent`) | **20/20 fail** | 20/20 pass | | Full suite, both modes | - | 129 binaries each, **0 failures** | The regression guard drives 32 concurrent `write_atomic` calls to one path and asserts all succeed + no `.partial` leak. It fails deterministically (20/20) on the pre-fix code and passes post-fix, so it pins the property without depending on the flaky interleave. CLAUDE.md note updated to record the real cause (intra-binary, `write_atomic` staging collision) and that LC-208 fixed it. ## Scope This is LC-208's fix, not a follow-up. The diagnosis landed on a small surgical production hardening (one function) plus one regression test, well within a single PR. No `#[serial]`, no fixture rework, no separate production-bug ticket needed. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
fix(uploads): unique write_atomic staging name to end routes_uploads flake (LC-208)
All checks were successful
check-secrets / Nosey parker (push) Successful in 3s
check-secrets / Kingfisher (push) Successful in 4s
check-secrets / TruffleHog (push) Successful in 5s
check-secrets / TruffleHog (pull_request) Successful in 4s
check-secrets / Nosey parker (pull_request) Successful in 5s
check-secrets / Kingfisher (pull_request) Successful in 5s
Create release / Create release from merged PR (pull_request) Has been skipped
Check / clippy + fmt + tests (pull_request) Successful in 2m14s
17c53a3b56
Investigated the long-standing routes_uploads flake (named in CLAUDE.md as "concurrent-binary load"). Reproduced, diagnosed, fixed, validated.

Reproduction: the flake is intra-binary, not cross-binary. Running the single routes_uploads binary in isolation at --test-threads=8 fired 4/40 (~10%); serialized at --test-threads=1 it passed 40/40. So concurrency within one binary is the trigger, and the CLAUDE.md "concurrent-binary load" framing was imprecise.

Root cause: every captured failure was a 500 (not a test-side assertion mismatch), including tiny_png tests, so the mechanism is the shared content-addressed original write, not the medium_png remove_file. The tests share a process-global upload dir (db::DATA_DIR is a OnceLock set once per process) and reuse identical fixture bytes, so concurrent uploads of the same bytes derive the same content-addressed final path. The dedup check in post_upload (metadata(final).is_err()) is TOCTOU: both writers see "absent" and both call write_atomic(final). write_atomic staged to a fixed {final}.partial, so both wrote the same staging file and both renamed it; whichever renamed second hit ENOENT after the first consumed the partial, and the upload returned 500.

This is a latent PRODUCTION race in write_atomic (two users uploading identical bytes at the same instant would hit it; one 500s and must retry), amplified by the test harness. So #[serial] would be the wrong fix - it would hide a real bug. The fix addresses the confirmed mechanism: a per-write unique staging name (a uuid nonce) so concurrent writers of the same final path never share a staging file. Both renames target the same identical-content final path, which is safe (rename is atomic, last writer wins with the same bytes). Fixes every write_atomic caller (uploads, admin regen, email attachments, bridge avatar), not just the test path.

Validation against the reproduction recipe: post-fix 0/160 at --test-threads=8 and =16 (vs 4/40 pre-fix). Added a deterministic regression guard (uploads_write_atomic_concurrent.rs) that drives 32 concurrent write_atomic calls to one path: it fails 20/20 on the pre-fix code and passes 20/20 post-fix, so it pins the property without depending on the flaky interleave. Full suite green both modes (standalone + saas, 129 binaries each, zero failures). Updated the CLAUDE.md note to record the real cause (intra-binary, write_atomic staging collision) and that LC-208 fixed it.

Scope: this is LC-208's fix, not a follow-up. The diagnosis landed on a small surgical production hardening (one function, unique staging name) plus one regression test, well within a single PR.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
longjacksonle deleted branch fix/lc-208-write-atomic-unique-staging 2026-05-30 04:02:16 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/lets-chat!274
No description provided.