1use std::collections::{HashMap, HashSet};
13use std::time::Duration;
14
15use crate::node_set::NodeSet;
16use crate::propagation::{PropagationChannel, PropagationResult, PropagationTreeRouter};
17use crate::topology::TopologyTree;
18use crate::worker::{WorkerError, WorkerState};
19
20fn nodeset_from_strings(names: &[String]) -> String {
22 names.join(",")
23}
24
25#[derive(Debug, Clone)]
27pub struct TreeWorkerConfig {
28 pub fanout: usize,
30 pub timeout: Option<Duration>,
32 pub stderr: bool,
34 pub remote: bool,
36 pub source: Option<String>,
38 pub dest: Option<String>,
40 pub reverse: bool,
42 pub preserve: bool,
44}
45
46impl Default for TreeWorkerConfig {
47 fn default() -> Self {
48 Self {
49 fanout: 64,
50 timeout: None,
51 stderr: false,
52 remote: true,
53 source: None,
54 dest: None,
55 reverse: false,
56 preserve: false,
57 }
58 }
59}
60
61#[derive(Debug)]
63struct GatewayState {
64 channel: PropagationChannel,
66 active_targets: HashSet<String>,
68 started: bool,
70}
71
72#[derive(Debug, Clone)]
74pub enum TreeWorkerEvent {
75 StdOut {
77 node: String,
78 gateway: String,
79 data: Vec<u8>,
80 },
81 StdErr {
83 node: String,
84 gateway: String,
85 data: Vec<u8>,
86 },
87 Close {
89 node: String,
90 gateway: String,
91 rc: i32,
92 },
93 Timeout { node: String, gateway: String },
95 Routing {
97 event: String,
98 gateway: String,
99 targets: String,
100 },
101}
102
103pub struct TreeWorker {
112 nodes: NodeSet,
114 command: Option<String>,
116 config: TreeWorkerConfig,
118 state: WorkerState,
120 router: Option<PropagationTreeRouter>,
122 gateways: HashMap<String, GatewayState>,
124 retcodes: HashMap<String, i32>,
126 events: Vec<TreeWorkerEvent>,
128 close_count: usize,
130 target_count: usize,
132 has_timeout: bool,
134 worker_id: u64,
136 timeout_nodes: HashSet<String>,
138}
139
140impl TreeWorker {
141 pub fn new(nodes: NodeSet, command: String, config: TreeWorkerConfig) -> Self {
148 static NEXT_ID: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
149
150 Self {
151 nodes,
152 command: Some(command),
153 config,
154 state: WorkerState::Pending,
155 router: None,
156 gateways: HashMap::new(),
157 retcodes: HashMap::new(),
158 events: Vec::new(),
159 close_count: 0,
160 target_count: 0,
161 has_timeout: false,
162 worker_id: NEXT_ID.fetch_add(1, std::sync::atomic::Ordering::SeqCst),
163 timeout_nodes: HashSet::new(),
164 }
165 }
166
167 pub fn set_router(&mut self, router: PropagationTreeRouter) {
172 self.router = Some(router);
173 }
174
175 pub fn set_topology(&mut self, root: &str, topology: &TopologyTree) -> Result<(), WorkerError> {
184 let router = PropagationTreeRouter::new(root, topology, self.config.fanout)
185 .map_err(|e| WorkerError::General(format!("topology error: {}", e)))?;
186 self.router = Some(router);
187 Ok(())
188 }
189
190 pub fn nodes(&self) -> &NodeSet {
192 &self.nodes
193 }
194
195 pub fn command(&self) -> Option<&str> {
197 self.command.as_deref()
198 }
199
200 pub fn state(&self) -> WorkerState {
202 self.state
203 }
204
205 pub fn retcodes(&self) -> &HashMap<String, i32> {
207 &self.retcodes
208 }
209
210 pub fn events(&self) -> &[TreeWorkerEvent] {
212 &self.events
213 }
214
215 pub fn drain_events(&mut self) -> Vec<TreeWorkerEvent> {
217 std::mem::take(&mut self.events)
218 }
219
220 pub fn num_timeout(&self) -> usize {
222 self.timeout_nodes.len()
223 }
224
225 pub fn timeout_nodes(&self) -> &HashSet<String> {
227 &self.timeout_nodes
228 }
229
230 pub fn has_timeout(&self) -> bool {
232 self.has_timeout
233 }
234
235 pub fn is_done(&self) -> bool {
237 self.state == WorkerState::Done || self.state == WorkerState::Aborted
238 }
239
240 fn distribute(&self) -> Result<Vec<(NodeSet, NodeSet)>, WorkerError> {
245 let router = self.router.as_ref().ok_or_else(|| {
246 WorkerError::General("router not set; call set_router() or set_topology()".to_string())
247 })?;
248
249 let mut distribution: HashMap<String, NodeSet> = HashMap::new();
250
251 for (gw, dstset) in router.dispatch(&self.nodes) {
252 let gw_str = gw.to_string();
253 distribution
254 .entry(gw_str)
255 .and_modify(|ns| ns.update(&dstset))
256 .or_insert(dstset);
257 }
258
259 Ok(distribution
260 .into_iter()
261 .map(|(k, v)| {
262 let gw = NodeSet::parse(&k).unwrap_or_default();
263 (gw, v)
264 })
265 .collect())
266 }
267
268 pub fn start(&mut self) -> Result<Vec<(NodeSet, NodeSet)>, WorkerError> {
273 if self.state != WorkerState::Pending {
274 return Err(WorkerError::General(
275 "worker already started or aborted".to_string(),
276 ));
277 }
278
279 self.state = WorkerState::Running;
280
281 let next_hops = self.distribute()?;
282
283 for (gw, targets) in &next_hops {
284 let target_count = targets.len();
285 self.target_count += target_count;
286
287 let gw_str = gw.to_string();
288 let targets_str = targets.to_string();
289
290 if gw_str == targets_str {
292 continue;
294 }
295
296 let channel = PropagationChannel::new(&gw_str);
298 let gw_state = GatewayState {
299 channel,
300 active_targets: targets.iter().collect(),
301 started: false,
302 };
303 self.gateways.insert(gw_str, gw_state);
304 }
305
306 Ok(next_hops)
307 }
308
309 pub fn start_gateway(
315 &mut self,
316 gateway: &str,
317 topology_bytes: &[u8],
318 ) -> Result<(), WorkerError> {
319 let gw_state = self
320 .gateways
321 .get_mut(gateway)
322 .ok_or_else(|| WorkerError::General(format!("unknown gateway: {}", gateway)))?;
323
324 if gw_state.started {
325 return Err(WorkerError::General(format!(
326 "gateway {} already started",
327 gateway
328 )));
329 }
330
331 gw_state.channel.start(topology_bytes);
332 gw_state.started = true;
333
334 if let Some(ref cmd) = self.command {
336 let targets: Vec<String> = gw_state.active_targets.iter().cloned().collect();
337 let targets_str = nodeset_from_strings(&targets);
338 gw_state
339 .channel
340 .shell(&targets_str, cmd.as_bytes(), self.worker_id);
341 }
342
343 Ok(())
344 }
345
346 pub fn process_result(
351 &mut self,
352 gateway: &str,
353 result: PropagationResult,
354 ) -> Result<bool, WorkerError> {
355 match result {
356 PropagationResult::StdOut { ref node, ref data } => {
357 self.events.push(TreeWorkerEvent::StdOut {
358 node: node.clone(),
359 gateway: gateway.to_string(),
360 data: data.clone(),
361 });
362 }
363 PropagationResult::StdErr { ref node, ref data } => {
364 self.events.push(TreeWorkerEvent::StdErr {
365 node: node.clone(),
366 gateway: gateway.to_string(),
367 data: data.clone(),
368 });
369 }
370 PropagationResult::Retcode { ref node, rc } => {
371 self.retcodes.insert(node.clone(), rc);
372 self.close_count += 1;
373
374 if let Some(gw_state) = self.gateways.get_mut(gateway) {
376 gw_state.active_targets.remove(node);
377 }
378
379 self.events.push(TreeWorkerEvent::Close {
380 node: node.clone(),
381 gateway: gateway.to_string(),
382 rc,
383 });
384 }
385 PropagationResult::Timeout { ref node } => {
386 self.timeout_nodes.insert(node.clone());
387 self.has_timeout = true;
388 self.close_count += 1;
389
390 if let Some(gw_state) = self.gateways.get_mut(gateway) {
391 gw_state.active_targets.remove(node);
392 }
393
394 self.events.push(TreeWorkerEvent::Timeout {
395 node: node.clone(),
396 gateway: gateway.to_string(),
397 });
398 }
399 PropagationResult::Routing {
400 ref event,
401 ref gateway,
402 ref targets,
403 } => {
404 self.events.push(TreeWorkerEvent::Routing {
405 event: event.clone(),
406 gateway: gateway.clone(),
407 targets: targets.clone(),
408 });
409 }
410 }
411
412 let done = self.close_count >= self.target_count && self.target_count > 0;
414 if done {
415 self.state = WorkerState::Done;
416 }
417
418 self.check_gateway_fini(gateway);
420
421 Ok(done)
422 }
423
424 pub fn record_direct_close(&mut self, node: &str, rc: i32) {
428 self.retcodes.insert(node.to_string(), rc);
429 self.close_count += 1;
430
431 if self.close_count >= self.target_count && self.target_count > 0 {
432 self.state = WorkerState::Done;
433 }
434 }
435
436 pub fn record_direct_timeout(&mut self, node: &str) {
438 self.timeout_nodes.insert(node.to_string());
439 self.has_timeout = true;
440 self.close_count += 1;
441
442 if self.close_count >= self.target_count && self.target_count > 0 {
443 self.state = WorkerState::Done;
444 }
445 }
446
447 fn check_gateway_fini(&mut self, gateway: &str) {
449 if let Some(gw_state) = self.gateways.get(gateway) {
450 if gw_state.active_targets.is_empty() {
451 }
454 }
455 }
456
457 pub fn write_remote(&mut self, buf: &[u8]) {
459 let worker_id = self.worker_id;
460 for (_, gw_state) in self.gateways.iter_mut() {
461 if !gw_state.active_targets.is_empty() {
462 let targets: Vec<String> = gw_state.active_targets.iter().cloned().collect();
463 let targets_str = nodeset_from_strings(&targets);
464 gw_state.channel.write(&targets_str, buf, worker_id);
465 }
466 }
467 }
468
469 pub fn set_write_eof_remote(&mut self) {
471 let worker_id = self.worker_id;
472 for (_, gw_state) in self.gateways.iter_mut() {
473 if !gw_state.active_targets.is_empty() {
474 let targets: Vec<String> = gw_state.active_targets.iter().cloned().collect();
475 let targets_str = nodeset_from_strings(&targets);
476 gw_state.channel.set_write_eof(&targets_str, worker_id);
477 }
478 }
479 }
480
481 pub fn abort(&mut self) {
485 if self.is_done() {
486 return;
487 }
488
489 for (gateway, gw_state) in self.gateways.iter_mut() {
491 let remaining: Vec<String> = gw_state.active_targets.drain().collect();
492 for node in remaining {
493 self.retcodes.insert(node.clone(), 76); self.close_count += 1;
495 self.events.push(TreeWorkerEvent::Close {
496 node,
497 gateway: gateway.clone(),
498 rc: 76,
499 });
500 }
501 }
502
503 self.state = WorkerState::Aborted;
504 }
505
506 pub fn active_gateway_count(&self) -> usize {
508 self.gateways
509 .values()
510 .filter(|gs| !gs.active_targets.is_empty())
511 .count()
512 }
513
514 pub fn active_gateways(&self) -> Vec<&str> {
516 self.gateways
517 .iter()
518 .filter(|(_, gs)| !gs.active_targets.is_empty())
519 .map(|(name, _)| name.as_str())
520 .collect()
521 }
522
523 pub fn num_nodes(&self) -> usize {
525 self.nodes.len()
526 }
527
528 pub fn worker_id(&self) -> u64 {
530 self.worker_id
531 }
532}
533
534#[cfg(test)]
535mod tests {
536 use super::*;
537
538 fn make_nodeset(s: &str) -> NodeSet {
539 NodeSet::parse(s).unwrap()
540 }
541
542 #[test]
543 fn test_tree_worker_config_default() {
544 let config = TreeWorkerConfig::default();
545 assert_eq!(config.fanout, 64);
546 assert!(config.timeout.is_none());
547 assert!(!config.stderr);
548 assert!(config.remote);
549 assert!(config.source.is_none());
550 assert!(config.dest.is_none());
551 assert!(!config.reverse);
552 assert!(!config.preserve);
553 }
554
555 #[test]
556 fn test_tree_worker_creation() {
557 let nodes = make_nodeset("node[1-10]");
558 let config = TreeWorkerConfig::default();
559 let worker = TreeWorker::new(nodes.clone(), "hostname".to_string(), config);
560
561 assert_eq!(worker.state(), WorkerState::Pending);
562 assert_eq!(worker.num_nodes(), 10);
563 assert_eq!(worker.command(), Some("hostname"));
564 assert!(worker.retcodes().is_empty());
565 assert!(worker.events().is_empty());
566 assert!(!worker.has_timeout());
567 assert_eq!(worker.num_timeout(), 0);
568 }
569
570 #[test]
571 fn test_tree_worker_start_without_router() {
572 let nodes = make_nodeset("node[1-3]");
573 let config = TreeWorkerConfig::default();
574 let mut worker = TreeWorker::new(nodes, "echo hi".to_string(), config);
575
576 let result = worker.start();
577 assert!(result.is_err());
578 assert!(result.unwrap_err().to_string().contains("router not set"));
579 }
580
581 #[test]
582 fn test_tree_worker_abort() {
583 let nodes = make_nodeset("node[1-5]");
584 let config = TreeWorkerConfig::default();
585 let mut worker = TreeWorker::new(nodes, "echo hi".to_string(), config);
586
587 worker.abort();
588 assert_eq!(worker.state(), WorkerState::Aborted);
589 assert!(worker.is_done());
590 }
591
592 #[test]
593 fn test_tree_worker_record_direct_close() {
594 let nodes = make_nodeset("node[1-3]");
595 let config = TreeWorkerConfig::default();
596 let mut worker = TreeWorker::new(nodes, "echo hi".to_string(), config);
597
598 worker.state = WorkerState::Running;
600 worker.target_count = 3;
601
602 worker.record_direct_close("node1", 0);
603 assert_eq!(worker.retcodes().get("node1"), Some(&0));
604 assert!(!worker.is_done());
605
606 worker.record_direct_close("node2", 0);
607 assert!(!worker.is_done());
608
609 worker.record_direct_close("node3", 1);
610 assert!(worker.is_done());
611 assert_eq!(worker.retcodes().get("node3"), Some(&1));
612 }
613
614 #[test]
615 fn test_tree_worker_record_direct_timeout() {
616 let nodes = make_nodeset("node[1-2]");
617 let config = TreeWorkerConfig::default();
618 let mut worker = TreeWorker::new(nodes, "sleep 100".to_string(), config);
619
620 worker.state = WorkerState::Running;
621 worker.target_count = 2;
622
623 worker.record_direct_timeout("node1");
624 assert!(worker.has_timeout());
625 assert_eq!(worker.num_timeout(), 1);
626 assert!(worker.timeout_nodes().contains("node1"));
627 assert!(!worker.is_done());
628
629 worker.record_direct_close("node2", 0);
630 assert!(worker.is_done());
631 }
632
633 #[test]
634 fn test_tree_worker_process_result_stdout() {
635 let nodes = make_nodeset("node[1-2]");
636 let config = TreeWorkerConfig::default();
637 let mut worker = TreeWorker::new(nodes, "echo hi".to_string(), config);
638
639 worker.state = WorkerState::Running;
640 worker.target_count = 2;
641
642 let result = PropagationResult::StdOut {
643 node: "node1".to_string(),
644 data: b"hello world".to_vec(),
645 };
646
647 let done = worker.process_result("gw1", result).unwrap();
648 assert!(!done);
649 assert_eq!(worker.events().len(), 1);
650
651 match &worker.events()[0] {
652 TreeWorkerEvent::StdOut { node, data, .. } => {
653 assert_eq!(node, "node1");
654 assert_eq!(data, b"hello world");
655 }
656 _ => panic!("expected StdOut event"),
657 }
658 }
659
660 #[test]
661 fn test_tree_worker_process_result_retcode() {
662 let nodes = make_nodeset("node[1-2]");
663 let config = TreeWorkerConfig::default();
664 let mut worker = TreeWorker::new(nodes, "echo hi".to_string(), config);
665
666 worker.state = WorkerState::Running;
667 worker.target_count = 2;
668
669 let result1 = PropagationResult::Retcode {
671 node: "node1".to_string(),
672 rc: 0,
673 };
674 let done = worker.process_result("gw1", result1).unwrap();
675 assert!(!done);
676
677 let result2 = PropagationResult::Retcode {
679 node: "node2".to_string(),
680 rc: 42,
681 };
682 let done = worker.process_result("gw1", result2).unwrap();
683 assert!(done);
684 assert_eq!(worker.state(), WorkerState::Done);
685 assert_eq!(worker.retcodes().get("node2"), Some(&42));
686 }
687
688 #[test]
689 fn test_tree_worker_process_result_timeout() {
690 let nodes = make_nodeset("node1");
691 let config = TreeWorkerConfig::default();
692 let mut worker = TreeWorker::new(nodes, "sleep 999".to_string(), config);
693
694 worker.state = WorkerState::Running;
695 worker.target_count = 1;
696
697 let result = PropagationResult::Timeout {
698 node: "node1".to_string(),
699 };
700 let done = worker.process_result("gw1", result).unwrap();
701 assert!(done);
702 assert!(worker.has_timeout());
703 assert!(worker.timeout_nodes().contains("node1"));
704 }
705
706 #[test]
707 fn test_tree_worker_drain_events() {
708 let nodes = make_nodeset("node1");
709 let config = TreeWorkerConfig::default();
710 let mut worker = TreeWorker::new(nodes, "echo hi".to_string(), config);
711
712 worker.state = WorkerState::Running;
713 worker.target_count = 1;
714
715 let result = PropagationResult::StdOut {
716 node: "node1".to_string(),
717 data: b"data".to_vec(),
718 };
719 worker.process_result("gw1", result).unwrap();
720
721 assert_eq!(worker.events().len(), 1);
722 let drained = worker.drain_events();
723 assert_eq!(drained.len(), 1);
724 assert!(worker.events().is_empty());
725 }
726
727 #[test]
728 fn test_tree_worker_unique_ids() {
729 let nodes = make_nodeset("node1");
730 let config1 = TreeWorkerConfig::default();
731 let config2 = TreeWorkerConfig::default();
732 let w1 = TreeWorker::new(nodes.clone(), "a".to_string(), config1);
733 let w2 = TreeWorker::new(nodes, "b".to_string(), config2);
734 assert_ne!(w1.worker_id(), w2.worker_id());
735 }
736
737 #[test]
738 fn test_tree_worker_active_gateways_empty() {
739 let nodes = make_nodeset("node[1-3]");
740 let config = TreeWorkerConfig::default();
741 let worker = TreeWorker::new(nodes, "echo hi".to_string(), config);
742
743 assert_eq!(worker.active_gateway_count(), 0);
744 assert!(worker.active_gateways().is_empty());
745 }
746
747 #[test]
748 fn test_tree_worker_routing_event() {
749 let nodes = make_nodeset("node1");
750 let config = TreeWorkerConfig::default();
751 let mut worker = TreeWorker::new(nodes, "echo".to_string(), config);
752
753 worker.state = WorkerState::Running;
754 worker.target_count = 1;
755
756 let result = PropagationResult::Routing {
757 event: "reroute".to_string(),
758 gateway: "gw1".to_string(),
759 targets: "node1".to_string(),
760 };
761 worker.process_result("gw1", result).unwrap();
762
763 assert_eq!(worker.events().len(), 1);
764 match &worker.events()[0] {
765 TreeWorkerEvent::Routing {
766 event,
767 gateway,
768 targets,
769 } => {
770 assert_eq!(event, "reroute");
771 assert_eq!(gateway, "gw1");
772 assert_eq!(targets, "node1");
773 }
774 _ => panic!("expected Routing event"),
775 }
776 }
777}