1use std::any::TypeId;
13use std::collections::HashMap;
14use std::fmt;
15use std::sync::Arc;
16use std::sync::Mutex;
17
18use hyperactor_config::AttrValue;
19use hyperactor_config::attrs::declare_attrs;
20use serde::Deserialize;
21use serde::Serialize;
22use typeuri::Named;
23use uuid::Uuid;
24
25use crate::ActorAddr;
26use crate::PortAddr;
27
28pub(crate) fn is_bypass_workq_type_id(id: TypeId) -> bool {
35 id == TypeId::of::<crate::introspect::IntrospectMessage>()
36 || id == TypeId::of::<crate::proc::StatusMessage>()
37}
38
39#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Named, AttrValue)]
42pub struct OrderingSessionSnapshot {
43 pub session_id: Uuid,
47
48 pub sender: Option<ActorAddr>,
52
53 pub last_released_seq: u64,
56
57 pub expected_next_seq: u64,
60
61 pub buffered_count: usize,
64
65 pub oldest_buffered_seq: Option<u64>,
67
68 pub newest_buffered_seq: Option<u64>,
70}
71
72#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Named, AttrValue)]
81pub struct OrderingSnapshot {
82 pub enabled: bool,
85
86 pub sessions: Vec<OrderingSessionSnapshot>,
89
90 pub skipped_session_count: usize,
94}
95
96impl OrderingSnapshot {
97 pub fn is_complete(&self) -> bool {
100 self.skipped_session_count == 0
101 }
102}
103
104impl fmt::Display for OrderingSessionSnapshot {
105 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
106 write!(f, "{}", serde_json::to_string(self).unwrap())
108 }
109}
110
111impl std::str::FromStr for OrderingSessionSnapshot {
112 type Err = serde_json::Error;
113 fn from_str(s: &str) -> Result<Self, Self::Err> {
114 serde_json::from_str(s)
115 }
116}
117
118impl fmt::Display for OrderingSnapshot {
119 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
120 write!(f, "{}", serde_json::to_string(self).unwrap())
121 }
122}
123
124impl std::str::FromStr for OrderingSnapshot {
125 type Err = serde_json::Error;
126 fn from_str(s: &str) -> Result<Self, Self::Err> {
127 serde_json::from_str(s)
128 }
129}
130
131#[derive(Clone, Debug, Hash, PartialEq, Eq)]
135enum SeqKey {
136 Actor(ActorAddr),
138 Port(PortAddr),
140}
141
142#[derive(Debug, Serialize, Deserialize, Clone, Named, AttrValue, PartialEq)]
144pub enum SeqInfo {
145 Session {
147 session_id: Uuid,
149 seq: u64,
151 },
152 Direct,
155}
156
157impl SeqInfo {
158 pub fn is_valid(&self) -> bool {
160 !matches!(self, Self::Session { seq: 0, .. })
161 }
162}
163
164impl fmt::Display for SeqInfo {
165 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
166 match self {
167 Self::Direct => write!(f, "direct"),
168 Self::Session { session_id, seq } => write!(f, "{}:{}", session_id, seq),
169 }
170 }
171}
172
173impl std::str::FromStr for SeqInfo {
174 type Err = anyhow::Error;
175
176 fn from_str(s: &str) -> Result<Self, Self::Err> {
177 if s == "direct" {
178 return Ok(SeqInfo::Direct);
179 }
180
181 let parts: Vec<_> = s.split(':').collect();
182 if parts.len() != 2 {
183 return Err(anyhow::anyhow!("invalid SeqInfo: {}", s));
184 }
185 let session_id: Uuid = parts[0].parse()?;
186 let seq: u64 = parts[1].parse()?;
187 Ok(SeqInfo::Session { session_id, seq })
188 }
189}
190
191declare_attrs! {
192 pub attr SEQ_INFO: SeqInfo;
195}
196
197#[derive(Clone, Debug)]
201pub struct Sequencer {
202 session_id: Uuid,
203 last_seqs: Arc<Mutex<HashMap<SeqKey, u64>>>,
205}
206
207impl Sequencer {
208 pub(crate) fn new(session_id: Uuid) -> Self {
209 Self {
210 session_id,
211 last_seqs: Arc::new(Mutex::new(HashMap::new())),
212 }
213 }
214
215 pub fn assign_seq(&self, port_id: &PortAddr) -> SeqInfo {
222 if port_id.port().is_control() {
223 return SeqInfo::Direct;
224 }
225
226 let key = if port_id.is_handler_port() {
227 SeqKey::Actor(port_id.actor_addr().clone())
228 } else {
229 SeqKey::Port(port_id.clone())
230 };
231
232 let mut guard = self.last_seqs.lock().unwrap();
233 let entry = guard.entry(key).or_default();
234 *entry += 1;
235 SeqInfo::Session {
236 session_id: self.session_id,
237 seq: *entry,
238 }
239 }
240
241 pub fn session_id(&self) -> Uuid {
243 self.session_id
244 }
245}
246
247#[cfg(test)]
248mod tests {
249 use std::collections::HashSet;
250 use std::sync::Arc;
251 use std::sync::atomic::AtomicUsize;
252 use std::sync::atomic::Ordering as AtomicOrdering;
253
254 use async_trait::async_trait;
255 use hyperactor_config::Flattrs;
256 use rand::SeedableRng;
257 use rand::rngs::StdRng;
258 use rand::seq::SliceRandom;
259 use timed_test::async_timed_test;
260 use tokio::sync::Barrier;
261 use tokio::sync::oneshot;
262
263 use super::*;
264 use crate as hyperactor;
265 use crate::Actor;
266 use crate::ActorHandle;
267 use crate::ActorRef;
268 use crate::Context;
269 use crate::Endpoint as _;
270 use crate::Handler;
271 use crate::Proc;
272 use crate::config;
273 use crate::introspect::IntrospectMessage;
274 use crate::mailbox::headers::SENDER_ACTOR_ID;
275 use crate::mailbox::headers::stamp_sender_actor_id;
276 use crate::port::ControlPort;
277 use crate::port::Port;
278 use crate::proc::StatusMessage;
279 use crate::testing::ids::test_actor_id;
280
281 #[derive(Named)]
283 struct TestMsg1;
284
285 #[derive(Named)]
287 struct TestMsg2;
288
289 fn get_seq(seq_info: SeqInfo) -> u64 {
291 match seq_info {
292 SeqInfo::Session { seq, .. } => seq,
293 SeqInfo::Direct => panic!("expected Session variant, got Direct"),
294 }
295 }
296
297 #[test]
298 fn seq_info_validity_rejects_zero_session_seq() {
299 let session_id = Uuid::now_v7();
300
301 assert!(SeqInfo::Direct.is_valid());
302 assert!(SeqInfo::Session { session_id, seq: 1 }.is_valid());
303 assert!(!SeqInfo::Session { session_id, seq: 0 }.is_valid());
304 }
305
306 #[test]
307 fn test_sequencer_clone() {
308 let sequencer = Sequencer {
309 session_id: Uuid::now_v7(),
310 last_seqs: Arc::new(Mutex::new(HashMap::new())),
311 };
312
313 let actor_ref: ActorAddr = test_actor_id("test_0", "test");
314 let port_ref = actor_ref.port_addr(Port::from(1));
315
316 sequencer.assign_seq(&port_ref);
318 sequencer.assign_seq(&port_ref);
319
320 let cloned_sequencer = sequencer.clone();
322 assert_eq!(sequencer.session_id(), cloned_sequencer.session_id(),);
323 assert_eq!(get_seq(cloned_sequencer.assign_seq(&port_ref)), 3);
324 }
325
326 #[test]
327 fn test_sequencer_handler_ports_share_sequence() {
328 let sequencer = Sequencer {
329 session_id: Uuid::now_v7(),
330 last_seqs: Arc::new(Mutex::new(HashMap::new())),
331 };
332
333 let actor_ref: ActorAddr = test_actor_id("worker_0", "worker");
334 let handler_port_1 = actor_ref.port_addr(Port::handler::<TestMsg1>());
336 let handler_port_2 = actor_ref.port_addr(Port::handler::<TestMsg2>());
337
338 assert_eq!(get_seq(sequencer.assign_seq(&handler_port_1)), 1);
340 assert_eq!(get_seq(sequencer.assign_seq(&handler_port_2)), 2); assert_eq!(get_seq(sequencer.assign_seq(&handler_port_1)), 3);
342
343 let actor_ref_2: ActorAddr = test_actor_id("worker_1", "worker");
345 let handler_port_3 = actor_ref_2.port_addr(Port::handler::<TestMsg1>());
346 assert_eq!(get_seq(sequencer.assign_seq(&handler_port_3)), 1); }
348
349 #[test]
350 fn test_sequencer_non_handler_ports_have_independent_sequences() {
351 let sequencer = Sequencer {
352 session_id: Uuid::now_v7(),
353 last_seqs: Arc::new(Mutex::new(HashMap::new())),
354 };
355
356 let actor_ref_0: ActorAddr = test_actor_id("worker_0", "worker");
357 let actor_ref_1: ActorAddr = test_actor_id("worker_1", "worker");
358
359 let port_1 = actor_ref_0.port_addr(Port::from(1));
361 let port_2 = actor_ref_0.port_addr(Port::from(2));
362
363 assert_eq!(get_seq(sequencer.assign_seq(&port_1)), 1);
365 assert_eq!(get_seq(sequencer.assign_seq(&port_2)), 1); assert_eq!(get_seq(sequencer.assign_seq(&port_1)), 2);
367 assert_eq!(get_seq(sequencer.assign_seq(&port_2)), 2);
368
369 let port_3 = actor_ref_1.port_addr(Port::from(1));
371 assert_eq!(get_seq(sequencer.assign_seq(&port_3)), 1); assert_eq!(get_seq(sequencer.assign_seq(&port_1)), 3);
373 assert_eq!(get_seq(sequencer.assign_seq(&port_3)), 2);
374 }
375
376 #[test]
377 fn test_sequencer_mixed_handler_and_non_handler_ports() {
378 let sequencer = Sequencer {
379 session_id: Uuid::now_v7(),
380 last_seqs: Arc::new(Mutex::new(HashMap::new())),
381 };
382
383 let actor_ref: ActorAddr = test_actor_id("worker_0", "worker");
384
385 let handler_port_1 = actor_ref.port_addr(Port::handler::<TestMsg1>());
387 let handler_port_2 = actor_ref.port_addr(Port::handler::<TestMsg2>());
388
389 let non_handler_port_1 = actor_ref.port_addr(Port::from(1));
391 let non_handler_port_2 = actor_ref.port_addr(Port::from(2));
392
393 assert_eq!(get_seq(sequencer.assign_seq(&handler_port_1)), 1);
395 assert_eq!(get_seq(sequencer.assign_seq(&non_handler_port_1)), 1); assert_eq!(get_seq(sequencer.assign_seq(&handler_port_2)), 2); assert_eq!(get_seq(sequencer.assign_seq(&non_handler_port_2)), 1); assert_eq!(get_seq(sequencer.assign_seq(&non_handler_port_1)), 2); assert_eq!(get_seq(sequencer.assign_seq(&handler_port_1)), 3); assert_eq!(get_seq(sequencer.assign_seq(&non_handler_port_2)), 2); }
402
403 #[test]
404 fn bypass_registry_introspect_message() {
405 assert!(is_bypass_workq_type_id(TypeId::of::<IntrospectMessage>()));
406 }
407
408 #[test]
409 fn bypass_registry_status_message() {
410 assert!(is_bypass_workq_type_id(TypeId::of::<StatusMessage>()));
411 }
412
413 #[test]
414 fn control_port_uses_direct_seq_info() {
415 let sequencer = Sequencer::new(Uuid::now_v7());
419 let actor_ref: ActorAddr = test_actor_id("agent_0", "proc_agent");
420
421 let introspect_port = actor_ref.port_addr(Port::control(ControlPort::Introspect));
422 let regular_actor_port = actor_ref.port_addr(Port::handler::<TestMsg1>());
423
424 assert_eq!(sequencer.assign_seq(&introspect_port), SeqInfo::Direct);
425 assert_eq!(get_seq(sequencer.assign_seq(®ular_actor_port)), 1);
426 assert_eq!(sequencer.assign_seq(&introspect_port), SeqInfo::Direct);
427 assert_eq!(get_seq(sequencer.assign_seq(®ular_actor_port)), 2);
428 }
429
430 #[test]
436 fn test_sequencer_skip_advances_counter() {
437 let sequencer = Sequencer::new(Uuid::now_v7());
438 let actor_ref: ActorAddr = test_actor_id("test_0", "test");
439 let dest = actor_ref.port_addr(Port::handler::<TestMsg1>());
440
441 assert_eq!(get_seq(sequencer.assign_seq(&dest)), 1);
443
444 for _ in 0..2 {
446 let _ = sequencer.assign_seq(&dest);
447 }
448
449 assert_eq!(get_seq(sequencer.assign_seq(&dest)), 4);
451 }
452
453 #[test]
457 fn test_snapshot_attr_roundtrip() {
458 let addr: ActorAddr = test_actor_id("a", "client");
459 let snap = OrderingSnapshot {
460 enabled: true,
461 sessions: vec![OrderingSessionSnapshot {
462 session_id: Uuid::from_u128(42),
463 sender: Some(addr),
464 last_released_seq: 7,
465 expected_next_seq: 8,
466 buffered_count: 2,
467 oldest_buffered_seq: Some(9),
468 newest_buffered_seq: Some(11),
469 }],
470 skipped_session_count: 3,
471 };
472 let s = AttrValue::display(&snap);
473 let parsed = <OrderingSnapshot as AttrValue>::parse(&s).unwrap();
474 assert_eq!(snap, parsed);
475 }
476
477 #[derive(Clone, Debug, Serialize, Deserialize, Named)]
484 struct Frame {
485 sender_idx: u32,
486 payload_idx: u64,
487 }
488
489 #[derive(Clone, Debug, Serialize, Deserialize, Named)]
490 struct SenderDone {
491 sender_idx: u32,
492 }
493
494 type ReceivedFrame = (SeqInfo, Option<ActorAddr>, Frame);
498
499 #[derive(Clone, Debug)]
502 struct Start {
503 count: u64,
504 target: ActorHandle<ChaosActor>,
505 start_barrier: Option<Arc<Barrier>>,
506 yield_between_frames: bool,
507 }
508
509 #[derive(Debug)]
510 struct SenderActor {
511 sender_idx: u32,
512 }
513
514 #[async_trait]
515 impl Actor for SenderActor {}
516
517 #[async_trait]
518 impl Handler<Start> for SenderActor {
519 async fn handle(&mut self, cx: &Context<Self>, msg: Start) -> Result<(), anyhow::Error> {
520 let Start {
521 count,
522 target,
523 start_barrier,
524 yield_between_frames,
525 } = msg;
526 if let Some(b) = start_barrier {
527 b.wait().await;
528 }
529 for payload_idx in 0..count {
530 target.post(
531 cx,
532 Frame {
533 sender_idx: self.sender_idx,
534 payload_idx,
535 },
536 );
537 if yield_between_frames {
538 tokio::task::yield_now().await;
539 }
540 }
541 target.post(
542 cx,
543 SenderDone {
544 sender_idx: self.sender_idx,
545 },
546 );
547 Ok(())
548 }
549 }
550
551 #[derive(Debug, Clone, Copy)]
554 enum DuplicatePolicy {
555 None,
556 EveryNth { stride: usize },
557 }
558
559 #[derive(Debug, Default)]
560 struct ChaosStats {
561 out_of_order_original_forwards: AtomicUsize,
566
567 duplicates_selected: AtomicUsize,
569
570 late_duplicate_forwards: AtomicUsize,
572
573 out_of_order_sessions: Mutex<HashSet<Uuid>>,
575
576 forwarded_trace: Mutex<Vec<ForwardedEntry>>,
579
580 receiver_overflow: AtomicUsize,
584 }
585
586 #[allow(dead_code)]
588 #[derive(Debug, Clone)]
589 struct ForwardedEntry {
590 session_id: Uuid,
591 seq: u64,
592 sender_idx: u32,
593 is_duplicate: bool,
594 }
595
596 #[derive(Debug)]
597 #[hyperactor::export(handlers = [Frame, SenderDone])]
598 struct ChaosActor {
599 window: Vec<(SeqInfo, Flattrs, Frame)>,
600 window_size: usize,
601 target_port: PortAddr,
602 rng: StdRng,
603 expected_dones: u32,
604 done_count: u32,
605 expected_count_per_sender: u64,
606 duplicate_policy: DuplicatePolicy,
607 duplicate_cursor: usize,
608 stats: Arc<ChaosStats>,
609 session_owners: HashMap<Uuid, ActorAddr>,
610 max_forwarded_seq_by_session: HashMap<Uuid, u64>,
611 }
612
613 #[async_trait]
614 impl Actor for ChaosActor {}
615
616 impl ChaosActor {
617 fn flush(&mut self, cx: &Context<Self>) -> Result<(), anyhow::Error> {
621 self.window.shuffle(&mut self.rng);
622
623 let mut first_seen_order: Vec<Uuid> = Vec::new();
627 let mut session_indices: HashMap<Uuid, Vec<usize>> = HashMap::new();
628 for (i, (seq_info, _, _)) in self.window.iter().enumerate() {
629 if let SeqInfo::Session { session_id, .. } = seq_info {
630 if !session_indices.contains_key(session_id) {
631 first_seen_order.push(*session_id);
632 }
633 session_indices.entry(*session_id).or_default().push(i);
634 }
635 }
636 for sid in &first_seen_order {
637 let indices = &session_indices[sid];
638 if indices.len() < 2 {
639 continue;
640 }
641 let mut already_inverted = false;
642 for w in indices.windows(2) {
643 if seq_of(&self.window[w[0]].0) > seq_of(&self.window[w[1]].0) {
644 already_inverted = true;
645 break;
646 }
647 }
648 if !already_inverted {
649 self.window.swap(indices[0], indices[1]);
650 break;
651 }
652 }
653
654 let mut duplicates: Vec<(SeqInfo, Flattrs, Frame)> = Vec::new();
657 let entries: Vec<(SeqInfo, Flattrs, Frame)> = self.window.drain(..).collect();
658 for (seq_info, headers, frame) in entries {
659 let (session_id, seq) = match &seq_info {
660 SeqInfo::Session { session_id, seq } => (*session_id, *seq),
661 SeqInfo::Direct => panic!("Direct SeqInfo at flush"),
662 };
663 let prev_max = self
666 .max_forwarded_seq_by_session
667 .get(&session_id)
668 .copied()
669 .unwrap_or(0);
670 if seq < prev_max {
671 self.stats
672 .out_of_order_original_forwards
673 .fetch_add(1, AtomicOrdering::Relaxed);
674 self.stats
675 .out_of_order_sessions
676 .lock()
677 .unwrap()
678 .insert(session_id);
679 }
680 self.max_forwarded_seq_by_session
681 .insert(session_id, prev_max.max(seq));
682 let should_duplicate = match self.duplicate_policy {
684 DuplicatePolicy::None => false,
685 DuplicatePolicy::EveryNth { stride } => {
686 assert!(stride > 0, "DuplicatePolicy::EveryNth requires stride > 0");
687 self.duplicate_cursor = self.duplicate_cursor.wrapping_add(1);
688 self.duplicate_cursor.is_multiple_of(stride)
689 }
690 };
691 if should_duplicate {
692 duplicates.push((seq_info.clone(), headers.clone(), frame.clone()));
693 self.stats
694 .duplicates_selected
695 .fetch_add(1, AtomicOrdering::Relaxed);
696 }
697 let owner = self
700 .session_owners
701 .get(&session_id)
702 .expect("session owner missing at original forward")
703 .clone();
704 let dest = self.target_port.clone();
705 let mut outbound = headers;
706 stamp_sender_actor_id(&mut outbound, &seq_info, &dest, &owner);
707 cx.post_with_external_seq_info(dest, outbound, wirevalue::Any::serialize(&frame)?);
708 self.stats
709 .forwarded_trace
710 .lock()
711 .unwrap()
712 .push(ForwardedEntry {
713 session_id,
714 seq,
715 sender_idx: frame.sender_idx,
716 is_duplicate: false,
717 });
718 }
719
720 for (seq_info, headers, frame) in duplicates {
725 let (session_id, seq) = match &seq_info {
726 SeqInfo::Session { session_id, seq } => (*session_id, *seq),
727 SeqInfo::Direct => unreachable!(),
728 };
729 let owner = self
730 .session_owners
731 .get(&session_id)
732 .expect("session owner missing at duplicate forward")
733 .clone();
734 let dest = self.target_port.clone();
735 let mut outbound = headers;
736 stamp_sender_actor_id(&mut outbound, &seq_info, &dest, &owner);
737 cx.post_with_external_seq_info(dest, outbound, wirevalue::Any::serialize(&frame)?);
738 self.stats
739 .late_duplicate_forwards
740 .fetch_add(1, AtomicOrdering::Relaxed);
741 self.stats
742 .forwarded_trace
743 .lock()
744 .unwrap()
745 .push(ForwardedEntry {
746 session_id,
747 seq,
748 sender_idx: frame.sender_idx,
749 is_duplicate: true,
750 });
751 }
752
753 Ok(())
754 }
755 }
756
757 fn seq_of(s: &SeqInfo) -> u64 {
758 match s {
759 SeqInfo::Session { seq, .. } => *seq,
760 SeqInfo::Direct => panic!("seq_of called on SeqInfo::Direct"),
761 }
762 }
763
764 #[async_trait]
765 impl Handler<Frame> for ChaosActor {
766 async fn handle(&mut self, cx: &Context<Self>, frame: Frame) -> Result<(), anyhow::Error> {
767 let seq_info = cx.headers().get(SEQ_INFO);
768 let (session_id, seq) = match &seq_info {
769 Some(SeqInfo::Session { session_id, seq }) => (*session_id, *seq),
770 Some(SeqInfo::Direct) => {
771 panic!("chaos inbound has SeqInfo::Direct; bind misconfigured")
772 }
773 None => panic!("chaos inbound missing SEQ_INFO"),
774 };
775 let sender_addr = cx.headers().get(SENDER_ACTOR_ID);
776 if seq <= 4 {
777 assert!(
778 sender_addr.is_some(),
779 "missing SENDER_ACTOR_ID on early-session chaos inbound (seq={seq})",
780 );
781 }
782 if let Some(addr) = &sender_addr {
783 match self.session_owners.entry(session_id) {
784 std::collections::hash_map::Entry::Vacant(v) => {
785 v.insert(addr.clone());
786 }
787 std::collections::hash_map::Entry::Occupied(o) => {
788 assert_eq!(
789 o.get(),
790 addr,
791 "session owner changed mid-stream for session_id={session_id}",
792 );
793 }
794 }
795 }
796 let inbound_headers = cx.headers().clone();
797 self.window
798 .push((seq_info.unwrap(), inbound_headers, frame));
799 if self.window.len() >= self.window_size {
800 self.flush(cx)?;
801 }
802 Ok(())
803 }
804 }
805
806 #[async_trait]
807 impl Handler<SenderDone> for ChaosActor {
808 async fn handle(
809 &mut self,
810 cx: &Context<Self>,
811 done: SenderDone,
812 ) -> Result<(), anyhow::Error> {
813 let seq_info = cx.headers().get(SEQ_INFO);
814 let (session_id, seq) = match &seq_info {
815 Some(SeqInfo::Session { session_id, seq }) => (*session_id, *seq),
816 Some(SeqInfo::Direct) => {
817 panic!("SenderDone arrived as SeqInfo::Direct; bind misconfigured")
818 }
819 None => panic!("SenderDone missing SEQ_INFO"),
820 };
821 assert_eq!(
824 seq,
825 self.expected_count_per_sender + 1,
826 "SenderDone(sender_idx={}) seq={seq}; expected {} (count+1)",
827 done.sender_idx,
828 self.expected_count_per_sender + 1,
829 );
830 assert!(
831 self.session_owners.contains_key(&session_id),
832 "SenderDone(sender_idx={}) for session_id={session_id} \
833 before any Frame from that session",
834 done.sender_idx,
835 );
836 if let Some(addr) = cx.headers().get(SENDER_ACTOR_ID) {
840 let owner = self.session_owners.get(&session_id).unwrap();
841 assert_eq!(
842 &addr, owner,
843 "SenderDone SENDER_ACTOR_ID inconsistent with session_owner \
844 for session_id={session_id}",
845 );
846 }
847 assert!(
848 self.done_count < self.expected_dones,
849 "extra SenderDone(sender_idx={}); done_count already at expected_dones={}",
850 done.sender_idx,
851 self.expected_dones,
852 );
853 self.done_count += 1;
854 if self.done_count == self.expected_dones && !self.window.is_empty() {
855 self.flush(cx)?;
856 }
857 Ok(())
858 }
859 }
860
861 #[derive(Debug)]
862 #[hyperactor::export(handlers = [Frame])]
863 struct ReceiverActor {
864 received: Vec<ReceivedFrame>,
865 expected_total: usize,
866 done: Option<oneshot::Sender<Vec<ReceivedFrame>>>,
867 stats: Arc<ChaosStats>,
868 }
869
870 #[async_trait]
871 impl Actor for ReceiverActor {}
872
873 #[async_trait]
874 impl Handler<Frame> for ReceiverActor {
875 async fn handle(&mut self, cx: &Context<Self>, frame: Frame) -> Result<(), anyhow::Error> {
876 let seq_info = match cx.headers().get(SEQ_INFO) {
877 Some(SeqInfo::Session { session_id, seq }) => SeqInfo::Session { session_id, seq },
878 Some(SeqInfo::Direct) => {
879 panic!("receiver inbound has SeqInfo::Direct; chaos forward bypassed SEQ_INFO")
880 }
881 None => panic!("receiver inbound missing SEQ_INFO"),
882 };
883 let sender_addr = cx.headers().get(SENDER_ACTOR_ID);
884 if self.done.is_none() {
887 self.stats
888 .receiver_overflow
889 .fetch_add(1, AtomicOrdering::Relaxed);
890 return Ok(());
891 }
892 self.received.push((seq_info, sender_addr, frame));
893 if self.received.len() == self.expected_total {
894 let tx = self.done.take().expect("done sender already consumed");
895 let snapshot = std::mem::take(&mut self.received);
896 let _ = tx.send(snapshot);
897 }
898 Ok(())
899 }
900 }
901
902 fn assert_received_protocol_correct(
905 received: &[ReceivedFrame],
906 expected_senders: usize,
907 expected_count_per_sender: u64,
908 sender_addrs: &[ActorAddr],
909 stats: &ChaosStats,
910 ) {
911 let expected_total = expected_senders * (expected_count_per_sender as usize);
912 let dump_trace = || -> String {
913 let trace = stats.forwarded_trace.lock().unwrap();
914 format!("forwarded_trace ({} entries) = {:#?}", trace.len(), *trace,)
915 };
916
917 assert_eq!(
918 received.len(),
919 expected_total,
920 "expected {} frames at receiver, got {}; {}",
921 expected_total,
922 received.len(),
923 dump_trace(),
924 );
925
926 let overflow = stats.receiver_overflow.load(AtomicOrdering::Acquire);
928 assert_eq!(
929 overflow,
930 0,
931 "stats.receiver_overflow = {overflow}; duplicate leaked past \
932 sequenced receiver drop branch after snapshot; {}",
933 dump_trace(),
934 );
935
936 let mut sender_session: HashMap<u32, Uuid> = HashMap::new();
938 for (seq_info, _, frame) in received {
939 let session_id = match seq_info {
940 SeqInfo::Session { session_id, .. } => *session_id,
941 SeqInfo::Direct => {
942 panic!("receiver captured Direct SEQ_INFO; {}", dump_trace())
943 }
944 };
945 match sender_session.entry(frame.sender_idx) {
946 std::collections::hash_map::Entry::Vacant(v) => {
947 v.insert(session_id);
948 }
949 std::collections::hash_map::Entry::Occupied(o) => {
950 assert_eq!(
951 o.get(),
952 &session_id,
953 "sender_idx={} mapped to two distinct sessions ({} and {}); {}",
954 frame.sender_idx,
955 o.get(),
956 session_id,
957 dump_trace(),
958 );
959 }
960 }
961 }
962
963 assert_eq!(
965 sender_session.len(),
966 expected_senders,
967 "expected {} distinct sender_idx, got {}; {}",
968 expected_senders,
969 sender_session.len(),
970 dump_trace(),
971 );
972 let distinct_sessions: HashSet<Uuid> = sender_session.values().copied().collect();
973 assert_eq!(
974 distinct_sessions.len(),
975 expected_senders,
976 "sender_idx -> session_id is not a bijection (got {} distinct sessions for {} senders); {}",
977 distinct_sessions.len(),
978 expected_senders,
979 dump_trace(),
980 );
981
982 for sender_idx in 0..(expected_senders as u32) {
985 let subseq: Vec<&ReceivedFrame> = received
986 .iter()
987 .filter(|(_, _, f)| f.sender_idx == sender_idx)
988 .collect();
989 assert_eq!(
990 subseq.len() as u64,
991 expected_count_per_sender,
992 "sender_idx={sender_idx}: expected {} frames, got {}; {}",
993 expected_count_per_sender,
994 subseq.len(),
995 dump_trace(),
996 );
997 for (i, (seq_info, _, frame)) in subseq.iter().enumerate() {
998 let expected_payload = i as u64;
999 assert_eq!(
1000 frame.payload_idx,
1001 expected_payload,
1002 "sender_idx={sender_idx}, position {i}: payload_idx={} (expected {}); {}",
1003 frame.payload_idx,
1004 expected_payload,
1005 dump_trace(),
1006 );
1007 let expected_seq = (i as u64) + 1;
1008 let actual_seq = seq_of(seq_info);
1009 assert_eq!(
1010 actual_seq,
1011 expected_seq,
1012 "sender_idx={sender_idx}, position {i}: SEQ_INFO.seq={} (expected {}); {}",
1013 actual_seq,
1014 expected_seq,
1015 dump_trace(),
1016 );
1017 }
1018 }
1019
1020 for (seq_info, sender_addr_opt, frame) in received {
1022 let seq = seq_of(seq_info);
1023 if seq > 4 {
1024 continue;
1025 }
1026 let expected_addr = &sender_addrs[frame.sender_idx as usize];
1027 let captured = sender_addr_opt.as_ref().unwrap_or_else(|| {
1028 panic!(
1029 "sender_idx={}, seq={seq}: SENDER_ACTOR_ID is None at early-session; {}",
1030 frame.sender_idx,
1031 dump_trace(),
1032 )
1033 });
1034 assert_eq!(
1035 captured,
1036 expected_addr,
1037 "sender_idx={}, seq={seq}: SENDER_ACTOR_ID {captured:?} != expected {expected_addr:?}; {}",
1038 frame.sender_idx,
1039 dump_trace(),
1040 );
1041 }
1042 }
1043
1044 #[async_timed_test(timeout_secs = 30)]
1046 async fn test_chaos_single_sender_preserves_order() {
1047 let config = hyperactor_config::global::lock();
1048 let _g = config.override_key(config::ENABLE_DEST_ACTOR_REORDERING_BUFFER, true);
1049
1050 let proc = Proc::isolated();
1051 let client = proc.client("client");
1052
1053 let stats = Arc::new(ChaosStats::default());
1054 let n: usize = 1;
1055 let k: u64 = 50;
1056
1057 let (done_tx, done_rx) = oneshot::channel();
1058 let receiver_handle = proc.spawn_with_label(
1059 "receiver",
1060 ReceiverActor {
1061 received: Vec::new(),
1062 expected_total: n * (k as usize),
1063 done: Some(done_tx),
1064 stats: stats.clone(),
1065 },
1066 );
1067 let receiver_ref: ActorRef<ReceiverActor> = receiver_handle.bind();
1068 let target_port = receiver_ref.port::<Frame>().port_addr().clone();
1069
1070 let chaos_handle = proc.spawn_with_label(
1071 "chaos",
1072 ChaosActor {
1073 window: Vec::new(),
1074 window_size: 10,
1075 target_port,
1076 rng: StdRng::seed_from_u64(0xC4A0_5EED),
1077 expected_dones: n as u32,
1078 done_count: 0,
1079 expected_count_per_sender: k,
1080 duplicate_policy: DuplicatePolicy::None,
1081 duplicate_cursor: 0,
1082 stats: stats.clone(),
1083 session_owners: HashMap::new(),
1084 max_forwarded_seq_by_session: HashMap::new(),
1085 },
1086 );
1087 let _bound_chaos_ref: ActorRef<ChaosActor> = chaos_handle.bind();
1090
1091 let sender_handle = proc.spawn_with_label("sender0", SenderActor { sender_idx: 0 });
1092 let sender_addr = sender_handle.actor_addr().clone();
1093
1094 sender_handle.post(
1095 &client,
1096 Start {
1097 count: k,
1098 target: chaos_handle.clone(),
1099 start_barrier: None,
1100 yield_between_frames: false,
1101 },
1102 );
1103
1104 let received = match tokio::time::timeout(std::time::Duration::from_secs(5), done_rx).await
1105 {
1106 Ok(Ok(v)) => v,
1107 Ok(Err(_)) => panic!("done_rx sender dropped before sending; receiver crashed?"),
1108 Err(_) => {
1109 let trace = stats.forwarded_trace.lock().unwrap();
1110 panic!(
1111 "timed out waiting for receiver; forwarded_trace ({} entries) = {:#?}",
1112 trace.len(),
1113 *trace,
1114 );
1115 }
1116 };
1117
1118 assert_received_protocol_correct(&received, n, k, &[sender_addr], &stats);
1119
1120 let oo = stats
1121 .out_of_order_original_forwards
1122 .load(AtomicOrdering::Acquire);
1123 assert!(
1124 oo > 0,
1125 "expected out_of_order_original_forwards > 0, got {oo}: \
1126 chaos shuffle never inverted a real frame (shuffle was identity?)",
1127 );
1128 }
1129
1130 #[async_timed_test(timeout_secs = 30)]
1133 async fn test_chaos_multi_sender_preserves_per_session_order() {
1134 let config = hyperactor_config::global::lock();
1135 let _g = config.override_key(config::ENABLE_DEST_ACTOR_REORDERING_BUFFER, true);
1136
1137 let proc = Proc::isolated();
1138 let client = proc.client("client");
1139
1140 let stats = Arc::new(ChaosStats::default());
1141 let n: usize = 4;
1142 let k: u64 = 25;
1143
1144 let (done_tx, done_rx) = oneshot::channel();
1145 let receiver_handle = proc.spawn_with_label(
1146 "receiver",
1147 ReceiverActor {
1148 received: Vec::new(),
1149 expected_total: n * (k as usize),
1150 done: Some(done_tx),
1151 stats: stats.clone(),
1152 },
1153 );
1154 let receiver_ref: ActorRef<ReceiverActor> = receiver_handle.bind();
1155 let target_port = receiver_ref.port::<Frame>().port_addr().clone();
1156
1157 let chaos_handle = proc.spawn_with_label(
1158 "chaos",
1159 ChaosActor {
1160 window: Vec::new(),
1161 window_size: 10,
1162 target_port,
1163 rng: StdRng::seed_from_u64(0xC4A0_5EED),
1164 expected_dones: n as u32,
1165 done_count: 0,
1166 expected_count_per_sender: k,
1167 duplicate_policy: DuplicatePolicy::None,
1168 duplicate_cursor: 0,
1169 stats: stats.clone(),
1170 session_owners: HashMap::new(),
1171 max_forwarded_seq_by_session: HashMap::new(),
1172 },
1173 );
1174 let _bound_chaos_ref: ActorRef<ChaosActor> = chaos_handle.bind();
1177
1178 let barrier = Arc::new(Barrier::new(n));
1179 let mut sender_addrs: Vec<ActorAddr> = Vec::with_capacity(n);
1180 let mut sender_handles: Vec<ActorHandle<SenderActor>> = Vec::with_capacity(n);
1181 for sender_idx in 0..n {
1182 let h = proc.spawn_with_label(
1183 &format!("sender{sender_idx}"),
1184 SenderActor {
1185 sender_idx: sender_idx as u32,
1186 },
1187 );
1188 sender_addrs.push(h.actor_addr().clone());
1189 sender_handles.push(h);
1190 }
1191 for h in &sender_handles {
1192 h.post(
1193 &client,
1194 Start {
1195 count: k,
1196 target: chaos_handle.clone(),
1197 start_barrier: Some(barrier.clone()),
1198 yield_between_frames: true,
1199 },
1200 );
1201 }
1202
1203 let received = match tokio::time::timeout(std::time::Duration::from_secs(5), done_rx).await
1204 {
1205 Ok(Ok(v)) => v,
1206 Ok(Err(_)) => panic!("done_rx sender dropped before sending; receiver crashed?"),
1207 Err(_) => {
1208 let trace = stats.forwarded_trace.lock().unwrap();
1209 panic!(
1210 "timed out waiting for receiver; forwarded_trace ({} entries) = {:#?}",
1211 trace.len(),
1212 *trace,
1213 );
1214 }
1215 };
1216
1217 assert_received_protocol_correct(&received, n, k, &sender_addrs, &stats);
1218
1219 let oo = stats
1220 .out_of_order_original_forwards
1221 .load(AtomicOrdering::Acquire);
1222 assert!(
1223 oo > 0,
1224 "expected out_of_order_original_forwards > 0, got {oo}",
1225 );
1226 let sessions_with_inversion = stats.out_of_order_sessions.lock().unwrap().len();
1228 assert!(
1229 sessions_with_inversion >= 1,
1230 "expected out_of_order_sessions.len() >= 1, got {sessions_with_inversion}",
1231 );
1232 }
1233
1234 #[async_timed_test(timeout_secs = 30)]
1237 async fn test_chaos_drops_duplicates() {
1238 let config = hyperactor_config::global::lock();
1239 let _g = config.override_key(config::ENABLE_DEST_ACTOR_REORDERING_BUFFER, true);
1240
1241 let proc = Proc::isolated();
1242 let client = proc.client("client");
1243
1244 let stats = Arc::new(ChaosStats::default());
1245 let n: usize = 4;
1246 let k: u64 = 25;
1247
1248 let (done_tx, done_rx) = oneshot::channel();
1249 let receiver_handle = proc.spawn_with_label(
1250 "receiver",
1251 ReceiverActor {
1252 received: Vec::new(),
1253 expected_total: n * (k as usize),
1254 done: Some(done_tx),
1255 stats: stats.clone(),
1256 },
1257 );
1258 let receiver_ref: ActorRef<ReceiverActor> = receiver_handle.bind();
1259 let target_port = receiver_ref.port::<Frame>().port_addr().clone();
1260
1261 let chaos_handle = proc.spawn_with_label(
1262 "chaos",
1263 ChaosActor {
1264 window: Vec::new(),
1265 window_size: 10,
1266 target_port,
1267 rng: StdRng::seed_from_u64(0xC4A0_5EED),
1268 expected_dones: n as u32,
1269 done_count: 0,
1270 expected_count_per_sender: k,
1271 duplicate_policy: DuplicatePolicy::EveryNth { stride: 10 },
1272 duplicate_cursor: 0,
1273 stats: stats.clone(),
1274 session_owners: HashMap::new(),
1275 max_forwarded_seq_by_session: HashMap::new(),
1276 },
1277 );
1278 let _bound_chaos_ref: ActorRef<ChaosActor> = chaos_handle.bind();
1281
1282 let barrier = Arc::new(Barrier::new(n));
1283 let mut sender_addrs: Vec<ActorAddr> = Vec::with_capacity(n);
1284 let mut sender_handles: Vec<ActorHandle<SenderActor>> = Vec::with_capacity(n);
1285 for sender_idx in 0..n {
1286 let h = proc.spawn_with_label(
1287 &format!("sender{sender_idx}"),
1288 SenderActor {
1289 sender_idx: sender_idx as u32,
1290 },
1291 );
1292 sender_addrs.push(h.actor_addr().clone());
1293 sender_handles.push(h);
1294 }
1295 for h in &sender_handles {
1296 h.post(
1297 &client,
1298 Start {
1299 count: k,
1300 target: chaos_handle.clone(),
1301 start_barrier: Some(barrier.clone()),
1302 yield_between_frames: true,
1303 },
1304 );
1305 }
1306
1307 let received = match tokio::time::timeout(std::time::Duration::from_secs(5), done_rx).await
1308 {
1309 Ok(Ok(v)) => v,
1310 Ok(Err(_)) => panic!("done_rx sender dropped before sending; receiver crashed?"),
1311 Err(_) => {
1312 let trace = stats.forwarded_trace.lock().unwrap();
1313 panic!(
1314 "timed out waiting for receiver; forwarded_trace ({} entries) = {:#?}",
1315 trace.len(),
1316 *trace,
1317 );
1318 }
1319 };
1320
1321 assert_received_protocol_correct(&received, n, k, &sender_addrs, &stats);
1322
1323 let selected = stats.duplicates_selected.load(AtomicOrdering::Acquire);
1324 let emitted = stats.late_duplicate_forwards.load(AtomicOrdering::Acquire);
1325 assert!(
1326 selected > 0,
1327 "expected duplicates_selected > 0, got {selected}: chaos policy never fired",
1328 );
1329 assert_eq!(
1330 emitted, selected,
1331 "duplicate selections did not match duplicate forwards: duplicates_selected={selected}, \
1332 late_duplicate_forwards={emitted}",
1333 );
1334 let oo = stats
1335 .out_of_order_original_forwards
1336 .load(AtomicOrdering::Acquire);
1337 assert!(
1338 oo > 0,
1339 "expected out_of_order_original_forwards > 0, got {oo}",
1340 );
1341 }
1342}