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;
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    /// Number of logical replies accumulated into a reduced update.
52    attr REDUCER_ACCUMULATED_UPDATES: NonZeroUsize;
53}
54
55/// Policy for handling SEQ_INFO in message headers.
56#[derive(Debug, Clone, Copy, PartialEq, Eq)]
57pub(crate) enum SeqInfoPolicy {
58    /// Assign a new sequence number. Panics if SEQ_INFO is already set.
59    AssignNew,
60    /// Allow externally-set SEQ_INFO for cast routing and ordered forwarding.
61    AllowExternal,
62}
63
64/// A mailbox context provides a mailbox.
65pub trait Mailbox: crate::private::Sealed + Send + Sync {
66    /// The mailbox associated with this context
67    fn mailbox(&self) -> &crate::Mailbox;
68}
69
70/// A typed actor context, providing both a [`Mailbox`] and an [`Instance`].
71///
72/// Note: Send and Sync markers are here only temporarily in order to bridge
73/// the transition to the context types, away from the [`crate::cap`] module.
74#[async_trait]
75pub trait Actor: Mailbox {
76    /// The type of actor associated with this context.
77    type A: crate::Actor;
78
79    /// The instance associated with this context.
80    fn instance(&self) -> &Instance<Self::A>;
81
82    /// Spawn a child actor under this actor context.
83    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    /// Spawn a child actor with a fresh uid carrying a display label.
91    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    /// Spawn a child actor using an explicit uid.
99    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    /// The inbound message headers associated with this context, if any.
111    ///
112    /// Plain [`Instance`] send contexts are not handling an inbound message, so
113    /// they use the default empty header set.
114    fn headers(&self) -> &Flattrs {
115        static EMPTY_HEADERS: OnceLock<Flattrs> = OnceLock::new();
116        EMPTY_HEADERS.get_or_init(Flattrs::new)
117    }
118}
119
120/// An internal extension trait for Mailbox contexts.
121/// TODO: consider moving this to another module.
122pub(crate) trait MailboxExt: Mailbox {
123    /// Post a message to the provided destination with the provided headers, and data.
124    /// All messages posted from actors should use this implementation.
125    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    /// Split a port, using a provided reducer spec, if provided.
135    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
144// Tracks mailboxes that have emitted a `CanSend::post` warning due to
145// missing an `Undeliverable<MessageEnvelope>` binding. In this
146// context, mailboxes are few and long-lived; unbounded growth is not
147// a realistic concern.
148static 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
156/// Only actors CanSend because they need a return port.
157impl<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            // This method is infallible so is okay to assign the sequence number
190            // without worrying about rollback.
191            let sequencer = self.instance().sequencer();
192            let seq_info = sequencer.assign_seq(&dest);
193            // Pair the SENDER_ACTOR_ID stamp with the seq we just assigned.
194            // Helper applies the (seq<=4 || stale) gate, the handler-port +
195            // non-bypass guard, and the framework-owned overwrite semantics.
196            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                // TODO(pzhang) figure out how to use upstream's return handle,
246                // instead of getting a new one like this.
247                // This is okay for now because upstream is currently also using
248                // the same handle singleton, but that could change in the future.
249                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                                    // We simply ignore errors here, and let them be propagated
325                                    // later in the enqueueing function.
326                                    //
327                                    // If this is the last update, then this strategy will cause a hang.
328                                    // We should obtain a supervisor here from our send context and notify
329                                    // it.
330                                    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                    // Note: alarm is held in the closure while the port is active;
340                    // when it is dropped, the alarm terminates, and so does the sleeper
341                    // task.
342                    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                    // Create exponential backoff for buffer flush interval, starting at
348                    // initial_interval and growing to max_interval
349                    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                        // Hold the lock until messages are sent. This is to avoid another
362                        // invocation of this method trying to send message concurrently and
363                        // cause messages delivered out of order.
364                        //
365                        // We also always acquire alarm *after* the buffer, to avoid deadlocks.
366                        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                    // The alarm starts with `abandon_timeout`, so a split port that
473                    // receives no updates is eventually removed. Each buffered update
474                    // switches the alarm to `idle_timeout`. After flushing, the alarm is
475                    // rearmed for the remaining `abandon_timeout` measured from the last
476                    // update. If no further update arrives, the buffer is marked done and
477                    // the split port is unbound from the mailbox.
478                    {
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                                // This is an idle-flush interval. Each accepted reply
584                                // starts a fresh countdown from now.
585                                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    /// Push a new item to the buffer, and optionally return any items that should
636    /// be flushed.
637    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    /// Push a new value and reduce incrementally. Returns Ok(Some(reduced)) when
694    /// the expected count is reached, Ok(None) while still accumulating. On error,
695    /// the buffer is broken and returns the rejected value.
696    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    /// Add an update that represents one or more logical replies. A completed
769    /// batch is returned when all expected replies have arrived.
770    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    /// Return the current reduced batch without completing the buffer.
846    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}