Skip to main content

hyperactor/
context.rs

1/*
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 * All rights reserved.
4 *
5 * This source code is licensed under the BSD-style license found in the
6 * LICENSE file in the root directory of this source tree.
7 */
8
9//! This module defines traits that are used as context arguments to various
10//! hyperactor APIs; usually [`crate::context::Actor`], implemented by
11//! [`crate::proc::Context`] (provided to actor handlers) and [`crate::proc::Instance`],
12//! representing a running actor instance.
13//!
14//! Context traits are sealed, and thus can only be implemented by data types in the
15//! core hyperactor crate.
16
17use 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/// Policy for handling SEQ_INFO in message headers.
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49pub(crate) enum SeqInfoPolicy {
50    /// Assign a new sequence number. Panics if SEQ_INFO is already set.
51    AssignNew,
52    /// Allow externally-set SEQ_INFO. Used only by CommActor for mesh routing.
53    AllowExternal,
54}
55
56/// A mailbox context provides a mailbox.
57pub trait Mailbox: crate::private::Sealed + Send + Sync {
58    /// The mailbox associated with this context
59    fn mailbox(&self) -> &crate::Mailbox;
60}
61
62/// A typed actor context, providing both a [`Mailbox`] and an [`Instance`].
63///
64/// Note: Send and Sync markers are here only temporarily in order to bridge
65/// the transition to the context types, away from the [`crate::cap`] module.
66#[async_trait]
67pub trait Actor: Mailbox {
68    /// The type of actor associated with this context.
69    type A: crate::Actor;
70
71    /// The instance associated with this context.
72    fn instance(&self) -> &Instance<Self::A>;
73
74    /// Spawn a child actor under this actor context.
75    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    /// Spawn a child actor with a fresh uid carrying a display label.
83    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    /// Spawn a child actor using an explicit uid.
91    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    /// The inbound message headers associated with this context, if any.
103    ///
104    /// Plain [`Instance`] send contexts are not handling an inbound message, so
105    /// they use the default empty header set.
106    fn headers(&self) -> &Flattrs {
107        static EMPTY_HEADERS: OnceLock<Flattrs> = OnceLock::new();
108        EMPTY_HEADERS.get_or_init(Flattrs::new)
109    }
110}
111
112/// An internal extension trait for Mailbox contexts.
113/// TODO: consider moving this to another module.
114pub(crate) trait MailboxExt: Mailbox {
115    /// Post a message to the provided destination with the provided headers, and data.
116    /// All messages posted from actors should use this implementation.
117    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    /// Split a port, using a provided reducer spec, if provided.
127    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
136// Tracks mailboxes that have emitted a `CanSend::post` warning due to
137// missing an `Undeliverable<MessageEnvelope>` binding. In this
138// context, mailboxes are few and long-lived; unbounded growth is not
139// a realistic concern.
140static 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
148/// Only actors CanSend because they need a return port.
149impl<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            // This method is infallible so is okay to assign the sequence number
181            // without worrying about rollback.
182            let sequencer = self.instance().sequencer();
183            let seq_info = sequencer.assign_seq(&dest);
184            // Pair the SENDER_ACTOR_ID stamp with the seq we just assigned.
185            // Helper applies the (seq<=4 || stale) gate, the handler-port +
186            // non-bypass guard, and the framework-owned overwrite semantics.
187            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                // TODO(pzhang) figure out how to use upstream's return handle,
237                // instead of getting a new one like this.
238                // This is okay for now because upstream is currently also using
239                // the same handle singleton, but that could change in the future.
240                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                                    // We simply ignore errors here, and let them be propagated
316                                    // later in the enqueueing function.
317                                    //
318                                    // If this is the last update, then this strategy will cause a hang.
319                                    // We should obtain a supervisor here from our send context and notify
320                                    // it.
321                                    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                    // Note: alarm is held in the closure while the port is active;
331                    // when it is dropped, the alarm terminates, and so does the sleeper
332                    // task.
333                    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                    // Create exponential backoff for buffer flush interval, starting at
339                    // initial_interval and growing to max_interval
340                    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                        // Hold the lock until messages are sent. This is to avoid another
353                        // invocation of this method trying to send message concurrently and
354                        // cause messages delivered out of order.
355                        //
356                        // We also always acquire alarm *after* the buffer, to avoid deadlocks.
357                        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    /// Push a new item to the buffer, and optionally return any items that should
486    /// be flushed.
487    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    /// Push a new value and reduce incrementally. Returns Ok(Some(reduced)) when
544    /// the expected count is reached, Ok(None) while still accumulating. On error,
545    /// the buffer is broken and returns the rejected value.
546    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}