Skip to main content

epics_base_rs/server/database/
scan_index.rs

1use crate::server::record::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 old_scan != ScanType::Passive {
38                if let Some(set) = index.get_mut(&old_scan) {
39                    set.retain(|(_, _, n)| n != name);
40                    if set.is_empty() {
41                        index.remove(&old_scan);
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        if cur_scan != ScanType::Passive {
63            // Re-use the record's existing load-order sequence so the
64            // scan-index secondary key stays stable across SCAN/PHAS
65            // edits. A record loaded before should always scan before
66            // a later-loaded record at the same PHAS.
67            let seq = self
68                .inner
69                .load_order
70                .read()
71                .await
72                .get(name)
73                .copied()
74                .unwrap_or(0);
75            self.inner
76                .scan_index
77                .write()
78                .await
79                .entry(cur_scan)
80                .or_default()
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    pub async fn records_for_scan(&self, scan_type: ScanType) -> Vec<String> {
88        self.inner
89            .scan_index
90            .read()
91            .await
92            .get(&scan_type)
93            .map(|s| s.iter().map(|(_, _, name)| name.clone()).collect())
94            .unwrap_or_default()
95    }
96
97    /// Get all record names that have PINI=true.
98    ///
99    /// Snapshot the records map under the outer
100    /// read lock, then drop it before fanning out per-record reads.
101    /// Pre-fix the outer `records.read()` lock was held across every
102    /// `rec.read().await` — under contention with a pending
103    /// `add_record` (which now takes the registration_mutex →
104    /// records.write()), startup could stall while every PINI
105    /// record was inspected serially.
106    pub async fn pini_records(&self) -> Vec<String> {
107        let snapshot: Vec<_> = {
108            let records = self.inner.records.read().await;
109            records
110                .iter()
111                .map(|(n, r)| (n.clone(), r.clone()))
112                .collect()
113        };
114        let mut result = Vec::new();
115        for (name, rec) in snapshot {
116            let instance = rec.read().await;
117            if instance.common.pini {
118                result.push(name);
119            }
120        }
121        result
122    }
123
124    /// Process all records with `SCAN=Event`, regardless of `EVNT`.
125    ///
126    /// Back-compat entry point for the iocsh `postEvent` command,
127    /// whose handler currently drops the numeric event argument.
128    /// Prefer [`Self::post_event_named`] for C-correct per-event
129    /// routing — see `dbScan.c:548-552` `post_event` →
130    /// `postEvent(pevent_list[event])`.
131    pub async fn post_event(&self) {
132        let names = self.records_for_scan(ScanType::Event).await;
133        for name in &names {
134            let mut visited = std::collections::HashSet::new();
135            let _ = self.process_record_with_links(name, &mut visited, 0).await;
136        }
137    }
138
139    /// Process only the `SCAN=Event` records whose `EVNT` resolves to
140    /// `event_name`. Mirrors C `dbScan.c` event routing: each
141    /// `event_list` (`eventNameToHandle`) holds exactly the records
142    /// whose `EVNT` matches, and `postEvent` walks only that list.
143    ///
144    /// Event-name matching follows `eventNameToHandle` (`dbScan.c:469`):
145    /// surrounding whitespace is trimmed, and a numeric string with an
146    /// integer part in `[1,255]` is normalised to its integer form so
147    /// `"5"`, `" 5 "` and `"5.0"` all name the same event.
148    pub async fn post_event_named(&self, event_name: &str) {
149        let want = normalize_event_name(event_name);
150        if want.is_empty() {
151            // `eventNameToHandle` returns NULL for "0"/empty — no event.
152            return;
153        }
154        let names = self.records_for_scan(ScanType::Event).await;
155        for name in &names {
156            // Read the record's EVNT and compare against the posted
157            // event name. Records that do not match are skipped — a
158            // record configured `EVNT=5` only fires on event 5.
159            let evnt = match self.get_record(name).await {
160                Some(rec) => rec.read().await.common.evnt.clone(),
161                None => continue,
162            };
163            if normalize_event_name(&evnt) != want {
164                continue;
165            }
166            let mut visited = std::collections::HashSet::new();
167            let _ = self.process_record_with_links(name, &mut visited, 0).await;
168        }
169    }
170}
171
172/// Normalise an EPICS event name for routing comparison.
173///
174/// Mirrors `dbScan.c::eventNameToHandle` (`dbScan.c:469-533`):
175/// * leading/trailing whitespace is stripped;
176/// * a string that parses as a number with an integer part in
177///   `[1,255]` is canonicalised to that integer's decimal form
178///   (so numeric events from calc records match symbolic "5");
179/// * `"0"` (and anything that resolves to event 0) becomes empty —
180///   C's `eventNameToHandle` returns NULL for event 0.
181pub(crate) fn normalize_event_name(name: &str) -> String {
182    let trimmed = name.trim();
183    if trimmed.is_empty() {
184        return String::new();
185    }
186    if let Ok(num) = trimmed.parse::<f64>() {
187        if num >= 0.0 && num < 256.0 {
188            let int = num as i64;
189            if int < 1 {
190                // event 0 → no event
191                return String::new();
192            }
193            return int.to_string();
194        }
195        // Numeric but outside [0,256): fall through to literal match.
196    }
197    trimmed.to_string()
198}
199
200#[cfg(test)]
201mod tests {
202    use super::normalize_event_name;
203
204    #[test]
205    fn event_name_numeric_normalisation() {
206        // Whitespace trimmed, numeric forms canonicalised.
207        assert_eq!(normalize_event_name(" 5 "), "5");
208        assert_eq!(normalize_event_name("5.0"), "5");
209        assert_eq!(normalize_event_name("5"), "5");
210        // Event 0 / empty → no event.
211        assert_eq!(normalize_event_name("0"), "");
212        assert_eq!(normalize_event_name(""), "");
213        assert_eq!(normalize_event_name("   "), "");
214        // Symbolic name preserved.
215        assert_eq!(normalize_event_name("myEvent"), "myEvent");
216        assert_eq!(normalize_event_name(" myEvent "), "myEvent");
217        // Numeric out of [0,256) is treated literally.
218        assert_eq!(normalize_event_name("999"), "999");
219    }
220}