Skip to main content

kernel/jobs/
job.rs

1//! The `Job` record and its state/progress value types.
2
3use serde::{Deserialize, Serialize};
4
5use crate::records::{Capability, JsonValue};
6
7/// A job's lifecycle state.
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
9#[serde(rename_all = "lowercase")]
10pub enum JobState {
11    Queued,
12    Preparing,
13    Running,
14    Done,
15    Failed,
16    Cancelled,
17}
18
19impl JobState {
20    /// Whether this is an end state (`Done`/`Failed`/`Cancelled`).
21    pub fn is_terminal(self) -> bool {
22        matches!(
23            self,
24            JobState::Done | JobState::Failed | JobState::Cancelled
25        )
26    }
27}
28
29/// A job's progress: a `[0, 1]` fraction plus the raw step counts it came from.
30#[derive(Debug, Default, Clone, Copy, PartialEq, Serialize, Deserialize)]
31pub struct JobProgress {
32    pub fraction: f64,
33    #[serde(skip_serializing_if = "Option::is_none", default)]
34    pub step: Option<i64>,
35    #[serde(skip_serializing_if = "Option::is_none", default)]
36    pub total_steps: Option<i64>,
37}
38
39impl JobProgress {
40    /// Progress at `fraction` derived from `step`/`total_steps`.
41    pub fn new(fraction: f64, step: Option<i64>, total_steps: Option<i64>) -> Self {
42        Self {
43            fraction,
44            step,
45            total_steps,
46        }
47    }
48}
49
50/// A discrete unit of work with progress and a persisted terminal result. Its
51/// on-disk form (in `jobs.json`) is internal, so field names are the native
52/// snake_case.
53#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
54pub struct Job {
55    pub id: String,
56    pub model_id: String,
57    pub capability: Capability,
58    pub payload: JsonValue,
59    pub state: JobState,
60    pub progress: JobProgress,
61    #[serde(skip_serializing_if = "Option::is_none", default)]
62    pub queue_reason: Option<String>,
63    /// The latest preview frame. Held in memory only — never persisted.
64    #[serde(skip)]
65    pub preview: Option<Vec<u8>>,
66    #[serde(default)]
67    pub result: Vec<String>,
68    #[serde(skip_serializing_if = "Option::is_none", default)]
69    pub error: Option<String>,
70    pub submitted_at: i64,
71    #[serde(skip_serializing_if = "Option::is_none", default)]
72    pub started_at: Option<i64>,
73    #[serde(skip_serializing_if = "Option::is_none", default)]
74    pub finished_at: Option<i64>,
75}
76
77impl Job {
78    /// A freshly-queued job submitted at `submitted_at` (epoch milliseconds).
79    pub fn new(
80        id: impl Into<String>,
81        model_id: impl Into<String>,
82        capability: Capability,
83        payload: JsonValue,
84        submitted_at: i64,
85    ) -> Self {
86        Self {
87            id: id.into(),
88            model_id: model_id.into(),
89            capability,
90            payload,
91            state: JobState::Queued,
92            progress: JobProgress::default(),
93            queue_reason: None,
94            preview: None,
95            result: Vec::new(),
96            error: None,
97            submitted_at,
98            started_at: None,
99            finished_at: None,
100        }
101    }
102}