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
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
//! The lifecycle lock cryptographic memory erasure needs, and who can hold it.
//!
//! Destroying a subject's wrapping key is not one operation. It reads the
//! subject's items, checks every legal hold, tombstones, and then asks a KMS to
//! destroy the scope — and between the hold check and the destroy, a write on
//! another instance can add an item, or an operator can place a hold on one.
//! Either makes the erasure wrong in a way nothing detects: the new item is
//! sealed under a scope that is about to stop existing, and the held item is
//! destroyed anyway.
//!
//! A `tokio::sync::Mutex` closes that window and is **process-local**, so on its
//! own it holds the contract on a single-writer deployment and silently does not
//! on an active-active one. That is worse than absent, because a configured key
//! ring reads as *this plane can erase*.
//!
//! So the lock is a seam. [`LocalCoordinator`] is the mutex, named for what it
//! is and refusing to pretend otherwise; [`PostgresCoordinator`] is a session
//! advisory lock in the database the plane already shares.
//!
//! # Why a session advisory lock, and not a row
//!
//! A row taken with `SELECT … FOR UPDATE` needs its transaction held open for
//! the whole erasure, and the erasure's own writes go through the store's other
//! connections — so the row lock would be held by a transaction that cannot see
//! the work it is protecting. A **session** advisory lock is held by the
//! connection rather than the transaction, and `PostgreSQL` releases it when the
//! session ends. That last property is the one that matters: an instance that
//! dies mid-erasure releases the lock by dying, where a lease with a TTL would
//! either strand the subject or hand it over while the KMS call is still in
//! flight.
use async_trait;
use crateStoreError;
/// Permission to run one subject's lifecycle operation, held until released.
///
/// **Dropping it releases the lock**, and that is what makes every path through
/// this seam safe rather than only the ones that remember to call
/// [`release`](ErasureCoordinator::release). The argument against an RAII guard
/// — that releasing a distributed lock is `async` and fallible while `Drop` is
/// neither — is true of a lease *table* and false of the two primitives here: a
/// process-local mutex releases by dropping its guard, and a `PostgreSQL`
/// session advisory lock releases when the session ends, so dropping the
/// connection is a complete release. Both are synchronous and cannot fail.
///
/// `release` still exists and is still what callers use through
/// [`under_lock`], because it does the *tidy* thing: it unlocks explicitly, so
/// the scope is free immediately rather than whenever a connection finishes
/// closing, and it can report a failure. Correctness does not depend on it
/// being reached.
///
/// An implementation whose release genuinely needs a round trip — a lease row,
/// a lock service with no session semantics — builds a lease with
/// [`new`](Self::new) and carries no guard. It is then back to needing
/// `release`, and its cancellation story is its own.
/// Proof that an acquire is happening inside [`under_lock`].
///
/// Its only field is private to this module, so a value of it cannot be
/// constructed anywhere else — which makes calling
/// [`acquire`](ErasureCoordinator::acquire) outside `under_lock` a compile
/// error rather than a rule in a doc comment. What that buys is the explicit
/// unlock and the reported failure: a dropped lease frees the scope on its own,
/// and a lease nobody ever drops is a scope held for as long as the caller
/// holds it.
///
/// The rule is the compiler's, and this pins it — widen the field to public
/// and this stops failing:
///
/// ```compile_fail
/// use agentplane::keyring::{ErasureCoordinator, LocalCoordinator, UnderLock};
/// # async fn f() {
/// let coordinator = LocalCoordinator::default();
/// // No way to make an `UnderLock` from out here, so no way to take a lock
/// // this caller has not promised to release.
/// let _ = coordinator.acquire("scope", UnderLock(())).await;
/// # }
/// ```
);
/// Who serialises a scope's lifecycle operations.
/// Run `work` with the scope's lifecycle lock held.
///
/// **Dropping this future releases the scope**, for both shipped coordinators,
/// because the lock travels in the [`Lease`] and dropping a lease is a complete
/// release: a process-local mutex gives up its guard, and a `PostgreSQL`
/// session advisory lock ends with the session. So an ordinary `timeout` around
/// something that reaches here — a `timeout` on an `EncryptedMemoryStore`
/// write, say — abandons the work without stranding the subject.
///
/// What is lost on that path is only the tidy half: the explicit unlock, so the
/// scope frees when the connection finishes closing rather than immediately,
/// and the release failure, which nobody is left to report. A coordinator whose
/// release genuinely needs a round trip carries no guard and has neither
/// property — see [`Lease`].
///
/// To ask *whether* a scope is locked without taking it, use a probe
/// (`PostgresStore::erasure_probe`).
///
/// A free function rather than a default method, so the release-on-both-paths
/// rule has exactly one implementation. Two copies of one rule agree everywhere
/// except the boundary nobody probed, and here that boundary is a lock nobody
/// released — a subject stranded for every other instance.
///
/// # Errors
///
/// The acquire failure, the work's own failure, or — only when the work
/// succeeded and the release did not — the release failure.
pub async
/// The process-local lock, named for what it is.
///
/// Correct for redb or any other single-writer deployment, and honest about
/// being nothing else: [`is_distributed`](ErasureCoordinator::is_distributed)
/// answers `false`, so a plane sharing a store can refuse it at build.
///
/// Per **scope**, not one lock for everything: two independent scopes never
/// contend, so the granularity is whatever the caller's scopes encode. That
/// is a capability, not a promise about any particular caller —
/// [`EncryptedMemoryStore`](super::EncryptedMemoryStore) deliberately passes
/// **one scope per tenant** (its id-addressed operations cannot know their
/// subject without a racy lookup), so for that wrapper this coordinator
/// behaves as a per-tenant lock and the finer granularity sits unused. A
/// caller with genuinely finer scopes — per case, per subject — gets the
/// finer lock for free.
/// A lifecycle lock held in the `PostgreSQL` the plane already shares.
///
/// `pg_advisory_lock` on a **session**, so it is held by the connection rather
/// than by a transaction — which is what this needs, because the erasure's own
/// writes go through the store's other connections and a transaction-scoped
/// lock would be held by something that cannot see the work it protects.
///
/// The property that made this the right primitive rather than a lease table:
/// `PostgreSQL` releases a session's advisory locks **when the session ends**. An
/// instance that dies mid-erasure therefore releases by dying. A lease with a
/// TTL has to choose between stranding the subject until the TTL expires and
/// handing it to another instance while the first one's KMS call may still be
/// in flight, and neither is a choice worth making when the database already
/// knows whether the holder is alive.