Skip to main content

behavior/
supervising.rs

1//! Pure supervision. Stable child identity is a proxy actor; replacement is a
2//! message to that proxy, which creates a fresh worker incarnation.
3//!
4//! Supervision is a Bombay-derived construction, not a privileged actor-model
5//! effect. Its transition law is the same pure fold as every other behavior:
6//! one typed termination observation updates the supervisor's explicit state
7//! and returns only sends, fresh creations, and become. Restart eligibility,
8//! candidate selection, budget admission, and the reaction to an unsatisfied
9//! topology are behavior policy. The interpreter only delivers observations
10//! and interprets the resulting [`Actions`].
11
12use std::time::Duration;
13
14use tokio::time::Instant;
15
16use crate::behavior::{
17    Actions, Address, Behavior, Births, Create, Delivery, Recipient, SendAlgebra, SendProduct,
18    ServiceSends, User, UserEvent,
19};
20use crate::deadlined::{TimeEvent, TimeReached};
21use crate::shutdown::{ShutdownEvent, ShutdownRequested};
22use crate::verdict::{Never, Step};
23use crate::watching::{PeerEvent, PeerStopped};
24use crate::{Become, Crash, Exit, RestartDenial, SupervisionFailureReason};
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum Strategy {
28    OneForOne,
29    OneForAll,
30    RestForOne,
31}
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub enum RestartPolicy {
35    Permanent,
36    Transient,
37    Temporary,
38}
39
40#[must_use]
41pub const fn restart_one() -> Strategy {
42    Strategy::OneForOne
43}
44
45#[must_use]
46pub const fn restart_all() -> Strategy {
47    Strategy::OneForAll
48}
49
50#[must_use]
51pub const fn restart_rest() -> Strategy {
52    Strategy::RestForOne
53}
54
55#[derive(Debug, Clone, PartialEq, Eq)]
56pub struct ChildStopped<A: Address> {
57    pub nonce: A::Nonce,
58    pub outcome: Result<Exit<A>, Crash>,
59    pub at: Instant,
60}
61
62#[derive(Debug, Clone, Copy, PartialEq, Eq)]
63pub struct ObserveChild<A: Address> {
64    pub nonce: A::Nonce,
65}
66
67/// A proxy's request for its interpreter to report a worker termination to
68/// the proxy's parent. The interpreter supplies the emitting proxy's child
69/// nonce when constructing [`WorkerStopped`].
70#[derive(Debug, Clone, PartialEq, Eq)]
71pub struct ReportWorkerStopped<A: Address> {
72    pub outcome: Result<Exit<A>, Crash>,
73    pub at: Instant,
74}
75
76/// A worker termination reported by a still-live supervised proxy.
77#[derive(Debug, Clone, PartialEq, Eq)]
78pub struct WorkerStopped<A: Address> {
79    pub proxy: A::Nonce,
80    pub outcome: Result<Exit<A>, Crash>,
81    pub at: Instant,
82}
83
84/// A typed failure of the supervisor's child-topology contract.
85///
86/// The original terminal outcome is owned here so a pure reaction can inspect
87/// it without consulting interpreter state. The compact `reason` is also the
88/// value propagated in [`Exit::SupervisionFailed`] when the supplied reaction
89/// chooses to stop.
90#[derive(Debug, Clone, Copy, PartialEq, Eq)]
91pub struct SupervisionFailure<A: Address> {
92    /// Stable slot whose termination made the topology unsatisfiable.
93    pub child: A::Nonce,
94    /// Original terminal outcome, preserved without cloning or erasure.
95    pub outcome: Result<Exit<A>, Crash>,
96    /// Exhaustive policy reason for the failure.
97    pub reason: SupervisionFailureReason,
98}
99
100impl<A: Address> SupervisionFailure<A> {
101    /// Reduce the local failure observation to the terminal value propagated
102    /// through ordinary child observation.
103    #[must_use]
104    pub const fn into_exit(self) -> Exit<A> {
105        Exit::SupervisionFailed(self.reason)
106    }
107}
108
109/// Pure policy applied when a supervisor cannot preserve its child topology.
110///
111/// A function pointer keeps the policy concrete, non-capturing, allocation
112/// free, and statically dispatched. It may update the wrapped behavior as part
113/// of the same serialized fold and chooses only its next behavior; it cannot
114/// interpret effects or consult runtime state.
115pub type SupervisionFailureReaction<B> =
116    fn(
117        &mut B,
118        &SupervisionFailure<<B as Behavior>::Addr>,
119    ) -> Result<Become<<B as Behavior>::Addr>, <B as Behavior>::Error>;
120
121/// Retire the failed slot and keep the supervisor alive.
122///
123/// This is Bombay's compatibility policy and the default for a newly built
124/// supervisor. It never produces a controlled behavior error.
125///
126/// # Errors
127///
128/// This supplied policy never returns a controlled behavior error.
129pub fn retire_on_supervision_failure<B: Behavior>(
130    _behavior: &mut B,
131    _failure: &SupervisionFailure<B::Addr>,
132) -> Result<Become<B::Addr>, B::Error> {
133    Ok(Step::Continue)
134}
135
136/// Stop the supervisor with a typed failure outcome.
137///
138/// A parent observing this ordinary actor exit may apply its own supervision
139/// policy. This derives escalation through the existing actor algebra rather
140/// than adding an interpreter-only effect.
141///
142/// # Errors
143///
144/// This supplied policy never returns a controlled behavior error.
145pub fn stop_on_supervision_failure<B: Behavior>(
146    _behavior: &mut B,
147    failure: &SupervisionFailure<B::Addr>,
148) -> Result<Become<B::Addr>, B::Error> {
149    Ok(Step::Stop(failure.into_exit()))
150}
151
152/// Construction of the worker-report lane through a composed event type.
153pub trait WorkerEvent<A: Address>: Sized {
154    fn worker_stopped(event: WorkerStopped<A>) -> Option<Self>;
155}
156
157#[derive(Clone, PartialEq, Eq)]
158pub enum SupervisionEvent<E, A: Address> {
159    Inner(E),
160    ChildStopped(ChildStopped<A>),
161    WorkerStopped(WorkerStopped<A>),
162}
163
164pub trait ChildEvent<A: Address>: Sized {
165    fn child_stopped(event: ChildStopped<A>) -> Option<Self>;
166}
167
168impl<E, A: Address> ChildEvent<A> for SupervisionEvent<E, A> {
169    fn child_stopped(event: ChildStopped<A>) -> Option<Self> {
170        Some(Self::ChildStopped(event))
171    }
172}
173
174impl<E, A: Address> WorkerEvent<A> for SupervisionEvent<E, A> {
175    fn worker_stopped(event: WorkerStopped<A>) -> Option<Self> {
176        Some(Self::WorkerStopped(event))
177    }
178}
179
180impl<E: UserEvent, A: Address> UserEvent for SupervisionEvent<E, A> {
181    type Addr = E::Addr;
182    type Message = E::Message;
183
184    fn user(from: Self::Addr, message: Self::Message) -> Self {
185        Self::Inner(E::user(from, message))
186    }
187
188    fn into_user(self) -> Result<User<Self::Addr, Self::Message>, Self> {
189        match self {
190            Self::Inner(event) => event.into_user().map_err(Self::Inner),
191            stopped @ (Self::ChildStopped(_) | Self::WorkerStopped(_)) => Err(stopped),
192        }
193    }
194}
195
196impl<E: TimeEvent, A: Address> TimeEvent for SupervisionEvent<E, A> {
197    fn time_reached(event: TimeReached) -> Option<Self> {
198        E::time_reached(event).map(Self::Inner)
199    }
200}
201
202impl<E: PeerEvent<A>, A: Address> PeerEvent<A> for SupervisionEvent<E, A> {
203    fn peer_stopped(event: PeerStopped<A>) -> Option<Self> {
204        E::peer_stopped(event).map(Self::Inner)
205    }
206}
207
208impl<E: ShutdownEvent, A: Address> ShutdownEvent for SupervisionEvent<E, A> {
209    fn shutdown_requested(event: ShutdownRequested) -> Option<Self> {
210        E::shutdown_requested(event).map(Self::Inner)
211    }
212}
213
214/// Commands accepted by a stable proxy.
215///
216/// A replacement requested while the current worker is alive is held until
217/// that worker's observed termination. Creation therefore never overlaps two
218/// worker incarnations beneath the same proxy.
219#[derive(Debug)]
220pub enum ProxyCommand<C: Behavior> {
221    /// Forward an application message to the current worker, if one is alive.
222    Forward(C::Msg),
223    /// Supply the worker behavior for the next fresh incarnation.
224    Replace(C),
225}
226
227pub type SupervisorSends<A, Sends, C> = SendProduct<
228    Sends,
229    SendProduct<ServiceSends<ObserveChild<A>>, Vec<Delivery<A, ProxyCommand<C>>>>,
230>;
231
232pub type SupervisorActions<B, C> = Actions<
233    <B as Behavior>::Addr,
234    <B as Behavior>::Ph,
235    SupervisorSends<<B as Behavior>::Addr, <B as Behavior>::Sends, C>,
236    Births<Proxy<C>>,
237>;
238
239/// The stable actor. Every replacement is an ordinary fresh birth beneath it.
240///
241/// Each emitted worker birth is paired with an [`ObserveChild`] request. A
242/// matching [`ChildStopped`] leaves the proxy alive and emits a
243/// [`ReportWorkerStopped`] carrying the outcome unchanged. Stale child-stop
244/// observations are inert.
245pub struct Proxy<C: Behavior<Ph = Never>> {
246    worker: Option<C>,
247    generation: u64,
248    worker_alive: bool,
249    pending: Option<C>,
250}
251
252impl<C: Behavior<Ph = Never>> Proxy<C> {
253    #[must_use]
254    pub fn new(worker: C) -> Self {
255        Self {
256            worker: Some(worker),
257            generation: 0,
258            worker_alive: false,
259            pending: None,
260        }
261    }
262}
263
264impl<C> Behavior for Proxy<C>
265where
266    C: Behavior<Ph = Never> + Send,
267    C::Addr: Send,
268    <C::Addr as Address>::Nonce: From<u64> + Send,
269    C::Msg: Send,
270    C: Send,
271{
272    type Addr = C::Addr;
273    type Msg = ProxyCommand<C>;
274    type Event = SupervisionEvent<User<C::Addr, ProxyCommand<C>>, C::Addr>;
275    type Sends = SendProduct<
276        Vec<Delivery<C::Addr, C::Msg>>,
277        SendProduct<
278            ServiceSends<ObserveChild<C::Addr>>,
279            ServiceSends<ReportWorkerStopped<C::Addr>>,
280        >,
281    >;
282    type Ph = Never;
283    type Error = Never;
284    type Birth = Births<C>;
285    type Effect = Actions<C::Addr, Never, Self::Sends, Births<C>>;
286    type Done = Exit<C::Addr>;
287
288    async fn init(&mut self) -> Result<Self::Effect, Never> {
289        let child = self.worker.take().expect("a proxy initializes once");
290        self.worker_alive = true;
291        Ok(Actions {
292            sends: SendProduct {
293                inner: Vec::new(),
294                own: SendProduct {
295                    inner: ServiceSends::one(ObserveChild {
296                        nonce: <C::Addr as Address>::Nonce::from(self.generation),
297                    }),
298                    own: ServiceSends::empty(),
299                },
300            },
301            creates: vec![Create {
302                nonce: <C::Addr as Address>::Nonce::from(self.generation),
303                child,
304            }],
305            become_: Step::Continue,
306        })
307    }
308
309    async fn step(&mut self, event: Self::Event) -> Result<Self::Effect, Never> {
310        let SupervisionEvent::Inner(event) = event else {
311            return match event {
312                SupervisionEvent::ChildStopped(event)
313                    if event.nonce == <C::Addr as Address>::Nonce::from(self.generation) =>
314                {
315                    self.worker_alive = false;
316                    let report = ReportWorkerStopped {
317                        outcome: event.outcome,
318                        at: event.at,
319                    };
320                    let creates = self.pending.take().map_or_else(Vec::new, |child| {
321                        self.generation = self
322                            .generation
323                            .checked_add(1)
324                            .expect("proxy generation exhausted");
325                        self.worker_alive = true;
326                        vec![Create {
327                            nonce: <C::Addr as Address>::Nonce::from(self.generation),
328                            child,
329                        }]
330                    });
331                    let observes = creates
332                        .iter()
333                        .map(|create| ObserveChild {
334                            nonce: create.nonce,
335                        })
336                        .collect();
337                    Ok(Actions {
338                        sends: SendProduct {
339                            inner: Vec::new(),
340                            own: SendProduct {
341                                inner: ServiceSends::new(observes),
342                                own: ServiceSends::one(report),
343                            },
344                        },
345                        creates,
346                        become_: Step::Continue,
347                    })
348                }
349                SupervisionEvent::ChildStopped(_) | SupervisionEvent::WorkerStopped(_) => {
350                    Ok(Actions::cont())
351                }
352                SupervisionEvent::Inner(_) => unreachable!(),
353            };
354        };
355        match event.message {
356            ProxyCommand::Forward(message) => Ok(Actions {
357                sends: SendProduct {
358                    inner: self
359                        .worker_alive
360                        .then(|| {
361                            Delivery::new(
362                                Recipient::child(<C::Addr as Address>::Nonce::from(
363                                    self.generation,
364                                )),
365                                message,
366                            )
367                        })
368                        .into_iter()
369                        .collect(),
370                    own: SendProduct {
371                        inner: ServiceSends::empty(),
372                        own: ServiceSends::empty(),
373                    },
374                },
375                creates: Vec::new(),
376                become_: Step::Continue,
377            }),
378            ProxyCommand::Replace(child) => {
379                if self.worker_alive {
380                    self.pending = Some(child);
381                    return Ok(Actions::cont());
382                }
383                self.generation = self
384                    .generation
385                    .checked_add(1)
386                    .expect("proxy generation exhausted");
387                self.worker_alive = true;
388                let nonce = <C::Addr as Address>::Nonce::from(self.generation);
389                Ok(Actions {
390                    sends: SendProduct {
391                        inner: Vec::new(),
392                        own: SendProduct {
393                            inner: ServiceSends::one(ObserveChild { nonce }),
394                            own: ServiceSends::empty(),
395                        },
396                    },
397                    creates: vec![Create { nonce, child }],
398                    become_: Step::Continue,
399                })
400            }
401        }
402    }
403}
404
405struct Slot {
406    alive: bool,
407    sequence: u64,
408}
409
410enum ReplacementDecision<A: Address, C: Behavior<Addr = A>> {
411    Retire,
412    Replace(Vec<Delivery<A, ProxyCommand<C>>>),
413    Failed(SupervisionFailure<A>),
414}
415
416pub struct Supervising<B: Behavior, C: Behavior<Ph = Never, Addr = B::Addr>> {
417    inner: B,
418    slots: Vec<(<B::Addr as Address>::Nonce, Slot)>,
419    configured_count: usize,
420    next_sequence: u64,
421    build: fn(usize) -> C,
422    strategy: Strategy,
423    policy: RestartPolicy,
424    max_restarts: u32,
425    window: Duration,
426    restarts: Vec<Instant>,
427    on_failure: SupervisionFailureReaction<B>,
428}
429
430impl<B, C> Supervising<B, C>
431where
432    B: Behavior<Birth = Births<C>>,
433    C: Behavior<Ph = Never, Addr = B::Addr>,
434{
435    #[allow(clippy::too_many_arguments, reason = "hidden by Spec")]
436    /// Construct the concrete supervisor behavior hidden by `Spec`.
437    ///
438    /// # Panics
439    /// Panics only if a fleet index cannot be represented by `u64`.
440    #[must_use]
441    pub fn new(
442        inner: B,
443        nonces: fn(usize) -> <B::Addr as Address>::Nonce,
444        count: usize,
445        build: fn(usize) -> C,
446        strategy: Strategy,
447        policy: RestartPolicy,
448        max_restarts: u32,
449        window: Duration,
450    ) -> Self {
451        let slots = (0..count)
452            .map(|index| {
453                (
454                    nonces(index),
455                    Slot {
456                        alive: true,
457                        sequence: u64::try_from(index).expect("fleet index fits u64"),
458                    },
459                )
460            })
461            .collect();
462        Self {
463            inner,
464            slots,
465            configured_count: count,
466            next_sequence: u64::try_from(count).expect("fleet size fits u64"),
467            build,
468            strategy,
469            policy,
470            max_restarts,
471            window,
472            restarts: Vec::new(),
473            on_failure: retire_on_supervision_failure::<B>,
474        }
475    }
476
477    #[must_use]
478    pub fn with_strategy(mut self, strategy: Strategy) -> Self {
479        self.strategy = strategy;
480        self
481    }
482
483    #[must_use]
484    pub fn with_policy(mut self, policy: RestartPolicy) -> Self {
485        self.policy = policy;
486        self
487    }
488
489    #[must_use]
490    pub fn with_budget(mut self, max: u32, window: Duration) -> Self {
491        self.max_restarts = max;
492        self.window = window;
493        self
494    }
495
496    #[must_use]
497    /// Replace the pure reaction used for typed supervision failures.
498    pub fn with_failure_reaction(mut self, reaction: SupervisionFailureReaction<B>) -> Self {
499        self.on_failure = reaction;
500        self
501    }
502
503    fn position(&self, nonce: <B::Addr as Address>::Nonce) -> Option<usize> {
504        self.slots.iter().position(|(known, _)| *known == nonce)
505    }
506
507    #[must_use]
508    /// Report whether a known supervised proxy is alive.
509    ///
510    /// # Panics
511    /// Panics when `nonce` is not part of this supervisor topology.
512    pub fn is_alive(&self, nonce: <B::Addr as Address>::Nonce) -> bool {
513        self.slots[self.position(nonce).expect("unknown supervised nonce")]
514            .1
515            .alive
516    }
517
518    #[must_use]
519    pub fn child_count(&self) -> usize {
520        self.slots.len()
521    }
522
523    #[must_use]
524    pub fn restarts_in_window(&self) -> usize {
525        self.restarts.len()
526    }
527
528    fn replacement_decision(
529        &mut self,
530        event: &WorkerStopped<B::Addr>,
531    ) -> ReplacementDecision<B::Addr, C> {
532        let dead = self
533            .position(event.proxy)
534            .expect("unknown supervised nonce");
535        let eligible = match self.policy {
536            RestartPolicy::Permanent => true,
537            RestartPolicy::Transient => {
538                !matches!(&event.outcome, Ok(Exit::Normal | Exit::Collected))
539            }
540            RestartPolicy::Temporary => false,
541        };
542        if !eligible {
543            self.slots[dead].1.alive = false;
544            return ReplacementDecision::Retire;
545        }
546        if self.window != Duration::MAX {
547            self.restarts.retain(|stamp| {
548                event
549                    .at
550                    .checked_duration_since(*stamp)
551                    .is_none_or(|age| age <= self.window)
552            });
553        }
554        let sequence = self.slots[dead].1.sequence;
555        let candidates: Vec<usize> = match self.strategy {
556            Strategy::OneForOne => vec![dead],
557            Strategy::OneForAll => self
558                .slots
559                .iter()
560                .enumerate()
561                .filter_map(|(index, (_, slot))| slot.alive.then_some(index))
562                .collect(),
563            Strategy::RestForOne => self
564                .slots
565                .iter()
566                .enumerate()
567                .filter_map(|(index, (_, slot))| {
568                    (slot.alive && slot.sequence >= sequence).then_some(index)
569                })
570                .collect(),
571        };
572        if self.restarts.len() + candidates.len() > self.max_restarts as usize {
573            self.slots[dead].1.alive = false;
574            return ReplacementDecision::Failed(SupervisionFailure {
575                child: event.proxy,
576                outcome: event.outcome,
577                reason: SupervisionFailureReason::RestartDenied(RestartDenial::BudgetExceeded {
578                    restarts_in_window: self.restarts.len(),
579                    replacements_requested: candidates.len(),
580                    maximum_restarts: self.max_restarts,
581                }),
582            });
583        }
584        self.restarts
585            .resize(self.restarts.len() + candidates.len(), event.at);
586        ReplacementDecision::Replace(
587            candidates
588                .into_iter()
589                .map(|index| {
590                    self.slots[index].1.alive = true;
591                    Delivery::new(
592                        Recipient::child(self.slots[index].0),
593                        ProxyCommand::Replace((self.build)(index)),
594                    )
595                })
596                .collect(),
597        )
598    }
599
600    fn react_to_failure(
601        &mut self,
602        failure: &SupervisionFailure<B::Addr>,
603    ) -> Result<Become<B::Addr, B::Ph>, B::Error> {
604        Ok(match (self.on_failure)(&mut self.inner, failure)? {
605            Step::Continue => Step::Continue,
606            Step::Goto(never) => match never {},
607            Step::Stop(exit) => Step::Stop(exit),
608        })
609    }
610
611    fn wrap(
612        &mut self,
613        actions: Actions<B::Addr, B::Ph, B::Sends, Births<C>>,
614    ) -> SupervisorActions<B, C> {
615        let born: Vec<_> = actions.creates.iter().map(|create| create.nonce).collect();
616        for create in &actions.creates {
617            assert!(
618                self.position(create.nonce).is_none(),
619                "a child birth nonce must be fresh"
620            );
621            self.slots.push((
622                create.nonce,
623                Slot {
624                    alive: true,
625                    sequence: self.next_sequence,
626                },
627            ));
628            self.next_sequence = self
629                .next_sequence
630                .checked_add(1)
631                .expect("birth sequence exhausted");
632        }
633        Actions {
634            sends: SendProduct {
635                inner: actions.sends,
636                own: SendProduct {
637                    inner: ServiceSends::new(
638                        born.into_iter()
639                            .map(|nonce| ObserveChild { nonce })
640                            .collect(),
641                    ),
642                    own: Vec::new(),
643                },
644            },
645            creates: actions
646                .creates
647                .into_iter()
648                .map(|create| Create {
649                    nonce: create.nonce,
650                    child: Proxy::new(create.child),
651                })
652                .collect(),
653            become_: actions.become_,
654        }
655    }
656}
657
658impl<B, C, A, Ph, Sends> Behavior for Supervising<B, C>
659where
660    A: Address + Send,
661    Sends: SendAlgebra,
662    B: Behavior<
663            Addr = A,
664            Ph = Ph,
665            Sends = Sends,
666            Birth = Births<C>,
667            Effect = Actions<A, Ph, Sends, Births<C>>,
668            Done = Exit<A>,
669        > + Send,
670    B::Event: ChildEvent<B::Addr> + Send,
671    A::Nonce: From<u64> + Send,
672    B::Msg: Send,
673    C: Behavior<Ph = Never, Addr = B::Addr> + Send,
674{
675    type Addr = A;
676    type Msg = B::Msg;
677    type Event = SupervisionEvent<B::Event, B::Addr>;
678    type Sends = SupervisorSends<A, Sends, C>;
679    type Ph = Ph;
680    type Error = B::Error;
681    type Birth = Births<Proxy<C>>;
682    type Effect = Actions<A, Ph, Self::Sends, Births<Proxy<C>>>;
683    type Done = Exit<A>;
684
685    async fn init(&mut self) -> Result<Self::Effect, B::Error> {
686        let actions = self.inner.init().await?;
687        let mut actions = self.wrap(actions);
688        actions
689            .creates
690            .extend(self.slots[..self.configured_count].iter().enumerate().map(
691                |(index, (nonce, _))| Create {
692                    nonce: *nonce,
693                    child: Proxy::new((self.build)(index)),
694                },
695            ));
696        actions.sends.own.inner.extend(
697            self.slots[..self.configured_count]
698                .iter()
699                .map(|(nonce, _)| ObserveChild { nonce: *nonce }),
700        );
701        Ok(actions)
702    }
703
704    async fn step(&mut self, event: Self::Event) -> Result<Self::Effect, B::Error> {
705        match event {
706            SupervisionEvent::WorkerStopped(event) => {
707                let decision = self.replacement_decision(&event);
708                match decision {
709                    ReplacementDecision::Retire => Ok(Actions::cont()),
710                    ReplacementDecision::Replace(replacements) => Ok(Actions {
711                        sends: SendProduct {
712                            inner: B::Sends::empty(),
713                            own: SendProduct {
714                                inner: ServiceSends::empty(),
715                                own: replacements,
716                            },
717                        },
718                        creates: Vec::new(),
719                        become_: Step::Continue,
720                    }),
721                    ReplacementDecision::Failed(failure) => {
722                        Ok(Actions::just(self.react_to_failure(&failure)?))
723                    }
724                }
725            }
726            SupervisionEvent::ChildStopped(event) => {
727                let dead = self
728                    .position(event.nonce)
729                    .expect("unknown supervised nonce");
730                self.slots[dead].1.alive = false;
731                let failure = SupervisionFailure {
732                    child: event.nonce,
733                    outcome: event.outcome,
734                    reason: SupervisionFailureReason::StableChildStopped,
735                };
736                Ok(Actions::just(self.react_to_failure(&failure)?))
737            }
738            SupervisionEvent::Inner(event) => {
739                let actions = self.inner.step(event).await?;
740                Ok(self.wrap(actions))
741            }
742        }
743    }
744}