Split out of #171 so it gets its own review rather than riding under an exactly-once fix.
Engine::submit_event takes an event claim, then calls spawn_claimed_run. On the Err path the claim is handed back with release_claim. On a panic it is not: the unwind passes the match without releasing, and because a same-boot claim is treated as in-flight (not wreckage) by design, every retry inside that process is told "duplicate" with no run to carry the event. It stays stranded for the life of the boot — no TTL, no reaper.
A restart clears it: the claim then belongs to a previous boot, and claim_event repairs it. So the blast radius is one process lifetime, not permanent data loss.
Fix shape: a Drop guard around the claim, disarmed once register succeeds. Deliberately not bundled into #171 because a Drop that opens a database and cannot report its own failure is a mechanism with its own failure modes, and it would have shipped unreviewed under a change about something else.
Found by the local maintainability lens on #171.
Split out of #171 so it gets its own review rather than riding under an exactly-once fix.
Engine::submit_eventtakes an event claim, then callsspawn_claimed_run. On theErrpath the claim is handed back withrelease_claim. On a panic it is not: the unwind passes thematchwithout releasing, and because a same-boot claim is treated as in-flight (not wreckage) by design, every retry inside that process is told "duplicate" with no run to carry the event. It stays stranded for the life of the boot — no TTL, no reaper.A restart clears it: the claim then belongs to a previous boot, and
claim_eventrepairs it. So the blast radius is one process lifetime, not permanent data loss.Fix shape: a
Dropguard around the claim, disarmed onceregistersucceeds. Deliberately not bundled into #171 because aDropthat opens a database and cannot report its own failure is a mechanism with its own failure modes, and it would have shipped unreviewed under a change about something else.Found by the local maintainability lens on #171.