Skip to main content

spvirit_server/
monitor.rs

1//! Monitor subscription management for the PVA server.
2//!
3//! Tracks per-PV subscriber lists and dispatches monitor update messages.
4
5use std::collections::HashMap;
6
7use tokio::sync::{Mutex, mpsc};
8use tracing::debug;
9
10use spvirit_codec::spvirit_encode::{
11    encode_monitor_data_response_delta, encode_monitor_data_response_filtered,
12    encode_monitor_data_response_payload,
13};
14use spvirit_types::NtPayload;
15
16use crate::state::MonitorSub;
17
18/// Active connection channels and monitor subscriptions managed by the server.
19pub struct MonitorRegistry {
20    /// PV name → list of active monitor subscriptions.
21    pub monitors: Mutex<HashMap<String, Vec<MonitorSub>>>,
22    /// Connection id → message sender.
23    pub conns: Mutex<HashMap<u64, mpsc::Sender<Vec<u8>>>>,
24}
25
26impl MonitorRegistry {
27    pub fn new() -> Self {
28        Self {
29            monitors: Mutex::new(HashMap::new()),
30            conns: Mutex::new(HashMap::new()),
31        }
32    }
33
34    /// Send a raw message to a connection.
35    pub async fn send_msg(&self, conn_id: u64, msg: Vec<u8>) {
36        let conns = self.conns.lock().await;
37        if let Some(tx) = conns.get(&conn_id) {
38            let _ = tx.send(msg).await;
39        }
40    }
41
42    /// Build the wire bytes (if any) to send for `sub` given `payload`.
43    ///
44    /// Returns `Some(bytes)` when there is something new to send, in which
45    /// case the caller should also update `sub.last_snapshot` and apply any
46    /// pipeline credit accounting. Returns `None` when the update is a no-op
47    /// (duplicate of the last snapshot under the subscriber's field view) —
48    /// in that case the caller must NOT decrement `nfree`.
49    fn build_monitor_frame(sub: &MonitorSub, payload: &NtPayload) -> Option<Vec<u8>> {
50        let subcmd = 0x00;
51        // First frame: send the whole (possibly filtered) payload with bit 0 set.
52        let Some(prev) = sub.last_snapshot.as_ref() else {
53            let bytes = if let Some(ref desc) = sub.filtered_desc {
54                encode_monitor_data_response_filtered(
55                    sub.ioid,
56                    subcmd,
57                    payload,
58                    desc,
59                    sub.version,
60                    sub.is_be,
61                )
62            } else {
63                encode_monitor_data_response_payload(
64                    sub.ioid,
65                    subcmd,
66                    payload,
67                    sub.version,
68                    sub.is_be,
69                )
70            };
71            return Some(bytes);
72        };
73        // Subsequent frames.
74        if let Some(ref desc) = sub.filtered_desc {
75            // Filtered subscribers get a true sparse delta (may be None if the
76            // filtered view is unchanged).
77            encode_monitor_data_response_delta(
78                sub.ioid,
79                subcmd,
80                prev,
81                payload,
82                desc,
83                sub.version,
84                sub.is_be,
85            )
86        } else if prev == payload {
87            // Unfiltered subscriber, unchanged payload: suppress.
88            None
89        } else {
90            // Unfiltered subscriber, payload changed: send full.
91            Some(encode_monitor_data_response_payload(
92                sub.ioid,
93                subcmd,
94                payload,
95                sub.version,
96                sub.is_be,
97            ))
98        }
99    }
100
101    /// Broadcast a monitor update for `pv_name` to all running subscribers.
102    pub async fn notify_monitors(&self, pv_name: &str, payload: &NtPayload) {
103        let mut to_send: Vec<(u64, Vec<u8>)> = Vec::new();
104        {
105            let mut monitors = self.monitors.lock().await;
106            if let Some(list) = monitors.get_mut(pv_name) {
107                for sub in list.iter_mut() {
108                    if !sub.running {
109                        continue;
110                    }
111                    if sub.pipeline_enabled && sub.nfree == 0 {
112                        continue;
113                    }
114                    let Some(msg) = Self::build_monitor_frame(sub, payload) else {
115                        // No-op update — preserve pipeline credit.
116                        continue;
117                    };
118                    if sub.pipeline_enabled && sub.nfree > 0 {
119                        sub.nfree -= 1;
120                    }
121                    sub.last_snapshot = Some(payload.clone());
122                    to_send.push((sub.conn_id, msg));
123                }
124            }
125        }
126
127        for (conn_id, msg) in to_send {
128            self.send_msg(conn_id, msg).await;
129            debug!("Monitor update pv='{}' conn={}", pv_name, conn_id);
130        }
131    }
132
133    /// Send a monitor update to a specific subscriber.
134    pub async fn send_monitor_update_for(
135        &self,
136        pv_name: &str,
137        conn_id: u64,
138        ioid: u32,
139        payload: &NtPayload,
140    ) {
141        let mut to_send: Option<(u64, Vec<u8>)> = None;
142        {
143            let mut monitors = self.monitors.lock().await;
144            if let Some(list) = monitors.get_mut(pv_name) {
145                if let Some(sub) = list
146                    .iter_mut()
147                    .find(|s| s.conn_id == conn_id && s.ioid == ioid)
148                {
149                    if !sub.running {
150                        return;
151                    }
152                    if sub.pipeline_enabled && sub.nfree == 0 {
153                        return;
154                    }
155                    let Some(msg) = Self::build_monitor_frame(sub, payload) else {
156                        return;
157                    };
158                    if sub.pipeline_enabled && sub.nfree > 0 {
159                        sub.nfree -= 1;
160                    }
161                    sub.last_snapshot = Some(payload.clone());
162                    to_send = Some((sub.conn_id, msg));
163                }
164            }
165        }
166
167        if let Some((conn_id, msg)) = to_send {
168            self.send_msg(conn_id, msg).await;
169        }
170    }
171
172    /// Update a monitor subscription's running/pipeline state.
173    pub async fn update_monitor_subscription(
174        &self,
175        conn_id: u64,
176        ioid: u32,
177        pv_name: &str,
178        running: bool,
179        nfree: Option<u32>,
180        pipeline_enabled: Option<bool>,
181    ) -> bool {
182        let mut monitors = self.monitors.lock().await;
183        if let Some(list) = monitors.get_mut(pv_name) {
184            if let Some(sub) = list
185                .iter_mut()
186                .find(|s| s.conn_id == conn_id && s.ioid == ioid)
187            {
188                sub.running = running;
189                if let Some(v) = nfree {
190                    sub.nfree = v;
191                }
192                if let Some(enabled) = pipeline_enabled {
193                    if enabled {
194                        sub.pipeline_enabled = true;
195                    }
196                }
197                return true;
198            }
199        }
200        false
201    }
202
203    /// Remove a monitor subscription.
204    pub async fn remove_monitor_subscription(&self, conn_id: u64, ioid: u32, pv_name: &str) {
205        let mut monitors = self.monitors.lock().await;
206        if let Some(list) = monitors.get_mut(pv_name) {
207            list.retain(|s| s.conn_id != conn_id || s.ioid != ioid);
208        }
209    }
210
211    /// Remove all subscriptions and connection entries for a given connection.
212    pub async fn cleanup_connection(&self, conn_id: u64) {
213        {
214            let mut monitors = self.monitors.lock().await;
215            for list in monitors.values_mut() {
216                list.retain(|s| s.conn_id != conn_id);
217            }
218        }
219        {
220            let mut conns = self.conns.lock().await;
221            conns.remove(&conn_id);
222        }
223    }
224}
225
226#[cfg(test)]
227mod tests {
228    use super::*;
229    use spvirit_codec::spvd_decode::StructureDesc;
230    use spvirit_codec::spvd_encode::{filter_structure_desc, nt_payload_desc};
231    use spvirit_types::{NtPayload, NtScalar, ScalarValue};
232
233    fn make_sub(filtered: Option<StructureDesc>) -> MonitorSub {
234        MonitorSub {
235            conn_id: 1,
236            ioid: 42,
237            version: 2,
238            is_be: false,
239            running: true,
240            pipeline_enabled: false,
241            nfree: 0,
242            filtered_desc: filtered,
243            last_snapshot: None,
244        }
245    }
246
247    fn nt_payload(value: f64, severity: i32) -> NtPayload {
248        let mut nt = NtScalar::from_value(ScalarValue::F64(value));
249        nt.alarm_severity = severity;
250        NtPayload::Scalar(nt)
251    }
252
253    #[test]
254    fn unfiltered_first_frame_full_then_suppress_duplicate_then_resend_on_change() {
255        let mut sub = make_sub(None);
256        let p1 = nt_payload(1.0, 0);
257
258        // First frame: full payload.
259        let f1 = MonitorRegistry::build_monitor_frame(&sub, &p1).expect("first frame");
260        assert!(!f1.is_empty());
261        sub.last_snapshot = Some(p1.clone());
262
263        // Same payload: suppressed.
264        assert!(
265            MonitorRegistry::build_monitor_frame(&sub, &p1).is_none(),
266            "identical unfiltered payload must be suppressed"
267        );
268
269        // Changed payload: full again.
270        let p2 = nt_payload(2.0, 0);
271        let f2 = MonitorRegistry::build_monitor_frame(&sub, &p2).expect("full on change");
272        assert!(!f2.is_empty());
273    }
274
275    #[test]
276    fn filtered_first_frame_then_delta_none_when_selected_fields_unchanged() {
277        // Subscriber only cares about alarm.severity.
278        let p1 = nt_payload(1.0, 0);
279        let full_desc = nt_payload_desc(&p1);
280        let filt = filter_structure_desc(&full_desc, &["alarm.severity".to_string()]);
281        let mut sub = make_sub(Some(filt));
282
283        let f1 = MonitorRegistry::build_monitor_frame(&sub, &p1).expect("first filtered frame");
284        assert!(!f1.is_empty());
285        sub.last_snapshot = Some(p1.clone());
286
287        // Value changed, but alarm.severity is unchanged in the filtered view.
288        let p2 = nt_payload(2.0, 0);
289        assert!(
290            MonitorRegistry::build_monitor_frame(&sub, &p2).is_none(),
291            "filtered delta must be None when selected fields unchanged"
292        );
293    }
294
295    #[test]
296    fn filtered_delta_emitted_when_selected_field_changes() {
297        let p1 = nt_payload(1.0, 0);
298        let full_desc = nt_payload_desc(&p1);
299        let filt = filter_structure_desc(&full_desc, &["alarm.severity".to_string()]);
300        let mut sub = make_sub(Some(filt));
301
302        let _ = MonitorRegistry::build_monitor_frame(&sub, &p1).expect("first");
303        sub.last_snapshot = Some(p1.clone());
304
305        // Severity changes: delta must be emitted.
306        let p2 = nt_payload(1.0, 2);
307        let delta = MonitorRegistry::build_monitor_frame(&sub, &p2)
308            .expect("delta required when selected field changes");
309        assert!(!delta.is_empty());
310    }
311}