1use std::mem::take;
18use std::sync::Arc;
19use std::sync::Mutex;
20use std::sync::OnceLock;
21
22use async_trait::async_trait;
23use backoff::ExponentialBackoffBuilder;
24use backoff::backoff::Backoff;
25use dashmap::DashSet;
26use hyperactor_config::Flattrs;
27use hyperactor_config::attrs::OPERATION_CONTEXT_HEADER;
28use hyperactor_config::attrs::copy_marked_flattrs;
29
30use crate::ActorAddr;
31use crate::Instance;
32use crate::PortAddr;
33use crate::Proc;
34use crate::accum;
35use crate::accum::ErasedCommReducer;
36use crate::accum::ReducerMode;
37use crate::accum::ReducerSpec;
38use crate::config;
39use crate::id::Uid;
40use crate::mailbox;
41use crate::mailbox::MailboxSender;
42use crate::mailbox::MessageEnvelope;
43use crate::ordering::SEQ_INFO;
44use crate::port::Port;
45use crate::time::Alarm;
46
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49pub(crate) enum SeqInfoPolicy {
50 AssignNew,
52 AllowExternal,
54}
55
56pub trait Mailbox: crate::private::Sealed + Send + Sync {
58 fn mailbox(&self) -> &crate::Mailbox;
60}
61
62#[async_trait]
67pub trait Actor: Mailbox {
68 type A: crate::Actor;
70
71 fn instance(&self) -> &Instance<Self::A>;
73
74 fn spawn<C: crate::Actor>(&self, actor: C) -> crate::ActorHandle<C>
76 where
77 Self: Sized,
78 {
79 self.instance().spawn(actor)
80 }
81
82 fn spawn_with_label<C: crate::Actor>(&self, label: &str, actor: C) -> crate::ActorHandle<C>
84 where
85 Self: Sized,
86 {
87 self.instance().spawn_with_label(label, actor)
88 }
89
90 fn spawn_with_uid<C: crate::Actor>(
92 &self,
93 uid: Uid,
94 actor: C,
95 ) -> anyhow::Result<crate::ActorHandle<C>>
96 where
97 Self: Sized,
98 {
99 self.instance().spawn_with_uid(uid, actor)
100 }
101
102 fn headers(&self) -> &Flattrs {
107 static EMPTY_HEADERS: OnceLock<Flattrs> = OnceLock::new();
108 EMPTY_HEADERS.get_or_init(Flattrs::new)
109 }
110}
111
112pub(crate) trait MailboxExt: Mailbox {
115 fn post(
118 &self,
119 dest: PortAddr,
120 headers: Flattrs,
121 data: wirevalue::Any,
122 return_undeliverable: bool,
123 seq_info_policy: SeqInfoPolicy,
124 );
125
126 fn split(
128 &self,
129 port_id: PortAddr,
130 reducer_spec: Option<ReducerSpec>,
131 reducer_mode: ReducerMode,
132 return_undeliverable: bool,
133 ) -> anyhow::Result<PortAddr>;
134}
135
136static CAN_SEND_WARNED_MAILBOXES: OnceLock<DashSet<ActorAddr>> = OnceLock::new();
141
142fn operation_context_headers(headers: &Flattrs) -> Flattrs {
143 let mut operation_headers = Flattrs::new();
144 copy_marked_flattrs(&mut operation_headers, headers, OPERATION_CONTEXT_HEADER);
145 operation_headers
146}
147
148impl<T: Actor + Send + Sync> MailboxExt for T {
150 fn post(
151 &self,
152 dest: PortAddr,
153 mut headers: Flattrs,
154 data: wirevalue::Any,
155 return_undeliverable: bool,
156 seq_info_policy: SeqInfoPolicy,
157 ) {
158 let return_handle = self.mailbox().bound_return_handle().unwrap_or_else(|| {
159 let actor_id = self.mailbox().actor_addr();
160 if CAN_SEND_WARNED_MAILBOXES
161 .get_or_init(DashSet::new)
162 .insert(actor_id.clone())
163 {
164 let bt = std::backtrace::Backtrace::force_capture();
165 tracing::warn!(
166 actor_id = ?actor_id,
167 backtrace = ?bt,
168 "mailbox attempted to post a message without binding Undeliverable<MessageEnvelope>"
169 );
170 }
171 mailbox::monitored_return_handle()
172 });
173
174 assert!(
175 !headers.contains_key(SEQ_INFO) || seq_info_policy == SeqInfoPolicy::AllowExternal,
176 "SEQ_INFO must not be set on headers outside of fn post unless explicitly allowed"
177 );
178
179 if !headers.contains_key(SEQ_INFO) {
180 let sequencer = self.instance().sequencer();
183 let seq_info = sequencer.assign_seq(&dest);
184 crate::mailbox::headers::stamp_sender_actor_id(
188 &mut headers,
189 &seq_info,
190 &dest,
191 self.mailbox().actor_addr(),
192 );
193 headers.set(SEQ_INFO, seq_info);
194 }
195
196 let mut envelope =
197 MessageEnvelope::new(self.mailbox().actor_addr().clone(), dest, data, headers);
198 envelope.set_return_undeliverable(return_undeliverable);
199 MailboxSender::post(self.instance().proc(), envelope, return_handle);
200 }
201
202 fn split(
203 &self,
204 port_id: PortAddr,
205 reducer_spec: Option<ReducerSpec>,
206 reducer_mode: ReducerMode,
207 return_undeliverable: bool,
208 ) -> anyhow::Result<PortAddr> {
209 fn post(
210 proc: &Proc,
211 sender: &ActorAddr,
212 sequencer: &crate::ordering::Sequencer,
213 port_id: PortAddr,
214 mut headers: Flattrs,
215 msg: wirevalue::Any,
216 return_undeliverable: bool,
217 ) {
218 assert!(
219 !headers.contains_key(SEQ_INFO),
220 "SEQ_INFO must not be set on split-port forwarded headers"
221 );
222 let seq_info = sequencer.assign_seq(&port_id);
223 crate::mailbox::headers::stamp_sender_actor_id(
224 &mut headers,
225 &seq_info,
226 &port_id,
227 sender,
228 );
229 headers.set(SEQ_INFO, seq_info);
230
231 let mut envelope = MessageEnvelope::new(sender.clone(), port_id, msg, headers);
232 envelope.set_return_undeliverable(return_undeliverable);
233 mailbox::MailboxSender::post(
234 proc,
235 envelope,
236 mailbox::monitored_return_handle(),
241 );
242 }
243
244 let port_index = self.mailbox().allocate_port();
245 let split_port = self
246 .mailbox()
247 .actor_addr()
248 .port_addr(Port::from(port_index));
249 let proc = self.instance().proc().clone();
250 let sender = self.mailbox().actor_addr().clone();
251 let sequencer = self.instance().sequencer().clone();
252 let reducer = reducer_spec
253 .map(
254 |ReducerSpec {
255 typehash,
256 builder_params,
257 }| { accum::resolve_reducer(typehash, builder_params) },
258 )
259 .transpose()?
260 .flatten();
261 let enqueue: Box<
262 dyn Fn(
263 Flattrs,
264 wirevalue::Any,
265 )
266 -> Result<mailbox::SerializedSendDisposition, mailbox::SerializedSendFailure>
267 + Send
268 + Sync,
269 > = match reducer {
270 None => {
271 let proc = proc.clone();
272 let sender = sender.clone();
273 let sequencer = sequencer.clone();
274 Box::new(move |headers: Flattrs, serialized: wirevalue::Any| {
275 post(
276 &proc,
277 &sender,
278 &sequencer,
279 port_id.clone(),
280 operation_context_headers(&headers),
281 serialized,
282 return_undeliverable,
283 );
284 Ok(mailbox::SerializedSendDisposition::Delivered)
285 })
286 }
287 Some(reducer) => match reducer_mode {
288 ReducerMode::Streaming(_) => {
289 let buffer: Arc<Mutex<UpdateBuffer>> =
290 Arc::new(Mutex::new(UpdateBuffer::new(reducer)));
291
292 let alarm = Alarm::new();
293
294 {
295 let mut sleeper = alarm.sleeper();
296 let buffer = Arc::clone(&buffer);
297 let port_id = port_id.clone();
298 let proc = proc.clone();
299 let sender = sender.clone();
300 let sequencer = sequencer.clone();
301 tokio::spawn(async move {
302 while sleeper.sleep().await {
303 let mut buf = buffer.lock().unwrap();
304 match buf.reduce() {
305 None => (),
306 Some(Ok((headers, reduced))) => post(
307 &proc,
308 &sender,
309 &sequencer,
310 port_id.clone(),
311 headers,
312 reduced,
313 return_undeliverable,
314 ),
315 Some(Err(e)) => tracing::error!(
322 "error while reducing update: {}; waiting until the next send to propagate",
323 e
324 ),
325 }
326 }
327 });
328 }
329
330 let alarm = Mutex::new(alarm);
334
335 let max_interval = reducer_mode.max_update_interval();
336 let initial_interval = reducer_mode.initial_update_interval();
337
338 let backoff = Mutex::new(
341 ExponentialBackoffBuilder::new()
342 .with_initial_interval(initial_interval)
343 .with_multiplier(2.0)
344 .with_max_interval(max_interval)
345 .with_max_elapsed_time(None)
346 .build(),
347 );
348
349 let error_port_id = split_port.clone();
350 let sequencer = sequencer.clone();
351 Box::new(move |headers: Flattrs, update: wirevalue::Any| {
352 let mut buf = buffer.lock().unwrap();
358 match buf.push(headers.clone(), update) {
359 None => {
360 let interval = backoff.lock().unwrap().next_backoff().unwrap();
361 alarm.lock().unwrap().rearm(interval);
362 Ok(mailbox::SerializedSendDisposition::Delivered)
363 }
364 Some(Ok((headers, reduced))) => {
365 alarm.lock().unwrap().disarm();
366 post(
367 &proc,
368 &sender,
369 &sequencer,
370 port_id.clone(),
371 headers,
372 reduced,
373 return_undeliverable,
374 );
375 Ok(mailbox::SerializedSendDisposition::Delivered)
376 }
377 Some(Err(error)) => Err(mailbox::SerializedSendFailure::Error(
378 mailbox::SerializedSendError {
379 data: buf
380 .pop()
381 .expect("reducer error should leave update buffered"),
382 error: crate::mailbox::MailboxSenderError::new_bound(
383 error_port_id.clone(),
384 crate::mailbox::MailboxSenderErrorKind::Other(error),
385 ),
386 headers,
387 },
388 )),
389 }
390 })
391 }
392 ReducerMode::Once(0) => {
393 let error_port_id = split_port.clone();
394 Box::new(move |headers: Flattrs, update: wirevalue::Any| {
395 Err(mailbox::SerializedSendFailure::Error(
396 mailbox::SerializedSendError {
397 data: update,
398 error: crate::mailbox::MailboxSenderError::new_bound(
399 error_port_id.clone(),
400 crate::mailbox::MailboxSenderErrorKind::Other(anyhow::anyhow!(
401 "invalid ReducerMode: Once must specify at least one update"
402 )),
403 ),
404 headers,
405 },
406 ))
407 })
408 }
409 ReducerMode::Once(expected) => {
410 let buffer: Arc<Mutex<OnceBuffer>> =
411 Arc::new(Mutex::new(OnceBuffer::new(reducer, expected)));
412 let error_port_id = split_port.clone();
413 let proc = proc.clone();
414 let sender = sender.clone();
415 let sequencer = sequencer.clone();
416
417 Box::new(move |headers: Flattrs, update: wirevalue::Any| {
418 let mut buf = buffer.lock().unwrap();
419 if buf.done {
420 return Err(mailbox::SerializedSendFailure::Dead {
421 data: update,
422 headers,
423 });
424 }
425 match buf.push(headers.clone(), update) {
426 Ok(Some((headers, reduced))) => {
427 post(
428 &proc,
429 &sender,
430 &sequencer,
431 port_id.clone(),
432 headers,
433 reduced,
434 return_undeliverable,
435 );
436 Ok(mailbox::SerializedSendDisposition::DeliveredAndExhausted)
437 }
438 Ok(None) => Ok(mailbox::SerializedSendDisposition::Delivered),
439 Err((data, error)) => Err(mailbox::SerializedSendFailure::Error(
440 mailbox::SerializedSendError {
441 data,
442 error: crate::mailbox::MailboxSenderError::new_bound(
443 error_port_id.clone(),
444 crate::mailbox::MailboxSenderErrorKind::Other(error),
445 ),
446 headers,
447 },
448 )),
449 }
450 })
451 }
452 },
453 };
454 self.mailbox().bind_untyped(
455 &split_port,
456 mailbox::UntypedUnboundedSender { sender: enqueue },
457 );
458 Ok(split_port)
459 }
460}
461
462struct UpdateBuffer {
463 buffered: Vec<wirevalue::Any>,
464 headers: Option<Flattrs>,
465 reducer: Box<dyn ErasedCommReducer + Send + Sync + 'static>,
466}
467
468impl UpdateBuffer {
469 fn new(reducer: Box<dyn ErasedCommReducer + Send + Sync + 'static>) -> Self {
470 Self {
471 buffered: Vec::new(),
472 headers: None,
473 reducer,
474 }
475 }
476
477 fn pop(&mut self) -> Option<wirevalue::Any> {
478 let value = self.buffered.pop();
479 if self.buffered.is_empty() {
480 self.headers = None;
481 }
482 value
483 }
484
485 fn push(
488 &mut self,
489 headers: Flattrs,
490 serialized: wirevalue::Any,
491 ) -> Option<anyhow::Result<(Flattrs, wirevalue::Any)>> {
492 let limit = hyperactor_config::global::get(config::SPLIT_MAX_BUFFER_SIZE);
493
494 if self.headers.is_none() {
495 self.headers = Some(operation_context_headers(&headers));
496 }
497 self.buffered.push(serialized);
498 if self.buffered.len() >= limit {
499 self.reduce()
500 } else {
501 None
502 }
503 }
504
505 fn reduce(&mut self) -> Option<anyhow::Result<(Flattrs, wirevalue::Any)>> {
506 if self.buffered.is_empty() {
507 None
508 } else {
509 let headers = self.headers.take().unwrap_or_else(Flattrs::new);
510 match self.reducer.reduce_updates(take(&mut self.buffered)) {
511 Ok(reduced) => Some(Ok((headers, reduced))),
512 Err((e, b)) => {
513 self.buffered = b;
514 self.headers = Some(headers);
515 Some(Err(e))
516 }
517 }
518 }
519 }
520}
521
522struct OnceBuffer {
523 accumulated: Option<wirevalue::Any>,
524 headers: Option<Flattrs>,
525 reducer: Box<dyn ErasedCommReducer + Send + Sync + 'static>,
526 expected: usize,
527 count: usize,
528 done: bool,
529}
530
531impl OnceBuffer {
532 fn new(reducer: Box<dyn ErasedCommReducer + Send + Sync + 'static>, expected: usize) -> Self {
533 Self {
534 accumulated: None,
535 headers: None,
536 reducer,
537 expected,
538 count: 0,
539 done: false,
540 }
541 }
542
543 fn push(
547 &mut self,
548 headers: Flattrs,
549 value: wirevalue::Any,
550 ) -> Result<Option<(Flattrs, wirevalue::Any)>, (wirevalue::Any, anyhow::Error)> {
551 self.count += 1;
552 if self.headers.is_none() {
553 self.headers = Some(operation_context_headers(&headers));
554 }
555 self.accumulated = match self.accumulated.take() {
556 None => Some(value),
557 Some(acc) => match self.reducer.reduce_updates(vec![acc, value]) {
558 Ok(reduced) => Some(reduced),
559 Err((e, mut rejected)) => {
560 return Err((
561 rejected
562 .pop()
563 .unwrap_or_else(|| wirevalue::Any::serialize(&()).unwrap()),
564 e,
565 ));
566 }
567 },
568 };
569 if self.count >= self.expected {
570 self.done = true;
571 Ok(self
572 .accumulated
573 .take()
574 .map(|reduced| (self.headers.take().unwrap_or_else(Flattrs::new), reduced)))
575 } else {
576 Ok(None)
577 }
578 }
579}