epics_base_rs/server/database/scan_index.rs
1use crate::server::record::{PiniMode, RecordInstance, ScanList, ScanType};
2
3use super::PvDatabase;
4
5impl PvDatabase {
6 /// Update scan index when a record's SCAN or PHAS field changes.
7 ///
8 /// Takes `registration_mutex` so the read of
9 /// the records map (to verify the record still exists) and the
10 /// scan_index mutation are atomic vs. concurrent `remove_record`.
11 ///
12 /// The `new_scan` / `new_phas` parameters
13 /// the caller passes are advisory only. After acquiring the
14 /// mutex we read the LIVE record's current scan/phas and insert
15 /// based on those. Pre-fix a put-then-update sequence could
16 /// race a remove+re-add of the same name: the caller's
17 /// `new_scan` reflected the old (now-removed) record's value;
18 /// inserting that under the fresh record's name produced a
19 /// stale scan-index entry pointing at a wrong scan rate. The
20 /// live-read makes the index strictly reflect the record's
21 /// current state at insert time.
22 ///
23 /// **Synchronous.** It had exactly two suspension points and neither was a
24 /// real one: the `registration_mutex` acquisition (L46) and two
25 /// `scan_index` write acquisitions (L8b) — both awaited before step 4.
26 /// Both are blocking PI mutexes now, so the whole scan-index update is a
27 /// bounded critical
28 /// section — which is what lets it run *inside* the L1 record-gate window
29 /// without putting an `.await` there. C reaches `scanAdd`/`scanDelete`
30 /// (`dbScan.c:241-330`) the same way, from inside `dbPut` under
31 /// `dbScanLock`. `doc/rtems-priority-locks-design.md` §5 step 4.
32 pub fn update_scan_index(
33 &self,
34 name: &str,
35 old_scan: ScanType,
36 _new_scan: ScanType,
37 old_phas: i16,
38 _new_phas: i16,
39 ) {
40 let _gate = self.lock_registration("update_scan_index");
41 let _ = old_phas; // entry matched by name; PHAS not needed.
42 // 1) Remove the OLD entry the caller knew about — even if
43 // remove_record already swept it. Match by record name so a
44 // stale PHAS / load_order does not leave a phantom entry.
45 if let Some(list) = old_scan.scan_list() {
46 self.inner
47 .scan_index
48 .bucket(list)
49 .lock()
50 .retain(|k| k.name != name);
51 }
52 // 2) Look up the LIVE record under the mutex. If concurrent
53 // remove+re-add replaced the Arc with a fresh one whose
54 // scan differs from the caller's `_new_scan`, we re-insert
55 // based on the fresh record's state. The fresh record's
56 // own `add_record` call also registered its scan index, so
57 // duplicate-insertion of the same (phas, name) pair into
58 // the same scan bucket is a no-op (`BTreeSet::insert`
59 // returns false on present key).
60 let rec_arc = match self.inner.records.read().get(name).cloned() {
61 Some(r) => r,
62 None => return,
63 };
64 let (cur_scan, cur_phas, cur_type) = {
65 let inst = rec_arc.read();
66 (
67 inst.common.scan,
68 inst.common.phas,
69 inst.record.record_type(),
70 )
71 };
72 // C `scanAdd` refuses both `Passive` and an out-of-menu index — the
73 // latter with "scanAdd detected illegal SCAN value" — so neither can
74 // key a bucket. [`ScanList::of`] is that refusal.
75 if let Some(list) = cur_scan.scan_list() {
76 // Re-use the record's existing load-order sequence so the
77 // scan-index secondary key stays stable across SCAN/PHAS
78 // edits. A record loaded before should always scan before
79 // a later-loaded record at the same PHAS.
80 let seq = self.inner.load_order.load().get(name).copied().unwrap_or(0);
81 self.inner
82 .scan_index
83 .bucket(list)
84 .lock()
85 .insert(super::ScanKey::new(cur_phas, cur_type, seq, name));
86 }
87 }
88
89 /// Count one over-run for `scan`'s list — C `ppsl->overruns++`
90 /// (`dbScan.c:827`). The periodic scan thread for that rate is the only
91 /// caller; a SCAN value naming no list has no counter to move.
92 pub(crate) fn record_scan_overrun(&self, scan: ScanType) {
93 if let Some(list) = scan.scan_list() {
94 self.inner.scan_index.overruns[list.slot()]
95 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
96 }
97 }
98
99 /// How many times `list`'s sweep has run past its deadline since boot —
100 /// what C's `scanppl` prints as `(%lu over-runs)` (`dbScan.c:408-409`).
101 pub(crate) fn scan_overruns(&self, list: crate::server::record::ScanList) -> u64 {
102 self.inner.scan_index.overruns[list.slot()].load(std::sync::atomic::Ordering::Relaxed)
103 }
104
105 /// A **snapshot** of a scan list, in C's order: PHAS, then DBD
106 /// record-type order, then `.db` load order — see `ScanKey`. The stable
107 /// same-PHAS FIFO is `addToList`; the order it is a FIFO over is
108 /// `buildScanLists`, and both are in the key.
109 ///
110 /// For reporting and counting only (`scanppl`, `dbstat`). A sweep that
111 /// PROCESSES the list must not use this: `dbProcess` can change the SCAN
112 /// field of an arbitrary number of records, so a snapshot processes
113 /// records the list no longer holds. `Self::scan_list_once` is the sweep.
114 ///
115 /// A SCAN value that names no list (`Passive`, or an index outside
116 /// `menuScan`) holds no records — there is no such bucket to look up.
117 pub async fn records_for_scan(&self, scan_type: ScanType) -> Vec<String> {
118 let Some(list) = scan_type.scan_list() else {
119 return Vec::new();
120 };
121 // One bucket's lock, held for the clone alone. C `scanList`
122 // (`dbScan.c:1007-1051`) also releases `psl->lock` around every
123 // `dbProcess` — but it releases it holding a cursor INTO the list and
124 // re-reads `ellNext` under the lock on the next step, so the two are
125 // not the same construction and this must not be read as parity with
126 // it. Detaching from the list is only sound because nothing here
127 // processes; [`Self::scan_list_once`] is what C's `scanList` is.
128 self.inner
129 .scan_index
130 .bucket(list)
131 .lock()
132 .iter()
133 .map(|k| k.name.clone())
134 .collect()
135 }
136
137 /// The scan key `name` would be inserted under RIGHT NOW — its live PHAS,
138 /// record type and load-order sequence. `None` if the record is gone.
139 fn live_scan_key(&self, name: &str) -> Option<super::ScanKey> {
140 let rec = self.get_record_no_resolve(name)?;
141 let (phas, record_type) = {
142 let inst = rec.read();
143 (inst.common.phas, inst.record.record_type())
144 };
145 let seq = self.inner.load_order.load().get(name).copied().unwrap_or(0);
146 Some(super::ScanKey::new(phas, record_type, seq, name))
147 }
148
149 /// A cursor over `list` as it stands at each step — see [`ScanCursor`].
150 pub(crate) fn scan_cursor(&self, list: ScanList) -> ScanCursor {
151 ScanCursor { list, at: None }
152 }
153
154 /// Sweep one scan list, processing every record still in it — C `scanList`
155 /// (`dbScan.c:998-1051`), the single owner of "walk a scan list and
156 /// process it". Every driver goes through here: the periodic threads, and
157 /// the event lists, whose `eventCallback` (`dbScan.c:459-465`) is a bare
158 /// `scanList` call.
159 pub(crate) async fn scan_list_once(&self, list: ScanList) {
160 let mut cursor = self.scan_cursor(list);
161 while let Some(name) = cursor.next(self) {
162 let mut visited = std::collections::HashSet::new();
163 let _ = self.process_record_with_links(&name, &mut visited, 0).await;
164 }
165 }
166
167 /// Get all record names whose `PINI` is **exactly** `mode`.
168 ///
169 /// C matches the menu index with `!=` (`iocInit.c:598`
170 /// `if (precord->pini != pphase->pini) return;`), so each `menuPini`
171 /// choice selects a disjoint set of records driven by a *different* pass:
172 /// `YES` at `initialProcess()` (`iocInit.c:656`), `RUN`/`RUNNING`/`PAUSE`/
173 /// `PAUSED` from `piniProcessHook` (`iocInit.c:629-646`). A `PINI=RUN`
174 /// record must NOT be processed by the `YES` pass.
175 ///
176 /// Snapshot the records map under the outer
177 /// read lock, then drop it before fanning out per-record reads.
178 /// Pre-fix the outer `records.read()` lock was held across every
179 /// `rec.read().await` — under contention with a pending
180 /// `add_record` (which now takes the registration_mutex →
181 /// records.write()), startup could stall while every PINI
182 /// record was inspected serially.
183 pub async fn pini_records(&self, mode: PiniMode) -> Vec<String> {
184 let mut result = Vec::new();
185 for (name, rec) in self.records_in_load_order().await {
186 if rec.read().common.pini == mode.to_u16() as i16 {
187 result.push(name);
188 }
189 }
190 result
191 }
192
193 /// Every record, in database **load order** — the port's analogue of C's
194 /// `iterateRecords`, which walks the record-type / record-instance lists in
195 /// the order the `.db` declared them. That order is what makes two
196 /// same-`PHAS` records process deterministically; `load_order` is one of
197 /// the `scan_index` sort keys for the same reason (see `ScanKey`, which
198 /// also carries the record-type ordinal C's `iterateRecords` walks first —
199 /// this PINI sweep does not, and that is a separate gap).
200 ///
201 /// Snapshots the map under the records read lock and releases it before the
202 /// caller takes any per-record lock.
203 async fn records_in_load_order(
204 &self,
205 ) -> Vec<(String, std::sync::Arc<parking_lot::RwLock<RecordInstance>>)> {
206 let snapshot: Vec<_> = {
207 let records = self.inner.records.read();
208 records
209 .iter()
210 .map(|(n, r)| (n.clone(), r.clone()))
211 .collect()
212 };
213 let mut keyed: Vec<_> = {
214 let load_order = self.inner.load_order.load();
215 snapshot
216 .into_iter()
217 .map(|(name, rec)| (load_order.get(&name).copied().unwrap_or(0), name, rec))
218 .collect()
219 };
220 keyed.sort_unstable_by(|a, b| (a.0, &a.1).cmp(&(b.0, &b.1)));
221 keyed
222 .into_iter()
223 .map(|(_seq, name, rec)| (name, rec))
224 .collect()
225 }
226
227 /// C `piniProcess` (`iocInit.c:608-627`) — process every record whose
228 /// `PINI` is exactly `mode`, **in ascending `PHAS` order**, each with its
229 /// full link chain.
230 ///
231 /// The single owner of "run a PINI pass": `initialProcess()` calls it with
232 /// [`PiniMode::Yes`], and the `initHook` lifecycle calls it with
233 /// [`PiniMode::Run`] / [`PiniMode::Running`]. Every driver goes through
234 /// here, so neither the pass selection nor the phase ordering can diverge
235 /// between them.
236 ///
237 /// The sweep is C's, not a pre-sorted list: each pass over the database
238 /// processes the records at the current phase and, while doing so, finds
239 /// the *next* lowest `PHAS` still ahead of it. C spells this out
240 /// (`iocInit.c:614-619`) — "PHAS fields can be changed at runtime, so we
241 /// have to look for the lowest value of PHAS each time" — so a record whose
242 /// phase is raised by an earlier PINI record's processing is still picked
243 /// up in the correct later pass, which a snapshot-and-sort would miss.
244 pub async fn pini_process(&self, mode: PiniMode) {
245 // C `dbScan.h:34-35`: MAX_PHASE = SHRT_MAX, MIN_PHASE = SHRT_MIN. The
246 // phase cursors are `int` (`phaseData_t`), so `MAX_PHASE + 1` — the
247 // "no further phase found" sentinel — does not overflow.
248 const MIN_PHASE: i32 = i16::MIN as i32;
249 const NO_NEXT_PHASE: i32 = i16::MAX as i32 + 1;
250
251 let mut next = MIN_PHASE;
252 loop {
253 let this = next;
254 next = NO_NEXT_PHASE;
255 // `doRecordPini` (`iocInit.c:592-606`) over the record list. PINI
256 // and PHAS are read at the moment the record is visited, not
257 // snapshotted up front, so a PHAS that an earlier record's
258 // processing changed is honoured — the reason C re-scans rather
259 // than sorting once.
260 for (name, rec) in self.records_in_load_order().await {
261 let (pini, phas) = {
262 let instance = rec.read();
263 (instance.common.pini, i32::from(instance.common.phas))
264 };
265 if pini != mode.to_u16() as i16 {
266 continue;
267 }
268 if phas == this {
269 let mut visited = std::collections::HashSet::new();
270 let _ = self.process_record_with_links(&name, &mut visited, 0).await;
271 } else if phas > this && phas < next {
272 next = phas;
273 }
274 }
275 if next == NO_NEXT_PHASE {
276 return;
277 }
278 }
279 }
280
281 /// Process all records with `SCAN=Event`, regardless of `EVNT`.
282 ///
283 /// Back-compat entry point for the iocsh `postEvent` command,
284 /// whose handler currently drops the numeric event argument.
285 /// Prefer [`Self::post_event_named`] for C-correct per-event
286 /// routing — see `dbScan.c:548-552` `post_event` →
287 /// `postEvent(pevent_list[event])`.
288 pub async fn post_event(&self) {
289 if let Some(list) = ScanType::Event.scan_list() {
290 self.scan_list_once(list).await;
291 }
292 }
293
294 /// Process only the `SCAN=Event` records whose `EVNT` resolves to
295 /// `event_name`. Mirrors C `dbScan.c` event routing: each
296 /// `event_list` (`eventNameToHandle`) holds exactly the records
297 /// whose `EVNT` matches, and `postEvent` walks only that list.
298 ///
299 /// Event-name matching follows `eventNameToHandle` (`dbScan.c:469`):
300 /// surrounding whitespace is trimmed, and a numeric string with an
301 /// integer part in `[1,255]` is normalised to its integer form so
302 /// `"5"`, `" 5 "` and `"5.0"` all name the same event.
303 pub async fn post_event_named(&self, event_name: &str) {
304 let want = normalize_event_name(event_name);
305 if want.is_empty() {
306 // `eventNameToHandle` returns NULL for "0"/empty — no event.
307 return;
308 }
309 let Some(list) = ScanType::Event.scan_list() else {
310 return;
311 };
312 // Same live cursor as every other sweep: C keeps one `scan_list` per
313 // (event, priority) and `eventCallback` hands it to `scanList`
314 // (`dbScan.c:459-465`), so a record whose SCAN changes mid-sweep leaves
315 // the walk here exactly as it does on a periodic list. The port keeps
316 // one Event list and filters by EVNT at the cursor instead.
317 let mut cursor = self.scan_cursor(list);
318 while let Some(name) = cursor.next(self) {
319 // Read the record's EVNT and compare against the posted
320 // event name. Records that do not match are skipped — a
321 // record configured `EVNT=5` only fires on event 5.
322 let evnt = match self.get_record(&name) {
323 Some(rec) => rec.read().common.evnt.clone(),
324 None => continue,
325 };
326 if normalize_event_name(&evnt) != want {
327 continue;
328 }
329 let mut visited = std::collections::HashSet::new();
330 let _ = self.process_record_with_links(&name, &mut visited, 0).await;
331 }
332 }
333}
334
335/// A cursor over a LIVE scan list — C `scanList`'s `pse` (`dbScan.c:998-1051`).
336///
337/// The invariant: **the sweep observes the list, it does not own a copy of it.**
338/// C re-reads `ellNext(&pse->node)` under `psl->lock` on every step and carries
339/// a cursor-repair walk that exists precisely because `dbProcess` can change
340/// the SCAN field of an arbitrary number of records mid-sweep. A snapshot taken
341/// once at the top of the tick processes records the list no longer holds.
342///
343/// The port's list is an ordered set, not a linked list, so "my element left
344/// the list" has one answer instead of C's three: the next key strictly greater
345/// than where the cursor stood. That subsumes C's prev/next repair
346/// (`dbScan.c:1030-1044`) — and, because the position is a key rather than a
347/// pointer, it has no counterpart to C's "too many changes, wait till the next
348/// period" (`:1045-1048`), which is an artefact of losing the place rather than
349/// a scanning rule.
350///
351/// The step re-reads the last record's CURRENT key before advancing, so a
352/// record that moved within this list (its own processing changed its PHAS) is
353/// advanced from where it is now — C's `pse->pscan_list == psl` branch
354/// (`:1023-1029`).
355pub(crate) struct ScanCursor {
356 list: ScanList,
357 /// Where the cursor stands: the key of the record it last handed out.
358 at: Option<super::ScanKey>,
359}
360
361impl ScanCursor {
362 /// The next record still in the list, or `None` at its end.
363 ///
364 /// Takes the bucket lock for the step only, never across processing — C
365 /// holds `psl->lock` for the cursor step and releases it around every
366 /// `dbProcess`.
367 pub(crate) fn next(&mut self, db: &PvDatabase) -> Option<String> {
368 use std::ops::Bound;
369
370 let resume = match self.at.take() {
371 None => None,
372 Some(was) => {
373 let live = db.live_scan_key(&was.name);
374 let moved_but_present = match &live {
375 Some(k) => db.inner.scan_index.bucket(self.list).lock().contains(k),
376 None => false,
377 };
378 Some(if moved_but_present {
379 live.expect("checked present")
380 } else {
381 was
382 })
383 }
384 };
385
386 let next = {
387 let bucket = db.inner.scan_index.bucket(self.list).lock();
388 match &resume {
389 None => bucket.iter().next().cloned(),
390 Some(at) => bucket
391 .range((Bound::Excluded(at.clone()), Bound::Unbounded))
392 .next()
393 .cloned(),
394 }
395 };
396 self.at = next.clone();
397 next.map(|k| k.name)
398 }
399}
400
401/// Normalise an EPICS event name for routing comparison.
402///
403/// Mirrors `dbScan.c::eventNameToHandle` (`dbScan.c:469-533`):
404/// * leading/trailing whitespace is stripped;
405/// * a string that parses as a number with an integer part in
406/// `[1,255]` is canonicalised to that integer's decimal form
407/// (so numeric events from calc records match symbolic "5");
408/// * `"0"` (and anything that resolves to event 0) becomes empty —
409/// C's `eventNameToHandle` returns NULL for event 0.
410pub(crate) fn normalize_event_name(name: &str) -> String {
411 let trimmed = name.trim();
412 if trimmed.is_empty() {
413 return String::new();
414 }
415 if let Ok(num) = trimmed.parse::<f64>() {
416 if num >= 0.0 && num < 256.0 {
417 let int = num as i64;
418 if int < 1 {
419 // event 0 → no event
420 return String::new();
421 }
422 return int.to_string();
423 }
424 // Numeric but outside [0,256): fall through to literal match.
425 }
426 trimmed.to_string()
427}
428
429#[cfg(test)]
430mod tests {
431 use super::PvDatabase;
432 use super::normalize_event_name;
433 use crate::server::record::ScanType;
434
435 /// The ordering rule, stated as the thing that must hold rather than as
436 /// the record shape that once broke it: **`update_scan_index` takes L46
437 /// itself, so no caller may hold L46 when calling it.**
438 ///
439 /// A caller that breaks it used to park on itself forever, because
440 /// `PriorityInheritanceMutex` is not reentrant. The failure reached CI as
441 /// a 120-second timeout on whatever test happened to register a record —
442 /// a shape that reads as a load flake and hides which caller is at fault.
443 /// This pins the replacement: the violating call panics, immediately, and
444 /// names both ends.
445 ///
446 /// Deliberately expressed with a bare `lock_registration` + direct
447 /// `update_scan_index` pair and no record, no SIML and no SCAN field: the
448 /// rule belongs to the caller/owner contract, not to the one composition
449 /// (`add_loaded_record` → `rec_gbl_init_simm` → `apply_simm_scan_swap`)
450 /// that first exposed it. Any future tail added under a registration gate
451 /// trips this the same way.
452 #[test]
453 fn a_caller_holding_l46_cannot_reach_the_scan_index_owner() {
454 let db = PvDatabase::new();
455 let held = db.lock_registration("a_test_standing_in_for_a_registration_entry_point");
456
457 let violation = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
458 db.update_scan_index("ANY", ScanType::Passive, ScanType::Sec01, 0, 0);
459 }));
460
461 let payload = violation
462 .expect_err("holding L46 across update_scan_index must panic, not park the thread");
463 let msg = payload
464 .downcast_ref::<String>()
465 .map(String::as_str)
466 .or_else(|| payload.downcast_ref::<&str>().copied())
467 .unwrap_or("");
468 assert!(
469 msg.contains("not reentrant") && msg.contains("update_scan_index"),
470 "the panic must name the rule and the violating site, got: {msg}"
471 );
472 drop(held);
473 }
474
475 /// The other side of the same boundary — with no gate held, the owner
476 /// takes L46 itself and completes. Without this case the test above would
477 /// still pass if `update_scan_index` panicked unconditionally.
478 #[test]
479 fn the_scan_index_owner_takes_l46_itself_when_no_caller_holds_it() {
480 let db = PvDatabase::new();
481 db.update_scan_index("ANY", ScanType::Passive, ScanType::Sec01, 0, 0);
482 }
483
484 /// The gate is released on drop, including by unwinding out of a panic
485 /// between acquisitions. A leaked flag would make every later
486 /// registration on this thread panic and turn the tripwire into its own
487 /// outage.
488 #[test]
489 fn the_registration_gate_clears_on_drop_and_on_unwind() {
490 let db = PvDatabase::new();
491 drop(db.lock_registration("first"));
492 let _second = db.lock_registration("second");
493 drop(_second);
494
495 let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
496 let _g = db.lock_registration("panics_while_held");
497 panic!("unwind with the gate live");
498 }));
499 drop(db.lock_registration("after_unwind"));
500 }
501
502 #[test]
503 fn event_name_numeric_normalisation() {
504 // Whitespace trimmed, numeric forms canonicalised.
505 assert_eq!(normalize_event_name(" 5 "), "5");
506 assert_eq!(normalize_event_name("5.0"), "5");
507 assert_eq!(normalize_event_name("5"), "5");
508 // Event 0 / empty → no event.
509 assert_eq!(normalize_event_name("0"), "");
510 assert_eq!(normalize_event_name(""), "");
511 assert_eq!(normalize_event_name(" "), "");
512 // Symbolic name preserved.
513 assert_eq!(normalize_event_name("myEvent"), "myEvent");
514 assert_eq!(normalize_event_name(" myEvent "), "myEvent");
515 // Numeric out of [0,256) is treated literally.
516 assert_eq!(normalize_event_name("999"), "999");
517 }
518}