Skip to main content

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}