Skip to main content

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}