1use std::collections::{BTreeMap, BTreeSet};
19
20use crate::agent::AgentName;
21use crate::flight::{Flight, ItineraryId};
22use crate::graph::JoinSpec;
23use crate::graph::RouteGraph;
24use crate::route::Join;
25
26#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
28pub struct BarrierKey {
29 pub itinerary: ItineraryId,
31 pub to: AgentName,
33}
34
35impl BarrierKey {
36 #[must_use]
38 pub fn new(itinerary: ItineraryId, to: AgentName) -> Self {
39 Self { itinerary, to }
40 }
41}
42
43#[derive(Debug, Clone, PartialEq, Eq)]
45pub enum Delivery {
46 Parked {
48 waiting_for: Vec<AgentName>,
50 },
51 Ready(Vec<Flight>),
53 Direct(Box<Flight>),
59 Late(Box<Flight>),
68}
69
70#[derive(Debug, Clone)]
72pub struct Barrier {
73 required: BTreeSet<AgentName>,
74 join: Join,
75 parked: BTreeMap<AgentName, Flight>,
76 seen: BTreeSet<AgentName>,
81 released: bool,
83}
84
85impl Barrier {
86 #[must_use]
88 pub fn from_spec(spec: &JoinSpec) -> Self {
89 Self {
90 required: spec.upstreams.clone(),
91 join: spec.join,
92 parked: BTreeMap::new(),
93 seen: BTreeSet::new(),
94 released: false,
95 }
96 }
97
98 pub fn deliver(&mut self, flight: Flight) -> Delivery {
112 let Some(sender) = flight.from.agent().cloned() else {
113 return Delivery::Direct(Box::new(flight));
114 };
115
116 if !self.required.contains(&sender) {
117 return Delivery::Direct(Box::new(flight));
118 }
119
120 if self.seen.contains(&sender) {
123 self.parked.clear();
124 self.seen.clear();
125 self.released = false;
126 }
127 self.seen.insert(sender.clone());
128
129 if self.released {
130 return Delivery::Late(Box::new(flight));
131 }
132
133 self.parked.insert(sender, flight);
134
135 let complete = match self.join {
136 Join::Any => true,
137 Join::All => self.parked.len() == self.required.len(),
138 };
139
140 if complete {
141 self.released = true;
142 Delivery::Ready(std::mem::take(&mut self.parked).into_values().collect())
143 } else {
144 Delivery::Parked {
145 waiting_for: self.waiting_for(),
146 }
147 }
148 }
149
150 #[must_use]
152 pub fn has_released(&self) -> bool {
153 self.released
154 }
155
156 #[must_use]
158 pub fn waiting_for(&self) -> Vec<AgentName> {
159 self.required
160 .iter()
161 .filter(|name| !self.parked.contains_key(*name))
162 .cloned()
163 .collect()
164 }
165
166 #[must_use]
171 pub fn is_reachable(&self, graph: &RouteGraph, live: &BTreeSet<AgentName>) -> bool {
172 let missing = self.waiting_for();
173 if missing.is_empty() {
174 return true;
175 }
176
177 let reachable = graph.reachable_from(live);
178 match self.join {
179 Join::All => missing.iter().all(|name| reachable.contains(name)),
180 Join::Any => missing.iter().any(|name| reachable.contains(name)),
181 }
182 }
183
184 #[must_use]
186 pub fn parked_count(&self) -> usize {
187 self.parked.len()
188 }
189}
190
191#[cfg(test)]
192mod tests {
193 use super::*;
194 use crate::config::Config;
195 use crate::flight::Origin;
196
197 #[test]
198 fn an_any_barrier_wakes_the_agent_once_however_many_upstreams_arrive() {
199 let mut barrier = barrier_any();
205
206 assert!(matches!(
207 barrier.deliver(flight_from("probe_a", "first")),
208 Delivery::Ready(_)
209 ));
210 assert!(barrier.has_released());
211
212 match barrier.deliver(flight_from("probe_b", "second")) {
213 Delivery::Late(flight) => assert_eq!(flight.body, "second"),
214 other => panic!("the straggler must not wake the agent again: {other:?}"),
215 }
216 }
217
218 #[test]
219 fn an_any_barrier_can_still_fire_again_on_the_next_time_round_the_loop() {
220 let mut barrier = barrier_any();
223 barrier.deliver(flight_from("probe_a", "first"));
224 barrier.deliver(flight_from("probe_b", "late"));
225
226 match barrier.deliver(flight_from("probe_a", "next time round")) {
227 Delivery::Ready(flights) => {
228 assert_eq!(flights.len(), 1);
229 assert_eq!(flights[0].body, "next time round");
230 }
231 other => panic!("a new wave should release: {other:?}"),
232 }
233 }
234
235 #[test]
236 fn an_all_barrier_also_wakes_the_agent_only_once_per_wave() {
237 let mut barrier = barrier_all();
238 barrier.deliver(flight_from("probe_a", "a"));
239
240 assert!(matches!(
241 barrier.deliver(flight_from("probe_b", "b")),
242 Delivery::Ready(_)
243 ));
244 assert!(barrier.has_released());
245 }
246
247 fn flight_from(sender: &str, body: &str) -> Flight {
248 Flight::new(
249 ItineraryId::generate(),
250 Origin::Agent(sender.into()),
251 "collector".into(),
252 body,
253 5,
254 )
255 }
256
257 fn barrier_any() -> Barrier {
258 Barrier {
259 required: ["probe_a".into(), "probe_b".into()].into_iter().collect(),
260 join: Join::Any,
261 parked: BTreeMap::new(),
262 seen: BTreeSet::new(),
263 released: false,
264 }
265 }
266
267 fn barrier_all() -> Barrier {
268 Barrier {
269 required: ["probe_a".into(), "probe_b".into()].into_iter().collect(),
270 join: Join::All,
271 parked: BTreeMap::new(),
272 seen: BTreeSet::new(),
273 released: false,
274 }
275 }
276
277 #[test]
278 fn parks_until_every_upstream_arrives() {
279 let mut barrier = barrier_all();
280
281 let first = barrier.deliver(flight_from("probe_a", "a"));
282
283 assert_eq!(
284 first,
285 Delivery::Parked {
286 waiting_for: vec!["probe_b".into()]
287 }
288 );
289 }
290
291 #[test]
292 fn releases_once_with_every_parked_flight() {
293 let mut barrier = barrier_all();
294 let _ = barrier.deliver(flight_from("probe_a", "a"));
295
296 let Delivery::Ready(flights) = barrier.deliver(flight_from("probe_b", "b")) else {
297 panic!("barrier should release once both upstreams arrive");
298 };
299
300 assert_eq!(flights.len(), 2);
301 assert_eq!(barrier.parked_count(), 0, "barrier drains on release");
302 }
303
304 #[test]
305 fn join_any_releases_on_the_first_arrival() {
306 let mut barrier = Barrier {
307 required: ["probe_a".into(), "probe_b".into()].into_iter().collect(),
308 join: Join::Any,
309 parked: BTreeMap::new(),
310 seen: BTreeSet::new(),
311 released: false,
312 };
313
314 let Delivery::Ready(flights) = barrier.deliver(flight_from("probe_a", "a")) else {
315 panic!("join = any should release immediately");
316 };
317
318 assert_eq!(flights.len(), 1);
319 }
320
321 #[test]
322 fn a_repeat_delivery_discards_stale_siblings() {
323 let mut barrier = barrier_all();
324 let _ = barrier.deliver(flight_from("probe_b", "stale verdict"));
325 let _ = barrier.deliver(flight_from("probe_a", "first attempt"));
326 let _ = barrier.deliver(flight_from("probe_a", "first attempt"));
328
329 let outcome = barrier.deliver(flight_from("probe_a", "second attempt"));
330
331 assert_eq!(
332 outcome,
333 Delivery::Parked {
334 waiting_for: vec!["probe_b".into()]
335 },
336 "a repeat delivery must discard partial state, not complete the barrier"
337 );
338 }
339
340 #[test]
341 fn a_sender_outside_the_join_is_delivered_directly() {
342 let mut barrier = barrier_all();
343
344 let outcome = barrier.deliver(flight_from("stranger", "hello"));
345
346 assert!(matches!(outcome, Delivery::Direct(_)));
347 assert_eq!(barrier.parked_count(), 0);
348 }
349
350 #[test]
351 fn a_human_flight_is_delivered_directly() {
352 let mut barrier = barrier_all();
353 let flight = Flight::new(
354 ItineraryId::generate(),
355 Origin::Human,
356 "collector".into(),
357 "go",
358 5,
359 );
360
361 assert!(matches!(barrier.deliver(flight), Delivery::Direct(_)));
362 }
363
364 #[test]
365 fn a_direct_flight_leaves_parked_state_intact() {
366 let mut barrier = barrier_all();
369 let _ = barrier.deliver(flight_from("probe_a", "a"));
370
371 let _ = barrier.deliver(flight_from("stranger", "unrelated work"));
372
373 assert_eq!(barrier.parked_count(), 1);
374 assert_eq!(barrier.waiting_for(), vec![AgentName::from("probe_b")]);
375
376 let Delivery::Ready(flights) = barrier.deliver(flight_from("probe_b", "b")) else {
377 panic!("the barrier should still complete normally");
378 };
379 assert_eq!(flights.len(), 2);
380 }
381
382 fn pipeline_graph() -> RouteGraph {
383 let config = Config::from_toml(
384 r#"
385 [[routes]]
386 from = "planner"
387 to = ["probe_a", "probe_b"]
388
389 [[routes]]
390 from = ["probe_a", "probe_b"]
391 to = "collector"
392 join = "all"
393
394 [[routes]]
395 from = "probe_a"
396 to = "planner"
397 "#,
398 "test.toml",
399 )
400 .expect("config parses");
401 RouteGraph::from_config(&config)
402 }
403
404 #[test]
405 fn a_live_upstream_keeps_the_barrier_alive() {
406 let mut barrier = barrier_all();
407 let _ = barrier.deliver(flight_from("probe_a", "a"));
408 let live = ["probe_b".into()].into_iter().collect();
409
410 assert!(barrier.is_reachable(&pipeline_graph(), &live));
411 }
412
413 #[test]
414 fn an_upstream_reachable_from_a_live_run_keeps_it_alive() {
415 let mut barrier = barrier_all();
416 let _ = barrier.deliver(flight_from("probe_a", "a"));
417 let live = ["planner".into()].into_iter().collect();
419
420 assert!(barrier.is_reachable(&pipeline_graph(), &live));
421 }
422
423 #[test]
424 fn a_barrier_no_live_run_can_reach_is_abandoned() {
425 let mut barrier = barrier_all();
428 let _ = barrier.deliver(flight_from("probe_a", "a"));
429 let live = ["collector".into()].into_iter().collect();
430
431 assert!(
432 !barrier.is_reachable(&pipeline_graph(), &live),
433 "a barrier that nothing can satisfy must be abandoned, not parked forever"
434 );
435 }
436
437 #[test]
438 fn a_barrier_with_nothing_live_is_abandoned() {
439 let mut barrier = barrier_all();
440 let _ = barrier.deliver(flight_from("probe_a", "a"));
441
442 assert!(!barrier.is_reachable(&pipeline_graph(), &BTreeSet::new()));
443 }
444}