Skip to main content

rust_supervisor/control/
outcome.rs

1//! Serializable child control result types used for generation fencing.
2//!
3//! These placeholder names align with the naming contract and future event payloads:
4//! ChildRestartFenceEntered, ChildRestartFenceAbortRequested, ChildRestartFenceReleased,
5//! ChildRestartConflict, ChildAttemptStaleReport.
6
7use crate::child_runner::run_exit::TaskExit;
8use crate::id::types::{ChildId, ChildStartCount, Generation, SupervisorPath};
9use crate::readiness::signal::ReadinessState;
10use crate::runtime::admission::AdmissionConflict;
11use serde::{Deserialize, Serialize};
12use std::time::Duration;
13use uuid::Uuid;
14
15/// Runtime phase for a child attempt.
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
17#[serde(rename_all = "snake_case")]
18pub enum ChildAttemptStatus {
19    /// The child attempt is starting.
20    Starting,
21    /// The child attempt is running.
22    Running,
23    /// The child attempt reported readiness.
24    Ready,
25    /// The child attempt is cancelling.
26    Cancelling,
27    /// The child attempt has stopped.
28    Stopped,
29}
30
31/// Control operation requested for a child runtime state record.
32#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
33#[serde(rename_all = "snake_case")]
34pub enum ChildControlOperation {
35    /// The child runtime state remains active.
36    Active,
37    /// The child runtime state is paused.
38    Paused,
39    /// The child runtime state is quarantined.
40    Quarantined,
41    /// The child runtime state is waiting for removal or already removed.
42    Removed,
43}
44
45/// Stop progress for child control commands.
46#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
47#[serde(rename_all = "snake_case")]
48pub enum ChildStopState {
49    /// No stop action is in progress.
50    Idle,
51    /// The child currently has no active attempt.
52    NoActiveAttempt,
53    /// Cancellation was delivered to the child.
54    CancelDelivered,
55    /// The child completed stopping.
56    Completed,
57    /// The child failed to stop.
58    Failed,
59}
60
61/// Failure phase for a child control command.
62#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
63#[serde(rename_all = "snake_case")]
64pub enum ChildControlFailurePhase {
65    /// Waiting for child completion failed.
66    WaitCompletion,
67}
68
69/// Structured child control failure.
70#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
71pub struct ChildControlFailure {
72    /// Phase where the failure occurred.
73    pub phase: ChildControlFailurePhase,
74    /// Human-readable failure reason.
75    pub reason: String,
76    /// Whether callers can retry to recover.
77    pub recoverable: bool,
78}
79
80impl ChildControlFailure {
81    /// Creates a child control failure.
82    ///
83    /// # Arguments
84    ///
85    /// - `phase`: Phase where the failure occurred.
86    /// - `reason`: Human-readable failure reason.
87    /// - `recoverable`: Whether callers can retry to recover.
88    ///
89    /// # Returns
90    ///
91    /// Returns a [`ChildControlFailure`] value.
92    pub fn new(
93        phase: ChildControlFailurePhase,
94        reason: impl Into<String>,
95        recoverable: bool,
96    ) -> Self {
97        Self {
98            phase,
99            reason: reason.into(),
100            recoverable,
101        }
102    }
103}
104
105/// Generation fencing phase: whether a new attempt may start at a restart boundary for one child.
106#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
107#[serde(rename_all = "snake_case")]
108pub enum GenerationFencePhase {
109    /// No fence wait, or the active attempt runs on the normal path.
110    #[default]
111    Open,
112    /// Restart accepted: cancellation was delivered to the old attempt; waiting for it to exit.
113    WaitingForOldStop,
114    /// Old attempt exceeded the graceful stop window; runtime requested abort.
115    AbortingOld,
116    /// Old attempt confirmed finished; allowed to start the new instance for the target generation.
117    ReadyToStart,
118    /// Record removed or supervisor tree in a shutdown window; must not proceed with restart start.
119    Closed,
120}
121
122/// Discrete outcome for one restart command at the generation fence from the control plane view.
123#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
124#[serde(rename_all = "snake_case")]
125pub enum GenerationFenceDecision {
126    /// No active attempt; target generation started immediately.
127    StartedImmediately,
128    /// Active attempt still present; restart queued to wait for the old stop.
129    QueuedAfterStop,
130    /// A pending restart already exists; this duplicate command was merged.
131    AlreadyPending,
132    /// Supervisor tree is shutting down; restart is not allowed.
133    BlockedByShutdown,
134    /// Request rejected; set [`GenerationFenceOutcome::conflict`] with the structured reason.
135    Rejected,
136}
137
138/// Label for how the runtime treats a late exit report from an old generation.
139#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
140#[serde(rename_all = "snake_case")]
141pub enum StaleReportHandling {
142    /// Do not change authoritative state; ignore the stale fact.
143    IgnoredForState,
144    /// Recorded for audit so operators can review it later.
145    RecordedForAudit,
146    /// Counted in a low-cardinality metrics bucket.
147    CountedForMetrics,
148}
149
150/// Minimal stale report payload carried with a control command for diagnostics and dashboard projection.
151#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
152pub struct StaleAttemptReport {
153    /// Stable identifier of the child this report belongs to.
154    pub child_id: ChildId,
155    /// Generation attached to the report that is now stale.
156    pub reported_generation: Generation,
157    /// Attempt index attached to the report that is now stale.
158    pub reported_attempt: ChildStartCount,
159    /// Active generation in the record when the report was classified as stale.
160    pub current_generation: Option<Generation>,
161    /// Active attempt in the record when the report was classified as stale.
162    pub current_attempt: Option<ChildStartCount>,
163    /// Exit shape for the old attempt; matches contract `ExitKind`.
164    pub exit_kind: TaskExit,
165    /// Branch the runtime chose when handling this stale report.
166    pub handled_as: StaleReportHandling,
167    /// Unix epoch nanoseconds when the report was classified as stale.
168    pub observed_at_unix_nanos: u128,
169}
170
171impl StaleAttemptReport {
172    /// Builds a stale attempt report record.
173    ///
174    /// # Arguments
175    ///
176    /// - `child_id`: Stable child identifier for this report.
177    /// - `reported_generation`: Stale generation carried from the report.
178    /// - `reported_attempt`: Stale attempt index carried from the report.
179    /// - `current_generation`: Active generation when classified as stale, or `None`.
180    /// - `current_attempt`: Active attempt when classified as stale, or `None`.
181    /// - `exit_kind`: Exit shape for the old attempt; matches contract `ExitKind`.
182    /// - `handled_as`: How the runtime handled this stale report.
183    /// - `observed_at_unix_nanos`: Unix epoch nanoseconds when the report was classified as stale.
184    ///
185    /// # Returns
186    ///
187    /// Returns an owned [`StaleAttemptReport`].
188    ///
189    /// # Examples
190    ///
191    /// ```
192    /// use rust_supervisor::control::outcome::{StaleAttemptReport, StaleReportHandling};
193    /// use rust_supervisor::child_runner::run_exit::TaskExit;
194    /// let report = StaleAttemptReport::new(
195    ///     rust_supervisor::id::types::ChildId::new("worker"),
196    ///     rust_supervisor::id::types::Generation::initial(),
197    ///     rust_supervisor::id::types::ChildStartCount::first(),
198    ///     None,
199    ///     None,
200    ///     TaskExit::Succeeded,
201    ///     StaleReportHandling::IgnoredForState,
202    ///     0,
203    /// );
204    /// assert_eq!(report.handled_as, StaleReportHandling::IgnoredForState);
205    /// ```
206    #[allow(clippy::too_many_arguments)]
207    pub fn new(
208        child_id: ChildId,
209        reported_generation: Generation,
210        reported_attempt: ChildStartCount,
211        current_generation: Option<Generation>,
212        current_attempt: Option<ChildStartCount>,
213        exit_kind: TaskExit,
214        handled_as: StaleReportHandling,
215        observed_at_unix_nanos: u128,
216    ) -> Self {
217        Self {
218            child_id,
219            reported_generation,
220            reported_attempt,
221            current_generation,
222            current_attempt,
223            exit_kind,
224            handled_as,
225            observed_at_unix_nanos,
226        }
227    }
228}
229
230/// Accepted but incomplete restart request; pins the old triple until the old attempt leaves.
231///
232/// `command_id` stores the same UUID bytes as [`crate::control::command::CommandMeta`] to avoid a
233/// module cycle between `command` and `outcome`.
234#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
235pub struct PendingRestart {
236    /// Restart command UUID bound to this request; matches audit `command_id`.
237    pub command_id: Uuid,
238    /// Human-readable restart initiator string.
239    pub requested_by: String,
240    /// Human-readable restart reason string.
241    pub reason: String,
242    /// Old generation that must be pinned when the restart is accepted.
243    pub old_generation: Generation,
244    /// Old attempt index that must be pinned when the restart is accepted.
245    pub old_attempt: ChildStartCount,
246    /// Target generation to start after the old attempt exits.
247    pub target_generation: Generation,
248    /// When the runtime accepted the request, in Unix epoch nanoseconds.
249    pub requested_at_unix_nanos: u128,
250    /// Graceful stop deadline for the old attempt after cancellation, in Unix epoch nanoseconds.
251    pub stop_deadline_at_unix_nanos: u128,
252    /// Whether the runtime has requested abort for the old attempt.
253    pub abort_requested: bool,
254    /// Count of duplicate restart requests merged into this pending request; must not bump generation allocation on merge.
255    pub duplicate_request_count: u32,
256}
257
258impl PendingRestart {
259    /// Creates a pending restart record.
260    ///
261    /// # Arguments
262    ///
263    /// Same meaning as the struct fields documented above.
264    ///
265    /// # Returns
266    ///
267    /// Returns an owned [`PendingRestart`].
268    #[allow(clippy::too_many_arguments)]
269    pub fn new(
270        command_id: Uuid,
271        requested_by: impl Into<String>,
272        reason: impl Into<String>,
273        old_generation: Generation,
274        old_attempt: ChildStartCount,
275        target_generation: Generation,
276        requested_at_unix_nanos: u128,
277        stop_deadline_at_unix_nanos: u128,
278        abort_requested: bool,
279        duplicate_request_count: u32,
280    ) -> Self {
281        Self {
282            command_id,
283            requested_by: requested_by.into(),
284            reason: reason.into(),
285            old_generation,
286            old_attempt,
287            target_generation,
288            requested_at_unix_nanos,
289            stop_deadline_at_unix_nanos,
290            abort_requested,
291            duplicate_request_count,
292        }
293    }
294}
295
296/// Minimal generation fence outcome bundled with one restart command.
297#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
298pub struct GenerationFenceOutcome {
299    /// Discrete fencing decision for this command.
300    pub decision: GenerationFenceDecision,
301    /// Old generation pinned or observed by this command, or `None`.
302    pub old_generation: Option<Generation>,
303    /// Old attempt pinned or observed by this command, or `None`.
304    pub old_attempt: Option<ChildStartCount>,
305    /// Generation planned to start after the old attempt exits.
306    pub target_generation: Option<Generation>,
307    /// Whether this command newly delivered cancellation to an active attempt.
308    pub cancel_delivered: bool,
309    /// Whether this command triggered or escalated abort semantics for the old attempt.
310    pub abort_requested: bool,
311    /// Structured failure when rejected or in conflict; required when `decision` is [`GenerationFenceDecision::Rejected`].
312    pub conflict: Option<ChildControlFailure>,
313}
314
315impl GenerationFenceOutcome {
316    /// Builds a minimal generation fence outcome.
317    ///
318    /// # Arguments
319    ///
320    /// - `decision`: Fencing decision for this command.
321    /// - `old_generation`: Recorded old generation at decision time, or `None`.
322    /// - `old_attempt`: Recorded old attempt at decision time, or `None`.
323    /// - `target_generation`: Planned next generation after the old attempt exits, or `None`.
324    /// - `cancel_delivered`: Whether cancellation was newly delivered.
325    /// - `abort_requested`: Whether abort was requested.
326    /// - `conflict`: Optional structured rejection or conflict payload.
327    ///
328    /// # Returns
329    ///
330    /// Returns a populated [`GenerationFenceOutcome`].
331    pub fn new(
332        decision: GenerationFenceDecision,
333        old_generation: Option<Generation>,
334        old_attempt: Option<ChildStartCount>,
335        target_generation: Option<Generation>,
336        cancel_delivered: bool,
337        abort_requested: bool,
338        conflict: Option<ChildControlFailure>,
339    ) -> Self {
340        Self {
341            decision,
342            old_generation,
343            old_attempt,
344            target_generation,
345            cancel_delivered,
346            abort_requested,
347            conflict,
348        }
349    }
350}
351
352/// Generation fence bookkeeping on the runtime side; no timeline, only whether start is allowed.
353#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
354pub struct GenerationFenceState {
355    /// Current discrete fencing phase.
356    pub phase: GenerationFencePhase,
357    /// Active attempt generation from this record perspective, if any.
358    pub active_generation: Option<Generation>,
359    /// Active attempt index from this record perspective, if any.
360    pub active_attempt: Option<ChildStartCount>,
361    /// When `Some`, a pending restart is waiting for the old attempt to exit.
362    pub pending_restart: Option<PendingRestart>,
363    /// Latest recorded stale exit report for diagnostics replay.
364    pub last_stale_report: Option<StaleAttemptReport>,
365}
366
367impl Default for GenerationFenceState {
368    /// Default placeholder: open phase with no pending restart.
369    fn default() -> Self {
370        Self {
371            phase: GenerationFencePhase::Open,
372            active_generation: None,
373            active_attempt: None,
374            pending_restart: None,
375            last_stale_report: None,
376        }
377    }
378}
379
380impl GenerationFenceState {
381    /// Creates a placeholder fence state record.
382    ///
383    /// # Arguments
384    ///
385    /// None.
386    ///
387    /// # Returns
388    ///
389    /// Returns the default record with phase [`GenerationFencePhase::Open`].
390    pub fn placeholder() -> Self {
391        Self::default()
392    }
393}
394
395/// Pending-restart triple summary shared with [`ChildRuntimeRecord`] and dashboards.
396#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
397pub struct PendingRestartSummary {
398    /// Pinned or observed old generation.
399    pub old_generation: Generation,
400    /// Pinned or observed old attempt index.
401    pub old_attempt: ChildStartCount,
402    /// Target generation expected after the old attempt exits.
403    pub target_generation: Generation,
404}
405
406impl From<&PendingRestart> for PendingRestartSummary {
407    /// Compresses a full [`PendingRestart`] into a dashboard-friendly summary.
408    fn from(source: &PendingRestart) -> Self {
409        Self {
410            old_generation: source.old_generation,
411            old_attempt: source.old_attempt,
412            target_generation: source.target_generation,
413        }
414    }
415}
416
417/// Runtime restart limit state.
418#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
419pub struct RestartLimitState {
420    /// Restart accounting window.
421    pub window: Duration,
422    /// Restart limit inside the window.
423    pub limit: u32,
424    /// Restart count used so far.
425    pub used: u32,
426    /// Remaining restart count.
427    pub remaining: u32,
428    /// Whether the restart limit is exhausted.
429    pub exhausted: bool,
430    /// Last update timestamp in Unix epoch nanoseconds.
431    pub updated_at_unix_nanos: u128,
432}
433
434impl RestartLimitState {
435    /// Creates a restart limit state.
436    ///
437    /// # Arguments
438    ///
439    /// - `window`: Restart accounting window.
440    /// - `limit`: Restart limit inside the window.
441    /// - `used`: Restart count used so far.
442    /// - `updated_at_unix_nanos`: Last update timestamp.
443    ///
444    /// # Returns
445    ///
446    /// Returns a [`RestartLimitState`] value.
447    pub fn new(window: Duration, limit: u32, used: u32, updated_at_unix_nanos: u128) -> Self {
448        let remaining = limit.saturating_sub(used);
449        Self {
450            window,
451            limit,
452            used,
453            remaining,
454            exhausted: remaining == 0,
455            updated_at_unix_nanos,
456        }
457    }
458}
459
460impl Default for RestartLimitState {
461    /// Creates the default restart limit state.
462    fn default() -> Self {
463        Self::new(Duration::from_secs(60), u32::MAX, 0, 0)
464    }
465}
466
467/// Liveness state for one child.
468#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
469pub struct ChildLivenessState {
470    /// Last heartbeat timestamp in Unix epoch nanoseconds.
471    pub last_heartbeat_at_unix_nanos: Option<u128>,
472    /// Whether heartbeat is stale.
473    pub heartbeat_stale: bool,
474    /// Latest readiness state.
475    pub readiness: ReadinessState,
476}
477
478impl ChildLivenessState {
479    /// Creates a child liveness state.
480    ///
481    /// # Arguments
482    ///
483    /// - `last_heartbeat_at_unix_nanos`: Last heartbeat timestamp.
484    /// - `heartbeat_stale`: Whether heartbeat is stale.
485    /// - `readiness`: Latest readiness state.
486    ///
487    /// # Returns
488    ///
489    /// Returns a [`ChildLivenessState`] value.
490    pub fn new(
491        last_heartbeat_at_unix_nanos: Option<u128>,
492        heartbeat_stale: bool,
493        readiness: ReadinessState,
494    ) -> Self {
495        Self {
496            last_heartbeat_at_unix_nanos,
497            heartbeat_stale,
498            readiness,
499        }
500    }
501}
502
503/// Public projection of one child runtime state record.
504#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
505pub struct ChildRuntimeRecord {
506    /// Stable child identifier.
507    pub child_id: ChildId,
508    /// Child path in the supervisor tree.
509    pub path: SupervisorPath,
510    /// Current active generation.
511    pub generation: Option<Generation>,
512    /// Current active attempt.
513    pub attempt: Option<ChildStartCount>,
514    /// Current attempt status.
515    pub status: Option<ChildAttemptStatus>,
516    /// Current control operation.
517    pub operation: ChildControlOperation,
518    /// Current liveness state.
519    pub liveness: ChildLivenessState,
520    /// Current restart limit state.
521    pub restart_limit: RestartLimitState,
522    /// Current stop progress.
523    pub stop_state: ChildStopState,
524    /// Most recent control failure.
525    pub failure: Option<ChildControlFailure>,
526    /// Generation fence phase returned with the `CurrentState` projection.
527    #[serde(default)]
528    pub generation_fence_phase: GenerationFencePhase,
529    /// Pending restart triple; present only while the fence queue still waits for the old attempt to exit.
530    #[serde(default)]
531    pub pending_restart: Option<PendingRestartSummary>,
532}
533
534impl ChildRuntimeRecord {
535    /// Creates a public child runtime record.
536    ///
537    /// # Arguments
538    ///
539    /// - `child_id`: Stable child identifier.
540    /// - `path`: Child path in the supervisor tree.
541    /// - `generation`: Current active generation.
542    /// - `attempt`: Current active attempt.
543    /// - `status`: Current attempt status.
544    /// - `operation`: Current control operation.
545    /// - `liveness`: Current liveness state.
546    /// - `restart_limit`: Current restart limit state.
547    /// - `stop_state`: Current stop progress.
548    /// - `failure`: Most recent control failure.
549    /// - `generation_fence_phase`: Projection of generation fencing phase enum.
550    /// - `pending_restart`: Optional queued restart fingerprint for dashboards.
551    ///
552    /// # Returns
553    ///
554    /// Returns a [`ChildRuntimeRecord`] value.
555    #[allow(clippy::too_many_arguments)]
556    pub fn new(
557        child_id: ChildId,
558        path: SupervisorPath,
559        generation: Option<Generation>,
560        attempt: Option<ChildStartCount>,
561        status: Option<ChildAttemptStatus>,
562        operation: ChildControlOperation,
563        liveness: ChildLivenessState,
564        restart_limit: RestartLimitState,
565        stop_state: ChildStopState,
566        failure: Option<ChildControlFailure>,
567        generation_fence_phase: GenerationFencePhase,
568        pending_restart: Option<PendingRestartSummary>,
569    ) -> Self {
570        Self {
571            child_id,
572            path,
573            generation,
574            attempt,
575            status,
576            operation,
577            liveness,
578            restart_limit,
579            stop_state,
580            failure,
581            generation_fence_phase,
582            pending_restart,
583        }
584    }
585}
586
587/// Result returned by a child control command.
588#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
589pub struct ChildControlResult {
590    /// Stable child identifier.
591    pub child_id: ChildId,
592    /// Active attempt targeted by the command.
593    pub attempt: Option<ChildStartCount>,
594    /// Active generation targeted by the command.
595    pub generation: Option<Generation>,
596    /// Control operation before command handling.
597    pub operation_before: ChildControlOperation,
598    /// Control operation after command handling.
599    pub operation_after: ChildControlOperation,
600    /// Current attempt status.
601    pub status: Option<ChildAttemptStatus>,
602    /// Whether this command delivered cancellation.
603    pub cancel_delivered: bool,
604    /// Stop progress after command handling.
605    pub stop_state: ChildStopState,
606    /// Current restart limit state.
607    pub restart_limit: RestartLimitState,
608    /// Current liveness state.
609    pub liveness: ChildLivenessState,
610    /// Whether this command reused existing state idempotently.
611    pub idempotent: bool,
612    /// Current failure reason.
613    pub failure: Option<ChildControlFailure>,
614    /// Optional generation fencing outcome exclusively used by restart control commands.
615    #[serde(default)]
616    pub generation_fence: Option<GenerationFenceOutcome>,
617    /// Admission conflict detail when a concurrent request was rejected.
618    #[serde(default)]
619    pub admission_conflict: Option<AdmissionConflict>,
620}
621
622impl ChildControlResult {
623    /// Creates a child control result.
624    ///
625    /// # Arguments
626    ///
627    /// - `child_id`: Stable child identifier.
628    /// - `attempt`: Active attempt targeted by the command.
629    /// - `generation`: Active generation targeted by the command.
630    /// - `operation_before`: Control operation before command handling.
631    /// - `operation_after`: Control operation after command handling.
632    /// - `status`: Current attempt status.
633    /// - `cancel_delivered`: Whether this command delivered cancellation.
634    /// - `stop_state`: Stop progress after command handling.
635    /// - `restart_limit`: Current restart limit state.
636    /// - `liveness`: Current liveness state.
637    /// - `idempotent`: Whether this command reused existing state idempotently.
638    /// - `failure`: Current failure reason.
639    /// - `generation_fence`: Optional restart-only fencing outcome payload.
640    ///
641    /// # Returns
642    ///
643    /// Returns a [`ChildControlResult`] value.
644    #[allow(clippy::too_many_arguments)]
645    pub fn new(
646        child_id: ChildId,
647        attempt: Option<ChildStartCount>,
648        generation: Option<Generation>,
649        operation_before: ChildControlOperation,
650        operation_after: ChildControlOperation,
651        status: Option<ChildAttemptStatus>,
652        cancel_delivered: bool,
653        stop_state: ChildStopState,
654        restart_limit: RestartLimitState,
655        liveness: ChildLivenessState,
656        idempotent: bool,
657        failure: Option<ChildControlFailure>,
658        generation_fence: Option<GenerationFenceOutcome>,
659    ) -> Self {
660        Self {
661            child_id,
662            attempt,
663            generation,
664            operation_before,
665            operation_after,
666            status,
667            cancel_delivered,
668            stop_state,
669            restart_limit,
670            liveness,
671            idempotent,
672            failure,
673            generation_fence,
674            admission_conflict: None,
675        }
676    }
677
678    /// Creates a conflict result when admission is denied.
679    ///
680    /// # Arguments
681    ///
682    /// - `child_id`: Child that already has an active attempt.
683    /// - `conflict`: Admission conflict detail.
684    ///
685    /// # Returns
686    ///
687    /// Returns a [`ChildControlResult`] carrying the conflict.
688    pub fn conflict(child_id: ChildId, conflict: AdmissionConflict) -> Self {
689        Self {
690            child_id,
691            attempt: Some(conflict.active_attempt),
692            generation: Some(conflict.active_generation),
693            operation_before: ChildControlOperation::Active,
694            operation_after: ChildControlOperation::Active,
695            status: Some(ChildAttemptStatus::Running),
696            cancel_delivered: false,
697            stop_state: ChildStopState::Idle,
698            restart_limit: RestartLimitState::default(),
699            liveness: ChildLivenessState::new(None, false, ReadinessState::Unreported),
700            idempotent: false,
701            failure: None,
702            generation_fence: None,
703            admission_conflict: Some(conflict),
704        }
705    }
706}