epics_base_rs/server/database/record_lock.rs
1//! Database-level record locking — the Rust counterpart of the
2//! C-EPICS `dbScanLock` / `dbScanLockMany` machinery and pvxs's
3//! `ioc::DBManyLock` / `ioc::DBManyLocker`.
4//!
5//! C EPICS / pvxs background
6//! -------------------------
7//! Every `dbPutField` / `dbProcess` in C EPICS takes the mutex of the
8//! target record's *lock set* via `dbScanLock(precord)`: `dbLock.c:187`
9//! reads `precord->lset`, and `:192`/`:196` resolve it to that record's
10//! current `lockSet` and lock the single `ls->lock` that `makeSet` gave
11//! the set (`:86`). There is no `dbCommon::lock` member, and
12//! `precord->mlok` is a different mutex — `dbEvent.c:123-124`'s
13//! `LOCKREC`/`UNLOCKREC` over the record's event list, created at
14//! `iocInit.c:516` (the review's `R7.0.10` pin; this machine's checkout
15//! carries one extra line at `:188`, so the same statement reads `:517`
16//! against the working tree). A multi-record transaction — a QSRV *atomic group*
17//! operation, or a pvalink *atomic* scan-on-update set — must apply,
18//! read or scan several records as one indivisible unit, so pvxs
19//! builds a `DBManyLock` over every member record and holds a
20//! `DBManyLocker` across the whole member loop:
21//!
22//! * `epics-base/modules/database/src/ioc/db/dbLock.c:349` —
23//! `dbLockerAlloc` builds a locker over a fixed record set.
24//! * `epics-base/modules/database/src/ioc/db/dbLock.c:384` —
25//! `dbScanLockMany` sorts the lock sets and acquires every one,
26//! skipping duplicates.
27//! * `pvxs/ioc/groupconfigprocessor.cpp:1165` `initialiseDbLocker` /
28//! `pvxs/ioc/groupsource.cpp:492,621` — atomic group GET/PUT.
29//! * `pvxs/ioc/pvalink_channel.cpp:409,423` — `DBManyLock` /
30//! `DBManyLocker` over the atomic pvalink scan-target records.
31//!
32//! `DBManyLock` locks the member records in a deadlock-free canonical
33//! order (`dbLock.c` sorts the lock set), and because those are the
34//! *same* mutexes a plain `dbPutField` takes, a direct CA/PVA write
35//! to a backing member record cannot interleave with the transaction.
36//!
37//! Rust port
38//! ---------
39//! `epics-base-rs` stores each record in a `RecordCell`, and the ONLY lock
40//! over its data is the record's lock set: `RecordCell::read` / `write` take
41//! the set (recursively, as `dbScanLock` does) and hand out a `RefCell`-style
42//! borrow that panics on same-thread aliasing. There is no second lock on the
43//! data — the `parking_lot::RwLock<RecordInstance>` an earlier port kept
44//! under the set cost a second acquisition on every field access and could
45//! wedge a thread that re-entered it while the set already serialised the
46//! access.
47//!
48//! This module is C's **lock sets**: one mutex per connected component of
49//! the DB-link graph, with the record-to-set map C keeps in
50//! `dbCommon::lset`. That cell is [`LockRecord`], and a record is born with
51//! one pointing at the **bootstrap set** ([`bootstrap_set`], id 0, never on
52//! the active or free list) — C's null `lset` made lockable — until the
53//! registry adopts the cell at registration ([`Registry::adopt`]) and moves
54//! it onto a set of its own ([`Registry::mint_for`]). After `iocInit` the
55//! two happen together: a record the registry adopts once
56//! [`PvDatabase::build_lock_sets`] has run is minted a set before it is
57//! published, so no registered record is ever on the bootstrap set then and
58//! no set-taker after `iocInit` needs to reach for id 0.
59//!
60//! * A plain CA/PVA write (`put_record_field_from_ca`, `put_pv`,
61//! `process_record`) takes the target record's set for the duration of the
62//! write via [`PvDatabase::lock_record`] — `dbScanLock`.
63//! * A multi-record transaction — the QSRV atomic group PUT/GET and the
64//! pvalink atomic scan-on-update epoch — takes every set its members are
65//! behind, up front and in set-id order, via [`PvDatabase::lock_records`] —
66//! `dbScanLockMany`, including its skip of the duplicates that appear when
67//! several members share one set.
68//!
69//! Because every path resolves through the same registry, a direct
70//! backing-record write blocks until the transaction owning that record
71//! finishes, a QSRV atomic group PUT and a pvalink atomic scan can never
72//! interleave on a shared record, and — this is what the per-record gate could
73//! not do — two records a link joins are serialised against each other exactly
74//! as C serialises them.
75//!
76//! **Rule R — a cell moves only under its set.** A [`LockRecord`] is
77//! re-pointed at another set only by a thread holding the set it currently
78//! points at (see [`Registry::place`]), which is what makes `set(); acquire;
79//! re-check set()` in [`LockRecord::acquire`] a stable hold: once the set is
80//! held, the record cannot leave it. Movers take the sets first — in id
81//! order through [`hold_sorted`] — and the registry mutex under them, and
82//! re-verify [`Registry::revision`] before moving anything.
83//!
84//! A link-field put takes both ends up front — the record and the local
85//! target its new text names — as C's `dbPutFieldLink` does through
86//! `dbScanLockMany`, and the relink runs inside that window
87//! ([`RelinkScope::LinkEdit`]). A change of membership (a record added,
88//! removed, or re-aliased after `iocInit`) relinks over every set with
89//! nothing held on entry ([`RelinkScope::Membership`]).
90//!
91//! ### How a set is built, and what still is not
92//!
93//! One `lockSet` carries a single `epicsMutexId lock` and an `ELLLIST
94//! lockRecordList` of the records behind it (`dbLockPvt.h:29-44`, the list at
95//! `:31`, the mutex at `:32`), and `dbCommon.LSET` is a `lockRecord *` pointing
96//! into that set (`:48`, `:53-70`). Membership is not static: creating a DB
97//! link merges the two records' sets (`dbDbLink.c:110` and `:124`, both
98//! `dbLockSetMerge`) and removing one splits them again (`:141`
99//! `dbLockSetSplit`), so a record and everything its links reach sit behind
100//! ONE mutex.
101//!
102//! [`PvDatabase::build_lock_sets`] reproduces both halves of C's construction
103//! in the order C performs them — `dbLockInitRecords` gives every record its
104//! own set, then one merge per DB link (`iocInit.c:178-179`) — which is why
105//! `dbLockShowLocked` reports `0` and `0` before `iocInit` and why the sets a
106//! merge empties show up on the free list rather than vanishing.
107//!
108//! ### The runtime relink, and who owns it
109//!
110//! **Invariant.** The partition the registry holds MUST equal the connected
111//! components of the DB-link graph over the records the database currently
112//! has. No path may leave a link field written and the partition unchanged.
113//!
114//! **Owner.** [`PvDatabase::relink_lock_sets`] is the only mutator of the
115//! partition after [`PvDatabase::build_lock_sets`], and it is private:
116//! nothing outside this module can call it. The only way to reach it is to
117//! hold a [`LockSetEdit`], whose destructor calls it — so a link-field
118//! write that does not relink is not something a caller can forget to do,
119//! it is something they cannot express. Every exit path of the write body
120//! (`?`, an early `return`, a panic unwind) drops the token and relinks.
121//!
122//! **Why a re-partition and not C's incremental pair.** C calls
123//! `dbLockSetMerge` on link creation and `dbLockSetSplit` on removal
124//! (`dbDbLink.c:110`, `:124`, `:141`), and the split is *itself* a
125//! breadth-first reachability recomputation over the live graph
126//! (`dbLock.c:710-760`) — C only avoids recomputing on the merge side
127//! because it already knows the two endpoints. Keeping the old target on
128//! the port's side to reproduce that pair would mean storing the edge set a
129//! second time, next to the link text that already is the edge set, and a
130//! second copy of a fact is what goes stale. The owner therefore re-derives
131//! the affected component from the live link text and re-partitions it,
132//! which is one rule for creation, removal, retarget, record deletion and
133//! alias changes alike instead of a case per verb.
134//!
135//! Set ids follow C where C fixes them: the component holding the edited
136//! record keeps its id (C's `dbLockSetMerge` keeps `pfirst`'s set, and
137//! `pfirst` is the record whose link moved), a component that splits off
138//! takes a fresh set from the free list as `makeSet` does, and the sets a
139//! merge empties go back on the free list.
140//!
141//! `field_io.rs`'s `NotifyClaim` is unchanged by this: it closes dbNotify's
142//! test-then-install window inside the critical section that tested the slot,
143//! which is a stronger guarantee than the lock-set region it was standing in
144//! for, not a substitute that lock sets now make unnecessary.
145//!
146//! What the gate *is* — a blocking priority-inheritance mutex
147//! ----------------------------------------------------------
148//! The gate is a [`crate::runtime::sync::PriorityInheritanceMutex`], the
149//! same primitive L46 (`registration_mutex`), L8a (`simple_pvs`) and L8b
150//! (one `scan_index` bucket) already use, and [`PvDatabase::lock_record`] /
151//! [`PvDatabase::lock_records`] are plain synchronous `fn`s returning RAII
152//! guards. This is the parity shape rather than a Rust-side invention: the
153//! `ls->lock` C's `dbScanLock` takes is a plain `epicsMutex` (`dbLock.c:86`),
154//! and on
155//! the RTEMS arm
156//! base compiles the POSIX implementation
157//! (`configure/toolchain.c:31-35` selects `OS_API = posix` for
158//! `__RTEMS_MAJOR__ >= 5`; `os/RTEMS-posix/osdMutex.c:8` is one `#include
159//! "../posix/osdMutex.c"`), whose `globalAttrInit`
160//! (`os/posix/osdMutex.c:71-88`) builds every `epicsMutex` with
161//! `PTHREAD_PRIO_INHERIT` — probing it once and silently degrading to
162//! `PTHREAD_PRIO_NONE` if the target refuses. `PriorityInheritanceMutex` is
163//! that same construction on that same API, including the probe.
164//!
165//! ### The band-ordered wait queue is gone, and why that is not a loss
166//!
167//! Until §5 step 4 this gate was an async lock, and between steps 2 and 5 it
168//! was a hand-rolled `PriorityGate` whose waiters were parked in a
169//! `BTreeMap` keyed by the waiter's declared EPICS band, highest band first,
170//! FIFO among equals. That queue existed for exactly one reason: while the
171//! gate was async, both ends of a contention pair were *tasks* parked on a
172//! userspace queue the kernel could not see, so nothing but our own code
173//! could order them. It was the async bridge, not the target design.
174//!
175//! With a blocking PI mutex the waiters are real threads blocked in
176//! `pthread_mutex_lock`, so the *OS* orders the queue — by thread priority,
177//! which on the RTEMS backend is the EPICS band the thread declared through
178//! `enter_ioc_thread` — and additionally boosts a preempted low-band holder
179//! to the highest waiting band. The band-ordered wake order is therefore
180//! replaced by the kernel's PI wait order, which is strictly stronger: it is
181//! what closes handoff §8.0 **gap 4** (priority inheritance), which no
182//! userspace queue could close at all. `PriorityGate`, its `BTreeMap` wait
183//! queue, `GateAcquire` and the `DECLARED_BAND` thread-local that fed it are
184//! deleted with this flip.
185//!
186//! ### Where the ordering actually holds — [`crate::runtime::sync::is_pi_mutex_active`]
187//!
188//! Priority inheritance is a property of the *build and the target*, and the
189//! function above is the single place that answers whether this process got
190//! it:
191//!
192//! * **RTEMS** — PI, and the answer is a *probe* result rather than a `cfg!`,
193//! matching C's own degrade path (`os/posix/osdMutex.c:77-85`, reported by
194//! `epicsMutexShowAll` at `:199-205`).
195//! * **Linux with the `linux-rt` Cargo feature** — PI unconditionally.
196//! * **every other build, including a default hosted Linux `cargo test`** —
197//! `parking_lot::Mutex`, which has **no** priority inheritance and no
198//! priority ordering. The host suite therefore verifies the *exclusion*
199//! this module provides, never its ordering; ordering is on-target
200//! territory.
201//!
202//! Read as a claim about *this* gate: on the host, `lock_record` excludes and
203//! nothing more, and no host test can be written that would catch a lost
204//! inversion. Two further conditions have to hold on target before the
205//! ordering is real, and neither is this module's to enforce — the probe must
206//! have returned `PTHREAD_PRIO_INHERIT`, and the contending threads must
207//! actually carry distinct scheduling priorities, which requires
208//! `RtPolicy::AllowRealtime` in [`crate::runtime::task`]. With the RT switch
209//! off, every thread is one priority and PI has nothing to inherit.
210//!
211//! Acquisition order — MUST
212//! ------------------------
213//! Written down because every lock in the chain is now *blocking* and a
214//! cycle would wedge a thread rather than a task. The order below is the
215//! one the code actually takes, not an aspiration — it was derived by reading
216//! every nesting site, and the bypass audit is in the commit that added it.
217//!
218//! > **A thread MUST acquire these in this order and MUST NOT acquire any of
219//! > them while holding one that appears later:**
220//! >
221//! > 0. **L33** — `epics-bridge-rs`' `GroupPvDef::atomic_write_lock`, the
222//! > QSRV per-group atomic-PUT gate (`PriorityInheritanceMutex`). Outside
223//! > this crate, and only the atomic group PUT takes it — see the L33
224//! > section below.
225//! > 1. **L1** — the per-record advisory gate ([`PvDatabase::lock_record`] /
226//! > [`PvDatabase::lock_records`]), *this* module
227//! > (`PriorityInheritanceMutex`).
228//! > 2. **L46** — `PvDatabaseInner::registration_mutex`
229//! > (`PriorityInheritanceMutex`).
230//! > 3. the leaves, none of which is ever held while another lock is taken:
231//! > **L8a** `simple_pvs`, **L8b** one `scan_index` bucket and **L7**
232//! > `ProcessVariable::subscribers` (all `PriorityInheritanceMutex`), plus
233//! > the `records` map and `aliases` (each a `RecursiveReadLock`, whose
234//! > readers never queue behind a waiting writer, so a reader under L1
235//! > cannot be wedged by a writer that is itself waiting for L1; and
236//! > whose readers never take L1 fresh under the guard, so a writer
237//! > under L1 — `remove_record_entry` — cannot be wedged by a reader).
238//! > A record's data has no lock of its own: it is behind L1.
239//! >
240//! > Every rung is a blocking lock. There is no async lock left anywhere on
241//! > the put/process path, which is what makes the order a MUST rather than a
242//! > preference: a cycle wedges a thread.
243//!
244//! [`RecordLockRegistry`]'s own mutex (a `std::sync::Mutex`) is *not* a rung of
245//! that order: it is a leaf taken UNDER the sets — a mover holds its region
246//! first and consults or edits the registry inside — and no lock set is ever
247//! acquired while it is held. A guard's release path deliberately does not
248//! touch it — everything a release needs lives in the set's own cell.
249//!
250//! **Owner/Gate:** [`PvDatabase::update_scan_index`] and
251//! `PvDatabase::remove_record` are the **only** production functions that
252//! take L46 from inside an L1-held window, and each does it the same way:
253//! the record is looked up, its set taken, the gate taken, and the map entry
254//! re-verified by `Arc::ptr_eq` under the gate, retrying if it changed. Every
255//! other L46 holder (`add_pv`, `add_pv_with_hooks_full`, `remove_simple_pv`,
256//! `add_loaded_record`, `add_alias`, `add_breaktables`) is a registration
257//! entry point reached from `.db` load, iocsh or the gateway, never from
258//! inside a put/process cycle, and reads no record data under the gate
259//! (`add_breaktables` drops it before installing its tables) — verified with
260//! `rg` over those symbols in `field_io.rs`, `processing.rs`, `links.rs`,
261//! `qsrv/group.rs` and `pvalink/integration.rs`, where every hit is inside a
262//! `#[cfg(test)]` module. `LockSet::acquire` asserts the rule in debug
263//! builds: a fresh L1 acquisition on a thread holding L46 panics naming both
264//! ends. A third function that nests L46 under L1 follows the same shape, or
265//! the order above stops being checkable by reading one function.
266//!
267//! The table orders the rungs against each other; it does not say what
268//! happens when one rung is taken twice. For L46 that matters, because
269//! `update_scan_index` takes L46 itself: **no caller may hold L46 when
270//! reaching it.** `PriorityInheritanceMutex` is not reentrant, so a caller
271//! that does parks on itself, and the symptom is a hung registration rather
272//! than an error. Every L46 acquisition therefore goes through
273//! [`PvDatabase::lock_registration`], which knows whether this thread already
274//! holds the gate and panics naming both ends instead of parking. The rule is
275//! C's too: `iterateRecords` (`iocInit.c:562-586`) walks an already-built
276//! database in a separate pass, holding no registration lock.
277//!
278//! **L1 does NOT have that rule: it recurses, as C's does.** That is forced by
279//! lock sets rather than chosen. Once a set spans every record a DB link
280//! reaches, processing a record and then following its `FLNK` takes ONE mutex
281//! under two different record names, so a non-reentrant L1 would wedge the
282//! ordinary process path. C is under the same constraint and answers it the
283//! same way: `epicsMutex` must be recursive — the header states the contract
284//! in prose, not code, at `epicsMutex.h:16` "An epicsMutex may be claimed
285//! recursively" and `:38` "MUST implement recursive locking" — and
286//! `dbLock.c:224-234` counts the nesting under `LOCKSET_DEBUG`.
287//!
288//! `PriorityInheritanceMutex` is not reentrant, so the recursion is built here,
289//! on top of it: each set records the thread inside it and how deep, the first
290//! acquisition takes the mutex and the rest only raise the count, and the mutex
291//! is released when the count returns to zero. The owner field is written only
292//! by the owning thread, so a non-owner can never match it. What this replaced
293//! — a thread-local held-name set that PANICKED on re-entry — was the right
294//! guard while a gate was one record, and is exactly wrong once a gate is a
295//! component: it would have fired on the first `FLNK`.
296//!
297//! `dbScanLockMany`'s own refusal (`cantProceed("dbScanLockMany(%p) already
298//! locked. Recursive locking not allowed")`, `dbLock.c:392-395`) is about
299//! re-using ONE `dbLocker` object, not about a thread holding two. Every
300//! [`PvDatabase::lock_records`] call builds its own, so there is nothing here
301//! to refuse.
302//!
303//! ### The rule's teeth are structural, and now they cover L1 too
304//!
305//! Every guard in the list above is `!Send`. A `!Send` value held across an
306//! `.await` makes the enclosing future `!Send`, which the compiler rejects at
307//! every `tokio::spawn` / `runtime::task::spawn` site in this workspace — so
308//! "no suspension point inside a gate window" is a build error rather than a
309//! review convention. That is the structural guarantee holders H1–H9 were
310//! staged to make reachable: each holder
311//! was first rewritten so its gate-held region contained zero `.await`s, and
312//! only then did the gate become a type that refuses to be held across one.
313//!
314//! `!Send`ness is deliberate on both arms of [`crate::runtime::sync::PriorityInheritanceMutex`],
315//! and on the PI arm it is also a correctness requirement, not only a
316//! lint: POSIX requires a mutex to be unlocked by the thread that locked it,
317//! so a guard that could migrate between threads would call
318//! `pthread_mutex_unlock` from a non-owner.
319//!
320//! The compiler only *reports* it at a spawn site, though, so the standing
321//! check is a direct one — for every binding of a gate guard, read forward to
322//! the end of its drop scope and find no `.await`:
323//!
324//! ```text
325//! rg -n 'let (mut )?\w+ = .*\.(lock_record|lock_records|acquire_put_gate)\(' crates/
326//! ```
327//!
328//! ### L33 — the QSRV atomic-PUT group lock, relative to L1
329//!
330//! `epics-bridge-rs`' `GroupPvDef::atomic_write_lock` (`qsrv/group_config.rs`)
331//! is a group-vs-group serialization aid: it lives in a different crate and
332//! has no nesting relationship with L46/L8a/L8b, but it *is* held across L1
333//! and so occupies rung 0 of the order above. It is acquired in
334//! `GroupChannel::put`'s atomic branch **before** [`PvDatabase::lock_records`]
335//! — first so a conversion failure in the up-front value-conversion phase
336//! aborts the whole atomic PUT before any member-record gate is even
337//! requested, second so two atomic PUTs to the *same* group serialize before
338//! either reaches L1 at all.
339//!
340//! It is a `PriorityInheritanceMutex`. It was a `tokio::sync::Mutex` for
341//! exactly as long as L1 was async: its window contains `lock_records`, which
342//! used to be a genuine suspension point, and a `!Send` guard across that
343//! await would not compile at the connection-task spawn site. That window is
344//! now the conversion phase, a synchronous `lock_records`, and a synchronous
345//! member loop — zero `.await`s — so the reason to keep it async is gone.
346
347// No RTEMS-EXEC-MODEL-ALLOW marker: this file's tests are all plain `#[test]`s
348// now that the gate is a blocking lock (a contender has to be a real thread),
349// so none of them needs a reactor and there is nothing to account for.
350
351use std::collections::{BTreeMap, BTreeSet, HashMap, VecDeque};
352use std::sync::Arc;
353use std::sync::atomic::{AtomicPtr, AtomicU64, AtomicUsize, Ordering};
354
355use crate::server::record::RecordCell;
356
357use crate::runtime::sync::{
358 MutexInfo, PriorityInheritanceMutex, PriorityInheritanceMutexGuard, mutex_report,
359};
360
361use super::PvDatabase;
362
363/// C's `lockSet` (`dbLockPvt.h:29-44`): the one mutex every member record
364/// locks through, plus the bookkeeping the guards need without the registry.
365///
366/// The membership list is NOT here — a merge rewrites it, and it lives in
367/// [`Registry`] behind the registry mutex. What a guard needs at release time
368/// is here instead, so releasing never takes the registry lock: doing that
369/// while holding the set would close a cycle against [`Registry::real_set_of`],
370/// which takes the registry lock and then the set.
371struct LockSet {
372 /// C's `lockSet::id` (`dbLockPvt.h:33`). Assigned once by
373 /// [`Registry::make_set`] and kept for the cell's whole life, free-list
374 /// round trips included, exactly as C's is.
375 id: u64,
376 /// C's `lockSet::lock` (`:32`) — the mutex `dbScanLock` takes.
377 lock: PriorityInheritanceMutex<()>,
378 /// Position of this set's mutex among this file's entries in the process
379 /// mutex list, so [`lock_set_mutex_rows`] can hand back the row
380 /// `epicsMutexShow` prints for it. See [`SET_MUTEX_SEQ`].
381 mutex_seq: u64,
382 /// The thread currently inside `lock`, or 0. Written only by that thread.
383 owner: AtomicU64,
384 /// How deep that thread's recursion is. C's `epicsMutex` is recursive by
385 /// contract (`epicsMutex.h:16`, `:38`) and `dbLock.c:224-234` counts the
386 /// same nesting under `LOCKSET_DEBUG`.
387 depth: AtomicUsize,
388 /// References ABOVE the one-per-member baseline. `dbScanLockMany` adds one
389 /// per set for its locked list (`dbLock.c:404`) and `dbScanLock` does not
390 /// — it drops its transient reference the moment it holds the mutex
391 /// (`:220-222`) — which is why an idle set reports exactly as many refs as
392 /// it has members, as the oracle capture in `iocsh`'s `dblsr` shows.
393 many_holds: AtomicUsize,
394 /// The mutex guard of the current outermost hold, kept here rather than
395 /// in that hold's [`SetGuard`] so that a frame which does not own the
396 /// guard can still give the set up and take it back — [`Self::unheld`],
397 /// C's `dbScanUnlock` from inside `dbBkpt` (`dbBkpt.c:795`). Touched only
398 /// by the thread named in `owner`, under the mutex itself.
399 held: std::cell::UnsafeCell<Option<PriorityInheritanceMutexGuard<'static, ()>>>,
400}
401
402// SAFETY: `held` is the one non-`Sync` field, and it is read or written only
403// by the thread that holds `lock` (`acquire`'s fresh arm, `SetGuard::drop`'s
404// outermost arm, and `unheld`, each of which checks or establishes
405// `owner == thread_key()` first). The guard never leaves that thread: the
406// same thread that stashes it takes it out, so the `!Send` guard's
407// unlock-by-owner rule holds.
408unsafe impl Sync for LockSet {}
409
410/// The `'static` handle to a lock set. Sets are leaked: a guard hands out a
411/// `'static` borrow of the mutex, and a set outlives every record it holds —
412/// C keeps its emptied sets on `lockSetsFree` for the same reason and frees
413/// them only in `dbLockCleanupRecords` (`dbLock.c:563-576`).
414type Set = &'static LockSet;
415
416/// C's `lockRecord` (`dbLockPvt.h:52-60`) — the cell `dbCommon::lset` points
417/// at, and the ONLY way a record reaches its lock set.
418///
419/// C's `dbScanLock` is `precord->lset` → `lr->plockSet` → `lock`: two pointer
420/// derefs, no name anywhere. The port had the same association spelled as a
421/// name-keyed map, so taking a record's gate cost an alias lookup plus two
422/// hashes of the record name under the registry mutex — 0.85 us of a 11.5 us
423/// calc cycle, measured. A record that owns its cell pays neither.
424///
425/// `plock_set` is the association C guards with the `lockRecord`'s spinlock
426/// (`dbLockPvt.h:53-57`): written only by [`Registry`], which holds the
427/// registry mutex, and read with no lock at all, exactly as C reads it under
428/// either lock. A stale read is not a hazard — it is what the re-check loop in
429/// [`Self::acquire_fresh`] exists to catch.
430pub(crate) struct LockRecord {
431 plock_set: AtomicPtr<LockSet>,
432}
433
434impl LockRecord {
435 fn new(set: Set) -> Arc<Self> {
436 Arc::new(Self {
437 plock_set: AtomicPtr::new(Self::as_ptr(set)),
438 })
439 }
440
441 /// The cell a record is born with — C's null `lset` before
442 /// `dbLockInitRecords`, spelled as a pointer at the one shared
443 /// [`bootstrap_set`] so that the record's data is guarded from its first
444 /// access, before the registry has given it a set of its own.
445 pub(crate) fn bootstrap() -> Arc<Self> {
446 Self::new(bootstrap_set())
447 }
448
449 /// Whether this record still locks through the bootstrap set, which is
450 /// what `dblsr` reports as "no lock set" (`dbLock.c:900-901`).
451 pub(crate) fn is_bootstrap(&self) -> bool {
452 std::ptr::eq(self.set(), bootstrap_set())
453 }
454
455 /// C `dbScanLock`'s body (`dbLock.c:184-213`) — take the set the cell
456 /// names, then check the cell still names it.
457 ///
458 /// The re-check is not optional: a merge running concurrently moves the
459 /// record behind another mutex, and the one just taken would guard
460 /// nothing. C compares `lockSet` pointers under the record's spinlock;
461 /// the cell's store is `Release` and the load `Acquire`, which is the same
462 /// publication. The mover holds the set it moves records OUT of (see
463 /// [`Registry::place`]), so a thread that took the old set and lost the
464 /// re-check was never inside the record's data — it merely waited.
465 #[inline]
466 pub(crate) fn acquire(&self) -> SetGuard {
467 let set = self.set();
468 if set.owner.load(Ordering::Acquire) == thread_key() {
469 // This thread holds the set, so by Rule R the record cannot leave
470 // it: no re-check, and nothing but the depth counter to touch.
471 // This is every `rec.read()` / `rec.write()` inside a process
472 // cycle, which is why it is inlined and the rest is not.
473 return set.reenter(false);
474 }
475 self.acquire_fresh()
476 }
477
478 /// The set was not held: take whatever set the record is behind by the
479 /// time the mutex is ours — a merge can move the record while this
480 /// thread waits, and the set it then holds would be the wrong one.
481 #[cold]
482 #[inline(never)]
483 fn acquire_fresh(&self) -> SetGuard {
484 loop {
485 let set = self.set();
486 let guard = set.acquire(false);
487 if std::ptr::eq(self.set(), set) {
488 return guard;
489 }
490 drop(guard);
491 }
492 }
493
494 /// See [`LockSet::unheld`]: give up the set this record is in for the
495 /// duration of `f`. This thread must hold that set exactly once.
496 pub(crate) fn unheld<R>(&self, f: impl FnOnce() -> R) -> R {
497 self.set().unheld(f)
498 }
499
500 fn as_ptr(set: Set) -> *mut LockSet {
501 set as *const LockSet as *mut LockSet
502 }
503
504 /// C `lr->plockSet`.
505 fn set(&self) -> Set {
506 let p = self.plock_set.load(Ordering::Acquire);
507 // SAFETY: every pointer stored here came from [`Registry::make_set`],
508 // which leaks its `LockSet`, so the referent is live for `'static`;
509 // a merge only ever moves the set onto `Registry::free`, which keeps
510 // the allocation. The cell is constructed with a set and `store` is
511 // the only other writer, so it is never null.
512 unsafe { &*p }
513 }
514
515 fn store(&self, set: Set) {
516 self.plock_set.store(Self::as_ptr(set), Ordering::Release);
517 }
518}
519
520/// C's `next_id` starts at 1 and `makeSet` uses the POST-increment
521/// (`dbLock.c:70`, `:87`), so C's first lock set is number 2. Matching it
522/// costs nothing and makes an A/B against a C IOC read straight across.
523const FIRST_SET_ID: u64 = 2;
524
525/// Serialises lock-set mutex creation with its sequence counter.
526///
527/// The process mutex list ([`mutex_report`]) has no per-mutex accessor, so a
528/// set finds its own row positionally. That is exact only if the order sets
529/// are appended to the list is the order they take sequence numbers, which
530/// this lock is what guarantees — several `PvDatabase`s in one process each
531/// run their own registry mutex and would otherwise interleave. Nothing is
532/// acquired while it is held.
533static SET_MUTEX_SEQ: std::sync::Mutex<u64> = std::sync::Mutex::new(0);
534
535/// Mint one set. Every `LockSet` in the process comes from here.
536fn new_set(id: u64) -> Set {
537 let mut seq = SET_MUTEX_SEQ.lock().unwrap_or_else(|e| e.into_inner());
538 let mutex_seq = *seq;
539 *seq += 1;
540 // The mutex is created UNDER `SET_MUTEX_SEQ` so its position in the
541 // process mutex list matches `mutex_seq`. This is the only
542 // `PriorityInheritanceMutex::new` in this file, which is what makes
543 // filtering that list by creating file select exactly these mutexes.
544 let set: Set = Box::leak(Box::new(LockSet {
545 id,
546 lock: PriorityInheritanceMutex::new(()),
547 mutex_seq,
548 owner: AtomicU64::new(0),
549 depth: AtomicUsize::new(0),
550 many_holds: AtomicUsize::new(0),
551 held: std::cell::UnsafeCell::new(None),
552 }));
553 drop(seq);
554 set
555}
556
557/// The set every record locks through until a registry gives it one — C's
558/// null `lset` made lockable, so that a record's data is guarded by a lock
559/// set from its first access rather than only from `iocInit` on.
560///
561/// One per process, never in any registry's `active` or `free` list, and
562/// its id `0` is never a set id (`FIRST_SET_ID` is 2). It is created in this
563/// file under [`SET_MUTEX_SEQ`] like every other set, so it holds a row in
564/// the process mutex list and shifts nothing — see [`lock_set_mutex_rows`].
565fn bootstrap_set() -> Set {
566 static BOOTSTRAP: std::sync::OnceLock<Set> = std::sync::OnceLock::new();
567 BOOTSTRAP.get_or_init(|| new_set(0))
568}
569
570/// A process-unique non-zero key for the current thread.
571///
572/// `ThreadId` has no stable integer form on stable Rust, and the value only
573/// has to be comparable and never reused while a thread lives.
574///
575/// The slot is `const`-initialised and holds a `Cell` rather than being
576/// initialised from `NEXT` directly, because that is what makes the read a
577/// plain TLS offset: a `thread_local!` with a runtime initialiser carries a
578/// lazy-init flag and a destructor registration, and goes through
579/// `LocalKey::try_with` on every access. `LockSet::acquire` asks for this key
580/// on every `dbScanLock`, so that check was the single largest cost in taking
581/// a record's lock set. Zero is the "not yet minted" value and never a key,
582/// which is why `NEXT` starts at 1.
583fn thread_key() -> u64 {
584 static NEXT: AtomicU64 = AtomicU64::new(1);
585 thread_local! {
586 static KEY: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
587 }
588 KEY.with(|k| match k.get() {
589 0 => {
590 let minted = NEXT.fetch_add(1, Ordering::Relaxed);
591 k.set(minted);
592 minted
593 }
594 key => key,
595 })
596}
597
598impl LockSet {
599 /// C `dbScanLock` (`dbLock.c:180-234`) minus the reference dance: take the
600 /// set's mutex, or count one more level if this thread is already inside
601 /// it.
602 ///
603 /// Recursion is not a convenience. Once a lock set spans every record a
604 /// DB link reaches, processing a record and then its `FLNK` target takes
605 /// the SAME mutex twice on one thread, which is precisely why C's
606 /// `epicsMutex` is required to be recursive.
607 ///
608 /// The re-entry is the hot path: every `rec.read()` / `rec.write()` on
609 /// the process path lands here with the set already held, so it is one
610 /// TLS load, one compare and a plain counter — no read-modify-write.
611 /// `depth` is written only by the owning thread, which is what makes a
612 /// load-then-store correct.
613 fn acquire(&'static self, many: bool) -> SetGuard {
614 let me = thread_key();
615 if self.owner.load(Ordering::Acquire) == me {
616 return self.reenter(many);
617 }
618 self.lock_fresh(me);
619 if many {
620 self.many_holds.fetch_add(1, Ordering::Relaxed);
621 }
622 SetGuard {
623 set: self,
624 outermost: true,
625 many,
626 }
627 }
628
629 /// One more level on a thread that already holds the set. The caller
630 /// has checked `owner`.
631 #[inline]
632 fn reenter(&'static self, many: bool) -> SetGuard {
633 self.depth
634 .store(self.depth.load(Ordering::Relaxed) + 1, Ordering::Relaxed);
635 if many {
636 self.many_holds.fetch_add(1, Ordering::Relaxed);
637 }
638 SetGuard {
639 set: self,
640 outermost: false,
641 many,
642 }
643 }
644
645 /// Take the mutex on a thread that does not hold it and publish the hold.
646 fn lock_fresh(&'static self, me: u64) {
647 // A fresh acquisition is rung L1 of the order in the module doc. L46
648 // sits below it, so a thread that holds the registration gate and
649 // reaches here is closing the cycle `update_scan_index` opens from
650 // the other side. Fail here, on the thread that made the mistake,
651 // rather than wedge two threads later.
652 debug_assert!(
653 !super::registration_gate_held(),
654 "a lock set (L1) was taken while this thread holds the registration \
655 gate (L46); record data is behind L1 and must be read before the \
656 gate is taken, or through a set this thread already holds"
657 );
658 // The records map and the alias table are leaves too, and
659 // `remove_record_entry` writes them while holding the removed
660 // record's set: a fresh set taken under a read guard on either is
661 // the reader that writer can be waiting on. Clone the cell out, drop
662 // the guard, then lock (`RecursiveReadLock`, `mod.rs`).
663 debug_assert!(
664 !super::map_read_held(),
665 "a lock set (L1) was taken while this thread holds a read guard on \
666 the records map or the alias table; clone the record handle out \
667 under the guard and drop it before locking the record"
668 );
669 let guard = self.lock.lock();
670 // SAFETY: this thread holds `lock`, so it is the only one allowed at
671 // `held` (see the `Sync` impl).
672 unsafe { *self.held.get() = Some(guard) };
673 self.owner.store(me, Ordering::Release);
674 self.depth.store(1, Ordering::Relaxed);
675 }
676
677 /// Release the mutex this thread holds and clear the hold. Only the
678 /// owner, at depth 1, may call this.
679 fn unlock_outermost(&'static self) {
680 // Clear ownership BEFORE the mutex is released, or the next owner
681 // could publish itself and be overwritten by this store.
682 self.depth.store(0, Ordering::Relaxed);
683 self.owner.store(0, Ordering::Release);
684 // SAFETY: as in `lock_fresh` — the mutex is still held here.
685 let guard = unsafe { (*self.held.get()).take() };
686 drop(guard);
687 }
688
689 /// Run `f` with this set given up, then take it back — C's
690 /// `dbScanUnlock(precord); epicsThreadSuspendSelf(); dbScanLock(precord);`
691 /// (`dbBkpt.c:794-796`), the breakpoint park.
692 ///
693 /// The set is released from a frame that does not own the outermost
694 /// [`SetGuard`] (a `FLNK` target's hook parks under the entry record's
695 /// gate), which is why the guard lives in [`Self::held`] and not in that
696 /// `SetGuard`. The same set is retaken so the outer guard's release
697 /// stays paired with its acquisition, whatever moved meanwhile: a record
698 /// re-linked out of this set while it was given up is guarded by its new
699 /// set on every later access, because every access goes through
700 /// [`LockRecord::acquire`] and not through the outer gate.
701 ///
702 /// # Panics
703 ///
704 /// If this thread does not hold the set, or holds it more than once —
705 /// a nested hold means a record borrow is live, and nothing may borrow
706 /// record data across a park.
707 fn unheld<R>(&'static self, f: impl FnOnce() -> R) -> R {
708 let me = thread_key();
709 assert_eq!(
710 self.owner.load(Ordering::Acquire),
711 me,
712 "a lock set was given up by a thread that does not hold it"
713 );
714 assert_eq!(
715 self.depth.load(Ordering::Relaxed),
716 1,
717 "a lock set was given up with a nested hold live"
718 );
719 assert_eq!(
720 self.many_holds.load(Ordering::Relaxed),
721 0,
722 "a lock set was given up from inside a many-lock transaction"
723 );
724 self.unlock_outermost();
725 let r = f();
726 self.lock_fresh(me);
727 r
728 }
729
730 /// C's `epicsMutexTryLock` probe in `dbLockShowLocked` (`dbLock.c:963-965`).
731 fn is_locked(&self) -> bool {
732 self.lock.try_lock().is_none()
733 }
734}
735
736/// One acquisition of one lock set — C's `dbScanLock`/`dbScanUnlock` pair.
737///
738/// `!Send` on both backends, because the inner guard is: an `Option` is `Send`
739/// only when its payload is, so the recursive re-entry that carries `None` is
740/// `!Send` too.
741pub(crate) struct SetGuard {
742 set: Set,
743 /// `false` for a recursive re-entry, which acquired no new mutex. The
744 /// outermost hold's mutex guard is in [`LockSet::held`].
745 outermost: bool,
746 many: bool,
747}
748
749impl SetGuard {
750 /// Whether `record` is in the set this guard holds — C `dbLockGetLockId`
751 /// equality. Rule R makes the answer stable for the guard's lifetime:
752 /// a record cannot leave a set while a thread holds it.
753 #[inline]
754 pub(crate) fn holds(&self, record: &LockRecord) -> bool {
755 std::ptr::eq(record.set(), self.set)
756 }
757}
758
759impl Drop for SetGuard {
760 fn drop(&mut self) {
761 if self.many {
762 self.set.many_holds.fetch_sub(1, Ordering::Relaxed);
763 }
764 if self.outermost {
765 self.set.unlock_outermost();
766 } else {
767 self.set.depth.store(
768 self.set.depth.load(Ordering::Relaxed) - 1,
769 Ordering::Relaxed,
770 );
771 }
772 }
773}
774
775/// Hold every set in `sets`, each once, in id order — the acquisition
776/// discipline of `dbScanLockMany` (`dbLock.c:384-440`), used by every path
777/// that takes more than one set so that two such paths cannot deadlock.
778fn hold_sorted(sets: &[Set]) -> Vec<SetGuard> {
779 let mut sets: Vec<Set> = sets.to_vec();
780 sets.sort_unstable_by_key(|set| set.id);
781 sets.dedup_by_key(|set| set.id);
782 sets.into_iter().map(|set| set.acquire(false)).collect()
783}
784
785/// C's `lockSetsActive` / `lockSetsFree` / `next_id` (`dbLock.c:42-70`) plus
786/// the record-to-set mapping C keeps in `dbCommon::lset`.
787struct Registry {
788 /// C `lockSetsActive`, keyed by set id — which is also C's list ORDER,
789 /// because ids ascend with creation and a merge only ever removes an
790 /// entry. `dblsr` and `dbLockShowLocked` walk this list in that order.
791 active: BTreeMap<u64, SetState>,
792 /// C `lockSetsFree` (`:44`): sets a merge emptied. Their id and mutex
793 /// survive and are handed back by the next [`Registry::make_set`], which
794 /// is why C's free count is not simply "sets that ever existed minus live
795 /// ones". `ellGet` takes the head, so this is a queue.
796 free: VecDeque<Set>,
797 /// C's `dbCommon::lset` by canonical record name — the port's answer for
798 /// the callers that have only a name: [`PvDatabase::lock_records`] accepts
799 /// names that were never records, and the reports below print by name.
800 ///
801 /// The [`LockRecord`] here is the SAME cell the record instance holds, so
802 /// there is one association and not two: everything that moves a record
803 /// between sets moves it by storing into this cell.
804 of_record: HashMap<String, Arc<LockRecord>>,
805 next_id: u64,
806 /// Bumped by every change to the partition — a set minted, a cell moved,
807 /// an entry dropped. A mover snapshots it, takes the sets it means to
808 /// move records out of, and re-reads it under the registry lock: equal
809 /// means the sets it holds are the sets those records are still behind.
810 revision: u64,
811 /// Whether `dbLockInitRecords` has run — [`PvDatabase::build_lock_sets`].
812 /// C reaches `dbLockSetMerge` only from links opened after it
813 /// (`dbDbInitLink`/`dbDbAddLink`), so a link written before it merges
814 /// nothing, even though this port's write gate has already minted the
815 /// two records their sets. The flag is that moment, kept apart from
816 /// "some set exists", which the gate makes true earlier.
817 built: bool,
818}
819
820/// One entry of [`Registry::active`]: the shared cell plus C's
821/// `lockSet::lockRecordList` (`dbLockPvt.h:31`).
822struct SetState {
823 set: Set,
824 members: BTreeSet<String>,
825}
826
827impl Default for Registry {
828 fn default() -> Self {
829 Self {
830 active: BTreeMap::new(),
831 free: VecDeque::new(),
832 of_record: HashMap::new(),
833 next_id: FIRST_SET_ID - 1,
834 revision: 0,
835 built: false,
836 }
837 }
838}
839
840impl Registry {
841 /// C `makeSet` (`dbLock.c:72-101`): reuse a freed set, keeping its id and
842 /// its mutex, or mint one.
843 fn make_set(&mut self) -> Set {
844 if let Some(set) = self.free.pop_front() {
845 debug_assert_eq!(
846 set.many_holds.load(Ordering::Relaxed),
847 0,
848 "C asserts refcount==0 for a set on the free list (dbLock.c:571)"
849 );
850 return set;
851 }
852 self.next_id += 1;
853 self.revision += 1;
854 new_set(self.next_id)
855 }
856
857 /// The cell `name` locks through, or `None` when the name has none —
858 /// including a registered record that is still on the bootstrap set,
859 /// which is C's null `lset` and what `dblsr` reports as no set at all
860 /// (`dbLock.c:900-901`).
861 fn real_set_of(&self, name: &str) -> Option<Set> {
862 self.of_record
863 .get(name)
864 .map(|lr| lr.set())
865 .filter(|set| !std::ptr::eq(*set, bootstrap_set()))
866 }
867
868 /// [`Self::real_set_of`] for a relink after `iocInit`, where a name the
869 /// link graph reaches either has a real set or is no longer registered
870 /// at all — it was removed between the graph read and this call, and
871 /// its cell went with it. A registered name still on the bootstrap set
872 /// is the invariant [`Self::adopt`] keeps, broken.
873 fn set_of_registered(&self, name: &str) -> Option<Set> {
874 let set = self.real_set_of(name);
875 debug_assert!(
876 set.is_some() || !self.of_record.contains_key(name),
877 "registered record {name} is on the bootstrap set after iocInit"
878 );
879 set
880 }
881
882 /// The cell a NAME reaches, minting a one-member set for a name that has
883 /// none — C `createLockRecord` (`dbLock.c:505-527`) for
884 /// [`PvDatabase::lock_records`]' callers, which may name a record that
885 /// never existed (`dbLockerAlloc` accepts the pointers it is given).
886 ///
887 /// Never mints for a registered record: its cell was adopted from the
888 /// record itself ([`Self::adopt`]) and points at the bootstrap set until
889 /// [`Self::mint_for`] moves it, under the hold that move requires.
890 fn lock_record_of(&mut self, name: &str) -> Arc<LockRecord> {
891 if let Some(lr) = self.of_record.get(name) {
892 return lr.clone();
893 }
894 let set = self.make_set();
895 let lr = LockRecord::new(set);
896 self.of_record.insert(name.to_string(), lr.clone());
897 self.active.insert(
898 set.id,
899 SetState {
900 set,
901 members: BTreeSet::from([name.to_string()]),
902 },
903 );
904 lr
905 }
906
907 /// Register the cell a record was born with as the one its name reaches
908 /// — C's `dbCommon::lset`, which `createLockRecord` allocates INTO the
909 /// record so that the record and the registry can never hold two answers
910 /// to "which set". Called before the record is published, so nothing can
911 /// hold it yet and the cell may be re-pointed without a hold.
912 ///
913 /// A name that was many-locked before its record existed already has a
914 /// registry-only cell with a real set; the record's cell takes that set
915 /// over, so the epoch that holds it goes on excluding the record.
916 ///
917 /// After [`PvDatabase::build_lock_sets`] the cell is minted a set here as
918 /// well — `createLockRecord` at the only moment C could not reach it —
919 /// so that a registered record is never on the bootstrap set once the
920 /// IOC is initialised. That is what lets every set-taker after `iocInit`
921 /// ([`Registry::every_set`], [`PvDatabase::relink_lock_sets`]) leave the
922 /// bootstrap set alone: a `LinkEdit` relink runs under a put that already
923 /// holds the seed's set, and reaching for id 0 from there would invert
924 /// the id order a `Membership` relink takes the sets in.
925 fn adopt(&mut self, name: &str, lr: &Arc<LockRecord>) {
926 match self.of_record.get(name) {
927 Some(existing) if Arc::ptr_eq(existing, lr) => {}
928 Some(existing) => {
929 lr.store(existing.set());
930 self.of_record.insert(name.to_string(), lr.clone());
931 self.revision += 1;
932 }
933 None => {
934 self.of_record.insert(name.to_string(), lr.clone());
935 if self.built {
936 self.mint_for(name, lr);
937 }
938 }
939 }
940 }
941
942 /// Give a bootstrap cell a set of its own — `dbLockInitRecords`'
943 /// `createLockRecord` for one record. **The caller holds the bootstrap
944 /// set, or nothing can hold the cell yet**: the cell is being moved out
945 /// of the bootstrap set, and [`Self::place`]'s rule is that a mover holds
946 /// the set it moves a record out of. [`Self::adopt`] is the one caller
947 /// that holds nothing — it runs before the record is in the records map,
948 /// so no thread can be inside the record's data through the cell.
949 fn mint_for(&mut self, name: &str, lr: &Arc<LockRecord>) {
950 if !lr.is_bootstrap() {
951 return;
952 }
953 let set = self.make_set();
954 lr.store(set);
955 self.active.insert(
956 set.id,
957 SetState {
958 set,
959 members: BTreeSet::from([name.to_string()]),
960 },
961 );
962 self.of_record
963 .entry(name.to_string())
964 .or_insert_with(|| lr.clone());
965 }
966
967 /// Move every member of `component` behind `set` — the ONE way a record
968 /// changes lock set, so the cell a record holds and the `of_record` entry
969 /// a name reaches can never disagree.
970 ///
971 /// **MUST be called holding the set each moved record is currently
972 /// behind.** The set is the record's data lock: a thread inside the
973 /// record's data holds that set, and moving the record out from under it
974 /// would let a holder of the destination in beside it. Holding the source
975 /// is what makes the store safe; the destination needs no hold, because
976 /// nothing is inside a record the mover has just excluded everyone from.
977 /// Every caller — [`Self::merge`], [`Self::repartition`],
978 /// [`Self::mint_for`] — is reached only through a [`PvDatabase`] method
979 /// that took those sets first, in id order, and re-read
980 /// [`Self::revision`] under the registry lock to prove they are still the
981 /// records' sets.
982 fn place(&mut self, names: impl IntoIterator<Item = String>, set: Set) {
983 self.revision += 1;
984 for name in names {
985 match self.of_record.get(&name) {
986 Some(lr) => lr.store(set),
987 None => {
988 self.of_record.insert(name, LockRecord::new(set));
989 }
990 }
991 }
992 }
993
994 /// C `dbLockSetMerge` (`dbLock.c:580-666`): every record behind
995 /// `second`'s mutex moves behind `first`'s, and the emptied set goes on
996 /// the free list with its id and mutex intact.
997 ///
998 /// The direction matters and is C's: the SOURCE record's set survives
999 /// (`dbDbLink.c:110` passes `plink->precord` first), so which id ends up
1000 /// holding a component depends on link order exactly as in C.
1001 ///
1002 /// Both names have real sets — [`PvDatabase::merge_sets`] gave them one
1003 /// and holds `second`'s, which is the one records move out of.
1004 fn merge(&mut self, first: &str, second: &str) {
1005 let (Some(a), Some(b)) = (self.real_set_of(first), self.real_set_of(second)) else {
1006 return;
1007 };
1008 let (a, b) = (a.id, b.id);
1009 if a == b {
1010 return;
1011 }
1012 let moved = self
1013 .active
1014 .remove(&b)
1015 .expect("every id in of_record names an active set");
1016 let survivor = self.active[&a].set;
1017 self.place(moved.members.iter().cloned(), survivor);
1018 let target = self
1019 .active
1020 .get_mut(&a)
1021 .expect("every id in of_record names an active set");
1022 target.members.extend(moved.members);
1023 self.free.push_back(moved.set);
1024 }
1025
1026 /// **The partition transition itself** — see [`PvDatabase::relink_lock_sets`],
1027 /// which is its only caller.
1028 ///
1029 /// `seed` is the record whose link text just moved. The affected region is
1030 /// the closure of `seed` under two relations at once: "is linked to" and
1031 /// "is currently in the same set as". Closing over both is what makes one
1032 /// rule cover creation, removal and retarget — a merge widens the region
1033 /// through the first relation, a split narrows it through the second, and
1034 /// neither needs to know which one happened.
1035 ///
1036 /// `held` is every set the caller holds, and every set a record of the
1037 /// region is behind MUST be among them — see [`Self::place`]. The seed's
1038 /// own set is real: [`PvDatabase::relink_lock_sets`] minted it before
1039 /// taking the region.
1040 fn repartition(
1041 &mut self,
1042 seed: &str,
1043 adjacency: &HashMap<String, BTreeSet<String>>,
1044 held: &[Set],
1045 ) {
1046 let Some(seed_set) = self.of_record.get(seed).map(|lr| lr.set()) else {
1047 return;
1048 };
1049 let seed_id = seed_set.id;
1050
1051 let mut affected: BTreeSet<String> = BTreeSet::new();
1052 let mut work: Vec<String> = vec![seed.to_string()];
1053 while let Some(name) = work.pop() {
1054 if !affected.insert(name.clone()) {
1055 continue;
1056 }
1057 if let Some(targets) = adjacency.get(&name) {
1058 work.extend(targets.iter().cloned());
1059 }
1060 if let Some(id) = self.real_set_of(&name).map(|set| set.id) {
1061 work.extend(self.active[&id].members.iter().cloned());
1062 }
1063 }
1064
1065 // Every set the region covers. Each is emptied below and either
1066 // re-used for one of the new components or returned to the free list.
1067 let touched: BTreeSet<u64> = affected
1068 .iter()
1069 .filter_map(|name| self.real_set_of(name).map(|set| set.id))
1070 .collect();
1071 debug_assert!(
1072 affected.iter().all(|name| {
1073 self.of_record
1074 .get(name)
1075 .is_none_or(|lr| held.iter().any(|set| std::ptr::eq(*set, lr.set())))
1076 }),
1077 "repartition reached a record behind a set the caller does not hold"
1078 );
1079
1080 // A record the database no longer has leaves the partition with it —
1081 // `dbDeleteRecord` frees the `lockRecord` — so it is dropped here
1082 // rather than being carried into a component of one.
1083 //
1084 // An instance still alive behind someone's `Arc` keeps its cell, which
1085 // goes on pointing at the set it had. C's does too, until
1086 // `dbLockCleanupRecords`. The set may later be re-minted off the free
1087 // list for another component, so such a gate can end up sharing a
1088 // mutex with live records — it over-locks a record nothing processes,
1089 // and can never under-lock one, because every LIVE record was placed
1090 // into its component's set above.
1091 for name in &affected {
1092 if !adjacency.contains_key(name) {
1093 self.of_record.remove(name);
1094 }
1095 }
1096
1097 // The components of the region, the seed's first when the seed is
1098 // still a record. Each is closed inside `affected` by construction:
1099 // `affected` was built by following the same adjacency.
1100 let seed_present = adjacency.contains_key(seed);
1101 let mut components: Vec<BTreeSet<String>> = Vec::new();
1102 let mut placed: BTreeSet<String> = BTreeSet::new();
1103 let starts = seed_present
1104 .then(|| seed.to_string())
1105 .into_iter()
1106 .chain(affected.iter().cloned());
1107 for start in starts {
1108 if placed.contains(&start) || !adjacency.contains_key(&start) {
1109 continue;
1110 }
1111 let mut component: BTreeSet<String> = BTreeSet::new();
1112 let mut walk = vec![start];
1113 while let Some(name) = walk.pop() {
1114 if !component.insert(name.clone()) {
1115 continue;
1116 }
1117 if let Some(targets) = adjacency.get(&name) {
1118 walk.extend(targets.iter().cloned());
1119 }
1120 }
1121 placed.extend(component.iter().cloned());
1122 components.push(component);
1123 }
1124
1125 // Which id each component keeps. C fixes two of these: the component
1126 // holding the edited record keeps the set that record was already in
1127 // (`dbLockSetMerge` keeps `pfirst`'s), and a component that was a
1128 // whole set already and is untouched keeps its own. A seed that has
1129 // been deleted reserves nothing — its set is freed with the rest.
1130 let mut keeps: BTreeSet<u64> = BTreeSet::new();
1131 if seed_present {
1132 keeps.insert(seed_id);
1133 }
1134 let mut assigned: Vec<Option<u64>> = Vec::with_capacity(components.len());
1135 for component in &components {
1136 if seed_present && component.contains(seed) {
1137 assigned.push(Some(seed_id));
1138 continue;
1139 }
1140 let mut ids = component
1141 .iter()
1142 .map(|name| self.real_set_of(name).map(|set| set.id));
1143 let first = ids.next().flatten();
1144 let uniform =
1145 first.filter(|id| !keeps.contains(id) && ids.all(|other| other == Some(*id)));
1146 if let Some(id) = uniform {
1147 keeps.insert(id);
1148 }
1149 assigned.push(uniform);
1150 }
1151
1152 // Mint before freeing, as C does: `dbLockSetSplit` calls `makeSet`
1153 // while the set it is splitting is still live, so the new set comes
1154 // off whatever was already on the free list rather than off the id
1155 // this very edit is about to release.
1156 let fresh: Vec<Set> = assigned
1157 .iter()
1158 .filter(|id| id.is_none())
1159 .map(|_| self.make_set())
1160 .collect();
1161 let mut fresh = fresh.into_iter();
1162
1163 for (component, id) in components.into_iter().zip(assigned) {
1164 let set = match id {
1165 Some(id) => self.active[&id].set,
1166 None => fresh
1167 .next()
1168 .expect("one fresh set per unassigned component"),
1169 };
1170 self.place(component.iter().cloned(), set);
1171 self.active.insert(
1172 set.id,
1173 SetState {
1174 set,
1175 members: component,
1176 },
1177 );
1178 }
1179
1180 for id in touched.difference(&keeps) {
1181 let dropped = self
1182 .active
1183 .remove(id)
1184 .expect("every touched id named an active set");
1185 self.free.push_back(dropped.set);
1186 }
1187 self.revision += 1;
1188 }
1189
1190 /// Every set a registered record can be behind after `iocInit`: the
1191 /// active ones. Not the bootstrap set — [`Self::adopt`] mints a set for
1192 /// every record registered after [`PvDatabase::build_lock_sets`], and
1193 /// [`PvDatabase::init_sets`] for every one registered before it, so no
1194 /// record a relink can reach is on id 0.
1195 fn every_set(&self) -> Vec<Set> {
1196 self.active.values().map(|state| state.set).collect()
1197 }
1198
1199 fn info(&self, id: u64, rows: &HashMap<u64, MutexInfo>) -> LockSetInfo {
1200 let state = &self.active[&id];
1201 LockSetInfo {
1202 id,
1203 members: state.members.iter().cloned().collect(),
1204 refs: state.members.len() + state.set.many_holds.load(Ordering::Relaxed),
1205 locked: state.set.is_locked(),
1206 mutex: rows.get(&state.set.mutex_seq).cloned(),
1207 }
1208 }
1209}
1210
1211/// The row `epicsMutexShow` prints for each lock-set mutex, keyed by
1212/// [`LockSet::mutex_seq`].
1213///
1214/// Positional because the process mutex list exposes no per-mutex accessor.
1215/// It is exact: [`Registry::make_set`] is the only `PriorityInheritanceMutex`
1216/// created in this file, so filtering by creating file selects exactly the
1217/// lock-set mutexes; creation is serialised by [`SET_MUTEX_SEQ`]; and a set's
1218/// cell is never dropped, so no entry ever leaves the list and shifts the
1219/// ones behind it.
1220fn lock_set_mutex_rows() -> HashMap<u64, MutexInfo> {
1221 mutex_report(false)
1222 .shown
1223 .into_iter()
1224 .filter(|info| info.file() == file!())
1225 .enumerate()
1226 .map(|(seq, info)| (seq as u64, info))
1227 .collect()
1228}
1229
1230/// One active lock set, as `dblsr` and `dbLockShowLocked` report it.
1231pub struct LockSetInfo {
1232 /// C's `lockSet::id`.
1233 pub id: u64,
1234 /// C's `lockRecordList`, in the order `dblsr` walks it.
1235 pub members: Vec<String>,
1236 /// C's `lockSet::refcount`: one per member record, plus one for each
1237 /// [`PvDatabase::lock_records`] epoch currently holding this set.
1238 pub refs: usize,
1239 /// Whether the set's mutex cannot be taken right now — C's
1240 /// `epicsMutexTryLock` filter in `dbLockShowLocked`.
1241 pub locked: bool,
1242 /// The `epicsMutexShow` row for this set's mutex.
1243 pub mutex: Option<MutexInfo>,
1244}
1245
1246/// What one `dblsr` / `dbLockShowLocked` call sees.
1247pub struct LockSetReport {
1248 /// C's `lockSetsActive`, in list order.
1249 pub active: Vec<LockSetInfo>,
1250 /// `ellCount(&lockSetsFree)`.
1251 pub free: usize,
1252}
1253
1254/// The lock sets of one database — C's `lockSetsActive` and `lockSetsFree`.
1255///
1256/// Every record is behind exactly one set, and a DB link puts both of its
1257/// records behind the same one. Sets are created by
1258/// [`PvDatabase::build_lock_sets`] at IOC init, and lazily for a name that
1259/// reaches [`PvDatabase::lock_record`] without one.
1260///
1261/// Nothing is ever destroyed: a merged-away set moves to the free list, and
1262/// its mutex must outlive it because a `'static` guard may still be unwinding
1263/// through it. That bounds memory by the record count, which is what C's own
1264/// free list does.
1265#[derive(Default)]
1266pub(crate) struct RecordLockRegistry {
1267 inner: std::sync::Mutex<Registry>,
1268}
1269
1270impl RecordLockRegistry {
1271 fn lock(&self) -> std::sync::MutexGuard<'_, Registry> {
1272 self.inner
1273 .lock()
1274 .unwrap_or_else(|poisoned| poisoned.into_inner())
1275 }
1276
1277 /// Which set `record` is behind right now, without creating one.
1278 fn set_id_of(&self, record: &str) -> Option<u64> {
1279 self.lock().real_set_of(record).map(|set| set.id)
1280 }
1281
1282 /// See [`Registry::adopt`].
1283 pub(crate) fn adopt(&self, name: &str, lr: &Arc<LockRecord>) {
1284 self.lock().adopt(name, lr);
1285 }
1286}
1287
1288impl PvDatabase {
1289 /// Build the lock sets — C `dbLockInitRecords` followed by the
1290 /// `dbLockSetMerge` every DB link performs as it is opened
1291 /// (`iocInit.c:178-179`, `dbDbLink.c:110`).
1292 ///
1293 /// Called once from `ioc_init`, which is why `dbLockShowLocked` on a
1294 /// loaded-but-not-initialised IOC reports `0` and `0` exactly as C's does:
1295 /// before this runs no record has a set.
1296 ///
1297 /// C merges incrementally because it cannot afford to recompute; the
1298 /// result is the same either way, because `dbLockSetSplit` is itself a
1299 /// reachability recomputation (`dbLock.c:710-717`). Doing it as C does —
1300 /// one set per record, then one merge per link — is what reproduces the
1301 /// free-list count, which a components-first construction would report as
1302 /// zero.
1303 pub fn build_lock_sets(&self) {
1304 let mut names: Vec<String> = self
1305 .inner
1306 .records
1307 .read()
1308 .keys()
1309 .map(|n| n.to_string())
1310 .collect();
1311 names.sort();
1312 // Every edge is collected BEFORE the registry lock is taken:
1313 // `record_link_fields` reads the record map and each record's own
1314 // lock, and neither may be acquired underneath the registry.
1315 let mut edges: Vec<(String, String)> = Vec::new();
1316 for name in &names {
1317 for target in self.db_link_targets(name) {
1318 edges.push((name.clone(), target));
1319 }
1320 }
1321 self.init_sets(&names);
1322 for (from, to) in edges {
1323 self.merge_sets(&from, &to);
1324 }
1325 }
1326
1327 /// C `dbLockInitRecords` (`dbLock.c:526-532`) through `createLockRecord`:
1328 /// one set per record, before any link has merged anything. The
1329 /// bootstrap set is held across the whole pass, because every record
1330 /// given a set here is being moved out of it.
1331 ///
1332 /// Every cell the registry has adopted is minted, not only `names`: a
1333 /// record adopted after `names` was read is registered but unnamed, and
1334 /// `built` promises that no registered record is on the bootstrap set
1335 /// from here on ([`Registry::every_set`]).
1336 fn init_sets(&self, names: &[String]) {
1337 let _out_of = bootstrap_set().acquire(false);
1338 let mut registry = self.inner.record_locks.lock();
1339 for name in names {
1340 let lr = registry.lock_record_of(name);
1341 registry.mint_for(name, &lr);
1342 }
1343 let mut adopted: Vec<(String, Arc<LockRecord>)> = registry
1344 .of_record
1345 .iter()
1346 .filter(|(_, lr)| lr.is_bootstrap())
1347 .map(|(name, lr)| (name.clone(), lr.clone()))
1348 .collect();
1349 adopted.sort_by(|a, b| a.0.cmp(&b.0));
1350 for (name, lr) in &adopted {
1351 registry.mint_for(name, lr);
1352 }
1353 registry.built = true;
1354 }
1355
1356 /// C `dbLockSetMerge` under C's protocol: both sets are taken before
1357 /// the registry decides anything, in id order, and the merge happens
1358 /// only if they are still the two records' sets once it is held.
1359 fn merge_sets(&self, first: &str, second: &str) {
1360 loop {
1361 let (a, b) = {
1362 let registry = self.inner.record_locks.lock();
1363 match (registry.real_set_of(first), registry.real_set_of(second)) {
1364 (Some(a), Some(b)) => (a, b),
1365 _ => return,
1366 }
1367 };
1368 if std::ptr::eq(a, b) {
1369 return;
1370 }
1371 let _held = hold_sorted(&[a, b]);
1372 let mut registry = self.inner.record_locks.lock();
1373 let unchanged = registry
1374 .real_set_of(first)
1375 .is_some_and(|set| std::ptr::eq(set, a))
1376 && registry
1377 .real_set_of(second)
1378 .is_some_and(|set| std::ptr::eq(set, b));
1379 if unchanged {
1380 registry.merge(first, second);
1381 return;
1382 }
1383 }
1384 }
1385
1386 /// The cell `name` locks through, with a real set — minted now if the
1387 /// name has none. The by-name form of [`Self::ensure_set_for`].
1388 fn ensure_set(&self, name: &str) -> Arc<LockRecord> {
1389 let lr = self.inner.record_locks.lock().lock_record_of(name);
1390 self.ensure_set_for(name, &lr);
1391 lr
1392 }
1393
1394 /// Move `lr` off the bootstrap set onto one of its own, if it is still
1395 /// there — `createLockRecord` for a record that reached a lock before
1396 /// `iocInit`; after it [`Registry::adopt`] has already done this. The
1397 /// bootstrap set is taken first and the registry under it: a mover holds
1398 /// the set it moves out of, and no set is ever taken under the registry
1399 /// lock.
1400 fn ensure_set_for(&self, name: &str, lr: &Arc<LockRecord>) {
1401 if !lr.is_bootstrap() {
1402 return;
1403 }
1404 let _out_of = bootstrap_set().acquire(false);
1405 self.inner.record_locks.lock().mint_for(name, lr);
1406 }
1407
1408 /// The records `record`'s DB links reach, canonicalised.
1409 ///
1410 /// Only a link that resolved to a LOCAL record merges: C reaches
1411 /// `dbLockSetMerge` from `dbDbInitLink`, and a target this IOC does not
1412 /// have falls through to `dbCaAddLink` instead (`dbLink.c:118-130`), which
1413 /// merges nothing. `record_link_fields` has already applied that same
1414 /// locality rule, so a `ca://` link to a local record is a `Ca` link here
1415 /// and does not widen the set.
1416 fn db_link_targets(&self, record: &str) -> Vec<String> {
1417 use crate::server::record::ParsedLink;
1418 self.record_link_fields(record)
1419 .into_iter()
1420 .filter_map(|(_, _, parsed)| match parsed {
1421 ParsedLink::Db(link) => {
1422 // `DbLink::target` is where the record name stops and the
1423 // channel filter begins — C reaches `dbLockSetMerge` with
1424 // `dbChannelRecord(chan)` (`dbDbLink.c:94-109`), the record
1425 // the whole `pvname` resolved to, so `SRC.[2]` merges into
1426 // SRC's set exactly as `SRC` does. Matching on the raw text
1427 // instead left every filtered reader in a set of its own,
1428 // and a slice read outside the source's set can tear
1429 // against the source's own processing.
1430 let name = self
1431 .resolve_alias(&link.target().record)
1432 .unwrap_or_else(|| link.target().record.clone());
1433 self.get_record_no_resolve(&name).map(|_| name)
1434 }
1435 _ => None,
1436 })
1437 .collect()
1438 }
1439
1440 /// C's `lockSetsActive` and `lockSetsFree` as `dblsr("*", n)` and
1441 /// `dbLockShowLocked(n)` read them.
1442 pub fn lock_set_report(&self) -> LockSetReport {
1443 let rows = lock_set_mutex_rows();
1444 let registry = self.inner.record_locks.lock();
1445 LockSetReport {
1446 active: registry
1447 .active
1448 .keys()
1449 .map(|id| registry.info(*id, &rows))
1450 .collect(),
1451 free: registry.free.len(),
1452 }
1453 }
1454
1455 /// The lock set one record is behind, or `None` when it has none —
1456 /// `dblsr`'s `if (!plockRecord) return 0;` before `iocInit`
1457 /// (`dbLock.c:900-901`).
1458 ///
1459 /// Does not create a set: asking about a record must not change the
1460 /// report.
1461 pub fn lock_set_of(&self, record: &str) -> Option<LockSetInfo> {
1462 let canonical = self
1463 .resolve_alias(record)
1464 .unwrap_or_else(|| record.to_string());
1465 let rows = lock_set_mutex_rows();
1466 let registry = self.inner.record_locks.lock();
1467 let id = registry.real_set_of(&canonical)?.id;
1468 Some(registry.info(id, &rows))
1469 }
1470
1471 /// The obligation to re-derive `record`'s lock set, taken out BEFORE a DB
1472 /// link field on it is written.
1473 ///
1474 /// `None` — no obligation — when the field is not a DBF link field, or
1475 /// when no lock sets exist yet: before `iocInit` C has no `lockRecord` to
1476 /// merge, so a `.db` load rewrites link text with nothing to maintain and
1477 /// [`PvDatabase::build_lock_sets`] does the whole job afterwards.
1478 ///
1479 /// Declare it ABOVE the record guard in the put body. Rust drops in
1480 /// reverse declaration order, so the record lock is down by the time the
1481 /// relink runs, which is the order the owner needs: it reads the link
1482 /// text of every record in the affected component.
1483 pub(crate) fn link_field_write<'a>(
1484 &'a self,
1485 record: &str,
1486 field: &str,
1487 ) -> Option<LockSetEdit<'a>> {
1488 let canonical = self
1489 .resolve_alias(record)
1490 .unwrap_or_else(|| record.to_string());
1491 if !self.is_dbf_link_field(&canonical, field) {
1492 return None;
1493 }
1494 if !self.inner.record_locks.lock().built {
1495 return None;
1496 }
1497 Some(LockSetEdit {
1498 db: self,
1499 record: canonical,
1500 scope: RelinkScope::LinkEdit,
1501 })
1502 }
1503
1504 /// The same obligation for a change of MEMBERSHIP rather than of link
1505 /// text: a record added or removed after `iocInit`, or an alias that
1506 /// makes a link resolve to a record it did not resolve to before.
1507 ///
1508 /// One owner covers all three because the owner re-derives the component
1509 /// instead of tracking a verb — see the module doc. Declare it above the
1510 /// mutation, so the relink sees the database as it is afterwards.
1511 pub(crate) fn lock_set_membership_change<'a>(
1512 &'a self,
1513 record: &str,
1514 ) -> Option<LockSetEdit<'a>> {
1515 if !self.inner.record_locks.lock().built {
1516 return None;
1517 }
1518 Some(LockSetEdit {
1519 db: self,
1520 record: self
1521 .resolve_alias(record)
1522 .unwrap_or_else(|| record.to_string()),
1523 scope: RelinkScope::Membership,
1524 })
1525 }
1526
1527 /// **The single owner of the lock-set partition after `iocInit`.**
1528 ///
1529 /// Re-derives the connected component `record` now sits in and
1530 /// re-partitions every set that component touches. C reaches the same
1531 /// result through `dbLockSetMerge` on link creation and `dbLockSetSplit`
1532 /// on removal (`dbDbLink.c:110`, `:124`, `:141`); see the module doc for
1533 /// why the port re-derives instead of tracking the endpoint pair.
1534 ///
1535 /// Private, and reachable only by dropping a [`LockSetEdit`].
1536 ///
1537 /// The sets are the records' data locks, so the region is taken before
1538 /// it is read and held while it is re-partitioned — C's `dbPutFieldLink`
1539 /// holds both `dbLockSetMerge` operands through `dbScanLockMany`
1540 /// (`dbAccess.c:1115-1123`, `dbLock.c:587-607`). Which sets make up the
1541 /// region is the scope's business:
1542 ///
1543 /// * [`RelinkScope::LinkEdit`] — the seed's set and the sets of the
1544 /// records its links now reach. Both are what the put that landed the
1545 /// link already holds ([`PvDatabase::acquire_put_gate`] takes them as
1546 /// `dbPutFieldLink` does), so the acquisition here is a re-entry. The
1547 /// partition invariant puts every OTHER link of the region inside it;
1548 /// a target found outside is an invariant already broken, and the loop
1549 /// widens the region to it rather than move a record it does not hold.
1550 /// * [`RelinkScope::Membership`] — every active set. A record added,
1551 /// removed or newly reachable through an alias can join or leave a
1552 /// component from anywhere, and the links that name it can be in any
1553 /// set. Registration-path only, with no set held by the caller, so
1554 /// taking them all in id order is the plain `dbScanLockMany`
1555 /// discipline.
1556 ///
1557 /// Neither scope takes the bootstrap set: after `iocInit` no registered
1558 /// record is on it ([`Registry::adopt`] mints the set before the record
1559 /// is published), and a `LinkEdit` relink, entered under the seed's set,
1560 /// could not take id 0 in id order anyway — a `Membership` relink that
1561 /// held it first would be waiting on the seed's set.
1562 ///
1563 /// Sets are taken first and the registry lock under them, never the
1564 /// reverse; [`Registry::revision`] proves, under that lock, that the sets
1565 /// held are still the ones the region's records are behind.
1566 fn relink_lock_sets(&self, record: &str, scope: RelinkScope) {
1567 let mut region: Vec<Set> = Vec::new();
1568 loop {
1569 let revision = {
1570 let registry = self.inner.record_locks.lock();
1571 match scope {
1572 RelinkScope::Membership => region = registry.every_set(),
1573 RelinkScope::LinkEdit => {
1574 if region.is_empty() {
1575 region.extend(registry.real_set_of(record));
1576 }
1577 }
1578 }
1579 registry.revision
1580 };
1581 if scope == RelinkScope::LinkEdit {
1582 // The targets are read under the seed's set, which the put
1583 // holds; their sets come from the registry, not from a lock.
1584 let targets = self.db_link_targets(record);
1585 let registry = self.inner.record_locks.lock();
1586 for target in &targets {
1587 let Some(set) = registry.set_of_registered(target) else {
1588 continue;
1589 };
1590 if !region.iter().any(|held| std::ptr::eq(*held, set)) {
1591 region.push(set);
1592 }
1593 }
1594 }
1595 let held = hold_sorted(®ion);
1596 let adjacency = match scope {
1597 RelinkScope::Membership => self.db_link_adjacency(),
1598 RelinkScope::LinkEdit => {
1599 let members: Vec<String> = {
1600 let registry = self.inner.record_locks.lock();
1601 region
1602 .iter()
1603 .filter_map(|set| registry.active.get(&set.id))
1604 .flat_map(|state| state.members.iter().cloned())
1605 .collect()
1606 };
1607 self.db_link_adjacency_of(&members)
1608 }
1609 };
1610 let mut registry = self.inner.record_locks.lock();
1611 if registry.revision != revision {
1612 drop(registry);
1613 drop(held);
1614 continue;
1615 }
1616 let outside: Vec<Set> = adjacency
1617 .values()
1618 .flatten()
1619 .filter_map(|name| registry.set_of_registered(name))
1620 .filter(|set| !region.iter().any(|held| std::ptr::eq(*held, *set)))
1621 .collect();
1622 if !outside.is_empty() {
1623 region.extend(outside);
1624 region.sort_unstable_by_key(|set| set.id);
1625 region.dedup_by_key(|set| set.id);
1626 drop(registry);
1627 drop(held);
1628 continue;
1629 }
1630 registry.repartition(record, &adjacency, ®ion);
1631 return;
1632 }
1633 }
1634
1635 /// The DB-link graph as an undirected adjacency map.
1636 ///
1637 /// Undirected because C's merge is: `dbLockSetMerge(locker, plink->precord,
1638 /// target)` puts both endpoints behind one mutex regardless of which way
1639 /// the link points, and `dbLockSetSplit` walks `bklnk` as well as the
1640 /// record's own links (`dbLock.c:735-770`) for the same reason. A record
1641 /// that is only ever pointed AT is as much a member as the one pointing.
1642 fn db_link_adjacency(&self) -> HashMap<String, BTreeSet<String>> {
1643 let names: Vec<String> = self
1644 .inner
1645 .records
1646 .read()
1647 .keys()
1648 .map(|n| n.to_string())
1649 .collect();
1650 self.db_link_adjacency_of(&names)
1651 }
1652
1653 /// [`Self::db_link_adjacency`] over `names` only — the members of the
1654 /// sets a [`RelinkScope::LinkEdit`] relink holds. A target outside
1655 /// `names` still appears as a neighbour, which is how the relink notices
1656 /// a set it does not hold.
1657 fn db_link_adjacency_of(&self, names: &[String]) -> HashMap<String, BTreeSet<String>> {
1658 let mut adjacency: HashMap<String, BTreeSet<String>> = HashMap::new();
1659 for name in names {
1660 adjacency.entry(name.clone()).or_default();
1661 }
1662 for name in names {
1663 for target in self.db_link_targets(name) {
1664 adjacency
1665 .entry(name.clone())
1666 .or_default()
1667 .insert(target.clone());
1668 adjacency.entry(target).or_default().insert(name.clone());
1669 }
1670 }
1671 adjacency
1672 }
1673}
1674
1675/// A DB link field write that has landed in the record but not yet in the
1676/// lock-set graph.
1677///
1678/// The token exists so that the illegal state — link text changed, partition
1679/// stale — cannot be constructed rather than merely being checked for. It is
1680/// minted by [`PvDatabase::link_field_write`], holds the only reference
1681/// through which [`PvDatabase::relink_lock_sets`] is reachable, and performs
1682/// the relink from its destructor, so no exit path of a put body can skip it.
1683#[must_use = "the lock-set graph is only re-derived when this is dropped; binding it to `_` drops it immediately and relinks too early"]
1684pub(crate) struct LockSetEdit<'a> {
1685 db: &'a PvDatabase,
1686 record: String,
1687 scope: RelinkScope,
1688}
1689
1690impl Drop for LockSetEdit<'_> {
1691 fn drop(&mut self) {
1692 self.db.relink_lock_sets(&self.record, self.scope);
1693 }
1694}
1695
1696/// Which sets a relink must hold — see [`PvDatabase::relink_lock_sets`].
1697#[derive(Clone, Copy, PartialEq, Eq, Debug)]
1698enum RelinkScope {
1699 /// A DB link field of the seed was written; the put holds the seed's set
1700 /// and the new target's.
1701 LinkEdit,
1702 /// The seed joined or left the database, or an alias changed what a
1703 /// link naming it resolves to; nothing is held.
1704 Membership,
1705}
1706
1707/// RAII guard for one record's lock set.
1708///
1709/// Held for the duration of a plain CA/PVA write — the `dbScanLock` +
1710/// `dbScanUnlock` pair around one `dbPutField`. `!Send`, so the compiler
1711/// refuses to let it live across an `.await` in any spawned future; see the
1712/// module doc.
1713#[must_use = "the lock set is released as soon as the guard is dropped"]
1714pub struct RecordWriteGuard {
1715 _guard: SetGuard,
1716}
1717
1718/// RAII guard for the lock sets of a declared record set — the `DBManyLocker`
1719/// equivalent.
1720///
1721/// Acquired by [`PvDatabase::lock_records`] over every member record of a
1722/// multi-record transaction (QSRV atomic group PUT/GET, pvalink atomic
1723/// scan-on-update epoch) and held across the whole member loop. While alive,
1724/// every plain write to any record of any of those sets blocks.
1725#[must_use = "the locked epoch ends as soon as the guard is dropped"]
1726pub struct ManyRecordWriteGuard {
1727 _guards: Vec<SetGuard>,
1728}
1729
1730impl PvDatabase {
1731 /// Acquire the lock set of a single record — the `dbScanLock(precord)`
1732 /// analogue.
1733 ///
1734 /// `record` is alias-resolved here and nowhere else: this is the single
1735 /// owner of "which lock set does this name name", so an alias and its
1736 /// target always reach the same set and no caller has to resolve first to
1737 /// make that true. Resolution borrows — a name that is not an alias, which
1738 /// is every name in a database that declares none, reaches the lookup
1739 /// without a copy of itself being made.
1740 ///
1741 /// **Blocks the calling thread** when another thread holds the set. A
1742 /// thread that already holds it recurses, as C's recursive `epicsMutex`
1743 /// does — which is not optional once a set spans a whole link component,
1744 /// because processing a record and then its link target takes one mutex
1745 /// twice.
1746 pub fn lock_record(&self, record: &str) -> RecordWriteGuard {
1747 let resolved = self.resolve_alias(record);
1748 let canonical: &str = resolved.as_deref().unwrap_or(record);
1749 let lr = self.ensure_set(canonical);
1750 RecordWriteGuard {
1751 _guard: lr.acquire(),
1752 }
1753 }
1754
1755 /// C `dbScanLock(precord)` itself — the gate taken by a caller that
1756 /// already holds the record, which in C is the only form there is.
1757 ///
1758 /// Prefer it to [`Self::lock_record`] wherever the record is in hand. It
1759 /// reads the record's own cell and takes the set's mutex, and does
1760 /// nothing else; reaching the same set by name costs an alias lookup plus
1761 /// a hash of the record name under the registry mutex, twice. On the
1762 /// process path — which always has the record, having just read it out of
1763 /// the records map — that was 0.85 us of a 11.5 us calc cycle, measured
1764 /// on 2000 records at 10 Hz.
1765 pub fn lock_instance(&self, rec: &Arc<RecordCell>) -> RecordWriteGuard {
1766 let lr = rec.lock_record();
1767 if lr.is_bootstrap() {
1768 // First gate this record has ever been given. C mints the set in
1769 // `dbLockInitRecords` before anything can lock; the port allows a
1770 // database with no `iocInit` at all, so the set is minted on
1771 // demand here — once per record, off every later pass. The name
1772 // is read and the guard dropped BEFORE the move: a guard taken
1773 // through the bootstrap set must not outlive the record's stay
1774 // in it.
1775 let name = rec.read().name.clone();
1776 self.ensure_set_for(&name, lr);
1777 }
1778 RecordWriteGuard {
1779 _guard: lr.acquire(),
1780 }
1781 }
1782
1783 /// Acquire the lock sets covering a set of records — the `DBManyLock` /
1784 /// `DBManyLocker` equivalent, C's `dbScanLockMany` (`dbLock.c:384-440`).
1785 ///
1786 /// Every name is alias-resolved, mapped to its lock set, then the sets are
1787 /// sorted by id and de-duplicated before any is taken. Sorting gives two
1788 /// overlapping transactions the same acquisition order so they cannot
1789 /// deadlock; de-duplication is C's own — several member records commonly
1790 /// share one set, and `dbScanLockMany` skips the repeats (`:399-402`).
1791 ///
1792 /// The returned [`ManyRecordWriteGuard`] must be held for the whole
1793 /// transaction. Each set it holds reports one extra ref while it lives,
1794 /// which is the `+1` C's locked list adds.
1795 ///
1796 /// Names that do not resolve to a record still get a set, matching
1797 /// `dbLockerAlloc`, which accepts the record pointers it is given without
1798 /// a liveness re-check.
1799 pub fn lock_records<I, S>(&self, records: I) -> ManyRecordWriteGuard
1800 where
1801 I: IntoIterator<Item = S>,
1802 S: AsRef<str>,
1803 {
1804 let names: Vec<String> = records
1805 .into_iter()
1806 .map(|record| {
1807 let record = record.as_ref();
1808 self.resolve_alias(record)
1809 .unwrap_or_else(|| record.to_string())
1810 })
1811 .collect();
1812 let cells: Vec<Arc<LockRecord>> = names.iter().map(|name| self.ensure_set(name)).collect();
1813
1814 loop {
1815 let mut sets: Vec<Set> = cells.iter().map(|lr| lr.set()).collect();
1816 sets.sort_unstable_by_key(|set| set.id);
1817 sets.dedup_by_key(|set| set.id);
1818
1819 let guards: Vec<SetGuard> = sets.iter().map(|set| set.acquire(true)).collect();
1820
1821 // `dbLockUpdateRefs(locker, 0)` (`dbLock.c:432-436`): if a merge
1822 // moved any member while the sets were being taken, release
1823 // everything and start again.
1824 let held: BTreeSet<u64> = sets.iter().map(|set| set.id).collect();
1825 if names
1826 .iter()
1827 .all(|name| matches!(self.inner.record_locks.set_id_of(name), Some(id) if held.contains(&id)))
1828 {
1829 return ManyRecordWriteGuard { _guards: guards };
1830 }
1831 drop(guards);
1832 }
1833 }
1834}
1835
1836#[cfg(test)]
1837mod tests {
1838 use super::*;
1839 use std::sync::Arc;
1840 use std::sync::atomic::{AtomicUsize, Ordering};
1841 use std::time::Duration;
1842
1843 // These are plain `#[test]`s, not `#[tokio::test]`s, and that is forced
1844 // rather than stylistic: the gate blocks the calling *thread*, so a
1845 // contending waiter has to be a real thread. Parking one on a
1846 // `current_thread` runtime's only worker would wedge the runtime instead
1847 // of demonstrating exclusion. Being reactor-free they also run on the exec
1848 // backend and add no site to this file's RTEMS-EXEC-MODEL-ALLOW census.
1849
1850 /// Long enough that a non-blocking (broken) gate would have let the
1851 /// contender through, short enough not to dominate the suite.
1852 const SETTLE: Duration = Duration::from_millis(50);
1853
1854 /// A single-record gate excludes a concurrent same-record locker.
1855 #[test]
1856 fn lock_record_excludes_same_record() {
1857 let db = PvDatabase::new();
1858 let order = Arc::new(AtomicUsize::new(0));
1859
1860 let g = db.lock_record("ai:1");
1861
1862 let db2 = db.clone();
1863 let order2 = order.clone();
1864 let h = std::thread::spawn(move || {
1865 let _g2 = db2.lock_record("ai:1");
1866 // This must observe the first holder having released (1).
1867 order2.fetch_add(10, Ordering::SeqCst);
1868 });
1869
1870 // Give the spawned thread time to block on the gate.
1871 std::thread::sleep(SETTLE);
1872 // First holder still owns the gate: counter untouched.
1873 assert_eq!(order.load(Ordering::SeqCst), 0);
1874 order.fetch_add(1, Ordering::SeqCst);
1875 drop(g);
1876
1877 h.join().unwrap();
1878 assert_eq!(order.load(Ordering::SeqCst), 11);
1879 }
1880
1881 /// `lock_records` blocks a plain single-record write to a member.
1882 #[test]
1883 fn lock_records_excludes_single_member_write() {
1884 let db = PvDatabase::new();
1885 let many = db.lock_records(["g:a", "g:b", "g:c"]);
1886
1887 let db2 = db.clone();
1888 let acquired = Arc::new(AtomicUsize::new(0));
1889 let acquired2 = acquired.clone();
1890 let h = std::thread::spawn(move || {
1891 // Plain write to a member must block until `many` drops.
1892 let _g = db2.lock_record("g:b");
1893 acquired2.store(1, Ordering::SeqCst);
1894 });
1895
1896 std::thread::sleep(SETTLE);
1897 assert_eq!(
1898 acquired.load(Ordering::SeqCst),
1899 0,
1900 "single-member write must block while ManyRecordWriteGuard is held"
1901 );
1902
1903 drop(many);
1904 h.join().unwrap();
1905 assert_eq!(acquired.load(Ordering::SeqCst), 1);
1906 }
1907
1908 /// Two overlapping `lock_records` sets acquire in canonical order
1909 /// and therefore cannot deadlock even with reversed input order.
1910 ///
1911 /// With blocking gates a violated order wedges both threads outright,
1912 /// which is why this runs the two sets on real threads and joins them
1913 /// under a bounded wait rather than trusting a scheduler yield.
1914 #[test]
1915 fn lock_records_overlapping_sets_no_deadlock() {
1916 let db = PvDatabase::new();
1917 let done = Arc::new(AtomicUsize::new(0));
1918
1919 let handles: Vec<_> = [["x", "y", "z"], ["z", "y", "x"]]
1920 .into_iter()
1921 .map(|set| {
1922 let db = db.clone();
1923 let done = done.clone();
1924 std::thread::spawn(move || {
1925 for _ in 0..500 {
1926 // Reversed input order on one side — sort makes the
1927 // real acquisition order identical, so no deadlock.
1928 let _g = db.lock_records(set);
1929 std::thread::yield_now();
1930 }
1931 done.fetch_add(1, Ordering::SeqCst);
1932 })
1933 })
1934 .collect();
1935
1936 let deadline = std::time::Instant::now() + Duration::from_secs(10);
1937 while done.load(Ordering::SeqCst) < 2 {
1938 assert!(
1939 std::time::Instant::now() < deadline,
1940 "overlapping lock_records sets must not deadlock"
1941 );
1942 std::thread::sleep(Duration::from_millis(10));
1943 }
1944 for h in handles {
1945 h.join().unwrap();
1946 }
1947 }
1948
1949 /// An epoch over a record set excludes a second epoch that shares
1950 /// any record until the first guard drops — and overlapping sets
1951 /// listed in opposite orders never deadlock (sorted acquisition).
1952 #[test]
1953 fn overlapping_epochs_are_mutually_exclusive_and_deadlock_free() {
1954 let db = PvDatabase::new();
1955 let a = vec!["RECA".to_string(), "RECB".to_string()];
1956 // Opposite order on purpose — sorted acquisition must still
1957 // make this safe.
1958 let b = vec!["RECB".to_string(), "RECC".to_string()];
1959
1960 let guard_a = db.lock_records(&a);
1961
1962 // A second epoch sharing RECB must not be acquirable while
1963 // `guard_a` is alive.
1964 let db2 = db.clone();
1965 let entered = Arc::new(AtomicUsize::new(0));
1966 let entered2 = entered.clone();
1967 let handle = std::thread::spawn(move || {
1968 let _guard_b = db2.lock_records(&b);
1969 entered2.store(1, Ordering::SeqCst);
1970 });
1971
1972 std::thread::sleep(SETTLE);
1973 assert_eq!(
1974 entered.load(Ordering::SeqCst),
1975 0,
1976 "epoch B must block on shared RECB"
1977 );
1978
1979 drop(guard_a);
1980 handle.join().expect("epoch B thread");
1981 assert_eq!(entered.load(Ordering::SeqCst), 1);
1982 }
1983
1984 /// Two non-overlapping epochs run concurrently — no false
1985 /// serialisation. Taking the second on *this* thread while the first is
1986 /// still held is the assertion: a gate keyed too coarsely would
1987 /// self-deadlock here rather than merely being slow.
1988 #[test]
1989 fn disjoint_epochs_do_not_block_each_other() {
1990 let db = PvDatabase::new();
1991 let _g1 = db.lock_records(&["X1".to_string()]);
1992 // Disjoint set: must acquire immediately without blocking.
1993 let _g2 = db.lock_records(&["X2".to_string()]);
1994 }
1995
1996 /// One name reaches one set, and two unlinked records reach two — C's
1997 /// state straight after `dbLockInitRecords`, before any link merges.
1998 #[test]
1999 fn a_name_reaches_one_set_and_two_unlinked_records_reach_two() {
2000 let db = PvDatabase::new();
2001 assert!(
2002 std::ptr::eq(db.ensure_set("REC:A").set(), db.ensure_set("REC:A").set()),
2003 "the same canonical name must map to the same lock set"
2004 );
2005 assert!(
2006 !std::ptr::eq(db.ensure_set("REC:A").set(), db.ensure_set("REC:B").set()),
2007 "records no link joins must not share a lock set"
2008 );
2009 }
2010
2011 /// A second epoch overlapping the first on one thread recurses rather
2012 /// than wedging. C's refusal (`cantProceed("dbScanLockMany(%p) already
2013 /// locked...")`, `dbLock.c:392-395`) is about re-using ONE `dbLocker`,
2014 /// which has no analogue here — every call builds its own — while the
2015 /// overlap itself is what C's recursive `epicsMutex` absorbs.
2016 #[test]
2017 fn a_second_overlapping_epoch_on_one_thread_recurses() {
2018 let db = PvDatabase::new();
2019 let _epoch = db.lock_records(&["RE:A".to_string(), "RE:B".to_string()]);
2020 let _overlapping = db.lock_records(&["RE:B".to_string(), "RE:C".to_string()]);
2021 }
2022
2023 /// `dbScanLock` recurses because `epicsMutex` must (`epicsMutex.h:16`,
2024 /// `:38`), and once a lock set spans a link component the port has no
2025 /// choice either: processing a record and then its link target is this
2026 /// sequence with two different names behind one mutex.
2027 #[test]
2028 fn re_taking_one_record_s_set_recurses_like_db_scan_lock() {
2029 let db = PvDatabase::new();
2030 let _held = db.lock_record("RE:SELF");
2031 let _again = db.lock_record("RE:SELF");
2032 }
2033
2034 /// The recursion is per THREAD: a second thread still blocks, and the
2035 /// depth the first one built up does not let it through early.
2036 #[test]
2037 fn recursion_does_not_let_a_second_thread_in() {
2038 let db = PvDatabase::new();
2039 let outer = db.lock_record("RE:DEPTH");
2040 let inner = db.lock_record("RE:DEPTH");
2041
2042 let db2 = db.clone();
2043 let entered = Arc::new(AtomicUsize::new(0));
2044 let entered2 = entered.clone();
2045 let h = std::thread::spawn(move || {
2046 let _g = db2.lock_record("RE:DEPTH");
2047 entered2.store(1, Ordering::SeqCst);
2048 });
2049
2050 std::thread::sleep(SETTLE);
2051 assert_eq!(entered.load(Ordering::SeqCst), 0, "outer level still held");
2052 drop(inner);
2053 std::thread::sleep(SETTLE);
2054 assert_eq!(
2055 entered.load(Ordering::SeqCst),
2056 0,
2057 "one release of two must not hand the set over"
2058 );
2059 drop(outer);
2060 h.join().unwrap();
2061 assert_eq!(entered.load(Ordering::SeqCst), 1);
2062 }
2063
2064 /// The positional association between a set and its `epicsMutexShow` row
2065 /// is only exact while `make_set` is the ONLY `PriorityInheritanceMutex`
2066 /// created in this file. This is that check: one row for every set ever
2067 /// made in this process, and not one more.
2068 #[test]
2069 fn this_file_creates_no_mutex_but_lock_sets() {
2070 let db = PvDatabase::new();
2071 for name in ["MS:1", "MS:2", "MS:3"] {
2072 drop(db.lock_record(name));
2073 }
2074 let made = *SET_MUTEX_SEQ.lock().unwrap();
2075 assert_eq!(
2076 lock_set_mutex_rows().len() as u64,
2077 made,
2078 "a second mutex created in this file would shift every set's row"
2079 );
2080 for set in db.lock_set_report().active {
2081 assert!(set.mutex.is_some(), "set {} has no row", set.id);
2082 }
2083 }
2084
2085 /// `RecordCell::read_in` rides the set its caller holds when the target
2086 /// is in that set — C's `dbScanLock` on a member of the held set is a
2087 /// recursion, which the port skips outright — and takes the target's own
2088 /// set when it is not.
2089 #[test]
2090 fn read_in_rides_the_held_set_and_takes_another() {
2091 use crate::server::record::{RecordCell, RecordInstance};
2092 use crate::server::records::calc::CalcRecord;
2093 let db = PvDatabase::new();
2094 let cell = |name: &str| {
2095 let cell = Arc::new(RecordCell::new(RecordInstance::new(
2096 name.into(),
2097 CalcRecord::default(),
2098 )));
2099 db.inner.record_locks.adopt(name, cell.lock_record());
2100 db.ensure_set_for(name, cell.lock_record());
2101 cell
2102 };
2103 let reader = cell("RI:A");
2104 let linked = cell("RI:B");
2105 let apart = cell("RI:C");
2106 db.merge_sets("RI:A", "RI:B");
2107
2108 let mut held = reader.write();
2109 let (_, set) = held.split();
2110 assert!(
2111 linked.read_in(set).rides_held_set(),
2112 "a record of the held set must be read without a second hold"
2113 );
2114 assert!(
2115 !apart.read_in(set).rides_held_set(),
2116 "a record of another set must take that set"
2117 );
2118 }
2119
2120 /// The set is released with the guard, so the ordinary sequential
2121 /// pattern — lock, write, drop, lock again — is untouched.
2122 #[test]
2123 fn the_set_is_released_when_the_guard_drops() {
2124 let db = PvDatabase::new();
2125 drop(db.lock_record("RE:SEQ"));
2126 drop(db.lock_record("RE:SEQ"));
2127 drop(db.lock_records(&["RE:SEQ".to_string()]));
2128 // And a disjoint pair may be held together on one thread.
2129 let _a = db.lock_record("RE:ONE");
2130 let _b = db.lock_record("RE:TWO");
2131 }
2132
2133 /// A record cell as `add_loaded_record` hands it to the registry:
2134 /// adopted, not yet in the records map, no lock taken through it.
2135 fn adopted_cell(db: &PvDatabase, name: &str) -> Arc<crate::server::record::RecordCell> {
2136 use crate::server::record::{RecordCell, RecordInstance};
2137 use crate::server::records::calc::CalcRecord;
2138 let cell = Arc::new(RecordCell::new(RecordInstance::new(
2139 name.into(),
2140 CalcRecord::default(),
2141 )));
2142 db.inner.record_locks.adopt(name, cell.lock_record());
2143 cell
2144 }
2145
2146 /// Boundary `built == false`: a record adopted before `iocInit` waits on
2147 /// the bootstrap set, as C's null `lset` does until `dbLockInitRecords`.
2148 #[test]
2149 fn adopt_before_build_leaves_the_record_on_the_bootstrap_set() {
2150 let db = PvDatabase::new();
2151 let cell = adopted_cell(&db, "AD:PRE");
2152 assert!(cell.lock_record().is_bootstrap());
2153 assert_eq!(db.inner.record_locks.set_id_of("AD:PRE"), None);
2154 }
2155
2156 /// Boundary `built == true`: a record adopted after `iocInit` has a real
2157 /// set before anything can lock it, so no relink has to reach for the
2158 /// bootstrap set on its behalf.
2159 #[test]
2160 fn adopt_after_build_mints_the_record_s_set() {
2161 let db = PvDatabase::new();
2162 db.build_lock_sets();
2163 let cell = adopted_cell(&db, "AD:POST");
2164 assert!(!cell.lock_record().is_bootstrap());
2165 assert_eq!(
2166 db.inner.record_locks.set_id_of("AD:POST"),
2167 Some(cell.lock_record().set().id)
2168 );
2169 }
2170
2171 /// A cell adopted before `build_lock_sets` but absent from the records
2172 /// map it reads its names from is still minted by `init_sets`: `built`
2173 /// promises no registered record is on the bootstrap set.
2174 #[test]
2175 fn build_lock_sets_mints_every_adopted_cell() {
2176 let db = PvDatabase::new();
2177 let cell = adopted_cell(&db, "AD:STRAGGLER");
2178 assert!(cell.lock_record().is_bootstrap());
2179 db.build_lock_sets();
2180 assert!(!cell.lock_record().is_bootstrap());
2181 assert!(db.inner.record_locks.set_id_of("AD:STRAGGLER").is_some());
2182 }
2183
2184 /// Boundary: a fresh set taken under a records-map read guard is the
2185 /// reader `remove_record_entry` can be waiting on while it holds that
2186 /// set. Debug builds fail on the offending thread.
2187 #[cfg(debug_assertions)]
2188 #[test]
2189 #[should_panic(expected = "read guard on the records map")]
2190 fn a_fresh_set_under_the_map_read_guard_is_refused() {
2191 let db = PvDatabase::new();
2192 let _map = db.inner.records.read();
2193 let _g = db.lock_record("MR:FRESH");
2194 }
2195
2196 /// Boundary: the same under the alias table's guard.
2197 #[cfg(debug_assertions)]
2198 #[test]
2199 #[should_panic(expected = "read guard on the records map")]
2200 fn a_fresh_set_under_the_alias_read_guard_is_refused() {
2201 let db = PvDatabase::new();
2202 let _map = db.inner.aliases.read();
2203 let _g = db.lock_record("MR:ALIAS");
2204 }
2205
2206 /// Boundary: a set this thread already holds is re-entered, not taken,
2207 /// so reading the map inside a held set and reaching the same record
2208 /// again — every `get_record` on the process path — is untouched. And
2209 /// once the guard is dropped, a fresh set is ordinary again.
2210 #[test]
2211 fn re_entry_under_the_map_read_guard_and_a_fresh_set_after_it_are_fine() {
2212 let db = PvDatabase::new();
2213 let _held = db.lock_record("MR:HELD");
2214 {
2215 let _map = db.inner.records.read();
2216 drop(db.lock_record("MR:HELD"));
2217 }
2218 drop(db.lock_record("MR:OTHER"));
2219 }
2220
2221 /// The set a `Membership` relink takes never includes the bootstrap set,
2222 /// so no relink can invert id order against a `LinkEdit` relink that
2223 /// enters already holding a seed's set.
2224 #[test]
2225 fn every_set_after_build_excludes_the_bootstrap_set() {
2226 let db = PvDatabase::new();
2227 adopted_cell(&db, "AD:ES1");
2228 db.build_lock_sets();
2229 adopted_cell(&db, "AD:ES2");
2230 let registry = db.inner.record_locks.lock();
2231 let sets = registry.every_set();
2232 assert_eq!(sets.len(), 2);
2233 assert!(
2234 sets.iter().all(|set| !std::ptr::eq(*set, bootstrap_set())),
2235 "every_set() must not hand a relink the bootstrap set"
2236 );
2237 }
2238}