1use std::mem::take;
18use std::sync::Arc;
19use std::sync::Mutex;
20use std::sync::OnceLock;
21use std::time::Duration;
22
23use async_trait::async_trait;
24use backoff::ExponentialBackoffBuilder;
25use backoff::backoff::Backoff;
26use dashmap::DashSet;
27use hyperactor_config::Flattrs;
28use hyperactor_config::NonZeroUsize;
29use hyperactor_config::attrs::OPERATION_CONTEXT_HEADER;
30use hyperactor_config::attrs::copy_marked_flattrs;
31use hyperactor_config::attrs::declare_attrs;
32
33use crate::ActorAddr;
34use crate::Instance;
35use crate::PortAddr;
36use crate::Proc;
37use crate::accum;
38use crate::accum::ErasedCommReducer;
39use crate::accum::ReducerMode;
40use crate::accum::ReducerSpec;
41use crate::config;
42use crate::id::Uid;
43use crate::mailbox;
44use crate::mailbox::MailboxSender;
45use crate::mailbox::MessageEnvelope;
46use crate::ordering::SEQ_INFO;
47use crate::port::Port;
48use crate::time::Alarm;
49
50declare_attrs! {
51 attr REDUCER_ACCUMULATED_UPDATES: NonZeroUsize;
53}
54
55#[derive(Debug, Clone, Copy, PartialEq, Eq)]
57pub(crate) enum SeqInfoPolicy {
58 AssignNew,
60 AllowExternal,
62}
63
64pub trait Mailbox: crate::private::Sealed + Send + Sync {
66 fn mailbox(&self) -> &crate::Mailbox;
68}
69
70#[async_trait]
75pub trait Actor: Mailbox {
76 type A: crate::Actor;
78
79 fn instance(&self) -> &Instance<Self::A>;
81
82 fn spawn<C: crate::Actor>(&self, actor: C) -> crate::ActorHandle<C>
84 where
85 Self: Sized,
86 {
87 self.instance().spawn(actor)
88 }
89
90 fn spawn_with_label<C: crate::Actor>(&self, label: &str, actor: C) -> crate::ActorHandle<C>
92 where
93 Self: Sized,
94 {
95 self.instance().spawn_with_label(label, actor)
96 }
97
98 fn spawn_with_uid<C: crate::Actor>(
100 &self,
101 uid: Uid,
102 actor: C,
103 ) -> anyhow::Result<crate::ActorHandle<C>>
104 where
105 Self: Sized,
106 {
107 self.instance().spawn_with_uid(uid, actor)
108 }
109
110 fn headers(&self) -> &Flattrs {
115 static EMPTY_HEADERS: OnceLock<Flattrs> = OnceLock::new();
116 EMPTY_HEADERS.get_or_init(Flattrs::new)
117 }
118}
119
120pub(crate) trait MailboxExt: Mailbox {
123 fn post(
126 &self,
127 dest: PortAddr,
128 headers: Flattrs,
129 data: wirevalue::Any,
130 return_undeliverable: bool,
131 seq_info_policy: SeqInfoPolicy,
132 );
133
134 fn split(
136 &self,
137 port_id: PortAddr,
138 reducer_spec: Option<ReducerSpec>,
139 reducer_mode: ReducerMode,
140 return_undeliverable: bool,
141 ) -> anyhow::Result<PortAddr>;
142}
143
144static CAN_SEND_WARNED_MAILBOXES: OnceLock<DashSet<ActorAddr>> = OnceLock::new();
149
150fn operation_context_headers(headers: &Flattrs) -> Flattrs {
151 let mut operation_headers = Flattrs::new();
152 copy_marked_flattrs(&mut operation_headers, headers, OPERATION_CONTEXT_HEADER);
153 operation_headers
154}
155
156impl<T: Actor + Send + Sync> MailboxExt for T {
158 fn post(
159 &self,
160 dest: PortAddr,
161 mut headers: Flattrs,
162 data: wirevalue::Any,
163 return_undeliverable: bool,
164 seq_info_policy: SeqInfoPolicy,
165 ) {
166 self.mailbox().ensure_return_handler();
167 let return_handle = self.mailbox().bound_return_handle().unwrap_or_else(|| {
168 let actor_id = self.mailbox().actor_addr();
169 if CAN_SEND_WARNED_MAILBOXES
170 .get_or_init(DashSet::new)
171 .insert(actor_id.clone())
172 {
173 let bt = std::backtrace::Backtrace::force_capture();
174 tracing::warn!(
175 actor_id = ?actor_id,
176 backtrace = ?bt,
177 "mailbox attempted to post a message without binding Undeliverable<MessageEnvelope>"
178 );
179 }
180 mailbox::monitored_return_handle()
181 });
182
183 assert!(
184 !headers.contains_key(SEQ_INFO) || seq_info_policy == SeqInfoPolicy::AllowExternal,
185 "SEQ_INFO must not be set on headers outside of fn post unless explicitly allowed"
186 );
187
188 if !headers.contains_key(SEQ_INFO) {
189 let sequencer = self.instance().sequencer();
192 let seq_info = sequencer.assign_seq(&dest);
193 crate::mailbox::headers::stamp_sender_actor_id(
197 &mut headers,
198 &seq_info,
199 &dest,
200 self.mailbox().actor_addr(),
201 );
202 headers.set(SEQ_INFO, seq_info);
203 }
204
205 let mut envelope =
206 MessageEnvelope::new(self.mailbox().actor_addr().clone(), dest, data, headers);
207 envelope.set_return_undeliverable(return_undeliverable);
208 MailboxSender::post(self.instance().proc(), envelope, return_handle);
209 }
210
211 fn split(
212 &self,
213 port_id: PortAddr,
214 reducer_spec: Option<ReducerSpec>,
215 reducer_mode: ReducerMode,
216 return_undeliverable: bool,
217 ) -> anyhow::Result<PortAddr> {
218 fn post(
219 proc: &Proc,
220 sender: &ActorAddr,
221 sequencer: &crate::ordering::Sequencer,
222 port_id: PortAddr,
223 mut headers: Flattrs,
224 msg: wirevalue::Any,
225 return_undeliverable: bool,
226 ) {
227 assert!(
228 !headers.contains_key(SEQ_INFO),
229 "SEQ_INFO must not be set on split-port forwarded headers"
230 );
231 let seq_info = sequencer.assign_seq(&port_id);
232 crate::mailbox::headers::stamp_sender_actor_id(
233 &mut headers,
234 &seq_info,
235 &port_id,
236 sender,
237 );
238 headers.set(SEQ_INFO, seq_info);
239
240 let mut envelope = MessageEnvelope::new(sender.clone(), port_id, msg, headers);
241 envelope.set_return_undeliverable(return_undeliverable);
242 mailbox::MailboxSender::post(
243 proc,
244 envelope,
245 mailbox::monitored_return_handle(),
250 );
251 }
252
253 let port_index = self.mailbox().allocate_port();
254 let split_port = self
255 .mailbox()
256 .actor_addr()
257 .port_addr(Port::from(port_index));
258 let proc = self.instance().proc().clone();
259 let sender = self.mailbox().actor_addr().clone();
260 let sequencer = self.instance().sequencer().clone();
261 let reducer = reducer_spec
262 .map(
263 |ReducerSpec {
264 typehash,
265 builder_params,
266 }| { accum::resolve_reducer(typehash, builder_params) },
267 )
268 .transpose()?
269 .flatten();
270 let enqueue: Box<
271 dyn Fn(
272 Flattrs,
273 wirevalue::Any,
274 )
275 -> Result<mailbox::SerializedSendDisposition, mailbox::SerializedSendFailure>
276 + Send
277 + Sync,
278 > = match reducer {
279 None => {
280 let proc = proc.clone();
281 let sender = sender.clone();
282 let sequencer = sequencer.clone();
283 Box::new(move |headers: Flattrs, serialized: wirevalue::Any| {
284 post(
285 &proc,
286 &sender,
287 &sequencer,
288 port_id.clone(),
289 operation_context_headers(&headers),
290 serialized,
291 return_undeliverable,
292 );
293 Ok(mailbox::SerializedSendDisposition::Delivered)
294 })
295 }
296 Some(reducer) => match reducer_mode {
297 ReducerMode::Streaming(_) => {
298 let buffer: Arc<Mutex<UpdateBuffer>> =
299 Arc::new(Mutex::new(UpdateBuffer::new(reducer)));
300
301 let alarm = Alarm::new();
302
303 {
304 let mut sleeper = alarm.sleeper();
305 let buffer = Arc::clone(&buffer);
306 let port_id = port_id.clone();
307 let proc = proc.clone();
308 let sender = sender.clone();
309 let sequencer = sequencer.clone();
310 tokio::spawn(async move {
311 while sleeper.sleep().await {
312 let mut buf = buffer.lock().unwrap();
313 match buf.reduce() {
314 None => (),
315 Some(Ok((headers, reduced))) => post(
316 &proc,
317 &sender,
318 &sequencer,
319 port_id.clone(),
320 headers,
321 reduced,
322 return_undeliverable,
323 ),
324 Some(Err(e)) => tracing::error!(
331 "error while reducing update: {}; waiting until the next send to propagate",
332 e
333 ),
334 }
335 }
336 });
337 }
338
339 let alarm = Mutex::new(alarm);
343
344 let max_interval = reducer_mode.max_update_interval();
345 let initial_interval = reducer_mode.initial_update_interval();
346
347 let backoff = Mutex::new(
350 ExponentialBackoffBuilder::new()
351 .with_initial_interval(initial_interval)
352 .with_multiplier(2.0)
353 .with_max_interval(max_interval)
354 .with_max_elapsed_time(None)
355 .build(),
356 );
357
358 let error_port_id = split_port.clone();
359 let sequencer = sequencer.clone();
360 Box::new(move |headers: Flattrs, update: wirevalue::Any| {
361 let mut buf = buffer.lock().unwrap();
367 match buf.push(headers.clone(), update) {
368 None => {
369 let interval = backoff.lock().unwrap().next_backoff().unwrap();
370 alarm.lock().unwrap().rearm(interval);
371 Ok(mailbox::SerializedSendDisposition::Delivered)
372 }
373 Some(Ok((headers, reduced))) => {
374 alarm.lock().unwrap().disarm();
375 post(
376 &proc,
377 &sender,
378 &sequencer,
379 port_id.clone(),
380 headers,
381 reduced,
382 return_undeliverable,
383 );
384 Ok(mailbox::SerializedSendDisposition::Delivered)
385 }
386 Some(Err(error)) => Err(mailbox::SerializedSendFailure::Error(
387 mailbox::SerializedSendError {
388 data: buf
389 .pop()
390 .expect("reducer error should leave update buffered"),
391 error: crate::mailbox::MailboxSenderError::new_bound(
392 error_port_id.clone(),
393 crate::mailbox::MailboxSenderErrorKind::Other(error),
394 ),
395 headers,
396 },
397 )),
398 }
399 })
400 }
401 ReducerMode::Once(0) => {
402 let error_port_id = split_port.clone();
403 Box::new(move |headers: Flattrs, update: wirevalue::Any| {
404 Err(mailbox::SerializedSendFailure::Error(
405 mailbox::SerializedSendError {
406 data: update,
407 error: crate::mailbox::MailboxSenderError::new_bound(
408 error_port_id.clone(),
409 crate::mailbox::MailboxSenderErrorKind::Other(anyhow::anyhow!(
410 "invalid ReducerMode: Once must specify at least one update"
411 )),
412 ),
413 headers,
414 },
415 ))
416 })
417 }
418 ReducerMode::Once(expected) => {
419 let buffer: Arc<Mutex<OnceBuffer>> =
420 Arc::new(Mutex::new(OnceBuffer::new(reducer, expected)));
421 let error_port_id = split_port.clone();
422 let proc = proc.clone();
423 let sender = sender.clone();
424 let sequencer = sequencer.clone();
425
426 Box::new(move |headers: Flattrs, update: wirevalue::Any| {
427 let mut buf = buffer.lock().unwrap();
428 if buf.done {
429 return Err(mailbox::SerializedSendFailure::Dead {
430 data: update,
431 headers,
432 });
433 }
434 match buf.push(headers.clone(), update) {
435 Ok(Some((headers, reduced))) => {
436 post(
437 &proc,
438 &sender,
439 &sequencer,
440 port_id.clone(),
441 headers,
442 reduced,
443 return_undeliverable,
444 );
445 Ok(mailbox::SerializedSendDisposition::DeliveredAndExhausted)
446 }
447 Ok(None) => Ok(mailbox::SerializedSendDisposition::Delivered),
448 Err((data, error)) => Err(mailbox::SerializedSendFailure::Error(
449 mailbox::SerializedSendError {
450 data,
451 error: crate::mailbox::MailboxSenderError::new_bound(
452 error_port_id.clone(),
453 crate::mailbox::MailboxSenderErrorKind::Other(error),
454 ),
455 headers,
456 },
457 )),
458 }
459 })
460 }
461 ReducerMode::IdleFlush {
462 expected,
463 idle_timeout,
464 abandon_timeout,
465 } => {
466 let buffer: Arc<Mutex<IdleFlushBuffer>> =
467 Arc::new(Mutex::new(IdleFlushBuffer::new(reducer, expected)));
468 let mut alarm = Alarm::new();
469 alarm.arm(abandon_timeout);
470 let alarm = Arc::new(Mutex::new(alarm));
471
472 {
479 let mut sleeper = alarm.lock().unwrap().sleeper();
480 let buffer = Arc::clone(&buffer);
481 let alarm = Arc::clone(&alarm);
482 let port_id = port_id.clone();
483 let proc = proc.clone();
484 let sender = sender.clone();
485 let sequencer = sequencer.clone();
486 let mailbox = self.mailbox().clone();
487 let split_port = split_port.clone();
488
489 tokio::spawn(async move {
490 while sleeper.sleep().await {
491 let mut buf = buffer.lock().unwrap();
492
493 if buf.done {
494 break;
495 }
496
497 let timeout = if buf.has_buffered_updates() {
498 idle_timeout
499 } else {
500 abandon_timeout
501 };
502
503 let duration_until_timeout =
504 timeout.saturating_sub(buf.inactive_for());
505
506 if !duration_until_timeout.is_zero() {
507 alarm.lock().unwrap().arm(duration_until_timeout);
508 continue;
509 }
510
511 if let Some((mut headers, reduced, count)) = buf.flush() {
512 headers.set(REDUCER_ACCUMULATED_UPDATES, count);
513
514 post(
515 &proc,
516 &sender,
517 &sequencer,
518 port_id.clone(),
519 headers,
520 reduced,
521 return_undeliverable,
522 );
523
524 let duration_until_abandonment =
525 abandon_timeout.saturating_sub(buf.inactive_for());
526
527 if !duration_until_abandonment.is_zero() {
528 alarm.lock().unwrap().arm(duration_until_abandonment);
529 continue;
530 }
531 }
532
533 buf.done = true;
534
535 drop(buf);
536
537 mailbox.unbind_untyped(&split_port);
538 break;
539 }
540 });
541 }
542
543 let error_port_id = split_port.clone();
544 let sequencer = sequencer.clone();
545
546 Box::new(move |headers: Flattrs, update: wirevalue::Any| {
547 let accumulated_updates = headers
548 .get(REDUCER_ACCUMULATED_UPDATES)
549 .unwrap_or(NonZeroUsize::MIN);
550
551 let mut buf = buffer.lock().unwrap();
552
553 if buf.done {
554 return Err(mailbox::SerializedSendFailure::Dead {
555 data: update,
556 headers,
557 });
558 }
559
560 match buf.push(headers.clone(), update, accumulated_updates) {
561 Ok(IdleFlushPush::Complete {
562 mut headers,
563 reduced,
564 count,
565 }) => {
566 alarm.lock().unwrap().fire();
567
568 headers.set(REDUCER_ACCUMULATED_UPDATES, count);
569
570 post(
571 &proc,
572 &sender,
573 &sequencer,
574 port_id.clone(),
575 headers,
576 reduced,
577 return_undeliverable,
578 );
579
580 Ok(mailbox::SerializedSendDisposition::DeliveredAndExhausted)
581 }
582 Ok(IdleFlushPush::Buffered) => {
583 alarm.lock().unwrap().arm(idle_timeout);
586
587 Ok(mailbox::SerializedSendDisposition::Delivered)
588 }
589 Err((data, error)) => Err(mailbox::SerializedSendFailure::Error(
590 mailbox::SerializedSendError {
591 data,
592 error: crate::mailbox::MailboxSenderError::new_bound(
593 error_port_id.clone(),
594 crate::mailbox::MailboxSenderErrorKind::Other(error),
595 ),
596 headers,
597 },
598 )),
599 }
600 })
601 }
602 },
603 };
604 self.mailbox().bind_untyped(
605 &split_port,
606 mailbox::UntypedUnboundedSender { sender: enqueue },
607 );
608 Ok(split_port)
609 }
610}
611
612struct UpdateBuffer {
613 buffered: Vec<wirevalue::Any>,
614 headers: Option<Flattrs>,
615 reducer: Box<dyn ErasedCommReducer + Send + Sync + 'static>,
616}
617
618impl UpdateBuffer {
619 fn new(reducer: Box<dyn ErasedCommReducer + Send + Sync + 'static>) -> Self {
620 Self {
621 buffered: Vec::new(),
622 headers: None,
623 reducer,
624 }
625 }
626
627 fn pop(&mut self) -> Option<wirevalue::Any> {
628 let value = self.buffered.pop();
629 if self.buffered.is_empty() {
630 self.headers = None;
631 }
632 value
633 }
634
635 fn push(
638 &mut self,
639 headers: Flattrs,
640 serialized: wirevalue::Any,
641 ) -> Option<anyhow::Result<(Flattrs, wirevalue::Any)>> {
642 let limit = hyperactor_config::global::get(config::SPLIT_MAX_BUFFER_SIZE);
643
644 if self.headers.is_none() {
645 self.headers = Some(operation_context_headers(&headers));
646 }
647 self.buffered.push(serialized);
648 if self.buffered.len() >= limit {
649 self.reduce()
650 } else {
651 None
652 }
653 }
654
655 fn reduce(&mut self) -> Option<anyhow::Result<(Flattrs, wirevalue::Any)>> {
656 if self.buffered.is_empty() {
657 None
658 } else {
659 let headers = self.headers.take().unwrap_or_default();
660 match self.reducer.reduce_updates(take(&mut self.buffered)) {
661 Ok(reduced) => Some(Ok((headers, reduced))),
662 Err((e, b)) => {
663 self.buffered = b;
664 self.headers = Some(headers);
665 Some(Err(e))
666 }
667 }
668 }
669 }
670}
671
672struct OnceBuffer {
673 accumulated: Option<wirevalue::Any>,
674 headers: Option<Flattrs>,
675 reducer: Box<dyn ErasedCommReducer + Send + Sync + 'static>,
676 expected: usize,
677 count: usize,
678 done: bool,
679}
680
681impl OnceBuffer {
682 fn new(reducer: Box<dyn ErasedCommReducer + Send + Sync + 'static>, expected: usize) -> Self {
683 Self {
684 accumulated: None,
685 headers: None,
686 reducer,
687 expected,
688 count: 0,
689 done: false,
690 }
691 }
692
693 fn push(
697 &mut self,
698 headers: Flattrs,
699 value: wirevalue::Any,
700 ) -> Result<Option<(Flattrs, wirevalue::Any)>, (wirevalue::Any, anyhow::Error)> {
701 self.count += 1;
702 if self.headers.is_none() {
703 self.headers = Some(operation_context_headers(&headers));
704 }
705 self.accumulated = match self.accumulated.take() {
706 None => Some(value),
707 Some(acc) => match self.reducer.reduce_updates(vec![acc, value]) {
708 Ok(reduced) => Some(reduced),
709 Err((e, mut rejected)) => {
710 return Err((
711 rejected
712 .pop()
713 .unwrap_or_else(|| wirevalue::Any::serialize(&()).unwrap()),
714 e,
715 ));
716 }
717 },
718 };
719 if self.count >= self.expected {
720 self.done = true;
721 Ok(self
722 .accumulated
723 .take()
724 .map(|reduced| (self.headers.take().unwrap_or_default(), reduced)))
725 } else {
726 Ok(None)
727 }
728 }
729}
730
731struct IdleFlushBuffer {
732 accumulated: Option<wirevalue::Any>,
733 headers: Option<Flattrs>,
734 reducer: Box<dyn ErasedCommReducer + Send + Sync + 'static>,
735 expected: NonZeroUsize,
736 received: usize,
737 buffered: usize,
738 last_activity: tokio::time::Instant,
739 done: bool,
740}
741
742enum IdleFlushPush {
743 Buffered,
744 Complete {
745 headers: Flattrs,
746 reduced: wirevalue::Any,
747 count: NonZeroUsize,
748 },
749}
750
751impl IdleFlushBuffer {
752 fn new(
753 reducer: Box<dyn ErasedCommReducer + Send + Sync + 'static>,
754 expected: NonZeroUsize,
755 ) -> Self {
756 Self {
757 accumulated: None,
758 headers: None,
759 reducer,
760 expected,
761 received: 0,
762 buffered: 0,
763 last_activity: tokio::time::Instant::now(),
764 done: false,
765 }
766 }
767
768 fn push(
771 &mut self,
772 headers: Flattrs,
773 value: wirevalue::Any,
774 accumulated_updates: NonZeroUsize,
775 ) -> Result<IdleFlushPush, (wirevalue::Any, anyhow::Error)> {
776 let received = match self.received.checked_add(accumulated_updates.get()) {
777 Some(received) if received <= self.expected.get() => received,
778 _ => {
779 return Err((
780 value,
781 anyhow::anyhow!(
782 "received more reducer updates than expected: {} + {} > {}",
783 self.received,
784 accumulated_updates,
785 self.expected
786 ),
787 ));
788 }
789 };
790
791 let buffered = self
792 .buffered
793 .checked_add(accumulated_updates.get())
794 .expect("buffered reply count cannot exceed expected reply count");
795
796 if self.headers.is_none() {
797 self.headers = Some(operation_context_headers(&headers));
798 }
799
800 self.accumulated = match self.accumulated.take() {
801 None => Some(value),
802 Some(acc) => match self.reducer.reduce_updates(vec![acc, value]) {
803 Ok(reduced) => Some(reduced),
804 Err((error, mut rejected)) => {
805 let value = rejected.pop().unwrap_or_else(|| {
806 wirevalue::Any::serialize(&()).expect("unit serialization must succeed")
807 });
808
809 self.accumulated = rejected.pop();
810
811 return Err((value, error));
812 }
813 },
814 };
815
816 self.received = received;
817 self.buffered = buffered;
818 self.last_activity = tokio::time::Instant::now();
819
820 if self.received == self.expected.get() {
821 self.done = true;
822
823 let (headers, reduced, count) = self
824 .flush()
825 .expect("a completed idle-flush buffer contains an update");
826
827 Ok(IdleFlushPush::Complete {
828 headers,
829 reduced,
830 count,
831 })
832 } else {
833 Ok(IdleFlushPush::Buffered)
834 }
835 }
836
837 fn has_buffered_updates(&self) -> bool {
838 self.accumulated.is_some()
839 }
840
841 fn inactive_for(&self) -> Duration {
842 tokio::time::Instant::now().saturating_duration_since(self.last_activity)
843 }
844
845 fn flush(&mut self) -> Option<(Flattrs, wirevalue::Any, NonZeroUsize)> {
847 self.accumulated.take().map(|reduced| {
848 let headers = self.headers.take().unwrap_or_default();
849
850 let count = NonZeroUsize::new(take(&mut self.buffered))
851 .expect("a nonempty idle-flush buffer represents at least one reply");
852
853 (headers, reduced, count)
854 })
855 }
856}