Expand description
Atomic locks — LockManager, Lock and LockGuard.
Named locks over the cache, and the machinery behind the scheduler’s
without_overlapping() and on_one_server().
let locks = LockManager::new(Arc::new(MemoryCache::new()));
let outcome = locks
.lock("reports:nightly", Duration::from_secs(600))
.run(async {
// …exactly one caller reaches here.
Ok::<_, rainier_support::Error>("built")
})
.await?;
assert_eq!(outcome, Some("built"));§What makes it a lock
Three things, and leaving out any one of them produces something that looks like a lock and is not.
An atomic acquire. Cache::add is “store only if absent”, decided by
the store rather than by the caller. A has followed by a put lets two
processes both observe the key absent and both write.
An owner token. Every acquire mints a random one and stores it as the
value. Release is Cache::forget_if, not forget — see below.
A TTL. The holder can die. A lock with no expiry, taken by a process
that is then kill -9ed, is a task that never runs again until somebody
notices and deletes a key by hand.
§Why release compares the token
The bug this avoids is worth spelling out, because it is invisible until it is a production incident:
t=0 A acquires `nightly`, ttl 60s A holds it
t=0 A begins work
t=61 A is still stalled — GC, a slow query, a suspended VM
t=61 the key expires nobody holds it
t=62 B acquires `nightly` B holds it
t=63 A finishes and releases
- with `forget`: A deletes B's lock. C can now acquire while
B is still running. Two copies.
- with `forget_if`: A's token no longer matches. Nothing happens.forget_if cannot make A’s overrun safe — B is already running — but it
stops one overrun becoming an unbounded number.
§The guarantee, stated honestly
This is a lease, not a mutex. If a holder overruns its TTL, another process will take the lock and both will run. No lock built on a TTL can prevent that, including Redlock; the standard advice applies:
- Set the TTL comfortably longer than the work takes.
extendit from long-running work, so the TTL tracks progress rather than a guess made at the start.- Where correctness genuinely cannot tolerate two runs, make the work idempotent or fence it with a token the downstream system checks. A lock is a coordination hint, not a correctness proof.
And the obvious one: a lock in a MemoryCache is
per-process, so on_one_server across three servers holding three memory
caches is three servers all believing they are the one.
LockManager::is_shared answers that question.
Structs§
- Lock
- A named lock, not yet held.
- Lock
Guard - A held lock.
- Lock
Manager - Mints
Locks against a cache.
Functions§
- contended
- The error a caller gets when a lock is required rather than optional.