Skip to main content

Module locking

Module locking 

Source
Expand description

Locking capability trait. See plan/ecosystem/02-capabilities.md §Locking.

A LockingPlugin provides distributed mutual exclusion keyed on a string. The canonical use case is “ensure this scheduled job fires once across the cluster, not once per instance” — hence the E2 pairing with the Scheduled retrofit — but the same shape is useful for leader election, exactly-once webhook delivery, and coordinating expensive cache rebuilds.

§Design notes

  • Contention is not failure. try_lock returns Ok(None) when the lock is held by someone else and Err(..) when the backend itself is broken (Redis unreachable, Postgres connection dropped). Callers almost always want to branch on that distinction — retry a moment later on contention, alarm an operator on backend failure — so encoding it in the type beats a single LockError enum with a Held variant. This is the load-bearing shape choice and is the reason the trait does not use a typed error enum.
  • Everything else is Result<_, String>. renew and release cannot meaningfully distinguish “lock expired out from under us” from “backend failure” for a caller: in both cases the correct response is “log and move on, you lost the lock.” Folding them into Err(String) keeps the trait lean and WASM-ABI friendly (matches the convention established by crate::scheduled::ScheduledPlugin and crate::lifecycle::LifecyclePlugin).
  • Opaque LockHandle. The handle carries a lock_id (a random token minted by the plugin when the lock was acquired) plus the key it was taken on. Backends use the token to implement CAS-style release — Redlock SETs with NX PX and a random value, then the release script checks the value before DEL — so a renew or release from a stale caller cannot stomp on a newer lock holder. The token shape is a String so WASM guests can marshal it through the JSON ABI unchanged.
  • Sync trait. Matches the convention in crate::scheduled::ScheduledPlugin and crate::session::SessionPlugin. Backends that need an async client drive their own runtime inside the call (same approach as bext-session-redis and bext-tracer-otlp).
  • No vendor leaks. The trait has no redis_url, no advisory_lock_key, no ttl_ms_override_for_etcd. Configuration lives on the concrete plugin’s constructor; the trait is one shape across memory / Redis / Postgres / etcd.

§Backends

Three reference backends ship alongside this trait in crates/bext-impls/:

  • bext-locking-memory — single-node fallback, Mutex<HashMap>.
  • bext-locking-redis — Redlock-style SET NX PX + Lua CAS release.
  • bext-locking-pg — Postgres pg_try_advisory_lock(hashtext(key)).

§Use by the Scheduled capability

When a crate::scheduled::ScheduledPlugin declares a LockingHint::RequireGlobal schedule, the host-owned scheduler in bext-core::scheduler acquires a LockingPlugin lock keyed on the schedule id before invoking ScheduledPlugin::run. On contention (Ok(None)) the scheduler skips this tick — another node is running it. On backend failure (Err(..)) the scheduler logs and skips, matching the existing “lost the lock” semantics.

Modules§

fuel
Fuel budgets for WASM locking plugin calls. Matches the convention in crate::scheduled::fuel.

Structs§

LockHandle
Handle returned by a successful LockingPlugin::try_lock.

Traits§

LockingPlugin
A plugin that provides distributed mutual exclusion.