Skip to main content

Module lock

Module lock 

Source
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.
  • extend it 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.
LockGuard
A held lock.
LockManager
Mints Locks against a cache.

Functions§

contended
The error a caller gets when a lock is required rather than optional.