Context
Flagged during review of #250 (per-participant advisory lock for concurrent same-participant scans). #250 fixes same-participant races; this is the remaining cross-participant facility-state hole. Pre-existing — not introduced by #250.
Problem
In scan-service.ts (the last-keyholder force-close path, ~line 118), an updateMany closes everyone's open visits when the keyholder checks out. But the scan route's advisory lock — pg_advisory_xact_lock(participant.id) — is keyed per participant.
A non-keyholder checking in concurrently holds a different lock key, so the two transactions don't contend. Sequence:
- Keyholder scan begins force-close (
updateMany closing all open visits).
- Non-keyholder scan (different lock key) creates a fresh open visit.
- Depending on interleave, the new visit can survive the force-close — the participant ends up checked into a just-closed facility.
Why it's not fixed by #250
#250's lock serializes only scans for the same participant. The force-close touches global facility state under a per-participant key, so cross-participant invariants remain unprotected.
Possible directions
- A facility-level advisory lock (fixed key) taken by the force-close path and by any check-in that depends on facility-open state, or
- A partial unique index / state check that rejects a check-in created against a facility being closed, or
- Re-validate facility-open state inside the check-in transaction after acquiring the lock.
Needs design — noting so the gap is tracked rather than assumed closed by #250.
Context
Flagged during review of #250 (per-participant advisory lock for concurrent same-participant scans). #250 fixes same-participant races; this is the remaining cross-participant facility-state hole. Pre-existing — not introduced by #250.
Problem
In scan-service.ts (the last-keyholder force-close path, ~line 118), an
updateManycloses everyone's open visits when the keyholder checks out. But the scan route's advisory lock —pg_advisory_xact_lock(participant.id)— is keyed per participant.A non-keyholder checking in concurrently holds a different lock key, so the two transactions don't contend. Sequence:
updateManyclosing all open visits).Why it's not fixed by #250
#250's lock serializes only scans for the same participant. The force-close touches global facility state under a per-participant key, so cross-participant invariants remain unprotected.
Possible directions
Needs design — noting so the gap is tracked rather than assumed closed by #250.