What happens when two programs write to the same file at the same time? A DB engine has a lock manager to prevent exactly that. Quipu-Log has no DB engine — and therefore no lock manager. Instead, a single OS file lock guarantees "this directory belongs to exactly one process." And that simple guarantee turns out to make tamper detection simpler too.
Quipu-Log places a LOCK file in the root directory and calls File::try_lock() on it. A second process that tries to open the same root fails immediately — single-writer is guaranteed.
First, what you already know: the DB's lock manager
A relational DB has a sophisticated lock manager that handles row-level, page-level, and table-level locks. It arbitrates conflicts when multiple transactions touch the same data, detects deadlocks, and manages wait queues. It's complex machinery — but it's what allows many concurrent writes to proceed safely.
Quipu-Log implements none of this. It chose a much simpler principle instead: "only one writer at a time."
In a DB, a row-level lock manager mediates between multiple concurrent write sessions. In Quipu-Log, a single OS file lock enforces "one process per root directory." Rather than arbitrating between two writers, it prevents a second writer from opening at all.
Advisory lock: the key the OS hands you
The OS provides a system call for placing an advisory lock on a file. "Advisory" means the OS doesn't enforce it mechanically — a process that opens the file directly without checking the lock won't be blocked by the OS. What the lock does is detect conflicts between processes that both try to acquire it. Think of it as a gentleman's agreement among cooperating processes.
Since Rust 1.89, std::fs::File ships try_lock() and try_lock_shared() as stable APIs. File locking now works with nothing but the standard library — no external crates needed.
Cargo.toml (workspace)# MSRV floor: the store guards its root with `std::fs::File::try_lock` and
# matches on `std::fs::TryLockError`, both stabilized in Rust 1.89.
rust-version = "1.89"
That's the reason the MSRV is 1.89: to use try_lock.
The root lock in code
The very first thing AuditStore::open() does is acquire the lock on the LOCK file.
crates/quipu-core/src/store.rs — AuditStore::open()let lock = std::fs::OpenOptions::new()
.create(true)
.truncate(false)
.write(true)
.open(cfg.root.join("LOCK"))?;
lock.try_lock().map_err(|e| match e {
std::fs::TryLockError::WouldBlock =>
Error::Locked(cfg.root.display().to_string()),
std::fs::TryLockError::Error(e) => Error::Io(e),
})?;
On success the file handle is stored in the _lock: std::fs::File field and held for as long as AuditStore is alive. If a second process (or thread) calls AuditStore::open() on the same root, try_lock() returns WouldBlock, which the code converts to Error::Locked — meaning "another process is already writing to this root."
The lock releases automatically when the process exits or AuditStore is dropped. Rust's Drop guarantee means no explicit "release lock" code is ever needed.
try_lock() returns WouldBlock and fails immediately. It does not wait.Why single-writer — complexity vs. simplicity
What would it actually take to allow multiple processes to write at the same time? You'd need per-segment locks so writes don't overlap at the file level; shared memory or message passing because multiple processes would all need access to the in-memory index; and extra coordination to stop separate BufWriter instances from interleaving their buffers. In other words, you'd be importing the complexity of a distributed system.
Quipu-Log doesn't want that complexity. Instead it adopts a pipeline model: multiple threads or requests emit events, those events flow through a channel to a single dedicated writer thread, and the channel serializes the writes. Multiple threads never touch the file simultaneously. Ch. 29, async pipeline
A single writer cuts down on debt. One file lock blocks two processes from opening at the same time; a channel blocks two threads from writing at the same time. No lock manager, no deadlock detection, no conflict resolution. This model fits an audit log perfectly — events flow in one direction.
Single-writer makes tamper detection simpler
single-writer isn't only about performance and simplicity. It matters for security too.
Quipu-Log commits every record to a Merkle tree Ch. 20, Merkle tree. The tree records the promise "record N has this hash." If that promise is broken — someone secretly edits a record on disk — the Merkle root changes and the tampering is detected.
Now imagine multiple writers. If several processes append simultaneously, the order in which leaves are added to the Merkle tree becomes ambiguous. There's no clear answer to "which process is responsible for leaf N?" Verification becomes hard. With a single writer guaranteed, "the Nth append = the Nth leaf" is unambiguous. Verification logic stays simple and ordering is straightforward.
An advisory lock only works among cooperating processes. An attacker with direct access to the root directory (a mounted disk, root privileges) can ignore the LOCK file and edit the files directly. But such an attacker could touch the filesystem itself anyway, so the lock's job is to prevent accidental or buggy concurrent opens, not to stop a malicious intruder. Detecting malicious tampering is the Merkle tree's job (Part 5).
Recap
- Instead of a DB lock manager, Quipu-Log uses a single OS advisory lock (
File::try_lock()) to enforce "one process per root directory." try_lock()does not block. If the lock is already held it returnsWouldBlockimmediately, which is converted toError::Locked.File::try_lock()was stabilized in Rust 1.89 — that's why the MSRV is 1.89.- single-writer keeps the Merkle leaf order unambiguous, which keeps tamper-detection logic simple.
- An advisory lock is a promise among cooperating processes. Defending against malicious intruders is the Merkle tree's responsibility.
① What's the difference between try_lock() and a blocking lock()? Why do you think Quipu-Log uses the non-blocking version?
② Explain why the single-writer principle simplifies Merkle tree verification.
③ If two processes were allowed to open the same root simultaneously, what problem would arise for the in-memory index?