1use std::sync::Arc;
14use std::sync::Mutex;
15use std::sync::atomic::{AtomicU64, Ordering};
16use std::time::{Duration, Instant};
17
18use anyhow::{Result, anyhow};
19use arc_swap::ArcSwap;
20use tokio::sync::broadcast;
21use zenoh::Session;
22use zenoh::sample::SampleKind;
23
24use crate::stats::StatsTable;
25use crate::tree::KeyTreeSnapshot;
26
27#[derive(Debug, Clone)]
30pub struct SampleView {
31 pub key: String,
33 pub payload: zenoh::bytes::ZBytes,
34 pub encoding: String,
36 pub kind: SampleKind,
37 pub timestamp: Option<zenoh::time::Timestamp>,
39}
40
41#[derive(Debug, Clone)]
43pub enum FleetEvent {
44 Sample(Arc<SampleView>),
45 NodeUp(String),
47 NodeDown(String),
49 StatsTick,
51}
52
53#[derive(Debug, Clone)]
55pub struct MonitorSpec {
56 pub selectors: Vec<String>,
58 pub liveliness: Option<String>,
61 pub stats_tick: Duration,
63 pub capacity: usize,
66}
67
68impl Default for MonitorSpec {
69 fn default() -> Self {
70 MonitorSpec {
71 selectors: Vec::new(),
72 liveliness: None,
73 stats_tick: Duration::from_millis(250),
74 capacity: 1024,
75 }
76 }
77}
78
79pub struct MonitorCore {
83 tx: broadcast::Sender<FleetEvent>,
84 stats: Mutex<StatsTable>,
85 tree: ArcSwap<KeyTreeSnapshot>,
86 dropped: AtomicU64,
87}
88
89impl MonitorCore {
90 pub fn new(capacity: usize) -> Arc<MonitorCore> {
91 let (tx, _) = broadcast::channel(capacity.max(2));
92 Arc::new(MonitorCore {
93 tx,
94 stats: Mutex::new(StatsTable::new()),
95 tree: ArcSwap::from_pointee(KeyTreeSnapshot::default()),
96 dropped: AtomicU64::new(0),
97 })
98 }
99
100 pub fn ingest(&self, view: SampleView, sn: Option<u32>) {
103 {
104 let mut stats = self.stats.lock().expect("stats lock");
105 stats.record(&view.key, view.payload.len(), sn, Instant::now());
106 }
107 let _ = self.tx.send(FleetEvent::Sample(Arc::new(view)));
109 }
110
111 pub fn node_event(&self, key: String, up: bool) {
112 let _ = self.tx.send(if up {
113 FleetEvent::NodeUp(key)
114 } else {
115 FleetEvent::NodeDown(key)
116 });
117 }
118
119 pub fn tick(&self) {
121 let snapshot = {
122 let stats = self.stats.lock().expect("stats lock");
123 KeyTreeSnapshot::build(&stats)
124 };
125 self.tree.store(Arc::new(snapshot));
126 let _ = self.tx.send(FleetEvent::StatsTick);
127 }
128
129 pub fn tree(&self) -> Arc<KeyTreeSnapshot> {
131 self.tree.load_full()
132 }
133
134 pub fn with_stats<R>(&self, f: impl FnOnce(&StatsTable) -> R) -> R {
136 f(&self.stats.lock().expect("stats lock"))
137 }
138
139 pub fn dropped(&self) -> u64 {
141 self.dropped.load(Ordering::Relaxed)
142 }
143
144 pub fn events(self: &Arc<Self>) -> EventStream {
146 EventStream {
147 rx: self.tx.subscribe(),
148 core: Arc::clone(self),
149 }
150 }
151}
152
153pub struct EventStream {
158 rx: broadcast::Receiver<FleetEvent>,
159 core: Arc<MonitorCore>,
160}
161
162#[derive(Debug, Clone)]
164pub enum StreamItem {
165 Event(FleetEvent),
166 Dropped(u64),
167}
168
169impl EventStream {
170 pub async fn recv(&mut self) -> Option<StreamItem> {
172 match self.rx.recv().await {
173 Ok(ev) => Some(StreamItem::Event(ev)),
174 Err(broadcast::error::RecvError::Lagged(n)) => {
175 self.core.dropped.fetch_add(n, Ordering::Relaxed);
176 Some(StreamItem::Dropped(n))
177 }
178 Err(broadcast::error::RecvError::Closed) => None,
179 }
180 }
181}
182
183pub struct Monitor {
185 core: Arc<MonitorCore>,
186 tasks: Vec<tokio::task::JoinHandle<()>>,
187}
188
189impl Monitor {
190 pub async fn start(session: &Session, spec: MonitorSpec) -> Result<Monitor> {
192 let core = MonitorCore::new(spec.capacity);
193 let mut tasks = Vec::new();
194
195 for selector in &spec.selectors {
196 let subscriber = session
197 .declare_subscriber(selector)
198 .await
199 .map_err(|e| anyhow!("subscribe {selector}: {e}"))?;
200 let core = Arc::clone(&core);
201 tasks.push(tokio::spawn(async move {
202 while let Ok(sample) = subscriber.recv_async().await {
203 let sn = sample.source_info().map(|si| si.source_sn());
204 core.ingest(
205 SampleView {
206 key: sample.key_expr().as_str().to_string(),
207 payload: sample.payload().clone(),
208 encoding: sample.encoding().to_string(),
209 kind: sample.kind(),
210 timestamp: sample.timestamp().copied(),
211 },
212 sn,
213 );
214 }
215 }));
216 }
217
218 if let Some(liveliness_sel) = &spec.liveliness {
219 let subscriber = session
220 .liveliness()
221 .declare_subscriber(liveliness_sel)
222 .history(true)
223 .await
224 .map_err(|e| anyhow!("liveliness subscribe {liveliness_sel}: {e}"))?;
225 let core = Arc::clone(&core);
226 tasks.push(tokio::spawn(async move {
227 while let Ok(sample) = subscriber.recv_async().await {
228 let key = sample.key_expr().as_str().to_string();
229 core.node_event(key, sample.kind() == SampleKind::Put);
230 }
231 }));
232 }
233
234 {
235 let core = Arc::clone(&core);
236 let period = spec.stats_tick;
237 tasks.push(tokio::spawn(async move {
238 let mut interval = tokio::time::interval(period);
239 interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
240 loop {
241 interval.tick().await;
242 core.tick();
243 }
244 }));
245 }
246
247 Ok(Monitor { core, tasks })
248 }
249
250 pub fn core(&self) -> &Arc<MonitorCore> {
251 &self.core
252 }
253
254 pub fn events(&self) -> EventStream {
255 self.core.events()
256 }
257
258 pub fn tree(&self) -> Arc<KeyTreeSnapshot> {
259 self.core.tree()
260 }
261
262 pub fn stop(self) {
264 for t in &self.tasks {
265 t.abort();
266 }
267 }
268}
269
270#[cfg(test)]
271mod tests {
272 use super::*;
273
274 fn view(key: &str, len: usize) -> SampleView {
275 SampleView {
276 key: key.to_string(),
277 payload: zenoh::bytes::ZBytes::from(vec![0u8; len]),
278 encoding: "zenoh/bytes".to_string(),
279 kind: SampleKind::Put,
280 timestamp: None,
281 }
282 }
283
284 #[tokio::test]
285 async fn events_flow_and_snapshots_rebuild_on_tick() {
286 let core = MonitorCore::new(8);
287 let mut events = core.events();
288 core.ingest(view("zs/v1/h-a/telemetry/x/m", 4), None);
289 core.tick();
290
291 let Some(StreamItem::Event(FleetEvent::Sample(s))) = events.recv().await else {
292 panic!("expected sample");
293 };
294 assert_eq!(s.key, "zs/v1/h-a/telemetry/x/m");
295 assert_eq!(s.payload.len(), 4);
296 let Some(StreamItem::Event(FleetEvent::StatsTick)) = events.recv().await else {
297 panic!("expected tick");
298 };
299 let snap = core.tree();
300 assert_eq!(snap.keys, 1);
301 assert_eq!(snap.root.subtree_count, 1);
302 }
303
304 #[tokio::test]
307 async fn overflow_surfaces_as_dropped_counts() {
308 let core = MonitorCore::new(2);
309 let mut slow = core.events();
310 for i in 0..10 {
311 core.ingest(view(&format!("zs/v1/h-a/telemetry/x/m{i}"), 1), None);
312 }
313 let Some(StreamItem::Dropped(n)) = slow.recv().await else {
314 panic!("expected a dropped count first");
315 };
316 assert!(n >= 8, "missed at least 8, reported {n}");
317 assert_eq!(core.dropped(), n);
318 let Some(StreamItem::Event(FleetEvent::Sample(_))) = slow.recv().await else {
320 panic!("expected a sample after the gap report");
321 };
322 }
323}