Skip to main content

runledger_runtime/
error.rs

1use thiserror::Error;
2
3use crate::config::JobsConfigValidationError;
4
5pub type Result<T> = std::result::Result<T, Error>;
6
7#[derive(Debug, Error)]
8pub enum Error {
9    #[error(transparent)]
10    Scheduler(#[from] SchedulerError),
11    #[error(transparent)]
12    Worker(#[from] WorkerError),
13    #[error(transparent)]
14    Reaper(#[from] ReaperError),
15    #[error(transparent)]
16    Runtime(#[from] RuntimeError),
17}
18
19#[derive(Debug, Error)]
20pub enum SchedulerError {
21    #[error("failed to begin scheduler transaction")]
22    BeginTransaction { source: runledger_postgres::Error },
23    #[error("failed to commit scheduler transaction")]
24    CommitTransaction { source: runledger_postgres::Error },
25    #[error("failed to claim due schedules")]
26    ClaimDueSchedules { source: runledger_postgres::Error },
27    #[error("failed to create savepoint using `{statement}`")]
28    SavepointCreate {
29        statement: &'static str,
30        source: runledger_postgres::Error,
31    },
32    #[error("failed to rollback savepoint using `{statement}`")]
33    SavepointRollback {
34        statement: &'static str,
35        source: runledger_postgres::Error,
36    },
37    #[error("failed to release savepoint using `{statement}`")]
38    SavepointRelease {
39        statement: &'static str,
40        source: runledger_postgres::Error,
41    },
42    #[error("failed deferring failed schedule `{schedule_id}`")]
43    DeferFailedSchedule {
44        schedule_id: uuid::Uuid,
45        source: runledger_postgres::Error,
46    },
47    #[error(
48        "invalid cron expression for schedule `{schedule_name}` ({schedule_id}): `{cron_expr}`"
49    )]
50    InvalidCronExpression {
51        schedule_id: uuid::Uuid,
52        schedule_name: String,
53        cron_expr: String,
54    },
55    #[error("failed enqueueing scheduled job `{job_type}` from schedule `{schedule_id}`")]
56    EnqueueScheduledJob {
57        schedule_id: uuid::Uuid,
58        job_type: String,
59        source: runledger_postgres::Error,
60    },
61    #[error("failed marking schedule `{schedule_id}` as fired")]
62    MarkScheduleFired {
63        schedule_id: uuid::Uuid,
64        source: runledger_postgres::Error,
65    },
66    /// A schedule row returned by the runtime claim query disappeared before the
67    /// scheduler could advance or defer its next fire cursor.
68    #[error("claimed schedule `{schedule_id}` was missing while {operation}")]
69    ClaimedScheduleMissing {
70        schedule_id: uuid::Uuid,
71        operation: &'static str,
72    },
73}
74
75#[derive(Debug, Error)]
76pub enum WorkerError {
77    #[error("failed claiming jobs for worker `{worker_id}`")]
78    ClaimJobs {
79        worker_id: String,
80        source: runledger_postgres::Error,
81    },
82    #[error("failed setting running progress for job `{job_id}` attempt `{attempt}`")]
83    SetRunningProgress {
84        job_id: uuid::Uuid,
85        attempt: i32,
86        source: runledger_postgres::Error,
87    },
88    #[error("failed releasing unstarted claim for job `{job_id}` attempt `{attempt}`")]
89    ReleaseUnstartedClaim {
90        job_id: uuid::Uuid,
91        attempt: i32,
92        source: runledger_postgres::Error,
93    },
94    #[error("failed completing job `{job_id}` attempt `{attempt}` as success")]
95    CompleteSuccess {
96        job_id: uuid::Uuid,
97        attempt: i32,
98        source: runledger_postgres::Error,
99    },
100    #[error("failed completing job `{job_id}` attempt `{attempt}` as failure")]
101    CompleteFailure {
102        job_id: uuid::Uuid,
103        attempt: i32,
104        source: runledger_postgres::Error,
105    },
106    #[error("failed heartbeat for job `{job_id}` attempt `{attempt}`")]
107    Heartbeat {
108        job_id: uuid::Uuid,
109        attempt: i32,
110        source: runledger_postgres::Error,
111    },
112}
113
114#[derive(Debug, Error)]
115pub enum ReaperError {
116    #[error(
117        "failed reaping expired leases with batch_size `{batch_size}` and retry_delay_ms `{retry_delay_ms}`"
118    )]
119    ReapExpiredLeases {
120        batch_size: i64,
121        retry_delay_ms: i32,
122        source: runledger_postgres::Error,
123    },
124}
125
126#[derive(Debug, Error)]
127#[non_exhaustive]
128pub enum RuntimeError {
129    /// The supervisor builder received an invalid [`JobsConfig`] value. This
130    /// usually means the config was constructed directly instead of through
131    /// [`JobsConfig::from_env`]. Validation is strict for the whole supervisor
132    /// config, even if the loop that would use an invalid field is disabled.
133    ///
134    /// [`JobsConfig`]: crate::config::JobsConfig
135    /// [`JobsConfig::from_env`]: crate::config::JobsConfig::from_env
136    #[error("invalid jobs runtime configuration: {source}")]
137    InvalidJobsConfig {
138        #[source]
139        source: JobsConfigValidationError,
140    },
141    /// The supervisor builder received both direct registry and catalog
142    /// registration sources. Choose either [`SupervisorBuilder::with_registry`]
143    /// or [`SupervisorBuilder::with_catalog`] for a single builder.
144    ///
145    /// [`SupervisorBuilder::with_catalog`]: crate::supervisor::SupervisorBuilder::with_catalog
146    /// [`SupervisorBuilder::with_registry`]: crate::supervisor::SupervisorBuilder::with_registry
147    #[error(
148        "supervisor builder received both a job registry and a job catalog; choose one registration source"
149    )]
150    MixedRegistrySources,
151    /// The supervisor builder requires a job registry when the worker or reaper
152    /// loop is enabled, but none was provided. Call
153    /// [`SupervisorBuilder::with_registry`] before [`SupervisorBuilder::build`].
154    ///
155    /// [`SupervisorBuilder::with_registry`]: crate::supervisor::SupervisorBuilder::with_registry
156    /// [`SupervisorBuilder::build`]: crate::supervisor::SupervisorBuilder::build
157    #[error(
158        "supervisor builder requires a job registry when worker or reaper loops are enabled \
159         (worker_enabled={worker_enabled}, reaper_enabled={reaper_enabled})"
160    )]
161    MissingRegistry {
162        worker_enabled: bool,
163        reaper_enabled: bool,
164    },
165    /// The supervisor builder must be called from within an active Tokio runtime
166    /// context. Ensure you are calling it inside a `#[tokio::main]`, `#[tokio::test]`,
167    /// or within a spawned task inside an existing runtime.
168    #[error("supervisor builder requires an active Tokio runtime")]
169    MissingTokioRuntime {
170        #[source]
171        source: tokio::runtime::TryCurrentError,
172    },
173    /// A supervised runtime task exited cleanly before shutdown was requested.
174    /// This is treated as an error because long-running loops should only exit
175    /// in response to a shutdown signal. Investigate logs for the specific task
176    /// that exited unexpectedly.
177    #[error("jobs runtime task `{task}` exited unexpectedly before shutdown")]
178    TaskExitedUnexpectedly { task: &'static str },
179    /// A supervised task panicked or failed to join cleanly. Examine logs and
180    /// process panic output for more details about the underlying task failure.
181    #[error("failed joining jobs runtime task `{task}`")]
182    TaskJoin {
183        task: &'static str,
184        #[source]
185        source: tokio::task::JoinError,
186    },
187    /// The supervisor did not complete shutdown within the requested timeout.
188    /// Some tasks may not have received or responded to the shutdown signal.
189    /// Consider increasing the timeout or investigating why tasks are shutting
190    /// down slowly.
191    #[error("jobs runtime shutdown exceeded timeout {timeout:?}")]
192    ShutdownTimeout { timeout: std::time::Duration },
193    /// The requested shutdown timeout is too large to represent as a deadline.
194    /// Use a smaller timeout value.
195    #[error("jobs runtime shutdown timeout {timeout:?} is too large to represent")]
196    ShutdownTimeoutTooLarge { timeout: std::time::Duration },
197    /// A supervised task failed (panicked or exited unexpectedly) and the
198    /// remaining tasks could not be shut down within the timeout. The original
199    /// task failure is available through the source error.
200    #[error(
201        "jobs runtime shutdown exceeded timeout {timeout:?} while draining after earlier task failure"
202    )]
203    ShutdownTimeoutAfterTaskError {
204        timeout: std::time::Duration,
205        #[source]
206        source: Box<RuntimeError>,
207    },
208}
209
210#[cfg(test)]
211mod tests {
212    use super::*;
213
214    #[test]
215    fn scheduler_invalid_cron_variant_contains_schedule_metadata() {
216        let schedule_id = uuid::Uuid::nil();
217        let error = SchedulerError::InvalidCronExpression {
218            schedule_id,
219            schedule_name: "nightly sync".to_string(),
220            cron_expr: "not cron".to_string(),
221        };
222
223        match error {
224            SchedulerError::InvalidCronExpression {
225                schedule_id: actual_id,
226                schedule_name,
227                cron_expr,
228            } => {
229                assert_eq!(actual_id, schedule_id);
230                assert_eq!(schedule_name, "nightly sync");
231                assert_eq!(cron_expr, "not cron");
232            }
233            other => panic!("expected invalid cron variant, got: {other:?}"),
234        }
235    }
236}