Skip to main content

epics_base_rs/server/database/
scan_index.rs

1use crate::server::record::{PiniMode, RecordInstance, 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.inner.registration_mutex.lock();
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(|(_, _, n)| n != 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) = {
65            let inst = rec_arc.read();
66            (inst.common.scan, inst.common.phas)
67        };
68        // C `scanAdd` refuses both `Passive` and an out-of-menu index — the
69        // latter with "scanAdd detected illegal SCAN value" — so neither can
70        // key a bucket. [`ScanList::of`] is that refusal.
71        if let Some(list) = cur_scan.scan_list() {
72            // Re-use the record's existing load-order sequence so the
73            // scan-index secondary key stays stable across SCAN/PHAS
74            // edits. A record loaded before should always scan before
75            // a later-loaded record at the same PHAS.
76            let seq = self.inner.load_order.load().get(name).copied().unwrap_or(0);
77            self.inner
78                .scan_index
79                .bucket(list)
80                .lock()
81                .insert((cur_phas, seq, name.to_string()));
82        }
83    }
84
85    /// Get record names for a given scan type, sorted by PHAS then
86    /// database load order (C `dbScan.c` stable same-PHAS FIFO).
87    ///
88    /// A SCAN value that names no list (`Passive`, or an index outside
89    /// `menuScan`) holds no records — there is no such bucket to look up.
90    pub async fn records_for_scan(&self, scan_type: ScanType) -> Vec<String> {
91        let Some(list) = scan_type.scan_list() else {
92            return Vec::new();
93        };
94        // One bucket's lock, held for the clone only and released before the
95        // caller's processing loop — C `scanList` (`dbScan.c:1007-1051`) holds
96        // `psl->lock` for the cursor step alone and releases it around every
97        // `dbProcess`. Neither holds its lock across processing.
98        self.inner
99            .scan_index
100            .bucket(list)
101            .lock()
102            .iter()
103            .map(|(_, _, name)| name.clone())
104            .collect()
105    }
106
107    /// Get all record names whose `PINI` is **exactly** `mode`.
108    ///
109    /// C matches the menu index with `!=` (`iocInit.c:598`
110    /// `if (precord->pini != pphase->pini) return;`), so each `menuPini`
111    /// choice selects a disjoint set of records driven by a *different* pass:
112    /// `YES` at `initialProcess()` (`iocInit.c:656`), `RUN`/`RUNNING`/`PAUSE`/
113    /// `PAUSED` from `piniProcessHook` (`iocInit.c:629-646`). A `PINI=RUN`
114    /// record must NOT be processed by the `YES` pass.
115    ///
116    /// Snapshot the records map under the outer
117    /// read lock, then drop it before fanning out per-record reads.
118    /// Pre-fix the outer `records.read()` lock was held across every
119    /// `rec.read().await` — under contention with a pending
120    /// `add_record` (which now takes the registration_mutex →
121    /// records.write()), startup could stall while every PINI
122    /// record was inspected serially.
123    pub async fn pini_records(&self, mode: PiniMode) -> Vec<String> {
124        let mut result = Vec::new();
125        for (name, rec) in self.records_in_load_order().await {
126            if rec.read().common.pini == mode.to_u16() as i16 {
127                result.push(name);
128            }
129        }
130        result
131    }
132
133    /// Every record, in database **load order** — the port's analogue of C's
134    /// `iterateRecords`, which walks the record-type / record-instance lists in
135    /// the order the `.db` declared them. That order is what makes two
136    /// same-`PHAS` records process deterministically; `load_order` is already
137    /// the `scan_index` secondary sort key for the same reason
138    /// (`database/mod.rs:184`).
139    ///
140    /// Snapshots the map under the records read lock and releases it before the
141    /// caller takes any per-record lock.
142    async fn records_in_load_order(
143        &self,
144    ) -> Vec<(String, std::sync::Arc<parking_lot::RwLock<RecordInstance>>)> {
145        let snapshot: Vec<_> = {
146            let records = self.inner.records.read();
147            records
148                .iter()
149                .map(|(n, r)| (n.clone(), r.clone()))
150                .collect()
151        };
152        let mut keyed: Vec<_> = {
153            let load_order = self.inner.load_order.load();
154            snapshot
155                .into_iter()
156                .map(|(name, rec)| (load_order.get(&name).copied().unwrap_or(0), name, rec))
157                .collect()
158        };
159        keyed.sort_unstable_by(|a, b| (a.0, &a.1).cmp(&(b.0, &b.1)));
160        keyed
161            .into_iter()
162            .map(|(_seq, name, rec)| (name, rec))
163            .collect()
164    }
165
166    /// C `piniProcess` (`iocInit.c:608-627`) — process every record whose
167    /// `PINI` is exactly `mode`, **in ascending `PHAS` order**, each with its
168    /// full link chain.
169    ///
170    /// The single owner of "run a PINI pass": `initialProcess()` calls it with
171    /// [`PiniMode::Yes`], and the `initHook` lifecycle calls it with
172    /// [`PiniMode::Run`] / [`PiniMode::Running`]. Every driver goes through
173    /// here, so neither the pass selection nor the phase ordering can diverge
174    /// between them.
175    ///
176    /// The sweep is C's, not a pre-sorted list: each pass over the database
177    /// processes the records at the current phase and, while doing so, finds
178    /// the *next* lowest `PHAS` still ahead of it. C spells this out
179    /// (`iocInit.c:614-619`) — "PHAS fields can be changed at runtime, so we
180    /// have to look for the lowest value of PHAS each time" — so a record whose
181    /// phase is raised by an earlier PINI record's processing is still picked
182    /// up in the correct later pass, which a snapshot-and-sort would miss.
183    pub async fn pini_process(&self, mode: PiniMode) {
184        // C `dbScan.h:34-35`: MAX_PHASE = SHRT_MAX, MIN_PHASE = SHRT_MIN. The
185        // phase cursors are `int` (`phaseData_t`), so `MAX_PHASE + 1` — the
186        // "no further phase found" sentinel — does not overflow.
187        const MIN_PHASE: i32 = i16::MIN as i32;
188        const NO_NEXT_PHASE: i32 = i16::MAX as i32 + 1;
189
190        let mut next = MIN_PHASE;
191        loop {
192            let this = next;
193            next = NO_NEXT_PHASE;
194            // `doRecordPini` (`iocInit.c:592-606`) over the record list. PINI
195            // and PHAS are read at the moment the record is visited, not
196            // snapshotted up front, so a PHAS that an earlier record's
197            // processing changed is honoured — the reason C re-scans rather
198            // than sorting once.
199            for (name, rec) in self.records_in_load_order().await {
200                let (pini, phas) = {
201                    let instance = rec.read();
202                    (instance.common.pini, i32::from(instance.common.phas))
203                };
204                if pini != mode.to_u16() as i16 {
205                    continue;
206                }
207                if phas == this {
208                    let mut visited = std::collections::HashSet::new();
209                    let _ = self.process_record_with_links(&name, &mut visited, 0).await;
210                } else if phas > this && phas < next {
211                    next = phas;
212                }
213            }
214            if next == NO_NEXT_PHASE {
215                return;
216            }
217        }
218    }
219
220    /// Process all records with `SCAN=Event`, regardless of `EVNT`.
221    ///
222    /// Back-compat entry point for the iocsh `postEvent` command,
223    /// whose handler currently drops the numeric event argument.
224    /// Prefer [`Self::post_event_named`] for C-correct per-event
225    /// routing — see `dbScan.c:548-552` `post_event` →
226    /// `postEvent(pevent_list[event])`.
227    pub async fn post_event(&self) {
228        let names = self.records_for_scan(ScanType::Event).await;
229        for name in &names {
230            let mut visited = std::collections::HashSet::new();
231            let _ = self.process_record_with_links(name, &mut visited, 0).await;
232        }
233    }
234
235    /// Process only the `SCAN=Event` records whose `EVNT` resolves to
236    /// `event_name`. Mirrors C `dbScan.c` event routing: each
237    /// `event_list` (`eventNameToHandle`) holds exactly the records
238    /// whose `EVNT` matches, and `postEvent` walks only that list.
239    ///
240    /// Event-name matching follows `eventNameToHandle` (`dbScan.c:469`):
241    /// surrounding whitespace is trimmed, and a numeric string with an
242    /// integer part in `[1,255]` is normalised to its integer form so
243    /// `"5"`, `" 5 "` and `"5.0"` all name the same event.
244    pub async fn post_event_named(&self, event_name: &str) {
245        let want = normalize_event_name(event_name);
246        if want.is_empty() {
247            // `eventNameToHandle` returns NULL for "0"/empty — no event.
248            return;
249        }
250        let names = self.records_for_scan(ScanType::Event).await;
251        for name in &names {
252            // Read the record's EVNT and compare against the posted
253            // event name. Records that do not match are skipped — a
254            // record configured `EVNT=5` only fires on event 5.
255            let evnt = match self.get_record(name) {
256                Some(rec) => rec.read().common.evnt.clone(),
257                None => continue,
258            };
259            if normalize_event_name(&evnt) != want {
260                continue;
261            }
262            let mut visited = std::collections::HashSet::new();
263            let _ = self.process_record_with_links(name, &mut visited, 0).await;
264        }
265    }
266}
267
268/// Normalise an EPICS event name for routing comparison.
269///
270/// Mirrors `dbScan.c::eventNameToHandle` (`dbScan.c:469-533`):
271/// * leading/trailing whitespace is stripped;
272/// * a string that parses as a number with an integer part in
273///   `[1,255]` is canonicalised to that integer's decimal form
274///   (so numeric events from calc records match symbolic "5");
275/// * `"0"` (and anything that resolves to event 0) becomes empty —
276///   C's `eventNameToHandle` returns NULL for event 0.
277pub(crate) fn normalize_event_name(name: &str) -> String {
278    let trimmed = name.trim();
279    if trimmed.is_empty() {
280        return String::new();
281    }
282    if let Ok(num) = trimmed.parse::<f64>() {
283        if num >= 0.0 && num < 256.0 {
284            let int = num as i64;
285            if int < 1 {
286                // event 0 → no event
287                return String::new();
288            }
289            return int.to_string();
290        }
291        // Numeric but outside [0,256): fall through to literal match.
292    }
293    trimmed.to_string()
294}
295
296#[cfg(test)]
297mod tests {
298    use super::normalize_event_name;
299
300    #[test]
301    fn event_name_numeric_normalisation() {
302        // Whitespace trimmed, numeric forms canonicalised.
303        assert_eq!(normalize_event_name(" 5 "), "5");
304        assert_eq!(normalize_event_name("5.0"), "5");
305        assert_eq!(normalize_event_name("5"), "5");
306        // Event 0 / empty → no event.
307        assert_eq!(normalize_event_name("0"), "");
308        assert_eq!(normalize_event_name(""), "");
309        assert_eq!(normalize_event_name("   "), "");
310        // Symbolic name preserved.
311        assert_eq!(normalize_event_name("myEvent"), "myEvent");
312        assert_eq!(normalize_event_name(" myEvent "), "myEvent");
313        // Numeric out of [0,256) is treated literally.
314        assert_eq!(normalize_event_name("999"), "999");
315    }
316}