← stitchkitDocumentation

ADR 0265 — A lock this process holds is a descriptor it holds

Invariants: I8, I13. Amends ADR 0210.

Context

ADR 0210 identifies a lock owner by machine, boot, PID namespace and process start: a record whose identity still matches a live process is held. That identity describes a process, not the program running in it. execve replaces the program and keeps the PID, the namespace and the kernel start time, and bun --watch restarts an application exactly that way on every source change. A restart in the middle of an update leaves the lock the earlier image held, the new image of the same PID finds a record that matches itself in every field, and the lock stays held until the PID changes. On a watched development server this was measured as 127 images under one PID and 2 202 LOCK_TIMEOUT refusals of a state store over three and a half hours, each one reading liveness alive, identity matched.

A token generated when the module loads would tell two images apart, and it would also tell apart things that share one hold: a worker thread has its own module state, and a second copy of the package bundled into the same process has its own. Each of them would read a live holder’s record as a previous image and take the lock from under it.

Decision

A holder keeps the descriptor it created the lock with open until it releases (it has since 0.94; every record that carries a process identity was written that way). So a record that names this very process with matching identity is held only while some descriptor of this process is open on the lock file. The descriptor table belongs to the process — workers and module copies share it — and close-on-exec descriptors, which is how Node and Bun open files, do not survive an image replacement.

When the probe finds no such descriptor, the owner is gone (identity: descriptor-closed) and the lock is reclaimed under the same guard as a dead PID. The probe proves its own listing: it opens the lock file and counts the answer only when /dev/fd lists that fresh descriptor, so a platform that lists a fixed few descriptors answers “unknown”, never “nobody holds it”. A record that names another process is judged exactly as before.

Consequences

A restarted image takes over the lock its predecessor left without waiting, and a hold taken by any thread or module copy of the same process is never displaced (tests/exclusive-lock-process-image.test.ts, which also restarts a real bun --watch process). A simulated owner record of this process with nothing holding it is an abandoned lock, so a test that needs a live owner in this process holds the lock rather than writing its record. An image of another process that was replaced in place is still judged alive: this process cannot see another process’s descriptors without privileges it does not have on every platform.