Skip to main content

behavior_actors/atomic/fixed_supervisor/
diagnostic.rs

1//! Owned operational diagnostics emitted by one fixed supervisor.
2
3use behavior::{
4    ActionItemResult, Behavior, BehaviorAddr, EndpointAddress, InterpreterFault, Protocol,
5};
6
7use crate::{
8    ActivationPlan, ChildStopped, ProxyOperation, ProxyOutcome, ProxyPhase, ScheduleAfter,
9    ScheduleAfterRejection, StableProxy, WorkerSubmission,
10};
11
12use behavior::ChildInputReason;
13
14use super::super::{PreparedWorker, RoleName, WorkerSource};
15use super::restart::RecoveryDenialReason;
16use super::{FixedSupervisorEvent, PrepareWorkers, WorkerPreparation};
17
18/// One operational diagnostic emitted by a fixed supervisor.
19pub enum FixedDiagnostic<Role, Worker, Plan, Source>
20where
21    Role: Send + Sync,
22    Worker: Behavior + Send,
23    Plan: ActivationPlan,
24    Source: WorkerSource<Role, Worker, Plan>,
25    BehaviorAddr<Worker>: EndpointAddress,
26    StableProxy<Worker, Plan>: Behavior<Protocol = Worker::Protocol>,
27{
28    /// The exact initial StableProxy operation could not make its worker ready.
29    ProxyOutcomeFailed(ProxyOutcomeFailure<Role, Worker, Plan>),
30    /// An exact replacement input was rejected and returned complete.
31    ProxyInputRejected(ProxyInputFailure<Role, Worker, Plan>),
32    /// An operating recovery could not prepare every selected worker.
33    WorkerPreparationFailed(
34        WorkerPreparationFailure<
35            Role,
36            Worker,
37            Plan,
38            Source::WorkerRejection,
39            Source::SourceRejection,
40        >,
41    ),
42    /// An otherwise prepared recovery was denied by restart policy.
43    RecoveryDenied(RecoveryDenied<Role, Worker>),
44    /// A delayed recovery's exact timer request was rejected.
45    RestartScheduleFailed(RestartScheduleFailure<Role>),
46    /// A service command reached an exact proxy without a ready worker.
47    WorkerUnavailable(WorkerUnavailable<Role, Worker>),
48    /// One complete typed input did not belong to the current supervisor state.
49    UnexpectedInput {
50        /// Complete unexpected input, unchanged.
51        input: FixedSupervisorEvent<
52            Role,
53            Worker,
54            Plan,
55            ActionItemResult<PrepareWorkers<Source, Role, Worker, Plan>>,
56            WorkerPreparation<Source, Role, Worker, Plan>,
57        >,
58    },
59}
60
61/// Complete service command returned by one exact unavailable StableProxy.
62pub struct WorkerUnavailable<Role, Worker>
63where
64    Worker: Behavior,
65{
66    role: RoleName<Role>,
67    sender: BehaviorAddr<Worker>,
68    phase: ProxyPhase,
69    command: <Worker::Protocol as Protocol>::Msg,
70}
71
72impl<Role, Worker> WorkerUnavailable<Role, Worker>
73where
74    Worker: Behavior,
75{
76    pub(super) const fn new(
77        role: RoleName<Role>,
78        sender: BehaviorAddr<Worker>,
79        phase: ProxyPhase,
80        command: <Worker::Protocol as Protocol>::Msg,
81    ) -> Self {
82        Self {
83            role,
84            sender,
85            phase,
86            command,
87        }
88    }
89
90    pub(super) fn into_parts(
91        self,
92    ) -> (
93        RoleName<Role>,
94        BehaviorAddr<Worker>,
95        ProxyPhase,
96        <Worker::Protocol as Protocol>::Msg,
97    ) {
98        (self.role, self.sender, self.phase, self.command)
99    }
100
101    /// Inspect the semantic role whose worker was unavailable.
102    #[must_use]
103    pub fn role(&self) -> &Role {
104        self.role.role()
105    }
106
107    /// Inspect the original command sender.
108    #[must_use]
109    pub const fn sender(&self) -> &BehaviorAddr<Worker> {
110        &self.sender
111    }
112
113    /// Inspect the exact StableProxy phase that rejected the command.
114    #[must_use]
115    pub const fn phase(&self) -> ProxyPhase {
116        self.phase
117    }
118
119    /// Inspect the complete returned service command.
120    #[must_use]
121    pub const fn command(&self) -> &<Worker::Protocol as Protocol>::Msg {
122        &self.command
123    }
124}
125
126/// Complete replacement input returned by a rejecting StableProxy capability.
127pub struct ProxyInputFailure<Role, Worker, Plan>
128where
129    Worker: Behavior,
130    Plan: ActivationPlan,
131    BehaviorAddr<Worker>: EndpointAddress,
132    StableProxy<Worker, Plan>: Behavior<Protocol = Worker::Protocol>,
133{
134    role: RoleName<Role>,
135    operation: ProxyOperation<behavior::Here, Worker, Plan>,
136    reason: ChildInputReason,
137}
138
139impl<Role, Worker, Plan> ProxyInputFailure<Role, Worker, Plan>
140where
141    Worker: Behavior,
142    Plan: ActivationPlan,
143    BehaviorAddr<Worker>: EndpointAddress,
144    StableProxy<Worker, Plan>: Behavior<Protocol = Worker::Protocol>,
145{
146    pub(super) const fn new(
147        role: RoleName<Role>,
148        operation: ProxyOperation<behavior::Here, Worker, Plan>,
149        reason: ChildInputReason,
150    ) -> Self {
151        Self {
152            role,
153            operation,
154            reason,
155        }
156    }
157
158    /// Inspect the semantic role whose replacement input was rejected.
159    #[must_use]
160    pub fn role(&self) -> &Role {
161        self.role.role()
162    }
163
164    /// Inspect the complete returned replacement operation.
165    #[must_use]
166    pub const fn operation(&self) -> &ProxyOperation<behavior::Here, Worker, Plan> {
167        &self.operation
168    }
169
170    /// Inspect the exact capability rejection.
171    #[must_use]
172    pub const fn reason(&self) -> ChildInputReason {
173        self.reason
174    }
175}
176
177/// Exact rejected timer request for one delayed recovery.
178pub struct RestartScheduleFailure<Role> {
179    trigger: RoleName<Role>,
180    request: ScheduleAfter,
181    reason: ScheduleAfterRejection,
182}
183
184impl<Role> RestartScheduleFailure<Role> {
185    pub(super) const fn new(
186        trigger: RoleName<Role>,
187        request: ScheduleAfter,
188        reason: ScheduleAfterRejection,
189    ) -> Self {
190        Self {
191            trigger,
192            request,
193            reason,
194        }
195    }
196
197    /// Inspect the recovery-triggering semantic role.
198    #[must_use]
199    pub fn role(&self) -> &Role {
200        self.trigger.role()
201    }
202
203    /// Recover the exact rejected timer request.
204    #[must_use]
205    pub const fn request(&self) -> ScheduleAfter {
206        self.request
207    }
208
209    /// Inspect the exact timer rejection.
210    #[must_use]
211    pub const fn reason(&self) -> ScheduleAfterRejection {
212        self.reason
213    }
214}
215
216/// Complete restart-policy denial emitted by a fixed supervisor.
217///
218/// The triggering role and complete worker stop are borrowed through
219/// [`RecoveryDenied::role`] and [`RecoveryDenied::stopped`]. Restart history,
220/// the worker source, and roster ownership remain private to the supervisor.
221pub struct RecoveryDenied<Role, Worker>
222where
223    Worker: Behavior,
224{
225    trigger: RoleName<Role>,
226    stopped: ChildStopped<BehaviorAddr<Worker>>,
227    reason: RecoveryDenialReason,
228}
229
230impl<Role, Worker> RecoveryDenied<Role, Worker>
231where
232    Worker: Behavior,
233{
234    pub(super) const fn new(
235        trigger: RoleName<Role>,
236        stopped: ChildStopped<BehaviorAddr<Worker>>,
237        reason: RecoveryDenialReason,
238    ) -> Self {
239        Self {
240            trigger,
241            stopped,
242            reason,
243        }
244    }
245
246    /// Inspect the recovery-triggering semantic role.
247    #[must_use]
248    pub fn role(&self) -> &Role {
249        self.trigger.role()
250    }
251
252    /// Inspect the complete worker stop that triggered the denied recovery.
253    #[must_use]
254    pub const fn stopped(&self) -> &ChildStopped<BehaviorAddr<Worker>> {
255        &self.stopped
256    }
257
258    /// Inspect the exact denial without exposing private restart history.
259    #[must_use]
260    pub const fn reason(&self) -> &RecoveryDenialReason {
261        &self.reason
262    }
263}
264
265enum PreparationFailureState<Role, Worker, Plan, WorkerRejection, SourceRejection> {
266    WorkerRejected {
267        prepared: Vec<PreparedWorker<RoleName<Role>, Worker, Plan>>,
268        failed_role: RoleName<Role>,
269        reason: WorkerRejection,
270        remaining: Vec<RoleName<Role>>,
271    },
272    SourceRejected {
273        reason: SourceRejection,
274        remaining: Vec<RoleName<Role>>,
275    },
276    InterpreterFault {
277        fault: InterpreterFault,
278        remaining: Vec<RoleName<Role>>,
279    },
280    Unattempted(Vec<RoleName<Role>>),
281}
282
283/// Complete operating worker-preparation failure emitted by a fixed supervisor.
284///
285/// The worker source is not stored here. It returns separately to the
286/// supervisor's recovery policy. Semantic roles are exposed by reference so
287/// they need not implement `Clone`.
288pub struct WorkerPreparationFailure<Role, Worker, Plan, WorkerRejection, SourceRejection> {
289    trigger: RoleName<Role>,
290    state: PreparationFailureState<Role, Worker, Plan, WorkerRejection, SourceRejection>,
291}
292
293/// Exact borrowed reason for one operating worker-preparation failure.
294pub enum WorkerPreparationFailureReason<'a, Role, WorkerRejection, SourceRejection> {
295    /// The source accepted the request but rejected one selected worker.
296    WorkerRejected {
297        /// Exact semantic role whose worker was rejected.
298        role: &'a Role,
299        /// Source-selected worker rejection.
300        reason: &'a WorkerRejection,
301    },
302    /// The source rejected the complete request before preparing a worker.
303    SourceRejected(&'a SourceRejection),
304    /// The interpreter returned the request with controlled corruption evidence.
305    InterpreterFault(InterpreterFault),
306    /// Product interpretation stopped before attempting the request.
307    Unattempted,
308}
309
310impl<Role, Worker, Plan, WorkerRejection, SourceRejection>
311    WorkerPreparationFailure<Role, Worker, Plan, WorkerRejection, SourceRejection>
312{
313    pub(super) fn worker_rejected(
314        trigger: RoleName<Role>,
315        prepared: Vec<PreparedWorker<RoleName<Role>, Worker, Plan>>,
316        failed_role: RoleName<Role>,
317        reason: WorkerRejection,
318        remaining: Vec<RoleName<Role>>,
319    ) -> Self {
320        Self {
321            trigger,
322            state: PreparationFailureState::WorkerRejected {
323                prepared,
324                failed_role,
325                reason,
326                remaining,
327            },
328        }
329    }
330
331    pub(super) fn source_rejected(
332        trigger: RoleName<Role>,
333        reason: SourceRejection,
334        remaining: Vec<RoleName<Role>>,
335    ) -> Self {
336        Self {
337            trigger,
338            state: PreparationFailureState::SourceRejected { reason, remaining },
339        }
340    }
341
342    pub(super) fn interpreter_fault(
343        trigger: RoleName<Role>,
344        fault: InterpreterFault,
345        remaining: Vec<RoleName<Role>>,
346    ) -> Self {
347        Self {
348            trigger,
349            state: PreparationFailureState::InterpreterFault { fault, remaining },
350        }
351    }
352
353    pub(super) fn unattempted(trigger: RoleName<Role>, remaining: Vec<RoleName<Role>>) -> Self {
354        Self {
355            trigger,
356            state: PreparationFailureState::Unattempted(remaining),
357        }
358    }
359
360    /// Inspect the recovery-triggering semantic role.
361    #[must_use]
362    pub fn role(&self) -> &Role {
363        self.trigger.role()
364    }
365
366    /// Inspect the exact failure alternative without exposing stored role names.
367    #[must_use]
368    pub fn reason(
369        &self,
370    ) -> WorkerPreparationFailureReason<'_, Role, WorkerRejection, SourceRejection> {
371        match &self.state {
372            PreparationFailureState::WorkerRejected {
373                failed_role,
374                reason,
375                ..
376            } => WorkerPreparationFailureReason::WorkerRejected {
377                role: failed_role.role(),
378                reason,
379            },
380            PreparationFailureState::SourceRejected { reason, .. } => {
381                WorkerPreparationFailureReason::SourceRejected(reason)
382            }
383            PreparationFailureState::InterpreterFault { fault, .. } => {
384                WorkerPreparationFailureReason::InterpreterFault(*fault)
385            }
386            PreparationFailureState::Unattempted(_) => WorkerPreparationFailureReason::Unattempted,
387        }
388    }
389
390    /// Inspect every worker prepared before the failure in selection order.
391    pub fn prepared(
392        &self,
393    ) -> impl ExactSizeIterator<Item = (&Role, &WorkerSubmission<Worker, Plan>)> {
394        let prepared = match &self.state {
395            PreparationFailureState::WorkerRejected { prepared, .. } => prepared.as_slice(),
396            PreparationFailureState::SourceRejected { .. }
397            | PreparationFailureState::InterpreterFault { .. }
398            | PreparationFailureState::Unattempted(_) => &[],
399        };
400        prepared
401            .iter()
402            .map(|prepared| (prepared.role.role(), &prepared.submission))
403    }
404
405    /// Inspect every selected role left untouched after the failure.
406    pub fn remaining_roles(&self) -> impl ExactSizeIterator<Item = &Role> {
407        let remaining = match &self.state {
408            PreparationFailureState::WorkerRejected { remaining, .. }
409            | PreparationFailureState::SourceRejected { remaining, .. }
410            | PreparationFailureState::InterpreterFault { remaining, .. } => remaining,
411            PreparationFailureState::Unattempted(remaining) => remaining,
412        };
413        remaining.iter().map(RoleName::role)
414    }
415}
416
417/// Complete role and proxy outcome retained after initial startup failed.
418pub struct ProxyOutcomeFailure<Role, Worker, Plan>
419where
420    Worker: Behavior,
421    Plan: ActivationPlan,
422    BehaviorAddr<Worker>: EndpointAddress,
423    StableProxy<Worker, Plan>: Behavior<Protocol = Worker::Protocol>,
424{
425    role: RoleName<Role>,
426    outcome: ProxyOutcome<Worker, Plan>,
427}
428
429impl<Role, Worker, Plan> ProxyOutcomeFailure<Role, Worker, Plan>
430where
431    Worker: Behavior,
432    Plan: ActivationPlan,
433    BehaviorAddr<Worker>: EndpointAddress,
434    StableProxy<Worker, Plan>: Behavior<Protocol = Worker::Protocol>,
435{
436    pub(super) const fn new(role: RoleName<Role>, outcome: ProxyOutcome<Worker, Plan>) -> Self {
437        Self { role, outcome }
438    }
439
440    /// Inspect the semantic role without exposing its private shared storage.
441    #[must_use]
442    pub fn role(&self) -> &Role {
443        self.role.role()
444    }
445
446    /// Inspect the complete non-ready proxy outcome.
447    #[must_use]
448    pub const fn outcome(&self) -> &ProxyOutcome<Worker, Plan> {
449        &self.outcome
450    }
451}
452
453#[cfg(test)]
454mod tests {
455    use super::{WorkerPreparationFailure, WorkerPreparationFailureReason};
456    use crate::WorkerSubmission;
457    use crate::atomic::{PreparedWorker, RoleName};
458    use behavior::Never;
459
460    fn submission(worker: u8) -> WorkerSubmission<u8, u16> {
461        WorkerSubmission::activated(worker, u16::from(worker))
462    }
463
464    #[test]
465    fn worker_rejection_preserves_prepared_order_and_untouched_roles() {
466        let trigger = RoleName::new(1_u8);
467        let failure: WorkerPreparationFailure<u8, u8, u16, u32, Never> =
468            WorkerPreparationFailure::worker_rejected(
469                trigger.clone(),
470                vec![
471                    PreparedWorker {
472                        role: RoleName::new(2),
473                        submission: submission(31),
474                    },
475                    PreparedWorker {
476                        role: RoleName::new(3),
477                        submission: submission(37),
478                    },
479                ],
480                RoleName::new(4),
481                41,
482                vec![RoleName::new(5)],
483            );
484
485        assert_eq!(failure.role(), trigger.role());
486        let prepared: Vec<_> = failure.prepared().collect();
487        assert_eq!(prepared.len(), 2);
488        assert_eq!(prepared[0], (&2, &submission(31)));
489        assert_eq!(prepared[1], (&3, &submission(37)));
490        let WorkerPreparationFailureReason::WorkerRejected { role, reason } = failure.reason()
491        else {
492            panic!("worker rejection retains its role and reason");
493        };
494        assert_eq!(role, &4);
495        assert_eq!(reason, &41);
496        assert_eq!(failure.remaining_roles().collect::<Vec<_>>(), [&5]);
497    }
498}