File Locking
Relevant Source Files
The palee CLI implements a robust file locking mechanism to prevent race conditions during concurrent vault access. It utilizes atomic directory creation and a heartbeat system to ensure that only one process can modify a specific resource at a time, while providing mechanisms to recover from crashed processes.
Overview
Locking is handled by the Lock class in src/storage/lock.ts. It manages the lifecycle of a lock, from acquisition and heartbeat maintenance to release and stale lock recovery. The system is designed to be platform-aware, adjusting timeouts based on the underlying operating system's filesystem behavior.
Lock Identity and Path Hashing
Locks are not stored alongside the target files but in a centralized directory: .palee/locks/. To ensure that the same file always maps to the same lock directory regardless of relative pathing or symlinks, the system:
- Resolves the absolute path of the target file using
fs.realpathSyncsrc/storage/lock.ts#32-47. - Calculates a SHA-256 hash of the path relative to the vault root src/storage/lock.ts#49-50.
- Creates a lock directory named
{hash}.lockdirsrc/storage/lock.ts#53.
Lock Acquisition Protocol
Acquisition uses fs.mkdirSync as an atomic primitive. Because directory creation is atomic at the OS level, it serves as a "test-and-set" operation src/storage/lock.ts#92-128.
Acquisition Flow
- Directory Creation: Attempt to create
{hash}.lockdir. - Success: Write a session-specific JSON file (e.g.,
L-20231027T103000-abcd.json) containing the PID, hostname, and timestamp src/storage/lock.ts#99-125. - Failure (EEXIST): If the directory exists, the process inspects the contents for stale locks src/storage/lock.ts#126-179.
Note on Exit Codes:
src/storage/lock.tsoperates purely at the storage layer and throws an error withcode = 'ECONFLICT'. Upstream CLI command handlers (such as src/cli/review.ts and src/cli/adopt.ts) catch this error and setprocess.exitCode = 4to signal concurrency contention to callers.
Heartbeat and Timeouts
To prevent a process from holding a lock indefinitely after a crash, the owner must "check in" periodically.
Heartbeat Mechanism
Once a lock is acquired, the Lock class starts a timer that calls updateHeartbeat every 15 seconds (HEARTBEAT_INTERVAL) src/storage/lock.ts#13. This function uses fs.utimesSync to update the access and modification times of the session JSON file without rewriting the content src/storage/lock.ts#187-197.
Platform-Specific Stale Timeouts
The system accounts for different filesystem latencies and clock skews by varying the stale threshold:
- Windows: 60 seconds src/storage/lock.ts#14
- Other (POSIX): 120 seconds src/storage/lock.ts#15
A lock is considered stale if Date.now() - mtime > STALE_TIMEOUT src/storage/lock.ts#80-89.
Stale Lock Recovery
When a process encounters an existing lock directory, it attempts to recover it if the lock is stale. This process uses a Quarantine-Rename strategy to avoid race conditions between two processes trying to clean up the same stale lock.
The Quarantine Pattern
- Rename: The process renames the stale
.jsonfile to.json.quarantine. This acts as an atomic test-and-set src/storage/lock.ts#190-194. - Verify: It checks the
mtimeof the quarantined file. If themtimewas updated just before the rename, it means the original owner is actually still alive; the process restores the file and aborts recovery src/storage/lock.ts#196-201. - Cleanup: If verified stale, the quarantined file is unlinked, and the process attempts to
rmdirSyncthe lock directory src/storage/lock.ts#197-206.
Stale Recovery Logic
| Step | Action | Failure Handling |
|---|---|---|
| 1 | readdirSync(lockDir) | If ENOENT, someone else cleaned it; retry loop src/storage/lock.ts#135 |
| 2 | Filter activeFiles | If no JSON files but directory is < 5s old, throw ECONFLICT (incoming process) src/storage/lock.ts#165-179 |
| 3 | renameSync to .quarantine | If it fails, the file was already moved/deleted; continue src/storage/lock.ts#194 |
| 4 | rmdirSync(lockDir) | If ENOTEMPTY, a new process won the lock; retry loop src/storage/lock.ts#208-212 |
Data Structures
LockData
The metadata stored within the session JSON file.
interface LockData {
lock_id: string; // Format: L-YYYYMMDDTHHMMSS-xxxx
target: string; // Absolute path of the locked file
pid: number; // Process ID of the owner
hostname: string; // Hostname for multi-machine vault sync safety
created_at: string; // ISO timestamp
}Summary of Key Functions
| Function | Role | Key Logic |
|---|---|---|
getLockDir | Path resolution | Resolves realpathSync to handle symlinks and computes SHA-256 hash src/storage/lock.ts#32-54 |
createLock | Atomic acquisition | The main loop using mkdirSync and stale recovery logic src/storage/lock.ts#99-220 |
updateHeartbeat | Liveness | Uses fs.utimesSync to touch the session file src/storage/lock.ts#187-197 |
releaseLock | Cleanup | Deletes the session file and attempts to remove the directory src/storage/lock.ts#282-315 |
