1use std::{
2 fmt,
3 sync::Arc,
4 time::{Duration, Instant, SystemTime},
5};
6
7use datum::{
8 Flow, KillSwitches, NotUsed, RestartSettings, RunnableGraph, SharedKillSwitch,
9 StreamCompletion, StreamError, StreamInstrumentationRegistry, StreamResult,
10};
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
14pub struct JobId(pub u64);
15
16pub type JobGraphFactory =
22 dyn Fn(JobContext) -> StreamResult<RunnableGraph<JobMat>> + Send + Sync + 'static;
23
24#[derive(Clone)]
26pub struct JobSpec {
27 pub name: String,
28 pub factory: Arc<JobGraphFactory>,
29 pub restart_policy: JobRestartPolicy,
30 pub drain_behavior: JobDrainBehavior,
31}
32
33impl JobSpec {
34 #[must_use]
35 pub fn new<F>(name: impl Into<String>, factory: F) -> Self
36 where
37 F: Fn(JobContext) -> StreamResult<RunnableGraph<JobMat>> + Send + Sync + 'static,
38 {
39 Self {
40 name: name.into(),
41 factory: Arc::new(factory),
42 restart_policy: JobRestartPolicy::Never,
43 drain_behavior: JobDrainBehavior::default(),
44 }
45 }
46
47 #[must_use]
48 pub fn with_restart_policy(mut self, restart_policy: JobRestartPolicy) -> Self {
49 self.restart_policy = restart_policy;
50 self
51 }
52
53 #[must_use]
54 pub fn with_drain_behavior(mut self, drain_behavior: JobDrainBehavior) -> Self {
55 self.drain_behavior = drain_behavior;
56 self
57 }
58}
59
60impl fmt::Debug for JobSpec {
61 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
62 f.debug_struct("JobSpec")
63 .field("name", &self.name)
64 .field("restart_policy", &self.restart_policy)
65 .field("drain_behavior", &self.drain_behavior)
66 .finish_non_exhaustive()
67 }
68}
69
70#[derive(Clone, Debug)]
72pub struct JobContext {
73 name: Arc<str>,
74 job_id: JobId,
75 generation: u64,
76 kill_switch: SharedKillSwitch,
77 instrumentation: StreamInstrumentationRegistry,
78}
79
80impl JobContext {
81 pub(crate) fn new(
82 name: impl Into<Arc<str>>,
83 job_id: JobId,
84 generation: u64,
85 kill_switch: SharedKillSwitch,
86 instrumentation: StreamInstrumentationRegistry,
87 ) -> Self {
88 Self {
89 name: name.into(),
90 job_id,
91 generation,
92 kill_switch,
93 instrumentation,
94 }
95 }
96
97 #[must_use]
98 pub fn name(&self) -> &str {
99 &self.name
100 }
101
102 #[must_use]
103 pub fn job_id(&self) -> JobId {
104 self.job_id
105 }
106
107 #[must_use]
108 pub fn generation(&self) -> u64 {
109 self.generation
110 }
111
112 #[must_use]
113 pub fn kill_switch(&self) -> SharedKillSwitch {
114 self.kill_switch.clone()
115 }
116
117 #[must_use]
118 pub fn instrumentation_registry(&self) -> &StreamInstrumentationRegistry {
119 &self.instrumentation
120 }
121
122 #[must_use]
126 pub fn drain_flow<T: Send + 'static>(&self) -> Flow<T, T, SharedKillSwitch> {
127 self.kill_switch.flow()
128 }
129
130 #[must_use]
131 pub fn control(&self) -> JobControl {
132 JobControl::graceful(self.kill_switch.clone())
133 }
134}
135
136pub struct JobMat {
138 completion: StreamCompletion<NotUsed>,
139 control: JobControl,
140}
141
142impl JobMat {
143 #[must_use]
144 pub fn new(completion: StreamCompletion<NotUsed>, control: JobControl) -> Self {
145 Self {
146 completion,
147 control,
148 }
149 }
150
151 #[must_use]
152 pub fn graceful(completion: StreamCompletion<NotUsed>, kill_switch: SharedKillSwitch) -> Self {
153 Self::new(completion, JobControl::graceful(kill_switch))
154 }
155
156 #[must_use]
157 pub fn cancel_only(completion: StreamCompletion<NotUsed>) -> Self {
158 Self::new(completion, JobControl::cancel_only())
159 }
160
161 pub(crate) fn into_parts(self) -> (StreamCompletion<NotUsed>, JobControl) {
162 (self.completion, self.control)
163 }
164}
165
166impl fmt::Debug for JobMat {
167 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
168 f.debug_struct("JobMat")
169 .field("control", &self.control)
170 .finish_non_exhaustive()
171 }
172}
173
174#[derive(Clone, Debug)]
176pub struct JobControl {
177 kill_switch: Option<SharedKillSwitch>,
178}
179
180impl JobControl {
181 #[must_use]
182 pub fn graceful(kill_switch: SharedKillSwitch) -> Self {
183 Self {
184 kill_switch: Some(kill_switch),
185 }
186 }
187
188 #[must_use]
189 pub fn cancel_only() -> Self {
190 Self { kill_switch: None }
191 }
192
193 #[must_use]
194 pub fn drain_supported(&self) -> bool {
195 self.kill_switch.is_some()
196 }
197
198 #[must_use]
199 pub fn kill_switch(&self) -> Option<SharedKillSwitch> {
200 self.kill_switch.clone()
201 }
202
203 pub(crate) fn shutdown(&self) -> bool {
204 if let Some(kill_switch) = &self.kill_switch {
205 kill_switch.shutdown();
206 true
207 } else {
208 false
209 }
210 }
211
212 pub(crate) fn abort(&self, error: StreamError) -> bool {
213 if let Some(kill_switch) = &self.kill_switch {
214 kill_switch.abort(error);
215 true
216 } else {
217 false
218 }
219 }
220}
221
222#[derive(Clone, Debug)]
224pub enum JobRestartPolicy {
225 Never,
226 OnFailure(RestartSettings),
227 Always(RestartSettings),
228 Manual,
229}
230
231impl JobRestartPolicy {
232 #[must_use]
233 pub fn never() -> Self {
234 Self::Never
235 }
236
237 #[must_use]
238 pub fn on_failure(settings: RestartSettings) -> Self {
239 Self::OnFailure(settings)
240 }
241
242 #[must_use]
243 pub fn always(settings: RestartSettings) -> Self {
244 Self::Always(settings)
245 }
246
247 #[must_use]
248 pub fn manual() -> Self {
249 Self::Manual
250 }
251
252 pub(crate) fn settings_for(&self, cause: RestartCause) -> Option<RestartSettings> {
253 match (self, cause) {
254 (Self::Always(settings), RestartCause::Failure | RestartCause::Completion)
255 | (Self::OnFailure(settings), RestartCause::Failure) => Some(settings.clone()),
256 (Self::Never | Self::Manual | Self::OnFailure(_), _) => None,
257 }
258 }
259}
260
261#[derive(Clone, Copy, Debug, PartialEq, Eq)]
262pub(crate) enum RestartCause {
263 Failure,
264 Completion,
265}
266
267#[derive(Clone, Debug, PartialEq, Eq)]
269pub enum JobDrainBehavior {
270 Graceful { timeout: Duration },
271 CancelOnly,
272}
273
274impl JobDrainBehavior {
275 #[must_use]
276 pub fn graceful(timeout: Duration) -> Self {
277 Self::Graceful { timeout }
278 }
279
280 #[must_use]
281 pub fn cancel_only() -> Self {
282 Self::CancelOnly
283 }
284
285 #[must_use]
286 pub fn timeout(&self) -> Option<Duration> {
287 match self {
288 Self::Graceful { timeout } => Some(*timeout),
289 Self::CancelOnly => None,
290 }
291 }
292}
293
294impl Default for JobDrainBehavior {
295 fn default() -> Self {
296 Self::Graceful {
297 timeout: Duration::from_secs(30),
298 }
299 }
300}
301
302#[derive(Debug, Clone, Copy, PartialEq, Eq)]
304pub enum DesiredJobState {
305 Running,
306 Draining,
307 Stopped,
308}
309
310#[derive(Debug, Clone, Copy, PartialEq, Eq)]
312pub enum JobState {
313 Submitted,
314 Starting,
315 Running,
316 Draining,
317 BackingOff,
318 Completed,
319 Drained,
320 Stopped,
321 Failed,
322}
323
324#[derive(Debug, Clone, PartialEq, Eq)]
326pub enum JobExitReason {
327 Completed,
328 Failed(StreamError),
329 Drained,
330 Stopped,
331 DrainTimedOut,
332}
333
334#[derive(Debug, Clone, Default, PartialEq, Eq)]
340#[non_exhaustive]
341pub struct JobInstrumentationSnapshot {}
342
343#[derive(Debug, Clone, PartialEq, Eq)]
345pub struct JobStatus {
346 pub name: String,
347 pub job_id: JobId,
348 pub state: JobState,
349 pub desired_state: DesiredJobState,
350 pub generation: u64,
351 pub starts_total: u64,
352 pub restarts_total: u64,
353 pub last_start_at: Option<SystemTime>,
354 pub last_exit_at: Option<SystemTime>,
355 pub last_exit_reason: Option<JobExitReason>,
356 pub backoff_until: Option<Instant>,
357 pub drain_deadline: Option<Instant>,
358 pub drain_supported: bool,
359 pub active_streams: Option<usize>,
360 pub instrumentation: Option<JobInstrumentationSnapshot>,
361}
362
363#[derive(Debug, Clone, PartialEq, Eq)]
365pub struct JobEvent {
366 pub sequence: u64,
367 pub timestamp: SystemTime,
368 pub name: String,
369 pub job_id: JobId,
370 pub generation: u64,
371 pub kind: JobEventKind,
372}
373
374#[derive(Debug, Clone, PartialEq, Eq)]
376pub enum JobEventKind {
377 Submitted,
378 Started,
379 Failed { reason: JobExitReason },
380 RestartScheduled { delay: Duration },
381 Restarted { previous_generation: u64 },
382 Draining,
383 Drained,
384 Stopped { reason: JobExitReason },
385 Completed,
386}
387
388pub(crate) fn new_generation_kill_switch(name: &str, generation: u64) -> SharedKillSwitch {
389 KillSwitches::shared(format!("job:{name}:{generation}"))
390}