Skip to content

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:

  1. Resolves the absolute path of the target file using fs.realpathSync src/storage/lock.ts#32-47.
  2. Calculates a SHA-256 hash of the path relative to the vault root src/storage/lock.ts#49-50.
  3. Creates a lock directory named {hash}.lockdir src/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

  1. Directory Creation: Attempt to create {hash}.lockdir.
  2. 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.
  3. 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.ts operates purely at the storage layer and throws an error with code = 'ECONFLICT'. Upstream CLI command handlers (such as src/cli/review.ts and src/cli/adopt.ts) catch this error and set process.exitCode = 4 to 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:

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

  1. Rename: The process renames the stale .json file to .json.quarantine. This acts as an atomic test-and-set src/storage/lock.ts#190-194.
  2. Verify: It checks the mtime of the quarantined file. If the mtime was 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.
  3. Cleanup: If verified stale, the quarantined file is unlinked, and the process attempts to rmdirSync the lock directory src/storage/lock.ts#197-206.

Stale Recovery Logic

StepActionFailure Handling
1readdirSync(lockDir)If ENOENT, someone else cleaned it; retry loop src/storage/lock.ts#135
2Filter activeFilesIf no JSON files but directory is < 5s old, throw ECONFLICT (incoming process) src/storage/lock.ts#165-179
3renameSync to .quarantineIf it fails, the file was already moved/deleted; continue src/storage/lock.ts#194
4rmdirSync(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.

typescript
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

FunctionRoleKey Logic
getLockDirPath resolutionResolves realpathSync to handle symlinks and computes SHA-256 hash src/storage/lock.ts#32-54
createLockAtomic acquisitionThe main loop using mkdirSync and stale recovery logic src/storage/lock.ts#99-220
updateHeartbeatLivenessUses fs.utimesSync to touch the session file src/storage/lock.ts#187-197
releaseLockCleanupDeletes the session file and attempts to remove the directory src/storage/lock.ts#282-315

Released under the MIT License.