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 #[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 #[error("invalid jobs runtime configuration: {source}")]
137 InvalidJobsConfig {
138 #[source]
139 source: JobsConfigValidationError,
140 },
141 #[error(
148 "supervisor builder received both a job registry and a job catalog; choose one registration source"
149 )]
150 MixedRegistrySources,
151 #[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 #[error("supervisor builder requires an active Tokio runtime")]
169 MissingTokioRuntime {
170 #[source]
171 source: tokio::runtime::TryCurrentError,
172 },
173 #[error("jobs runtime task `{task}` exited unexpectedly before shutdown")]
178 TaskExitedUnexpectedly { task: &'static str },
179 #[error("failed joining jobs runtime task `{task}`")]
182 TaskJoin {
183 task: &'static str,
184 #[source]
185 source: tokio::task::JoinError,
186 },
187 #[error("jobs runtime shutdown exceeded timeout {timeout:?}")]
192 ShutdownTimeout { timeout: std::time::Duration },
193 #[error("jobs runtime shutdown timeout {timeout:?} is too large to represent")]
196 ShutdownTimeoutTooLarge { timeout: std::time::Duration },
197 #[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}