ADR-0002: Atomic File Locking and Optimistic Concurrency Control
Status
Accepted
Context
Obsidian vaults are local directories modified by users, sync services (Obsidian Sync, iCloud, Dropbox, OneDrive), and automated CLI tools concurrently. We must prevent data loss, partial writes, and lost updates without corrupting user notes or locking the user out indefinitely.
Decision
We implemented a two-tier synchronization mechanism:
- Cross-Process Mutex via Atomic Lock Directories:
- Uses
fs.mkdirSyncon a target-hashed directory (.palee/locks/<hash>.lockdir) to eliminate TOCTOU (Time-of-Check-to-Time-of-Use) races. - Writes a session-specific JSON descriptor holding PID, hostname, and timestamp.
- Background heartbeat refreshes lock file
mtimeevery 15 seconds. - Automatic stale recovery reclaims abandoned locks after 60s on Windows or 120s on other OSes using quarantine renaming.
- Uses
- Optimistic Concurrency Control (OCC) & Atomic Overwrites:
- Computes SHA-256 fingerprint of target files before modification.
- Flushes writes to temporary files using
fsyncSyncbefore atomicrenameSync. - On Windows
EPERM/EBUSYlock collisions, retries with exponential backoff and randomized jitter.
Consequences
- Positive:
- Zero partial-write corruption even on sudden crash or process termination.
- Robust cross-platform concurrency on both POSIX and Windows filesystem semantics.
- Detects concurrent modifications between fingerprint read and atomic write via SHA-256 OCC validation.
- Negative / Tradeoffs:
- Requires momentary temporary files during writes.
fs.renameSyncwill atomically replace the target file; an external edit occurring after the fingerprint check could be replaced unless cooperating through PALEE locks or filesystem-level compare-and-replace.
Alternatives Considered
POSIX
flock/fcntl& WindowsLockFileEx(via native addons):- Description: Kernel-level advisory or mandatory file descriptor locks via native C++ bindings (e.g.
fs-ext). - Pros: Direct OS-enforced mutual exclusion without temporary lock directories.
- Why Rejected: Violates the strict Zero Native Binaries / pure JavaScript invariant across Linux, macOS, and Windows. Native addons break portable
npm installworkflows across cross-platform environments.
- Description: Kernel-level advisory or mandatory file descriptor locks via native C++ bindings (e.g.
Off-the-Shelf Locking Packages (
proper-lockfile):- Description: Third-party npm package for filesystem locking.
- Pros: Pre-packaged open-source utility.
- Why Rejected: Introduces additional supply-chain attack surface and lacks PALEE's specific stale-lock quarantine recovery mechanics and platform-tuned timeout horizons (60s Windows vs 120s POSIX).
Process-Local / In-Memory Async Mutex Queue:
- Description: Node.js in-memory mutex to synchronize concurrent write promises.
- Pros: Zero filesystem I/O overhead.
- Why Rejected: Completely ineffective across independent CLI process invocations, concurrent terminal windows, background scripts, or sync engines.
Single Global SQLite Database with WAL (Write-Ahead Logging):
- Description: Storing all topic metadata, locks, and logs in
.palee/state.db. - Pros: Built-in ACID transactions and concurrency control.
- Why Rejected: Violates the Markdown-First / Vault-First architecture where Markdown frontmatter is the single source of truth; SQLite creates state divergence and synchronization conflicts with Obsidian git/cloud sync.
- Description: Storing all topic metadata, locks, and logs in
