Remediates review findings on the eviction fence:
- The EXISTS/NOT EXISTS fences read independent snapshots under READ
COMMITTED and take no conflicting locks, so a concurrent evict/link
pair can both commit (reproduced empirically in both interleavings),
leaving a ready download pointing at a deleted artifact. New
downloads_artifact_id_fkey (ON DELETE RESTRICT) is the enforcement
layer: RI triggers take FOR KEY SHARE and re-check on a fresh snapshot.
The fences stay as fast-path filters; 23503 maps to ErrArtifactEvicted
on insert/replace and reads as 'not evictable' on delete. Terminal
transitions null artifact_id so dead rows cannot pin artifacts; the
migration heals legacy dangling/terminal links before the constraint.
- Row-first eviction could leak bytes when the unlink failed after the
row was gone (no local orphan queue, no reaper; remote leaked when the
node delete and orphan enqueue both failed). Cleanup intents are now
recorded durably BEFORE the row delete — a new
download_artifact_local_orphans queue mirrors the remote one — and a
failed enqueue skips the candidate so the row survives for retry.
- Output paths now include the artifact id, so a replacement row can
never claim an evicted row's path; the OutputPathInUse probe (an
unindexed seq scan with a TOCTOU window) is gone.
- The failed-artifact hygiene sweep is fenced (DeleteIfEvictable):
Ensure requeues failed rows, so the unfenced delete could remove an
artifact a fresh download had just linked, stranding it in preparing.
- ReplaceManagedEntry drops the non-atomic artifact probe; zero rows now
unambiguously means revision/identity conflict and self-heals via
GetManagedEntry. The replace retry refreshes its row snapshot so the
second attempt's revision fence can succeed.
- Reuse/cleanups: shared withEnsuredArtifact retry helper, downloadColumns
interpolated into all insert sites, execInsertDownload shared by
Create/CreateBatch, EnsureDevice hoisted out of the retry loop,
HasActiveLink and unconditional DeleteArtifact deleted, distinct log
messages for the two sweep failure paths.
DB-backed tests cover the FK both directions, the requeued-artifact sweep
fence, local orphan round-trip, terminal unpinning, revision-conflict
self-healing, and a 100-round concurrent evict-vs-link race asserting no
dangling links.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>