Skip to main content

oxide_batch/
listener.rs

1//! Job and step listener contracts with redacted failure diagnostics.
2
3use std::error::Error;
4use std::fmt;
5
6use crate::{
7    BoxFuture, ExecutionCorrelation, FailureSummary, JobParameters, StopToken,
8    TaskletExecutionOutcome,
9};
10
11/// Borrowed execution data supplied to job and step listeners.
12#[derive(Clone, Copy, Debug)]
13pub struct ListenerContext<'a> {
14    correlation: &'a ExecutionCorrelation,
15    parameters: &'a JobParameters,
16    stop: &'a StopToken,
17}
18
19impl<'a> ListenerContext<'a> {
20    pub(crate) const fn new(
21        correlation: &'a ExecutionCorrelation,
22        parameters: &'a JobParameters,
23        stop: &'a StopToken,
24    ) -> Self {
25        Self {
26            correlation,
27            parameters,
28            stop,
29        }
30    }
31
32    /// Borrows the complete bounded execution correlation.
33    #[must_use]
34    pub const fn correlation(&self) -> &'a ExecutionCorrelation {
35        self.correlation
36    }
37
38    /// Borrows launch parameters for authorized application use.
39    ///
40    /// The framework never copies these values into listener diagnostics.
41    #[must_use]
42    pub const fn parameters(&self) -> &'a JobParameters {
43        self.parameters
44    }
45
46    /// Borrows the cooperative stop token.
47    #[must_use]
48    pub const fn stop_token(&self) -> &'a StopToken {
49        self.stop
50    }
51}
52
53/// A dynamically dispatched job lifecycle listener.
54pub trait JobExecutionListener: Send + Sync {
55    /// Runs before the job becomes `STARTED`.
56    fn before_job<'a>(
57        &'a self,
58        context: ListenerContext<'a>,
59    ) -> BoxFuture<'a, Result<(), ListenerError>>;
60
61    /// Runs after the nested step has a provisional outcome and before the job
62    /// receives its final status.
63    fn after_job<'a>(
64        &'a self,
65        context: ListenerContext<'a>,
66        outcome: TaskletExecutionOutcome,
67    ) -> BoxFuture<'a, Result<(), ListenerError>>;
68}
69
70/// A dynamically dispatched step lifecycle listener.
71pub trait StepExecutionListener: Send + Sync {
72    /// Runs before the step becomes `STARTED`.
73    fn before_step<'a>(
74        &'a self,
75        context: ListenerContext<'a>,
76    ) -> BoxFuture<'a, Result<(), ListenerError>>;
77
78    /// Runs after tasklet work has a provisional outcome and before the step
79    /// receives its final status.
80    fn after_step<'a>(
81        &'a self,
82        context: ListenerContext<'a>,
83        outcome: TaskletExecutionOutcome,
84    ) -> BoxFuture<'a, Result<(), ListenerError>>;
85}
86
87/// A value-redacted listener failure.
88#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
89pub struct ListenerError;
90
91impl ListenerError {
92    /// Constructs a classified listener failure.
93    #[must_use]
94    pub const fn new() -> Self {
95        Self
96    }
97
98    /// Classifies an arbitrary user error without retaining its payload.
99    #[must_use]
100    pub fn from_error(error: impl Error + Send + Sync + 'static) -> Self {
101        drop(error);
102        Self
103    }
104}
105
106impl fmt::Display for ListenerError {
107    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
108        formatter.write_str("the execution listener failed")
109    }
110}
111
112impl Error for ListenerError {}
113
114/// The listener callback boundary where a failure occurred.
115#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
116#[non_exhaustive]
117pub enum ListenerPhase {
118    /// A job before-listener.
119    BeforeJob,
120    /// A step before-listener.
121    BeforeStep,
122    /// A step after-listener.
123    AfterStep,
124    /// A job after-listener.
125    AfterJob,
126}
127
128/// Stable classification of a listener boundary failure.
129#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
130#[non_exhaustive]
131pub enum ListenerFailureKind {
132    /// The listener returned [`ListenerError`].
133    Error,
134    /// The listener panicked before or while its future was polled.
135    Panic,
136}
137
138/// One value-redacted listener failure retained by a launch report.
139#[derive(Clone, Copy, Debug, Eq, PartialEq)]
140pub struct ListenerFailure {
141    phase: ListenerPhase,
142    registration_index: usize,
143    kind: ListenerFailureKind,
144    summary: FailureSummary,
145}
146
147impl ListenerFailure {
148    pub(crate) const fn new(
149        phase: ListenerPhase,
150        registration_index: usize,
151        kind: ListenerFailureKind,
152        summary: FailureSummary,
153    ) -> Self {
154        Self {
155            phase,
156            registration_index,
157            kind,
158            summary,
159        }
160    }
161
162    /// Returns the callback phase.
163    #[must_use]
164    pub const fn phase(self) -> ListenerPhase {
165        self.phase
166    }
167
168    /// Returns the zero-based listener registration index.
169    #[must_use]
170    pub const fn registration_index(self) -> usize {
171        self.registration_index
172    }
173
174    /// Returns whether the boundary returned an error or panicked.
175    #[must_use]
176    pub const fn kind(self) -> ListenerFailureKind {
177        self.kind
178    }
179
180    /// Returns the redacted failure category and opaque ID.
181    #[must_use]
182    pub const fn summary(self) -> FailureSummary {
183        self.summary
184    }
185}