Skip to main content

runledger_runtime/catalog/
error.rs

1use runledger_core::jobs::{IdentifierValidationError, WorkflowBuildError};
2use runledger_postgres::jobs::JobDefinitionCatalogSyncError;
3use thiserror::Error;
4
5/// Error returned by [`super::JobCatalog`] validation, sync, and helper methods.
6#[non_exhaustive]
7#[derive(Debug, Error)]
8pub enum CatalogError {
9    /// A caller supplied an invalid catalog job type.
10    #[error("job type {job_type:?} is invalid: {source}")]
11    InvalidJobType {
12        /// Invalid job type value supplied by the caller.
13        job_type: String,
14        /// Identifier validation failure reported by `runledger-core`.
15        #[source]
16        source: IdentifierValidationError,
17    },
18    /// A registered handler returned an invalid job type.
19    #[error("handler job type {handler_job_type:?} is invalid: {source}")]
20    InvalidHandlerJobType {
21        /// Invalid job type returned by the handler.
22        handler_job_type: String,
23        /// Identifier validation failure reported by `runledger-core`.
24        #[source]
25        source: IdentifierValidationError,
26    },
27    /// The declared catalog job type did not match the handler's job type.
28    #[error("job type {declared} does not match handler job type {handler}")]
29    HandlerJobTypeMismatch {
30        /// Job type declared at the catalog registration site.
31        declared: String,
32        /// Job type returned by the handler.
33        handler: String,
34    },
35    /// The catalog already contains the requested job type.
36    #[error("job type {job_type} is already registered in the catalog")]
37    DuplicateJobType {
38        /// Duplicate job type.
39        job_type: String,
40    },
41    /// A catalog definition default failed validation.
42    #[error("catalog defaults are invalid: {field} must be positive")]
43    InvalidDefinitionValue {
44        /// Name of the invalid defaults field.
45        field: &'static str,
46    },
47    /// A job-specific catalog definition override failed validation.
48    #[error("catalog definition for job type {job_type} is invalid: {field} must be positive")]
49    InvalidJobDefinitionValue {
50        /// Job type whose effective definition values are invalid.
51        job_type: String,
52        /// Name of the invalid definition field.
53        field: &'static str,
54    },
55    /// Retry-delay override failure codes must be non-empty.
56    #[error("failure code must be non-empty")]
57    InvalidFailureCode,
58    /// Retry-delay override values must be positive.
59    #[error("retry delay override must be positive")]
60    InvalidRetryDelay,
61    /// Exact sync requires a non-empty owned job-type scope.
62    #[error("exact sync scope must include at least one job type")]
63    InvalidExactSyncScope,
64    /// Exact sync scope construction received an invalid job type.
65    #[error("exact sync scope job type {job_type:?} is invalid: {source}")]
66    InvalidExactSyncScopeJobType {
67        /// Invalid job type supplied for the exact-sync scope.
68        job_type: String,
69        /// Identifier validation failure reported by `runledger-core`.
70        #[source]
71        source: IdentifierValidationError,
72    },
73    /// Exact sync cannot run against an empty catalog.
74    #[error("exact sync requires at least one catalog job")]
75    EmptyExactSyncCatalog,
76    /// A catalog job was not included in the exact-sync scope.
77    #[error("catalog job type {job_type} is outside the exact sync scope")]
78    JobTypeOutsideExactSyncScope {
79        /// Catalog job type missing from the exact-sync scope.
80        job_type: String,
81    },
82    /// An active schedule still references an enabled definition absent from the catalog.
83    #[error("active schedule {schedule_name} still references absent catalog job type {job_type}")]
84    ActiveScheduleForAbsentJobType {
85        /// Active schedule name that blocks disabling the absent definition.
86        schedule_name: String,
87        /// Absent catalog job type referenced by the active schedule.
88        job_type: String,
89    },
90    /// An active schedule still references a catalog job that would be disabled.
91    #[error(
92        "active schedule {schedule_name} still references disabled catalog job type {job_type}"
93    )]
94    ActiveScheduleForDisabledJobType {
95        /// Active schedule name that blocks disabling the catalog definition.
96        schedule_name: String,
97        /// Catalog job type referenced by the active schedule.
98        job_type: String,
99    },
100    /// The requested job type is not registered in the catalog.
101    #[error("job type {job_type} is not registered in the catalog")]
102    UnknownJobType {
103        /// Missing job type.
104        job_type: String,
105    },
106    /// The requested job type is disabled by effective catalog definition values.
107    #[error("job type {job_type} is disabled in the catalog")]
108    DisabledJobType {
109        /// Disabled job type.
110        job_type: String,
111    },
112    /// Workflow enqueue construction failed.
113    #[error(transparent)]
114    WorkflowBuild(#[from] WorkflowBuildError),
115    /// Starting a catalog sync transaction failed.
116    #[error("failed to start job definition sync transaction: {0}")]
117    SyncFailure(#[source] Box<runledger_postgres::Error>),
118    /// A persistence-layer catalog sync failure had no runtime-specific mapping.
119    #[error("failed to sync job definitions with an unmapped persistence error: {0}")]
120    DefinitionCatalogSyncFailure(#[source] Box<dyn std::error::Error + Send + Sync>),
121    /// Syncing a specific catalog job definition failed.
122    #[error("failed to sync job definition {job_type}: {source}")]
123    DefinitionSyncFailure {
124        /// Catalog job type whose definition failed to sync.
125        job_type: String,
126        /// Persistence-layer failure that occurred while syncing the definition.
127        #[source]
128        source: Box<runledger_postgres::Error>,
129    },
130    /// Committing a catalog sync transaction failed.
131    #[error("failed to commit job definition sync transaction: {0}")]
132    CommitFailure(#[source] sqlx::Error),
133    /// Applying the transaction-local bounds for definition-disabling sync failed.
134    #[error("failed to bound job definition sync critical section: {0}")]
135    CriticalSectionTimeoutFailure(#[source] Box<runledger_postgres::Error>),
136    /// Catalog definition sync input failed persistence-layer validation.
137    #[error("job definition sync input is invalid: {0}")]
138    DefinitionSyncValidationFailure(#[source] Box<runledger_postgres::Error>),
139    /// Locking schedules before disabling definitions failed.
140    #[error("failed to lock job schedules before disabling job definitions: {0}")]
141    ScheduleLockFailure(#[source] Box<runledger_postgres::Error>),
142    /// Locking definitions before checking and disabling definitions failed.
143    #[error("failed to lock job definitions before disabling job definitions: {0}")]
144    DefinitionLockFailure(#[source] Box<runledger_postgres::Error>),
145    /// Checking active schedules before disabling definitions failed.
146    #[error("failed to check active schedules before disabling job definitions: {0}")]
147    ScheduleCheckFailure(#[source] Box<runledger_postgres::Error>),
148    /// Inspecting existing job definitions before sync failed.
149    #[error("failed to inspect job definitions before syncing catalog: {0}")]
150    DefinitionInspectFailure(#[source] Box<runledger_postgres::Error>),
151    /// Disabling absent job definitions failed.
152    #[error("failed to disable absent job definitions: {0}")]
153    DisableAbsentFailure(#[source] Box<runledger_postgres::Error>),
154    /// Exact schedule sync requires a non-empty owned schedule-name scope.
155    #[error("exact schedule sync scope must include at least one schedule name")]
156    InvalidExactScheduleSyncScope,
157    /// Exact schedule sync scope construction received an invalid schedule name.
158    #[error("exact schedule sync scope schedule name {name:?} is invalid")]
159    InvalidExactScheduleSyncScopeName {
160        /// Invalid schedule name supplied for the exact-sync scope.
161        name: String,
162    },
163    /// Exact schedule sync scope construction received a duplicate schedule name.
164    #[error("exact schedule sync scope schedule name {name} is duplicated")]
165    DuplicateExactScheduleSyncScopeName {
166        /// Duplicate schedule name supplied for the exact-sync scope.
167        name: String,
168    },
169    /// A catalog schedule was not included in the exact-sync scope.
170    #[error("catalog schedule {name} is outside the exact schedule sync scope")]
171    ScheduleNameOutsideExactSyncScope {
172        /// Catalog schedule name missing from the exact-sync scope.
173        name: String,
174    },
175    /// The catalog already contains the requested schedule name.
176    #[error("schedule name {name} is already registered in the catalog")]
177    DuplicateScheduleName {
178        /// Duplicate schedule name.
179        name: String,
180    },
181    /// A catalog schedule spec failed validation.
182    #[error("catalog schedule {name:?} is invalid: {field}")]
183    InvalidScheduleSpec {
184        /// Schedule name from the invalid spec.
185        name: String,
186        /// Name of the invalid schedule field.
187        field: &'static str,
188    },
189    /// Duplicate schedule names were supplied in one sync batch.
190    #[error("schedule sync batch contains duplicate schedule name {name}")]
191    DuplicateScheduleNameInSyncBatch {
192        /// Duplicate schedule name in the sync batch.
193        name: String,
194    },
195    /// Starting a catalog schedule sync transaction failed.
196    #[error("failed to start job catalog schedule sync transaction: {0}")]
197    ScheduleSyncFailure(#[source] Box<runledger_postgres::Error>),
198    /// Applying the transaction-local bounds for exact schedule sync failed.
199    #[error("failed to bound exact schedule sync critical section: {0}")]
200    ScheduleSyncCriticalSectionFailure(#[source] Box<runledger_postgres::Error>),
201    /// Syncing a specific catalog schedule failed.
202    #[error("failed to sync schedule {name}: {source}")]
203    ScheduleSyncEntryFailure {
204        /// Schedule name whose sync failed.
205        name: String,
206        /// Persistence-layer failure that occurred while syncing the schedule.
207        #[source]
208        source: Box<runledger_postgres::Error>,
209    },
210    /// Deactivating absent catalog schedules failed.
211    #[error("failed to deactivate absent catalog schedules: {0}")]
212    DeactivateAbsentSchedulesFailure(#[source] Box<runledger_postgres::Error>),
213    /// Committing a catalog schedule sync transaction failed.
214    #[error("failed to commit job catalog schedule sync transaction: {0}")]
215    ScheduleSyncCommitFailure(#[source] Box<runledger_postgres::Error>),
216}
217
218impl CatalogError {
219    pub(crate) fn from_definition_catalog_sync_error(error: JobDefinitionCatalogSyncError) -> Self {
220        match error {
221            JobDefinitionCatalogSyncError::ActiveScheduleForAbsentJobType(reference) => {
222                Self::ActiveScheduleForAbsentJobType {
223                    schedule_name: reference.schedule_name,
224                    job_type: reference.job_type.to_string(),
225                }
226            }
227            JobDefinitionCatalogSyncError::ActiveScheduleForDisabledJobType(reference) => {
228                Self::ActiveScheduleForDisabledJobType {
229                    schedule_name: reference.schedule_name,
230                    job_type: reference.job_type.to_string(),
231                }
232            }
233            JobDefinitionCatalogSyncError::CriticalSectionTimeoutFailure(source) => {
234                Self::CriticalSectionTimeoutFailure(source)
235            }
236            JobDefinitionCatalogSyncError::ScheduleLockFailure(source) => {
237                Self::ScheduleLockFailure(source)
238            }
239            JobDefinitionCatalogSyncError::DefinitionLockFailure(source) => {
240                Self::DefinitionLockFailure(source)
241            }
242            JobDefinitionCatalogSyncError::ScheduleCheckFailure(source) => {
243                Self::ScheduleCheckFailure(source)
244            }
245            JobDefinitionCatalogSyncError::ValidationFailure(source) => {
246                Self::DefinitionSyncValidationFailure(source)
247            }
248            JobDefinitionCatalogSyncError::DefinitionInspectFailure(source) => {
249                Self::DefinitionInspectFailure(source)
250            }
251            JobDefinitionCatalogSyncError::DefinitionSyncFailure { job_type, source } => {
252                Self::DefinitionSyncFailure { job_type, source }
253            }
254            JobDefinitionCatalogSyncError::DisableAbsentFailure(source) => {
255                Self::DisableAbsentFailure(source)
256            }
257            // Fallback for future #[non_exhaustive] variants that this runtime
258            // version cannot map to a more specific CatalogError.
259            _ => Self::DefinitionCatalogSyncFailure(Box::new(error)),
260        }
261    }
262}