1use std::time::Duration;
17
18use super::{
19 error::BspError,
20 graph::{EdgeId, NodeId},
21 run::PendingSuspension,
22};
23
24pub trait BspInterceptor: Send + Sync {
28 fn on_superstep_start(&self, _step: u64, _frontier: &[NodeId]) {}
30
31 fn on_node_start(&self, _node_id: NodeId, _name: &str) {}
33
34 fn on_node_complete(&self, _node_id: NodeId, _name: &str, _duration: Duration) {}
37
38 fn on_node_error(&self, _node_id: NodeId, _name: &str, _err: &BspError) {}
41
42 fn on_node_retry(&self, _node_id: NodeId, _attempt: u32, _wait: Duration, _err: &BspError) {}
45
46 fn on_edge_taken(&self, _source: NodeId, _target: NodeId, _condition_label: &str) {}
49
50 fn on_loop_iteration(&self, _edge: EdgeId, _iteration: u64, _max: Option<u64>) {}
52
53 fn on_suspended(&self, _suspensions: &[PendingSuspension]) {}
55
56 fn on_complete(&self, _steps: u64) {}
58}
59
60#[derive(Debug, Clone)]
62pub enum BspEvent {
63 SuperstepStart { step: u64, frontier: Vec<NodeId> },
65 NodeStart { node_id: NodeId, name: String },
67 NodeComplete {
69 node_id: NodeId,
70 name: String,
71 duration: Duration,
72 },
73 NodeError {
75 node_id: NodeId,
76 name: String,
77 error: BspError,
78 },
79 NodeRetry {
81 node_id: NodeId,
82 attempt: u32,
83 wait: Duration,
84 error: BspError,
85 },
86 EdgeTaken {
88 source: NodeId,
89 target: NodeId,
90 condition_label: String,
91 },
92 LoopIteration {
94 edge: EdgeId,
95 iteration: u64,
96 max: Option<u64>,
97 },
98 Suspended { suspensions: Vec<PendingSuspension> },
100 Complete { steps: u64 },
102}
103
104#[cfg(feature = "tokio")]
110pub struct ChannelInterceptor {
111 tx: tokio::sync::broadcast::Sender<BspEvent>,
112}
113
114#[cfg(feature = "tokio")]
115impl ChannelInterceptor {
116 #[must_use]
118 pub fn new(capacity: usize) -> Self {
119 let (tx, _) = tokio::sync::broadcast::channel(capacity);
120 Self { tx }
121 }
122
123 #[must_use]
125 pub fn subscribe(&self) -> tokio::sync::broadcast::Receiver<BspEvent> {
126 self.tx.subscribe()
127 }
128
129 fn send(&self, event: BspEvent) {
130 let _ = self.tx.send(event);
131 }
132}
133
134#[cfg(feature = "tokio")]
135impl BspInterceptor for ChannelInterceptor {
136 fn on_superstep_start(&self, step: u64, frontier: &[NodeId]) {
137 self.send(BspEvent::SuperstepStart {
138 step,
139 frontier: frontier.to_vec(),
140 });
141 }
142
143 fn on_node_start(&self, node_id: NodeId, name: &str) {
144 self.send(BspEvent::NodeStart {
145 node_id,
146 name: name.to_string(),
147 });
148 }
149
150 fn on_node_complete(&self, node_id: NodeId, name: &str, duration: Duration) {
151 self.send(BspEvent::NodeComplete {
152 node_id,
153 name: name.to_string(),
154 duration,
155 });
156 }
157
158 fn on_node_error(&self, node_id: NodeId, name: &str, err: &BspError) {
159 self.send(BspEvent::NodeError {
160 node_id,
161 name: name.to_string(),
162 error: err.clone(),
163 });
164 }
165
166 fn on_node_retry(&self, node_id: NodeId, attempt: u32, wait: Duration, err: &BspError) {
167 self.send(BspEvent::NodeRetry {
168 node_id,
169 attempt,
170 wait,
171 error: err.clone(),
172 });
173 }
174
175 fn on_edge_taken(&self, source: NodeId, target: NodeId, condition_label: &str) {
176 self.send(BspEvent::EdgeTaken {
177 source,
178 target,
179 condition_label: condition_label.to_string(),
180 });
181 }
182
183 fn on_loop_iteration(&self, edge: EdgeId, iteration: u64, max: Option<u64>) {
184 self.send(BspEvent::LoopIteration {
185 edge,
186 iteration,
187 max,
188 });
189 }
190
191 fn on_suspended(&self, suspensions: &[PendingSuspension]) {
192 self.send(BspEvent::Suspended {
193 suspensions: suspensions.to_vec(),
194 });
195 }
196
197 fn on_complete(&self, steps: u64) {
198 self.send(BspEvent::Complete { steps });
199 }
200}
201
202#[cfg(test)]
203mod tests {
204 use super::*;
205
206 struct LegacyInterceptor;
208
209 impl BspInterceptor for LegacyInterceptor {
210 fn on_node_start(&self, _node_id: NodeId, _name: &str) {}
211 fn on_node_complete(&self, _node_id: NodeId, _name: &str, _duration: Duration) {}
212 fn on_node_error(&self, _node_id: NodeId, _name: &str, _err: &BspError) {}
213 }
214
215 #[test]
216 fn test_default_methods_are_noops() {
217 let interceptor = LegacyInterceptor;
218 interceptor.on_superstep_start(0, &[1, 2]);
220 interceptor.on_node_retry(1, 1, Duration::ZERO, &BspError::Deadlock);
221 interceptor.on_edge_taken(1, 2, "always");
222 interceptor.on_loop_iteration(0, 1, Some(30));
223 interceptor.on_suspended(&[]);
224 interceptor.on_complete(3);
225 }
226
227 #[cfg(feature = "tokio")]
228 #[test]
229 fn test_channel_interceptor_broadcasts() {
230 let interceptor = ChannelInterceptor::new(16);
231 let mut rx = interceptor.subscribe();
232 interceptor.on_edge_taken(1, 2, "always");
233 interceptor.on_complete(7);
234 let first = rx.try_recv().unwrap();
235 let second = rx.try_recv().unwrap();
236 assert!(matches!(
237 first,
238 BspEvent::EdgeTaken {
239 source: 1,
240 target: 2,
241 ..
242 }
243 ));
244 assert!(matches!(second, BspEvent::Complete { steps: 7 }));
245 }
246}