Relationships
#3148 A lock file read during its holder's heartbeat rewrite is treated as no lock
Opened by zack · 10/7/2026· Shipped 10/7/2026
Found while planning swamp-club#3111. Traced by reading the code; not reproduced.
What happens
FileLock's heartbeat (extend() in src/infrastructure/persistence/file_lock.ts) rewrites the lock file in place with Deno.writeTextFile every ttl/3. That call truncates the file and then writes it, so another process can read it empty or partly written. Two readers treat such a file as no lock at all:
- The structural drain. The scan in waitForPerModelLocks (src/cli/repo_context.ts) wraps the read and JSON.parse of each per-model lock file in a try whose catch is commented Skip unreadable lock files. A lock caught mid-rewrite is not counted as held, so a structural command such as swamp data gc can finish its drain and start work while a writer still holds that lock.
- inspect(). FileLock.readLockFile returns null on any read or parse error, and inspect() reports that as no lock. acquireModelLocks inspects the global lock this way, both before taking per-model locks and in its recheck after, so a writer can see a held global lock as free and write while a structural command is working.
FileLock's own acquire path already handles this: it treats a fresh unreadable file as held (a holder is mid-write) and backs off. The two readers above do not.
Impact
A narrow window per heartbeat (a truncate and a write of about 200 bytes), hit only if a reader's read lands inside it, but the result is the mutual exclusion between structural commands and writers being skipped silently.
Possible directions
- Write the heartbeat through the existing atomic write in src/infrastructure/persistence/atomic_write.ts (temp file and rename), so a reader always sees a whole file.
- Have the drain scan and inspect() treat a present but unreadable lock file as held, as the acquire path does.
The fix for swamp-club#3111 adds new lock-file writes and makes those atomic, but leaves the heartbeat as it is.
Shipped
Click a lifecycle step above to view its details.
Sign in to post a ripple.