How the Daemon Singleton Pattern Prevents Multiple Instances in no-mistakes
The no-mistakes daemon implements a singleton guard using an exclusive OS-level file lock on <NM_HOME>/daemon.lock, acquired before any initialization work and held until process termination to ensure only one live daemon owns a given home directory.
The kunchenguid/no-mistakes repository protects against concurrent daemon corruption through a robust daemon singleton pattern that leverages kernel-enforced file locking. This mechanism guarantees that only one instance can execute destructive startup operations like stale-run recovery or worktree cleanup within a specific NM_HOME directory, rejecting any subsequent start attempts with diagnostic error information.
OS-Level File Locking in internal/daemon/lock.go
The core protection lives in internal/daemon/lock.go, specifically within the acquireSingletonLock function. This method attempts to create and exclusively lock a file at <NM_HOME>/daemon.lock using platform-specific helpers (tryLockFile and unlockFile).
The lock is exclusive and non-blocking. If another process already holds the lock, the kernel immediately returns a busy status, causing acquireSingletonLock to return ErrSingletonLockHeld. Because this is an OS-level advisory lock, the kernel automatically releases it if the owning process exits, crashes, or receives a SIGKILL—eliminating the need for heartbeat checks or PID files.
// Acquire the singleton lock – fails if another daemon is already running.
lock, err := acquireSingletonLock(p)
if err != nil {
// ErrSingletonLockHeld is returned when another daemon holds the lock.
return err
}
defer lock.Release() // Guarantees the lock is released on shutdown.
Guard Placement and Early Acquisition
The daemon enforces the singleton pattern at the earliest possible moment. In internal/daemon/daemon.go, the RunWithOptions function calls acquireSingletonLock immediately upon startup (around lines 19–28), before any destructive work like orphan worktree cleanup or IPC socket binding occurs.
This placement ensures that a racing second instance aborts cleanly before it can corrupt shared state. The code defers lock.Release() right after acquisition, leveraging Go's defer stack to guarantee cleanup even if subsequent initialization fails.
// Inside RunWithOptions – the lock is taken before any other initialization.
func RunWithOptions(p *paths.Paths, d *db.DB, stepFactory StepFactory) error {
// Singleton guard: only one live daemon may own this NM_HOME at a time.
lock, err := acquireSingletonLock(p)
if err != nil {
return err
}
defer lock.Release()
// ... rest of daemon startup …
}
Diagnostic Records and Error Handling
When the lock is successfully acquired, the daemon writes a human-readable JSON diagnostic record to the lock file. This record, defined as lockHolderRecord, contains the process PID and UTC start time (lines 53–62 of lock.go). While the actual safety mechanism relies on the OS lock, this metadata helps operators identify which process currently holds the lock.
If acquisition fails, the returned error wraps the holder's PID and start time, allowing client code to surface precise information:
// lock.go – writes a diagnostic JSON record after the lock is taken.
record := lockHolderRecord{PID: os.Getpid(), StartedAt: time.Now().UTC()}
if data, err := json.Marshal(record); err == nil {
_ = f.Truncate(0)
_, _ = f.WriteAt(data, 0)
_ = f.Sync()
}
Callers in daemon.go (lines 28–32) handle this by returning the error directly, preventing the second daemon from proceeding and informing the user exactly which instance is blocking startup.
Automatic Lock Release and Cleanup Safety
The Release method in internal/daemon/lock.go unlocks the file descriptor and closes the underlying file. It is implemented to be nil-safe—calling Release() on a nil lock pointer returns without error. This design pattern allows the code to defer lock.Release() immediately after the acquisition attempt, even when lock might be nil due to an error.
The test suite in internal/daemon/lock_test.go (lines 74–80) validates this nil-safety behavior, ensuring that cleanup logic never panics. Combined with the kernel's automatic lock release on process termination, this guarantees that the singleton protection is both robust against crashes and safe to maintain.
Summary
- OS-level exclusive lock: The
acquireSingletonLockfunction ininternal/daemon/lock.gocreates an exclusive, non-blocking lock on<NM_HOME>/daemon.lockusing kernel file locking primitives. - Early enforcement:
RunWithOptionsininternal/daemon/daemon.goacquires the lock before any destructive startup work, preventing race conditions in run metadata or worktree management. - Diagnostic clarity: Failed acquisitions return
ErrSingletonLockHeldwrapped with the blocking process's PID and start time, enabling precise troubleshooting. - Crash-safe release: The kernel automatically releases the lock when the process exits or is killed, requiring no separate cleanup daemon or heartbeat mechanism.
- Defensive programming: The
Releasemethod is nil-safe and deferred immediately after acquisition, preventing resource leaks even when startup fails.
Frequently Asked Questions
What happens to the lock if the no-mistakes daemon crashes unexpectedly?
The kernel automatically releases the OS-level file lock when the owning process terminates, regardless of whether it exits cleanly, crashes, or is killed via SIGKILL. This happens without requiring any cleanup code to run, ensuring that a new daemon can start immediately after the previous one disappears, as implemented in internal/daemon/lock.go.
How can I identify which process is blocking a new daemon startup?
When acquireSingletonLock detects an existing lock, it returns ErrSingletonLockHeld wrapped with a JSON diagnostic record containing the holder's PID and start time. This information is written to the lock file at <NM_HOME>/daemon.lock and surfaced in the error returned by RunWithOptions in internal/daemon/daemon.go.
Where is the singleton lock file stored?
The lock file is created at <NM_HOME>/daemon.lock within the configured no-mistakes home directory. The path is derived from the *paths.Paths configuration passed to acquireSingletonLock in internal/daemon/lock.go.
Is it safe to call the lock release method if acquisition fails?
Yes. The Release method is explicitly designed to be safe when called on a nil receiver, as validated in internal/daemon/lock_test.go (lines 74–80). This allows the code to defer lock.Release() immediately after the acquisition attempt without checking for nil, preventing resource leaks in error paths.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →