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