1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
//! 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`](crate::scheduled::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.
//!
//! [`LockingHint::RequireGlobal`]: crate::scheduled::LockingHint::RequireGlobal
use ;
/// Handle returned by a successful [`LockingPlugin::try_lock`].
///
/// The handle is **opaque** to callers — they pass it back to
/// [`LockingPlugin::renew`] and [`LockingPlugin::release`] unchanged. The
/// plugin uses the embedded `lock_id` token to detect stale operations
/// (see module docs).
/// A plugin that provides distributed mutual exclusion.
///
/// **Compile-time and WASM execution.** All methods use JSON-friendly
/// POD types so the trait is ABI-compatible with WASM guest plugins
/// (matches the convention used by the other capability traits in this
/// crate). Backends that need a real async client run their own runtime
/// inside the call.
///
/// Concurrency: implementations MUST be safe to call from multiple
/// threads simultaneously. The host may contend on the same key from
/// different request-handling threads.
/// Fuel budgets for WASM locking plugin calls. Matches the convention
/// in [`crate::scheduled::fuel`].