Coordination Locks

The abstraction

CoordinationLock is a pluggable trait for mutual exclusion across processes. It is the shared primitive for both fault-tolerance strategies:

The trait

#[async_trait]
pub trait CoordinationLock: Send + Sync {
    async fn acquire(&self, key: &str, lease: Duration)
        -> Result<Box<dyn LockGuard>, LockError>;
}

#[async_trait]
pub trait LockGuard: Send + Sync {
    async fn release(&mut self);
}

Backends

FileLock (POSIX flock) — feature file-lock

malkuth = { features = ["file-lock"] }
use malkuth::lock::FileLock;
use malkuth::CoordinationLock;
use std::time::Duration;

let lock = FileLock::new("/var/lib/myapp/locks");
let mut guard = lock.acquire("write-queue", Duration::from_secs(30)).await?;
// ... exclusive work ...
guard.release().await; // or just drop guard

One lock file per key, created under the root directory. Uses flock(LOCK_EX | LOCK_NB) for non-blocking exclusive locks. If another process holds the lock, returns LockError::Contended.

Unix-only. FileLock uses POSIX flock and is only available on Unix targets (Linux, macOS, BSD).

LeaseLock (file lease with TTL) — feature lease

malkuth = { features = ["lease"] }
use malkuth::lease::LeaseLock;
use malkuth::CoordinationLock;

let lock = LeaseLock::new("/var/lib/myapp/leases");
let mut guard = lock.acquire("device-leader", Duration::from_secs(10)).await?;
// The lease auto-renews in the background. If the process crashes,
// the lease expires after the TTL and another process can acquire it.
guard.release().await;

Uses atomic temp-file rename (CAS) to claim the lease. A background thread renews it at TTL/3 intervals. On crash, the lease file's expires_at_ms timestamp allows the next acquirer to take over.

PgLock (PostgreSQL advisory lock) — feature pg-lock

malkuth = { features = ["pg-lock"] }
use malkuth::pg_lock::PgLock;
use malkuth::CoordinationLock;
use tokio_postgres::NoTls;

let (client, connection) = tokio_postgres::connect("host=localhost dbname=myapp", NoTls).await?;
let lock = PgLock::new(std::sync::Arc::new(client));
let mut guard = lock.acquire("shared-config", Duration::from_secs(30)).await?;
guard.release().await;

Uses session-level pg_try_advisory_lock / pg_advisory_unlock on a bigint key derived from the key string via FNV-1a hash.

When to use which

ScenarioBackend
Single host, lock holder won't crashFileLock
Single host, lock holder might crashLeaseLock
Multiple hosts, shared PostgresPgLock
Multiple hosts, no shared DBExternal (etcd, Consul)