Skip to main content

runledger_postgres/jobs/types/
lifecycle.rs

1use std::time::Duration;
2
3use chrono::{DateTime, Utc};
4use runledger_core::jobs::{JobFailureKind, JobRetryTiming, JobStage, JobTypeName};
5use serde_json::Value;
6use sqlx::types::Uuid;
7
8/// Exact identity of a live job lease.
9///
10/// Lifecycle mutations use every field to fence an operation to one claimed
11/// attempt. Reusing this value avoids accidentally pairing a job identifier
12/// with the run, attempt, or worker from another lease.
13#[derive(Clone, Copy, Debug, Eq, PartialEq)]
14pub struct JobLeaseIdentity<'a> {
15    pub job_id: Uuid,
16    pub run_number: i32,
17    pub attempt: i32,
18    pub worker_id: &'a str,
19}
20
21impl<'a> JobLeaseIdentity<'a> {
22    /// Creates an identity for one exact live job lease.
23    #[must_use]
24    pub const fn new(job_id: Uuid, run_number: i32, attempt: i32, worker_id: &'a str) -> Self {
25        Self {
26            job_id,
27            run_number,
28            attempt,
29            worker_id,
30        }
31    }
32}
33
34/// The progress, checkpoint, and audit data committed with a `RUNNING`
35/// transition.
36///
37/// [`crate::jobs::mark_job_running`] persists this update atomically with the
38/// stage transition and execution-start marker. Do not split that transition
39/// into a stage-only call followed by an ordinary progress update: a crash
40/// between separate writes would lose the durable resume state for the started
41/// attempt.
42#[derive(Clone, Debug)]
43pub struct JobRunningUpdate<'a> {
44    pub progress_done: Option<i64>,
45    pub progress_total: Option<i64>,
46    pub checkpoint: Option<&'a Value>,
47}
48
49/// An ordinary in-flight progress and checkpoint update.
50///
51/// This input intentionally cannot change a job stage. Use
52/// [`JobRunningUpdate`] with [`crate::jobs::mark_job_running`] when execution
53/// starts so the `RUNNING` transition and durable resume state remain one
54/// transaction.
55#[derive(Clone, Debug)]
56pub struct JobOrdinaryProgressUpdate<'a> {
57    pub progress_done: Option<i64>,
58    pub progress_total: Option<i64>,
59    pub checkpoint: Option<&'a Value>,
60}
61
62/// Legacy stage-bearing progress input.
63///
64/// New callers should use [`JobRunningUpdate`] with
65/// [`crate::jobs::mark_job_running`] for a `RUNNING` transition, or
66/// [`JobOrdinaryProgressUpdate`] with
67/// [`crate::jobs::update_job_ordinary_progress`] for ordinary progress. This
68/// compatibility input preserves arbitrary historical stage writes while
69/// downstream callers migrate to the typed lifecycle APIs.
70#[deprecated(
71    since = "0.11.0",
72    note = "use JobRunningUpdate with mark_job_running for RUNNING, or JobOrdinaryProgressUpdate with update_job_ordinary_progress for ordinary progress"
73)]
74#[derive(Clone, Debug)]
75pub struct JobProgressUpdate<'a> {
76    pub stage: Option<JobStage>,
77    pub progress_done: Option<i64>,
78    pub progress_total: Option<i64>,
79    pub checkpoint: Option<&'a Value>,
80}
81
82#[derive(Clone, Debug)]
83pub struct JobCompletionUpdate<'a> {
84    pub progress_done: Option<i64>,
85    pub progress_total: Option<i64>,
86    pub checkpoint: Option<&'a Value>,
87    pub output: Option<&'a Value>,
88}
89
90/// Progress and scheduling data for a successful handler continuation.
91#[derive(Clone, Debug)]
92pub struct JobContinuationUpdate<'a> {
93    /// How long to wait before the next run becomes claimable. Zero means the
94    /// next run is immediately eligible. Delays whose resulting timestamp is
95    /// outside the persistence driver's representable range are rejected with
96    /// `job.invalid_continuation_delay`.
97    pub delay: Duration,
98    pub progress_done: Option<i64>,
99    pub progress_total: Option<i64>,
100    pub checkpoint: Option<&'a Value>,
101}
102
103#[derive(Clone, Debug)]
104#[non_exhaustive]
105pub struct JobContinuationOutcome {
106    pub job_id: Uuid,
107    pub job_type: JobTypeName,
108    pub organization_id: Option<Uuid>,
109    /// The run whose attempt completed successfully.
110    pub completed_run_number: i32,
111    /// The newly pending run number.
112    pub next_run_number: i32,
113    pub attempt: i32,
114    pub max_attempts: i32,
115    pub next_run_at: DateTime<Utc>,
116    pub progress_done: Option<i64>,
117    pub progress_total: Option<i64>,
118}
119
120#[derive(Clone, Debug)]
121#[non_exhaustive]
122pub struct JobSuccessCompletionOutcome {
123    pub job_id: Uuid,
124    pub job_type: JobTypeName,
125    pub organization_id: Option<Uuid>,
126    pub run_number: i32,
127    pub attempt: i32,
128    pub max_attempts: i32,
129    pub progress_done: Option<i64>,
130    pub progress_total: Option<i64>,
131}
132
133/// Failure details supplied to the persistence lifecycle.
134///
135/// `policy_retry_delay_ms` supplies the ordinary retry policy. `retry_timing`
136/// is an optional handler lower bound. PostgreSQL commits the later schedule.
137#[derive(Clone, Debug)]
138#[non_exhaustive]
139pub struct JobFailureUpdate<'a> {
140    pub kind: JobFailureKind,
141    pub code: &'a str,
142    pub message: &'a str,
143    /// Optional handler-selected not-before lower bound.
144    pub retry_timing: Option<JobRetryTiming>,
145    /// Required ordinary policy backoff when the failure remains retryable.
146    pub policy_retry_delay_ms: Option<i32>,
147}
148
149impl<'a> JobFailureUpdate<'a> {
150    /// Creates a failure update with ordinary policy backoff and no handler
151    /// lower bound.
152    #[must_use]
153    pub const fn new(
154        kind: JobFailureKind,
155        code: &'a str,
156        message: &'a str,
157        policy_retry_delay_ms: Option<i32>,
158    ) -> Self {
159        Self {
160            kind,
161            code,
162            message,
163            retry_timing: None,
164            policy_retry_delay_ms,
165        }
166    }
167
168    /// Adds the handler-selected retry lower bound.
169    #[must_use]
170    pub const fn with_retry_timing(mut self, retry_timing: JobRetryTiming) -> Self {
171        self.retry_timing = Some(retry_timing);
172        self
173    }
174}
175
176/// Durable outcome of completing one failed attempt.
177#[derive(Clone, Debug, Eq, PartialEq)]
178#[non_exhaustive]
179pub enum JobFailureCompletionDisposition {
180    /// Another attempt was scheduled from a relative delay.
181    RetryScheduled {
182        /// Persisted positive delay, rounded up to millisecond precision.
183        retry_delay_ms: i32,
184        /// Effective claim time calculated from the PostgreSQL completion clock.
185        next_run_at: DateTime<Utc>,
186    },
187    /// The handler's lower bound selected the effective retry schedule.
188    RetryScheduledAt {
189        /// Handler not-before time, rounded up to PostgreSQL microsecond
190        /// precision when necessary.
191        requested_retry_at: DateTime<Utc>,
192        /// Effective claim time. This is never earlier than policy backoff.
193        next_run_at: DateTime<Utc>,
194    },
195    DeadLettered {
196        reason: runledger_core::jobs::JobDeadLetterReason,
197    },
198}
199
200#[derive(Clone, Debug)]
201#[non_exhaustive]
202pub struct JobFailureCompletionOutcome {
203    pub job_id: Uuid,
204    pub job_type: JobTypeName,
205    pub organization_id: Option<Uuid>,
206    pub run_number: i32,
207    pub attempt: i32,
208    pub max_attempts: i32,
209    pub failure_kind: JobFailureKind,
210    pub failure_code: String,
211    pub failure_message: String,
212    /// Latest durable checkpoint observed while locking the failed attempt.
213    pub checkpoint: Option<Value>,
214    pub disposition: JobFailureCompletionDisposition,
215}
216
217#[cfg(test)]
218mod job_lease_identity_tests {
219    use super::*;
220
221    #[test]
222    fn construction_retains_each_lease_fence() {
223        let identity = JobLeaseIdentity::new(Uuid::nil(), 7, 3, "worker-identity-test");
224
225        assert_eq!(identity.job_id, Uuid::nil());
226        assert_eq!(identity.run_number, 7);
227        assert_eq!(identity.attempt, 3);
228        assert_eq!(identity.worker_id, "worker-identity-test");
229    }
230}