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