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 behind its own
40//! `parking_lot::RwLock<RecordInstance>`, but the put/process helpers
41//! (`put_record_field_from_ca`, `put_pv`, `process_record`,
42//! `process_record_with_links`) acquire that `RwLock` *internally*
43//! and recurse into link targets, so a caller cannot hold N
44//! `write_owned()` guards across the member loop without dead-locking
45//! the recursive link processing.
46//!
47//! This module adds the missing layer: C's **lock sets**, one mutex per
48//! connected component of the DB-link graph, with the record-to-set map C
49//! keeps in `dbCommon::lset`.
50//!
51//! * A plain CA/PVA write (`put_record_field_from_ca`, `put_pv`,
52//! `process_record`) takes the target record's set for the duration of the
53//! write via [`PvDatabase::lock_record`] — `dbScanLock`.
54//! * A multi-record transaction — the QSRV atomic group PUT/GET and the
55//! pvalink atomic scan-on-update epoch — takes every set its members are
56//! behind, up front and in set-id order, via [`PvDatabase::lock_records`] —
57//! `dbScanLockMany`, including its skip of the duplicates that appear when
58//! several members share one set.
59//!
60//! Because every path resolves through the same registry, a direct
61//! backing-record write blocks until the transaction owning that record
62//! finishes, a QSRV atomic group PUT and a pvalink atomic scan can never
63//! interleave on a shared record, and — this is what the per-record gate could
64//! not do — two records a link joins are serialised against each other exactly
65//! as C serialises them.
66//!
67//! The gate is *advisory*: it does not replace the per-record
68//! `parking_lot::RwLock<RecordInstance>` that still guards the record's data. It
69//! is an additional serialization layer that the multi-record
70//! transaction owner and the single-record writers both honour,
71//! exactly as `dbScanLock` is a layer above the record's own field
72//! storage.
73//!
74//! ### How a set is built, and what still is not
75//!
76//! One `lockSet` carries a single `epicsMutexId lock` and an `ELLLIST
77//! lockRecordList` of the records behind it (`dbLockPvt.h:29-44`, the list at
78//! `:31`, the mutex at `:32`), and `dbCommon.LSET` is a `lockRecord *` pointing
79//! into that set (`:48`, `:53-70`). Membership is not static: creating a DB
80//! link merges the two records' sets (`dbDbLink.c:110` and `:124`, both
81//! `dbLockSetMerge`) and removing one splits them again (`:141`
82//! `dbLockSetSplit`), so a record and everything its links reach sit behind
83//! ONE mutex.
84//!
85//! [`PvDatabase::build_lock_sets`] reproduces both halves of C's construction
86//! in the order C performs them — `dbLockInitRecords` gives every record its
87//! own set, then one merge per DB link (`iocInit.c:178-179`) — which is why
88//! `dbLockShowLocked` reports `0` and `0` before `iocInit` and why the sets a
89//! merge empties show up on the free list rather than vanishing.
90//!
91//! ### The runtime relink, and who owns it
92//!
93//! **Invariant.** The partition the registry holds MUST equal the connected
94//! components of the DB-link graph over the records the database currently
95//! has. No path may leave a link field written and the partition unchanged.
96//!
97//! **Owner.** [`PvDatabase::relink_lock_sets`] is the only mutator of the
98//! partition after [`PvDatabase::build_lock_sets`], and it is private:
99//! nothing outside this module can call it. The only way to reach it is to
100//! hold a [`LockSetEdit`], whose destructor calls it — so a link-field
101//! write that does not relink is not something a caller can forget to do,
102//! it is something they cannot express. Every exit path of the write body
103//! (`?`, an early `return`, a panic unwind) drops the token and relinks.
104//!
105//! **Why a re-partition and not C's incremental pair.** C calls
106//! `dbLockSetMerge` on link creation and `dbLockSetSplit` on removal
107//! (`dbDbLink.c:110`, `:124`, `:141`), and the split is *itself* a
108//! breadth-first reachability recomputation over the live graph
109//! (`dbLock.c:710-760`) — C only avoids recomputing on the merge side
110//! because it already knows the two endpoints. Keeping the old target on
111//! the port's side to reproduce that pair would mean storing the edge set a
112//! second time, next to the link text that already is the edge set, and a
113//! second copy of a fact is what goes stale. The owner therefore re-derives
114//! the affected component from the live link text and re-partitions it,
115//! which is one rule for creation, removal, retarget, record deletion and
116//! alias changes alike instead of a case per verb.
117//!
118//! Set ids follow C where C fixes them: the component holding the edited
119//! record keeps its id (C's `dbLockSetMerge` keeps `pfirst`'s set, and
120//! `pfirst` is the record whose link moved), a component that splits off
121//! takes a fresh set from the free list as `makeSet` does, and the sets a
122//! merge empties go back on the free list.
123//!
124//! `field_io.rs`'s `NotifyClaim` is unchanged by this: it closes dbNotify's
125//! test-then-install window inside the critical section that tested the slot,
126//! which is a stronger guarantee than the lock-set region it was standing in
127//! for, not a substitute that lock sets now make unnecessary.
128//!
129//! What the gate *is* — a blocking priority-inheritance mutex
130//! ----------------------------------------------------------
131//! The gate is a [`crate::runtime::sync::PriorityInheritanceMutex`], the
132//! same primitive L46 (`registration_mutex`), L8a (`simple_pvs`) and L8b
133//! (one `scan_index` bucket) already use, and [`PvDatabase::lock_record`] /
134//! [`PvDatabase::lock_records`] are plain synchronous `fn`s returning RAII
135//! guards. This is the parity shape rather than a Rust-side invention: the
136//! `ls->lock` C's `dbScanLock` takes is a plain `epicsMutex` (`dbLock.c:86`),
137//! and on
138//! the RTEMS arm
139//! base compiles the POSIX implementation
140//! (`configure/toolchain.c:31-35` selects `OS_API = posix` for
141//! `__RTEMS_MAJOR__ >= 5`; `os/RTEMS-posix/osdMutex.c:8` is one `#include
142//! "../posix/osdMutex.c"`), whose `globalAttrInit`
143//! (`os/posix/osdMutex.c:71-88`) builds every `epicsMutex` with
144//! `PTHREAD_PRIO_INHERIT` — probing it once and silently degrading to
145//! `PTHREAD_PRIO_NONE` if the target refuses. `PriorityInheritanceMutex` is
146//! that same construction on that same API, including the probe.
147//!
148//! ### The band-ordered wait queue is gone, and why that is not a loss
149//!
150//! Until §5 step 4 this gate was an async lock, and between steps 2 and 5 it
151//! was a hand-rolled `PriorityGate` whose waiters were parked in a
152//! `BTreeMap` keyed by the waiter's declared EPICS band, highest band first,
153//! FIFO among equals. That queue existed for exactly one reason: while the
154//! gate was async, both ends of a contention pair were *tasks* parked on a
155//! userspace queue the kernel could not see, so nothing but our own code
156//! could order them. It was the async bridge, not the target design.
157//!
158//! With a blocking PI mutex the waiters are real threads blocked in
159//! `pthread_mutex_lock`, so the *OS* orders the queue — by thread priority,
160//! which on the RTEMS backend is the EPICS band the thread declared through
161//! `enter_ioc_thread` — and additionally boosts a preempted low-band holder
162//! to the highest waiting band. The band-ordered wake order is therefore
163//! replaced by the kernel's PI wait order, which is strictly stronger: it is
164//! what closes handoff §8.0 **gap 4** (priority inheritance), which no
165//! userspace queue could close at all. `PriorityGate`, its `BTreeMap` wait
166//! queue, `GateAcquire` and the `DECLARED_BAND` thread-local that fed it are
167//! deleted with this flip.
168//!
169//! ### Where the ordering actually holds — [`crate::runtime::sync::is_pi_mutex_active`]
170//!
171//! Priority inheritance is a property of the *build and the target*, and the
172//! function above is the single place that answers whether this process got
173//! it:
174//!
175//! * **RTEMS** — PI, and the answer is a *probe* result rather than a `cfg!`,
176//! matching C's own degrade path (`os/posix/osdMutex.c:77-85`, reported by
177//! `epicsMutexShowAll` at `:199-205`).
178//! * **Linux with the `linux-rt` Cargo feature** — PI unconditionally.
179//! * **every other build, including a default hosted Linux `cargo test`** —
180//! `parking_lot::Mutex`, which has **no** priority inheritance and no
181//! priority ordering. The host suite therefore verifies the *exclusion*
182//! this module provides, never its ordering; ordering is on-target
183//! territory.
184//!
185//! Read as a claim about *this* gate: on the host, `lock_record` excludes and
186//! nothing more, and no host test can be written that would catch a lost
187//! inversion. Two further conditions have to hold on target before the
188//! ordering is real, and neither is this module's to enforce — the probe must
189//! have returned `PTHREAD_PRIO_INHERIT`, and the contending threads must
190//! actually carry distinct scheduling priorities, which requires
191//! `RtPolicy::AllowRealtime` in [`crate::runtime::task`]. With the RT switch
192//! off, every thread is one priority and PI has nothing to inherit.
193//!
194//! Acquisition order — MUST
195//! ------------------------
196//! Written down because every lock in the chain is now *blocking* and a
197//! cycle would wedge a thread rather than a task. The order below is the
198//! one the code actually takes, not an aspiration — it was derived by reading
199//! every nesting site, and the bypass audit is in the commit that added it.
200//!
201//! > **A thread MUST acquire these in this order and MUST NOT acquire any of
202//! > them while holding one that appears later:**
203//! >
204//! > 0. **L33** — `epics-bridge-rs`' `GroupPvDef::atomic_write_lock`, the
205//! > QSRV per-group atomic-PUT gate (`PriorityInheritanceMutex`). Outside
206//! > this crate, and only the atomic group PUT takes it — see the L33
207//! > section below.
208//! > 1. **L1** — the per-record advisory gate ([`PvDatabase::lock_record`] /
209//! > [`PvDatabase::lock_records`]), *this* module
210//! > (`PriorityInheritanceMutex`).
211//! > 2. **L46** — `PvDatabaseInner::registration_mutex`
212//! > (`PriorityInheritanceMutex`).
213//! > 3. the leaves, none of which is ever held while another lock is taken:
214//! > **L8a** `simple_pvs`, **L8b** one `scan_index` bucket and **L7**
215//! > `ProcessVariable::subscribers` (all `PriorityInheritanceMutex`), plus
216//! > the `records` map, `aliases`, and a record's own
217//! > `RwLock<RecordInstance>` (`parking_lot::RwLock` — C has no
218//! > reader-writer lock to be PI-faithful to, §5.3 addendum).
219//! >
220//! > Every rung is a blocking lock. There is no async lock left anywhere on
221//! > the put/process path, which is what makes the order a MUST rather than a
222//! > preference: a cycle wedges a thread.
223//!
224//! [`RecordLockRegistry`]'s own mutex (a `std::sync::Mutex`) is *not* a rung of
225//! that order: it is taken and released inside `RecordLockRegistry::set_of`,
226//! strictly before the lock set it returns is taken, and no other lock is ever
227//! acquired while it is held. A guard's release path deliberately does not
228//! touch it — everything a release needs lives in the set's own cell — because
229//! taking it while holding a set would close a cycle against that rule.
230//!
231//! **Owner/Gate:** [`PvDatabase::update_scan_index`] is the **only** production
232//! function that takes L46 from inside an L1-held window, and therefore the
233//! single owner of the whole L1 → L46 → L8b chain. Every other L46 holder
234//! (`add_pv`, `add_pv_with_hooks_full`, `remove_simple_pv`,
235//! `add_loaded_record`, `remove_record`, `add_alias`, `add_breaktables`) is a
236//! registration entry point reached from `.db` load, iocsh or the gateway,
237//! never from inside a put/process cycle — verified with `rg` over those
238//! symbols in `field_io.rs`, `processing.rs`, `links.rs`, `qsrv/group.rs` and
239//! `pvalink/integration.rs`, where every hit is inside a `#[cfg(test)]`
240//! module. A second function that nests L46 under L1 is a second owner of
241//! this chain: route it through `update_scan_index` instead, or the order
242//! above stops being checkable by reading one function.
243//!
244//! The table orders the rungs against each other; it does not say what
245//! happens when one rung is taken twice. For L46 that matters, because
246//! `update_scan_index` takes L46 itself: **no caller may hold L46 when
247//! reaching it.** `PriorityInheritanceMutex` is not reentrant, so a caller
248//! that does parks on itself, and the symptom is a hung registration rather
249//! than an error. Every L46 acquisition therefore goes through
250//! [`PvDatabase::lock_registration`], which knows whether this thread already
251//! holds the gate and panics naming both ends instead of parking. The rule is
252//! C's too: `iterateRecords` (`iocInit.c:562-586`) walks an already-built
253//! database in a separate pass, holding no registration lock.
254//!
255//! **L1 does NOT have that rule: it recurses, as C's does.** That is forced by
256//! lock sets rather than chosen. Once a set spans every record a DB link
257//! reaches, processing a record and then following its `FLNK` takes ONE mutex
258//! under two different record names, so a non-reentrant L1 would wedge the
259//! ordinary process path. C is under the same constraint and answers it the
260//! same way: `epicsMutex` must be recursive — the header states the contract
261//! in prose, not code, at `epicsMutex.h:16` "An epicsMutex may be claimed
262//! recursively" and `:38` "MUST implement recursive locking" — and
263//! `dbLock.c:224-234` counts the nesting under `LOCKSET_DEBUG`.
264//!
265//! `PriorityInheritanceMutex` is not reentrant, so the recursion is built here,
266//! on top of it: each set records the thread inside it and how deep, the first
267//! acquisition takes the mutex and the rest only raise the count, and the mutex
268//! is released when the count returns to zero. The owner field is written only
269//! by the owning thread, so a non-owner can never match it. What this replaced
270//! — a thread-local held-name set that PANICKED on re-entry — was the right
271//! guard while a gate was one record, and is exactly wrong once a gate is a
272//! component: it would have fired on the first `FLNK`.
273//!
274//! `dbScanLockMany`'s own refusal (`cantProceed("dbScanLockMany(%p) already
275//! locked. Recursive locking not allowed")`, `dbLock.c:392-395`) is about
276//! re-using ONE `dbLocker` object, not about a thread holding two. Every
277//! [`PvDatabase::lock_records`] call builds its own, so there is nothing here
278//! to refuse.
279//!
280//! ### The rule's teeth are structural, and now they cover L1 too
281//!
282//! Every guard in the list above is `!Send`. A `!Send` value held across an
283//! `.await` makes the enclosing future `!Send`, which the compiler rejects at
284//! every `tokio::spawn` / `runtime::task::spawn` site in this workspace — so
285//! "no suspension point inside a gate window" is a build error rather than a
286//! review convention. That is the structural guarantee holders H1–H9 were
287//! staged to make reachable: each holder
288//! was first rewritten so its gate-held region contained zero `.await`s, and
289//! only then did the gate become a type that refuses to be held across one.
290//!
291//! `!Send`ness is deliberate on both arms of [`crate::runtime::sync::PriorityInheritanceMutex`],
292//! and on the PI arm it is also a correctness requirement, not only a
293//! lint: POSIX requires a mutex to be unlocked by the thread that locked it,
294//! so a guard that could migrate between threads would call
295//! `pthread_mutex_unlock` from a non-owner.
296//!
297//! The compiler only *reports* it at a spawn site, though, so the standing
298//! check is a direct one — for every binding of a gate guard, read forward to
299//! the end of its drop scope and find no `.await`:
300//!
301//! ```text
302//! rg -n 'let (mut )?\w+ = .*\.(lock_record|lock_records|acquire_put_gate)\(' crates/
303//! ```
304//!
305//! ### L33 — the QSRV atomic-PUT group lock, relative to L1
306//!
307//! `epics-bridge-rs`' `GroupPvDef::atomic_write_lock` (`qsrv/group_config.rs`)
308//! is a group-vs-group serialization aid: it lives in a different crate and
309//! has no nesting relationship with L46/L8a/L8b, but it *is* held across L1
310//! and so occupies rung 0 of the order above. It is acquired in
311//! `GroupChannel::put`'s atomic branch **before** [`PvDatabase::lock_records`]
312//! — first so a conversion failure in the up-front value-conversion phase
313//! aborts the whole atomic PUT before any member-record gate is even
314//! requested, second so two atomic PUTs to the *same* group serialize before
315//! either reaches L1 at all.
316//!
317//! It is a `PriorityInheritanceMutex`. It was a `tokio::sync::Mutex` for
318//! exactly as long as L1 was async: its window contains `lock_records`, which
319//! used to be a genuine suspension point, and a `!Send` guard across that
320//! await would not compile at the connection-task spawn site. That window is
321//! now the conversion phase, a synchronous `lock_records`, and a synchronous
322//! member loop — zero `.await`s — so the reason to keep it async is gone.
323
324// No RTEMS-EXEC-MODEL-ALLOW marker: this file's tests are all plain `#[test]`s
325// now that the gate is a blocking lock (a contender has to be a real thread),
326// so none of them needs a reactor and there is nothing to account for.
327
328use std::collections::{BTreeMap, BTreeSet, HashMap, VecDeque};
329use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
330
331use crate::runtime::sync::{
332 MutexInfo, PriorityInheritanceMutex, PriorityInheritanceMutexGuard, mutex_report,
333};
334
335use super::PvDatabase;
336
337/// C's `lockSet` (`dbLockPvt.h:29-44`): the one mutex every member record
338/// locks through, plus the bookkeeping the guards need without the registry.
339///
340/// The membership list is NOT here — a merge rewrites it, and it lives in
341/// [`Registry`] behind the registry mutex. What a guard needs at release time
342/// is here instead, so releasing never takes the registry lock: doing that
343/// while holding the set would close a cycle against [`Registry::set_of`],
344/// which takes the registry lock and then the set.
345struct LockSet {
346 /// C's `lockSet::id` (`dbLockPvt.h:33`). Assigned once by
347 /// [`Registry::make_set`] and kept for the cell's whole life, free-list
348 /// round trips included, exactly as C's is.
349 id: u64,
350 /// C's `lockSet::lock` (`:32`) — the mutex `dbScanLock` takes.
351 lock: PriorityInheritanceMutex<()>,
352 /// Position of this set's mutex among this file's entries in the process
353 /// mutex list, so [`lock_set_mutex_rows`] can hand back the row
354 /// `epicsMutexShow` prints for it. See [`SET_MUTEX_SEQ`].
355 mutex_seq: u64,
356 /// The thread currently inside `lock`, or 0. Written only by that thread.
357 owner: AtomicU64,
358 /// How deep that thread's recursion is. C's `epicsMutex` is recursive by
359 /// contract (`epicsMutex.h:16`, `:38`) and `dbLock.c:224-234` counts the
360 /// same nesting under `LOCKSET_DEBUG`.
361 depth: AtomicUsize,
362 /// References ABOVE the one-per-member baseline. `dbScanLockMany` adds one
363 /// per set for its locked list (`dbLock.c:404`) and `dbScanLock` does not
364 /// — it drops its transient reference the moment it holds the mutex
365 /// (`:220-222`) — which is why an idle set reports exactly as many refs as
366 /// it has members, as the oracle capture in `iocsh`'s `dblsr` shows.
367 many_holds: AtomicUsize,
368}
369
370/// The `'static` handle to a lock set. Sets are leaked: a guard hands out a
371/// `'static` borrow of the mutex, and a set outlives every record it holds —
372/// C keeps its emptied sets on `lockSetsFree` for the same reason and frees
373/// them only in `dbLockCleanupRecords` (`dbLock.c:563-576`).
374type Set = &'static LockSet;
375
376/// C's `next_id` starts at 1 and `makeSet` uses the POST-increment
377/// (`dbLock.c:70`, `:87`), so C's first lock set is number 2. Matching it
378/// costs nothing and makes an A/B against a C IOC read straight across.
379const FIRST_SET_ID: u64 = 2;
380
381/// Serialises lock-set mutex creation with its sequence counter.
382///
383/// The process mutex list ([`mutex_report`]) has no per-mutex accessor, so a
384/// set finds its own row positionally. That is exact only if the order sets
385/// are appended to the list is the order they take sequence numbers, which
386/// this lock is what guarantees — several `PvDatabase`s in one process each
387/// run their own registry mutex and would otherwise interleave. Nothing is
388/// acquired while it is held.
389static SET_MUTEX_SEQ: std::sync::Mutex<u64> = std::sync::Mutex::new(0);
390
391/// A process-unique non-zero key for the current thread.
392///
393/// `ThreadId` has no stable integer form on stable Rust, and the value only
394/// has to be comparable and never reused while a thread lives.
395fn thread_key() -> u64 {
396 static NEXT: AtomicU64 = AtomicU64::new(1);
397 thread_local! {
398 static KEY: u64 = NEXT.fetch_add(1, Ordering::Relaxed);
399 }
400 KEY.with(|k| *k)
401}
402
403impl LockSet {
404 /// C `dbScanLock` (`dbLock.c:180-234`) minus the reference dance: take the
405 /// set's mutex, or count one more level if this thread is already inside
406 /// it.
407 ///
408 /// Recursion is not a convenience. Once a lock set spans every record a
409 /// DB link reaches, processing a record and then its `FLNK` target takes
410 /// the SAME mutex twice on one thread, which is precisely why C's
411 /// `epicsMutex` is required to be recursive.
412 fn acquire(&'static self, many: bool) -> SetGuard {
413 let me = thread_key();
414 if self.owner.load(Ordering::Acquire) == me {
415 self.depth.fetch_add(1, Ordering::Relaxed);
416 if many {
417 self.many_holds.fetch_add(1, Ordering::Relaxed);
418 }
419 return SetGuard {
420 set: self,
421 guard: None,
422 many,
423 };
424 }
425 let guard = self.lock.lock();
426 self.owner.store(me, Ordering::Release);
427 self.depth.store(1, Ordering::Relaxed);
428 if many {
429 self.many_holds.fetch_add(1, Ordering::Relaxed);
430 }
431 SetGuard {
432 set: self,
433 guard: Some(guard),
434 many,
435 }
436 }
437
438 /// C's `epicsMutexTryLock` probe in `dbLockShowLocked` (`dbLock.c:963-965`).
439 fn is_locked(&self) -> bool {
440 self.lock.try_lock().is_none()
441 }
442}
443
444/// One acquisition of one lock set — C's `dbScanLock`/`dbScanUnlock` pair.
445///
446/// `!Send` on both backends, because the inner guard is: an `Option` is `Send`
447/// only when its payload is, so the recursive re-entry that carries `None` is
448/// `!Send` too.
449struct SetGuard {
450 set: Set,
451 /// `None` for a recursive re-entry, which acquired no new mutex.
452 guard: Option<PriorityInheritanceMutexGuard<'static, ()>>,
453 many: bool,
454}
455
456impl Drop for SetGuard {
457 fn drop(&mut self) {
458 if self.many {
459 self.set.many_holds.fetch_sub(1, Ordering::Relaxed);
460 }
461 if self.guard.is_some() {
462 // Clear ownership BEFORE the mutex is released, or the next owner
463 // could publish itself and be overwritten by this store.
464 self.set.depth.store(0, Ordering::Relaxed);
465 self.set.owner.store(0, Ordering::Release);
466 } else {
467 self.set.depth.fetch_sub(1, Ordering::Relaxed);
468 }
469 }
470}
471
472/// C's `lockSetsActive` / `lockSetsFree` / `next_id` (`dbLock.c:42-70`) plus
473/// the record-to-set mapping C keeps in `dbCommon::lset`.
474struct Registry {
475 /// C `lockSetsActive`, keyed by set id — which is also C's list ORDER,
476 /// because ids ascend with creation and a merge only ever removes an
477 /// entry. `dblsr` and `dbLockShowLocked` walk this list in that order.
478 active: BTreeMap<u64, SetState>,
479 /// C `lockSetsFree` (`:44`): sets a merge emptied. Their id and mutex
480 /// survive and are handed back by the next [`Registry::make_set`], which
481 /// is why C's free count is not simply "sets that ever existed minus live
482 /// ones". `ellGet` takes the head, so this is a queue.
483 free: VecDeque<Set>,
484 /// C's `dbCommon::lset` → `lockRecord::plockSet` chain, by canonical
485 /// record name.
486 of_record: HashMap<String, u64>,
487 next_id: u64,
488}
489
490/// One entry of [`Registry::active`]: the shared cell plus C's
491/// `lockSet::lockRecordList` (`dbLockPvt.h:31`).
492struct SetState {
493 set: Set,
494 members: BTreeSet<String>,
495}
496
497impl Default for Registry {
498 fn default() -> Self {
499 Self {
500 active: BTreeMap::new(),
501 free: VecDeque::new(),
502 of_record: HashMap::new(),
503 next_id: FIRST_SET_ID - 1,
504 }
505 }
506}
507
508impl Registry {
509 /// C `makeSet` (`dbLock.c:72-101`): reuse a freed set, keeping its id and
510 /// its mutex, or mint one.
511 fn make_set(&mut self) -> Set {
512 if let Some(set) = self.free.pop_front() {
513 debug_assert_eq!(
514 set.many_holds.load(Ordering::Relaxed),
515 0,
516 "C asserts refcount==0 for a set on the free list (dbLock.c:571)"
517 );
518 return set;
519 }
520 self.next_id += 1;
521 let id = self.next_id;
522 let mut seq = SET_MUTEX_SEQ.lock().unwrap_or_else(|e| e.into_inner());
523 let mutex_seq = *seq;
524 *seq += 1;
525 // The mutex is created UNDER `SET_MUTEX_SEQ` so its position in the
526 // process mutex list matches `mutex_seq`. This is the only
527 // `PriorityInheritanceMutex::new` in this file, which is what makes
528 // filtering that list by creating file select exactly these mutexes.
529 let set: Set = Box::leak(Box::new(LockSet {
530 id,
531 lock: PriorityInheritanceMutex::new(()),
532 mutex_seq,
533 owner: AtomicU64::new(0),
534 depth: AtomicUsize::new(0),
535 many_holds: AtomicUsize::new(0),
536 }));
537 drop(seq);
538 set
539 }
540
541 /// The set `record` belongs to, creating a one-member set if it has none.
542 ///
543 /// C has no lazy path: `dbLockInitRecords` gives every record a set before
544 /// anything can lock one, and `dblsr` returns early for a record whose
545 /// `lset` is still null (`dbLock.c:900-901`). This port lets a
546 /// programmatic database take a gate with no `iocInit`, and
547 /// [`PvDatabase::lock_records`] accepts a name that never was a record at
548 /// all, so a missing set is created here exactly as `createLockRecord`
549 /// (`:505-527`) would have created it.
550 fn set_of(&mut self, record: &str) -> Set {
551 if let Some(id) = self.of_record.get(record) {
552 return self.active[id].set;
553 }
554 let set = self.make_set();
555 self.of_record.insert(record.to_string(), set.id);
556 self.active.insert(
557 set.id,
558 SetState {
559 set,
560 members: BTreeSet::from([record.to_string()]),
561 },
562 );
563 set
564 }
565
566 /// C `dbLockSetMerge` (`dbLock.c:580-666`): every record behind
567 /// `second`'s mutex moves behind `first`'s, and the emptied set goes on
568 /// the free list with its id and mutex intact.
569 ///
570 /// The direction matters and is C's: the SOURCE record's set survives
571 /// (`dbDbLink.c:110` passes `plink->precord` first), so which id ends up
572 /// holding a component depends on link order exactly as in C.
573 fn merge(&mut self, first: &str, second: &str) {
574 let a = self.set_of(first).id;
575 let b = self.set_of(second).id;
576 if a == b {
577 return;
578 }
579 let moved = self
580 .active
581 .remove(&b)
582 .expect("every id in of_record names an active set");
583 for name in &moved.members {
584 self.of_record.insert(name.clone(), a);
585 }
586 let target = self
587 .active
588 .get_mut(&a)
589 .expect("every id in of_record names an active set");
590 target.members.extend(moved.members);
591 self.free.push_back(moved.set);
592 }
593
594 /// **The partition transition itself** — see [`PvDatabase::relink_lock_sets`],
595 /// which is its only caller.
596 ///
597 /// `seed` is the record whose link text just moved. The affected region is
598 /// the closure of `seed` under two relations at once: "is linked to" and
599 /// "is currently in the same set as". Closing over both is what makes one
600 /// rule cover creation, removal and retarget — a merge widens the region
601 /// through the first relation, a split narrows it through the second, and
602 /// neither needs to know which one happened.
603 fn repartition(&mut self, seed: &str, adjacency: &HashMap<String, BTreeSet<String>>) {
604 // A record that joined the database after `iocInit` has no set yet.
605 // C's `dbCreateRecord` cannot run then at all; the port allows it, so
606 // the record is given its own set here exactly as `createLockRecord`
607 // would have, and the components below fold it in.
608 let seed_id = if adjacency.contains_key(seed) {
609 self.set_of(seed).id
610 } else {
611 let Some(id) = self.of_record.get(seed).copied() else {
612 return;
613 };
614 id
615 };
616
617 let mut affected: BTreeSet<String> = BTreeSet::new();
618 let mut work: Vec<String> = vec![seed.to_string()];
619 while let Some(name) = work.pop() {
620 if !affected.insert(name.clone()) {
621 continue;
622 }
623 if let Some(targets) = adjacency.get(&name) {
624 work.extend(targets.iter().cloned());
625 }
626 if let Some(id) = self.of_record.get(&name).copied() {
627 work.extend(self.active[&id].members.iter().cloned());
628 }
629 }
630
631 // Every set the region covers. Each is emptied below and either
632 // re-used for one of the new components or returned to the free list.
633 let touched: BTreeSet<u64> = affected
634 .iter()
635 .filter_map(|name| self.of_record.get(name).copied())
636 .collect();
637
638 // A record the database no longer has leaves the partition with it —
639 // `dbDeleteRecord` frees the `lockRecord` — so it is dropped here
640 // rather than being carried into a component of one.
641 for name in &affected {
642 if !adjacency.contains_key(name) {
643 self.of_record.remove(name);
644 }
645 }
646
647 // The components of the region, the seed's first when the seed is
648 // still a record. Each is closed inside `affected` by construction:
649 // `affected` was built by following the same adjacency.
650 let seed_present = adjacency.contains_key(seed);
651 let mut components: Vec<BTreeSet<String>> = Vec::new();
652 let mut placed: BTreeSet<String> = BTreeSet::new();
653 let starts = seed_present
654 .then(|| seed.to_string())
655 .into_iter()
656 .chain(affected.iter().cloned());
657 for start in starts {
658 if placed.contains(&start) || !adjacency.contains_key(&start) {
659 continue;
660 }
661 let mut component: BTreeSet<String> = BTreeSet::new();
662 let mut walk = vec![start];
663 while let Some(name) = walk.pop() {
664 if !component.insert(name.clone()) {
665 continue;
666 }
667 if let Some(targets) = adjacency.get(&name) {
668 walk.extend(targets.iter().cloned());
669 }
670 }
671 placed.extend(component.iter().cloned());
672 components.push(component);
673 }
674
675 // Which id each component keeps. C fixes two of these: the component
676 // holding the edited record keeps the set that record was already in
677 // (`dbLockSetMerge` keeps `pfirst`'s), and a component that was a
678 // whole set already and is untouched keeps its own. A seed that has
679 // been deleted reserves nothing — its set is freed with the rest.
680 let mut keeps: BTreeSet<u64> = BTreeSet::new();
681 if seed_present {
682 keeps.insert(seed_id);
683 }
684 let mut assigned: Vec<Option<u64>> = Vec::with_capacity(components.len());
685 for component in &components {
686 if seed_present && component.contains(seed) {
687 assigned.push(Some(seed_id));
688 continue;
689 }
690 let mut ids = component
691 .iter()
692 .map(|name| self.of_record.get(name).copied());
693 let first = ids.next().flatten();
694 let uniform =
695 first.filter(|id| !keeps.contains(id) && ids.all(|other| other == Some(*id)));
696 if let Some(id) = uniform {
697 keeps.insert(id);
698 }
699 assigned.push(uniform);
700 }
701
702 // Mint before freeing, as C does: `dbLockSetSplit` calls `makeSet`
703 // while the set it is splitting is still live, so the new set comes
704 // off whatever was already on the free list rather than off the id
705 // this very edit is about to release.
706 let fresh: Vec<Set> = assigned
707 .iter()
708 .filter(|id| id.is_none())
709 .map(|_| self.make_set())
710 .collect();
711 let mut fresh = fresh.into_iter();
712
713 for (component, id) in components.into_iter().zip(assigned) {
714 let set = match id {
715 Some(id) => self.active[&id].set,
716 None => fresh
717 .next()
718 .expect("one fresh set per unassigned component"),
719 };
720 for name in &component {
721 self.of_record.insert(name.clone(), set.id);
722 }
723 self.active.insert(
724 set.id,
725 SetState {
726 set,
727 members: component,
728 },
729 );
730 }
731
732 for id in touched.difference(&keeps) {
733 let dropped = self
734 .active
735 .remove(id)
736 .expect("every touched id named an active set");
737 self.free.push_back(dropped.set);
738 }
739 }
740
741 /// C `dbLockInitRecords` (`dbLock.c:526-532`) through `createLockRecord`:
742 /// one set per record, before any link has merged anything.
743 fn init_records(&mut self, names: &[String]) {
744 for name in names {
745 self.set_of(name);
746 }
747 }
748
749 fn info(&self, id: u64, rows: &HashMap<u64, MutexInfo>) -> LockSetInfo {
750 let state = &self.active[&id];
751 LockSetInfo {
752 id,
753 members: state.members.iter().cloned().collect(),
754 refs: state.members.len() + state.set.many_holds.load(Ordering::Relaxed),
755 locked: state.set.is_locked(),
756 mutex: rows.get(&state.set.mutex_seq).cloned(),
757 }
758 }
759}
760
761/// The row `epicsMutexShow` prints for each lock-set mutex, keyed by
762/// [`LockSet::mutex_seq`].
763///
764/// Positional because the process mutex list exposes no per-mutex accessor.
765/// It is exact: [`Registry::make_set`] is the only `PriorityInheritanceMutex`
766/// created in this file, so filtering by creating file selects exactly the
767/// lock-set mutexes; creation is serialised by [`SET_MUTEX_SEQ`]; and a set's
768/// cell is never dropped, so no entry ever leaves the list and shifts the
769/// ones behind it.
770fn lock_set_mutex_rows() -> HashMap<u64, MutexInfo> {
771 mutex_report(false)
772 .shown
773 .into_iter()
774 .filter(|info| info.file() == file!())
775 .enumerate()
776 .map(|(seq, info)| (seq as u64, info))
777 .collect()
778}
779
780/// One active lock set, as `dblsr` and `dbLockShowLocked` report it.
781pub struct LockSetInfo {
782 /// C's `lockSet::id`.
783 pub id: u64,
784 /// C's `lockRecordList`, in the order `dblsr` walks it.
785 pub members: Vec<String>,
786 /// C's `lockSet::refcount`: one per member record, plus one for each
787 /// [`PvDatabase::lock_records`] epoch currently holding this set.
788 pub refs: usize,
789 /// Whether the set's mutex cannot be taken right now — C's
790 /// `epicsMutexTryLock` filter in `dbLockShowLocked`.
791 pub locked: bool,
792 /// The `epicsMutexShow` row for this set's mutex.
793 pub mutex: Option<MutexInfo>,
794}
795
796/// What one `dblsr` / `dbLockShowLocked` call sees.
797pub struct LockSetReport {
798 /// C's `lockSetsActive`, in list order.
799 pub active: Vec<LockSetInfo>,
800 /// `ellCount(&lockSetsFree)`.
801 pub free: usize,
802}
803
804/// The lock sets of one database — C's `lockSetsActive` and `lockSetsFree`.
805///
806/// Every record is behind exactly one set, and a DB link puts both of its
807/// records behind the same one. Sets are created by
808/// [`PvDatabase::build_lock_sets`] at IOC init, and lazily for a name that
809/// reaches [`PvDatabase::lock_record`] without one.
810///
811/// Nothing is ever destroyed: a merged-away set moves to the free list, and
812/// its mutex must outlive it because a `'static` guard may still be unwinding
813/// through it. That bounds memory by the record count, which is what C's own
814/// free list does.
815#[derive(Default)]
816pub(crate) struct RecordLockRegistry {
817 inner: std::sync::Mutex<Registry>,
818}
819
820impl RecordLockRegistry {
821 fn lock(&self) -> std::sync::MutexGuard<'_, Registry> {
822 self.inner
823 .lock()
824 .unwrap_or_else(|poisoned| poisoned.into_inner())
825 }
826
827 /// The set `record` locks through, created on first use.
828 ///
829 /// The registry lock is released before the caller takes the set — it is
830 /// not a rung of the acquisition order, and holding it across an
831 /// acquisition would serialise every record in the database behind one
832 /// writer.
833 fn set_of(&self, record: &str) -> Set {
834 self.lock().set_of(record)
835 }
836
837 /// Which set `record` is behind right now, without creating one.
838 fn set_id_of(&self, record: &str) -> Option<u64> {
839 self.lock().of_record.get(record).copied()
840 }
841}
842
843impl PvDatabase {
844 /// Build the lock sets — C `dbLockInitRecords` followed by the
845 /// `dbLockSetMerge` every DB link performs as it is opened
846 /// (`iocInit.c:178-179`, `dbDbLink.c:110`).
847 ///
848 /// Called once from `ioc_init`, which is why `dbLockShowLocked` on a
849 /// loaded-but-not-initialised IOC reports `0` and `0` exactly as C's does:
850 /// before this runs no record has a set.
851 ///
852 /// C merges incrementally because it cannot afford to recompute; the
853 /// result is the same either way, because `dbLockSetSplit` is itself a
854 /// reachability recomputation (`dbLock.c:710-717`). Doing it as C does —
855 /// one set per record, then one merge per link — is what reproduces the
856 /// free-list count, which a components-first construction would report as
857 /// zero.
858 pub fn build_lock_sets(&self) {
859 let mut names: Vec<String> = self.inner.records.read().keys().cloned().collect();
860 names.sort();
861 // Every edge is collected BEFORE the registry lock is taken:
862 // `record_link_fields` reads the record map and each record's own
863 // lock, and neither may be acquired underneath the registry.
864 let mut edges: Vec<(String, String)> = Vec::new();
865 for name in &names {
866 for target in self.db_link_targets(name) {
867 edges.push((name.clone(), target));
868 }
869 }
870 let mut registry = self.inner.record_locks.lock();
871 registry.init_records(&names);
872 for (from, to) in edges {
873 registry.merge(&from, &to);
874 }
875 }
876
877 /// The records `record`'s DB links reach, canonicalised.
878 ///
879 /// Only a link that resolved to a LOCAL record merges: C reaches
880 /// `dbLockSetMerge` from `dbDbInitLink`, and a target this IOC does not
881 /// have falls through to `dbCaAddLink` instead (`dbLink.c:118-130`), which
882 /// merges nothing. `record_link_fields` has already applied that same
883 /// locality rule, so a `ca://` link to a local record is a `Ca` link here
884 /// and does not widen the set.
885 fn db_link_targets(&self, record: &str) -> Vec<String> {
886 use crate::server::record::ParsedLink;
887 self.record_link_fields(record)
888 .into_iter()
889 .filter_map(|(_, _, parsed)| match parsed {
890 ParsedLink::Db(link) => {
891 // `DbLink::target` is where the record name stops and the
892 // channel filter begins — C reaches `dbLockSetMerge` with
893 // `dbChannelRecord(chan)` (`dbDbLink.c:94-109`), the record
894 // the whole `pvname` resolved to, so `SRC.[2]` merges into
895 // SRC's set exactly as `SRC` does. Matching on the raw text
896 // instead left every filtered reader in a set of its own,
897 // and a slice read outside the source's set can tear
898 // against the source's own processing.
899 let name = self
900 .resolve_alias(&link.target().record)
901 .unwrap_or_else(|| link.target().record);
902 self.get_record_no_resolve(&name).map(|_| name)
903 }
904 _ => None,
905 })
906 .collect()
907 }
908
909 /// C's `lockSetsActive` and `lockSetsFree` as `dblsr("*", n)` and
910 /// `dbLockShowLocked(n)` read them.
911 pub fn lock_set_report(&self) -> LockSetReport {
912 let rows = lock_set_mutex_rows();
913 let registry = self.inner.record_locks.lock();
914 LockSetReport {
915 active: registry
916 .active
917 .keys()
918 .map(|id| registry.info(*id, &rows))
919 .collect(),
920 free: registry.free.len(),
921 }
922 }
923
924 /// The lock set one record is behind, or `None` when it has none —
925 /// `dblsr`'s `if (!plockRecord) return 0;` before `iocInit`
926 /// (`dbLock.c:900-901`).
927 ///
928 /// Does not create a set: asking about a record must not change the
929 /// report.
930 pub fn lock_set_of(&self, record: &str) -> Option<LockSetInfo> {
931 let canonical = self
932 .resolve_alias(record)
933 .unwrap_or_else(|| record.to_string());
934 let rows = lock_set_mutex_rows();
935 let registry = self.inner.record_locks.lock();
936 let id = registry.of_record.get(&canonical).copied()?;
937 Some(registry.info(id, &rows))
938 }
939
940 /// The obligation to re-derive `record`'s lock set, taken out BEFORE a DB
941 /// link field on it is written.
942 ///
943 /// `None` — no obligation — when the field is not a DBF link field, or
944 /// when no lock sets exist yet: before `iocInit` C has no `lockRecord` to
945 /// merge, so a `.db` load rewrites link text with nothing to maintain and
946 /// [`PvDatabase::build_lock_sets`] does the whole job afterwards.
947 ///
948 /// Declare it ABOVE the record guard in the put body. Rust drops in
949 /// reverse declaration order, so the record lock is down by the time the
950 /// relink runs, which is the order the owner needs: it reads the link
951 /// text of every record in the affected component.
952 pub(crate) fn link_field_write<'a>(
953 &'a self,
954 record: &str,
955 field: &str,
956 ) -> Option<LockSetEdit<'a>> {
957 let canonical = self
958 .resolve_alias(record)
959 .unwrap_or_else(|| record.to_string());
960 if !self.is_dbf_link_field(&canonical, field) {
961 return None;
962 }
963 if self.inner.record_locks.lock().of_record.is_empty() {
964 return None;
965 }
966 Some(LockSetEdit {
967 db: self,
968 record: canonical,
969 })
970 }
971
972 /// The same obligation for a change of MEMBERSHIP rather than of link
973 /// text: a record added or removed after `iocInit`, or an alias that
974 /// makes a link resolve to a record it did not resolve to before.
975 ///
976 /// One owner covers all three because the owner re-derives the component
977 /// instead of tracking a verb — see the module doc. Declare it above the
978 /// mutation, so the relink sees the database as it is afterwards.
979 pub(crate) fn lock_set_membership_change<'a>(
980 &'a self,
981 record: &str,
982 ) -> Option<LockSetEdit<'a>> {
983 if self.inner.record_locks.lock().of_record.is_empty() {
984 return None;
985 }
986 Some(LockSetEdit {
987 db: self,
988 record: self
989 .resolve_alias(record)
990 .unwrap_or_else(|| record.to_string()),
991 })
992 }
993
994 /// **The single owner of the lock-set partition after `iocInit`.**
995 ///
996 /// Re-derives the connected component `record` now sits in and
997 /// re-partitions every set that component touches. C reaches the same
998 /// result through `dbLockSetMerge` on link creation and `dbLockSetSplit`
999 /// on removal (`dbDbLink.c:110`, `:124`, `:141`); see the module doc for
1000 /// why the port re-derives instead of tracking the endpoint pair.
1001 ///
1002 /// Private, and reachable only by dropping a [`LockSetEdit`].
1003 fn relink_lock_sets(&self, record: &str) {
1004 // The whole adjacency is read BEFORE the registry lock, because
1005 // `record_link_fields` takes the record map and each record's own
1006 // lock and neither may be acquired underneath the registry — the same
1007 // rule `build_lock_sets` states.
1008 let adjacency = self.db_link_adjacency();
1009 let mut registry = self.inner.record_locks.lock();
1010 registry.repartition(record, &adjacency);
1011 }
1012
1013 /// The DB-link graph as an undirected adjacency map.
1014 ///
1015 /// Undirected because C's merge is: `dbLockSetMerge(locker, plink->precord,
1016 /// target)` puts both endpoints behind one mutex regardless of which way
1017 /// the link points, and `dbLockSetSplit` walks `bklnk` as well as the
1018 /// record's own links (`dbLock.c:735-770`) for the same reason. A record
1019 /// that is only ever pointed AT is as much a member as the one pointing.
1020 fn db_link_adjacency(&self) -> HashMap<String, BTreeSet<String>> {
1021 let names: Vec<String> = self.inner.records.read().keys().cloned().collect();
1022 let mut adjacency: HashMap<String, BTreeSet<String>> = HashMap::new();
1023 for name in &names {
1024 adjacency.entry(name.clone()).or_default();
1025 }
1026 for name in &names {
1027 for target in self.db_link_targets(name) {
1028 adjacency
1029 .entry(name.clone())
1030 .or_default()
1031 .insert(target.clone());
1032 adjacency.entry(target).or_default().insert(name.clone());
1033 }
1034 }
1035 adjacency
1036 }
1037}
1038
1039/// A DB link field write that has landed in the record but not yet in the
1040/// lock-set graph.
1041///
1042/// The token exists so that the illegal state — link text changed, partition
1043/// stale — cannot be constructed rather than merely being checked for. It is
1044/// minted by [`PvDatabase::link_field_write`], holds the only reference
1045/// through which [`PvDatabase::relink_lock_sets`] is reachable, and performs
1046/// the relink from its destructor, so no exit path of a put body can skip it.
1047#[must_use = "the lock-set graph is only re-derived when this is dropped; binding it to `_` drops it immediately and relinks too early"]
1048pub(crate) struct LockSetEdit<'a> {
1049 db: &'a PvDatabase,
1050 record: String,
1051}
1052
1053impl Drop for LockSetEdit<'_> {
1054 fn drop(&mut self) {
1055 self.db.relink_lock_sets(&self.record);
1056 }
1057}
1058
1059/// RAII guard for one record's lock set.
1060///
1061/// Held for the duration of a plain CA/PVA write — the `dbScanLock` +
1062/// `dbScanUnlock` pair around one `dbPutField`. `!Send`, so the compiler
1063/// refuses to let it live across an `.await` in any spawned future; see the
1064/// module doc.
1065#[must_use = "the lock set is released as soon as the guard is dropped"]
1066pub struct RecordWriteGuard {
1067 _guard: SetGuard,
1068}
1069
1070/// RAII guard for the lock sets of a declared record set — the `DBManyLocker`
1071/// equivalent.
1072///
1073/// Acquired by [`PvDatabase::lock_records`] over every member record of a
1074/// multi-record transaction (QSRV atomic group PUT/GET, pvalink atomic
1075/// scan-on-update epoch) and held across the whole member loop. While alive,
1076/// every plain write to any record of any of those sets blocks.
1077#[must_use = "the locked epoch ends as soon as the guard is dropped"]
1078pub struct ManyRecordWriteGuard {
1079 _guards: Vec<SetGuard>,
1080}
1081
1082impl PvDatabase {
1083 /// Acquire the lock set of a single record — the `dbScanLock(precord)`
1084 /// analogue.
1085 ///
1086 /// `record` is alias-resolved internally, so an alias and its target
1087 /// always reach the same set.
1088 ///
1089 /// **Blocks the calling thread** when another thread holds the set. A
1090 /// thread that already holds it recurses, as C's recursive `epicsMutex`
1091 /// does — which is not optional once a set spans a whole link component,
1092 /// because processing a record and then its link target takes one mutex
1093 /// twice.
1094 pub fn lock_record(&self, record: &str) -> RecordWriteGuard {
1095 let canonical = self
1096 .resolve_alias(record)
1097 .unwrap_or_else(|| record.to_string());
1098 loop {
1099 let set = self.inner.record_locks.set_of(&canonical);
1100 let guard = set.acquire(false);
1101 // C `dbScanLock`'s `retry:` (`dbLock.c:194-213`): a merge can move
1102 // the record to another set between the lookup and the
1103 // acquisition, and the set just taken would then guard nothing.
1104 if self.inner.record_locks.set_id_of(&canonical) == Some(set.id) {
1105 return RecordWriteGuard { _guard: guard };
1106 }
1107 drop(guard);
1108 }
1109 }
1110
1111 /// Acquire the lock sets covering a set of records — the `DBManyLock` /
1112 /// `DBManyLocker` equivalent, C's `dbScanLockMany` (`dbLock.c:384-440`).
1113 ///
1114 /// Every name is alias-resolved, mapped to its lock set, then the sets are
1115 /// sorted by id and de-duplicated before any is taken. Sorting gives two
1116 /// overlapping transactions the same acquisition order so they cannot
1117 /// deadlock; de-duplication is C's own — several member records commonly
1118 /// share one set, and `dbScanLockMany` skips the repeats (`:399-402`).
1119 ///
1120 /// The returned [`ManyRecordWriteGuard`] must be held for the whole
1121 /// transaction. Each set it holds reports one extra ref while it lives,
1122 /// which is the `+1` C's locked list adds.
1123 ///
1124 /// Names that do not resolve to a record still get a set, matching
1125 /// `dbLockerAlloc`, which accepts the record pointers it is given without
1126 /// a liveness re-check.
1127 pub fn lock_records<I, S>(&self, records: I) -> ManyRecordWriteGuard
1128 where
1129 I: IntoIterator<Item = S>,
1130 S: AsRef<str>,
1131 {
1132 let names: Vec<String> = records
1133 .into_iter()
1134 .map(|record| {
1135 let record = record.as_ref();
1136 self.resolve_alias(record)
1137 .unwrap_or_else(|| record.to_string())
1138 })
1139 .collect();
1140
1141 loop {
1142 let mut sets: Vec<Set> = names
1143 .iter()
1144 .map(|name| self.inner.record_locks.set_of(name))
1145 .collect();
1146 sets.sort_unstable_by_key(|set| set.id);
1147 sets.dedup_by_key(|set| set.id);
1148
1149 let guards: Vec<SetGuard> = sets.iter().map(|set| set.acquire(true)).collect();
1150
1151 // `dbLockUpdateRefs(locker, 0)` (`dbLock.c:432-436`): if a merge
1152 // moved any member while the sets were being taken, release
1153 // everything and start again.
1154 let held: BTreeSet<u64> = sets.iter().map(|set| set.id).collect();
1155 if names
1156 .iter()
1157 .all(|name| matches!(self.inner.record_locks.set_id_of(name), Some(id) if held.contains(&id)))
1158 {
1159 return ManyRecordWriteGuard { _guards: guards };
1160 }
1161 drop(guards);
1162 }
1163 }
1164}
1165
1166#[cfg(test)]
1167mod tests {
1168 use super::*;
1169 use std::sync::Arc;
1170 use std::sync::atomic::{AtomicUsize, Ordering};
1171 use std::time::Duration;
1172
1173 // These are plain `#[test]`s, not `#[tokio::test]`s, and that is forced
1174 // rather than stylistic: the gate blocks the calling *thread*, so a
1175 // contending waiter has to be a real thread. Parking one on a
1176 // `current_thread` runtime's only worker would wedge the runtime instead
1177 // of demonstrating exclusion. Being reactor-free they also run on the exec
1178 // backend and add no site to this file's RTEMS-EXEC-MODEL-ALLOW census.
1179
1180 /// Long enough that a non-blocking (broken) gate would have let the
1181 /// contender through, short enough not to dominate the suite.
1182 const SETTLE: Duration = Duration::from_millis(50);
1183
1184 /// A single-record gate excludes a concurrent same-record locker.
1185 #[test]
1186 fn lock_record_excludes_same_record() {
1187 let db = PvDatabase::new();
1188 let order = Arc::new(AtomicUsize::new(0));
1189
1190 let g = db.lock_record("ai:1");
1191
1192 let db2 = db.clone();
1193 let order2 = order.clone();
1194 let h = std::thread::spawn(move || {
1195 let _g2 = db2.lock_record("ai:1");
1196 // This must observe the first holder having released (1).
1197 order2.fetch_add(10, Ordering::SeqCst);
1198 });
1199
1200 // Give the spawned thread time to block on the gate.
1201 std::thread::sleep(SETTLE);
1202 // First holder still owns the gate: counter untouched.
1203 assert_eq!(order.load(Ordering::SeqCst), 0);
1204 order.fetch_add(1, Ordering::SeqCst);
1205 drop(g);
1206
1207 h.join().unwrap();
1208 assert_eq!(order.load(Ordering::SeqCst), 11);
1209 }
1210
1211 /// `lock_records` blocks a plain single-record write to a member.
1212 #[test]
1213 fn lock_records_excludes_single_member_write() {
1214 let db = PvDatabase::new();
1215 let many = db.lock_records(["g:a", "g:b", "g:c"]);
1216
1217 let db2 = db.clone();
1218 let acquired = Arc::new(AtomicUsize::new(0));
1219 let acquired2 = acquired.clone();
1220 let h = std::thread::spawn(move || {
1221 // Plain write to a member must block until `many` drops.
1222 let _g = db2.lock_record("g:b");
1223 acquired2.store(1, Ordering::SeqCst);
1224 });
1225
1226 std::thread::sleep(SETTLE);
1227 assert_eq!(
1228 acquired.load(Ordering::SeqCst),
1229 0,
1230 "single-member write must block while ManyRecordWriteGuard is held"
1231 );
1232
1233 drop(many);
1234 h.join().unwrap();
1235 assert_eq!(acquired.load(Ordering::SeqCst), 1);
1236 }
1237
1238 /// Two overlapping `lock_records` sets acquire in canonical order
1239 /// and therefore cannot deadlock even with reversed input order.
1240 ///
1241 /// With blocking gates a violated order wedges both threads outright,
1242 /// which is why this runs the two sets on real threads and joins them
1243 /// under a bounded wait rather than trusting a scheduler yield.
1244 #[test]
1245 fn lock_records_overlapping_sets_no_deadlock() {
1246 let db = PvDatabase::new();
1247 let done = Arc::new(AtomicUsize::new(0));
1248
1249 let handles: Vec<_> = [["x", "y", "z"], ["z", "y", "x"]]
1250 .into_iter()
1251 .map(|set| {
1252 let db = db.clone();
1253 let done = done.clone();
1254 std::thread::spawn(move || {
1255 for _ in 0..500 {
1256 // Reversed input order on one side — sort makes the
1257 // real acquisition order identical, so no deadlock.
1258 let _g = db.lock_records(set);
1259 std::thread::yield_now();
1260 }
1261 done.fetch_add(1, Ordering::SeqCst);
1262 })
1263 })
1264 .collect();
1265
1266 let deadline = std::time::Instant::now() + Duration::from_secs(10);
1267 while done.load(Ordering::SeqCst) < 2 {
1268 assert!(
1269 std::time::Instant::now() < deadline,
1270 "overlapping lock_records sets must not deadlock"
1271 );
1272 std::thread::sleep(Duration::from_millis(10));
1273 }
1274 for h in handles {
1275 h.join().unwrap();
1276 }
1277 }
1278
1279 /// An epoch over a record set excludes a second epoch that shares
1280 /// any record until the first guard drops — and overlapping sets
1281 /// listed in opposite orders never deadlock (sorted acquisition).
1282 #[test]
1283 fn overlapping_epochs_are_mutually_exclusive_and_deadlock_free() {
1284 let db = PvDatabase::new();
1285 let a = vec!["RECA".to_string(), "RECB".to_string()];
1286 // Opposite order on purpose — sorted acquisition must still
1287 // make this safe.
1288 let b = vec!["RECB".to_string(), "RECC".to_string()];
1289
1290 let guard_a = db.lock_records(&a);
1291
1292 // A second epoch sharing RECB must not be acquirable while
1293 // `guard_a` is alive.
1294 let db2 = db.clone();
1295 let entered = Arc::new(AtomicUsize::new(0));
1296 let entered2 = entered.clone();
1297 let handle = std::thread::spawn(move || {
1298 let _guard_b = db2.lock_records(&b);
1299 entered2.store(1, Ordering::SeqCst);
1300 });
1301
1302 std::thread::sleep(SETTLE);
1303 assert_eq!(
1304 entered.load(Ordering::SeqCst),
1305 0,
1306 "epoch B must block on shared RECB"
1307 );
1308
1309 drop(guard_a);
1310 handle.join().expect("epoch B thread");
1311 assert_eq!(entered.load(Ordering::SeqCst), 1);
1312 }
1313
1314 /// Two non-overlapping epochs run concurrently — no false
1315 /// serialisation. Taking the second on *this* thread while the first is
1316 /// still held is the assertion: a gate keyed too coarsely would
1317 /// self-deadlock here rather than merely being slow.
1318 #[test]
1319 fn disjoint_epochs_do_not_block_each_other() {
1320 let db = PvDatabase::new();
1321 let _g1 = db.lock_records(&["X1".to_string()]);
1322 // Disjoint set: must acquire immediately without blocking.
1323 let _g2 = db.lock_records(&["X2".to_string()]);
1324 }
1325
1326 /// One name reaches one set, and two unlinked records reach two — C's
1327 /// state straight after `dbLockInitRecords`, before any link merges.
1328 #[test]
1329 fn a_name_reaches_one_set_and_two_unlinked_records_reach_two() {
1330 let db = PvDatabase::new();
1331 let registry = &db.inner.record_locks;
1332 assert!(
1333 std::ptr::eq(registry.set_of("REC:A"), registry.set_of("REC:A")),
1334 "the same canonical name must map to the same lock set"
1335 );
1336 assert!(
1337 !std::ptr::eq(registry.set_of("REC:A"), registry.set_of("REC:B")),
1338 "records no link joins must not share a lock set"
1339 );
1340 }
1341
1342 /// A second epoch overlapping the first on one thread recurses rather
1343 /// than wedging. C's refusal (`cantProceed("dbScanLockMany(%p) already
1344 /// locked...")`, `dbLock.c:392-395`) is about re-using ONE `dbLocker`,
1345 /// which has no analogue here — every call builds its own — while the
1346 /// overlap itself is what C's recursive `epicsMutex` absorbs.
1347 #[test]
1348 fn a_second_overlapping_epoch_on_one_thread_recurses() {
1349 let db = PvDatabase::new();
1350 let _epoch = db.lock_records(&["RE:A".to_string(), "RE:B".to_string()]);
1351 let _overlapping = db.lock_records(&["RE:B".to_string(), "RE:C".to_string()]);
1352 }
1353
1354 /// `dbScanLock` recurses because `epicsMutex` must (`epicsMutex.h:16`,
1355 /// `:38`), and once a lock set spans a link component the port has no
1356 /// choice either: processing a record and then its link target is this
1357 /// sequence with two different names behind one mutex.
1358 #[test]
1359 fn re_taking_one_record_s_set_recurses_like_db_scan_lock() {
1360 let db = PvDatabase::new();
1361 let _held = db.lock_record("RE:SELF");
1362 let _again = db.lock_record("RE:SELF");
1363 }
1364
1365 /// The recursion is per THREAD: a second thread still blocks, and the
1366 /// depth the first one built up does not let it through early.
1367 #[test]
1368 fn recursion_does_not_let_a_second_thread_in() {
1369 let db = PvDatabase::new();
1370 let outer = db.lock_record("RE:DEPTH");
1371 let inner = db.lock_record("RE:DEPTH");
1372
1373 let db2 = db.clone();
1374 let entered = Arc::new(AtomicUsize::new(0));
1375 let entered2 = entered.clone();
1376 let h = std::thread::spawn(move || {
1377 let _g = db2.lock_record("RE:DEPTH");
1378 entered2.store(1, Ordering::SeqCst);
1379 });
1380
1381 std::thread::sleep(SETTLE);
1382 assert_eq!(entered.load(Ordering::SeqCst), 0, "outer level still held");
1383 drop(inner);
1384 std::thread::sleep(SETTLE);
1385 assert_eq!(
1386 entered.load(Ordering::SeqCst),
1387 0,
1388 "one release of two must not hand the set over"
1389 );
1390 drop(outer);
1391 h.join().unwrap();
1392 assert_eq!(entered.load(Ordering::SeqCst), 1);
1393 }
1394
1395 /// The positional association between a set and its `epicsMutexShow` row
1396 /// is only exact while `make_set` is the ONLY `PriorityInheritanceMutex`
1397 /// created in this file. This is that check: one row for every set ever
1398 /// made in this process, and not one more.
1399 #[test]
1400 fn this_file_creates_no_mutex_but_lock_sets() {
1401 let db = PvDatabase::new();
1402 for name in ["MS:1", "MS:2", "MS:3"] {
1403 drop(db.lock_record(name));
1404 }
1405 let made = *SET_MUTEX_SEQ.lock().unwrap();
1406 assert_eq!(
1407 lock_set_mutex_rows().len() as u64,
1408 made,
1409 "a second mutex created in this file would shift every set's row"
1410 );
1411 for set in db.lock_set_report().active {
1412 assert!(set.mutex.is_some(), "set {} has no row", set.id);
1413 }
1414 }
1415
1416 /// The set is released with the guard, so the ordinary sequential
1417 /// pattern — lock, write, drop, lock again — is untouched.
1418 #[test]
1419 fn the_set_is_released_when_the_guard_drops() {
1420 let db = PvDatabase::new();
1421 drop(db.lock_record("RE:SEQ"));
1422 drop(db.lock_record("RE:SEQ"));
1423 drop(db.lock_records(&["RE:SEQ".to_string()]));
1424 // And a disjoint pair may be held together on one thread.
1425 let _a = db.lock_record("RE:ONE");
1426 let _b = db.lock_record("RE:TWO");
1427 }
1428}