Skip to main content

runledger_runtime/catalog/
sync.rs

1use runledger_postgres::DbPool;
2use runledger_postgres::jobs::{
3    JobDefinitionCatalogSyncMode, sync_catalog_job_definitions_exact_tx,
4    sync_catalog_job_definitions_tx,
5};
6
7use super::{
8    CatalogError, JobCatalog, JobCatalogExactSyncReport, JobCatalogSyncReport, JobCatalogSyncScope,
9};
10
11impl JobCatalog {
12    /// Upserts every catalog job into `job_definitions`.
13    ///
14    /// The catalog owns the synced definition fields: repeated syncs overwrite
15    /// `version`, `max_attempts`, `default_timeout_seconds`, and
16    /// `default_priority` for registered jobs. Effectively enabled catalog jobs
17    /// preserve an existing disabled row so operator pauses survive worker
18    /// restarts; effectively disabled catalog jobs explicitly write
19    /// `is_enabled = false`.
20    /// Safe to call repeatedly. Does not delete or disable definitions absent from the catalog.
21    /// Use [`Self::sync_definitions_exact`] with an explicit scope when removed
22    /// catalog entries should be disabled.
23    ///
24    /// Syncs that disable catalog jobs briefly lock `job_schedules` and
25    /// `job_definitions` so active schedule checks and definition disables are
26    /// evaluated against a stable write boundary.
27    ///
28    /// # Errors
29    /// Returns [`CatalogError`] when defaults are invalid or persistence fails.
30    pub async fn sync_definitions(
31        &self,
32        pool: &DbPool,
33    ) -> Result<JobCatalogSyncReport, CatalogError> {
34        self.validate_defaults()?;
35
36        let mut tx = pool.begin().await.map_err(|error| {
37            CatalogError::SyncFailure(Box::new(
38                runledger_postgres::Error::from_query_sqlx_with_context(
39                    "begin job catalog definition sync",
40                    error,
41                ),
42            ))
43        })?;
44
45        let definitions = self.catalog_definitions();
46        let mode = JobDefinitionCatalogSyncMode::PreserveExistingEnabledForEnabledDefinitions;
47        let report = sync_catalog_job_definitions_tx(&mut tx, &definitions, mode)
48            .await
49            .map_err(CatalogError::from_definition_catalog_sync_error)?;
50
51        tx.commit().await.map_err(CatalogError::CommitFailure)?;
52
53        Ok(JobCatalogSyncReport {
54            disabled_catalog_job_types: report.disabled_catalog_job_types,
55        })
56    }
57
58    /// Upserts catalog jobs, then disables enabled `job_definitions` rows in
59    /// `scope` whose job type is absent from the catalog.
60    ///
61    /// This is the stricter startup mode for applications that want the catalog
62    /// to be the active job-definition source of truth. It never deletes rows,
63    /// never operates outside the supplied owned job-type set, and rejects active
64    /// schedules that still reference a job type it would disable. Unlike
65    /// [`Self::sync_definitions`], exact sync restores catalog entries'
66    /// effective `is_enabled` value from catalog defaults and job-specific
67    /// overrides.
68    ///
69    /// Exact sync briefly locks `job_schedules` and `job_definitions` before it
70    /// checks active schedules or disables definitions, so it is heavier than the
71    /// additive sync path.
72    ///
73    /// # Errors
74    /// Returns [`CatalogError`] when the scope is invalid, the catalog is empty,
75    /// a catalog job is outside the scope, active schedules still reference
76    /// absent scoped jobs, or persistence fails.
77    pub async fn sync_definitions_exact(
78        &self,
79        pool: &DbPool,
80        scope: &JobCatalogSyncScope,
81    ) -> Result<JobCatalogExactSyncReport, CatalogError> {
82        self.validate_defaults()?;
83        self.validate_exact_sync_scope(scope)?;
84
85        let mut tx = pool.begin().await.map_err(|error| {
86            CatalogError::SyncFailure(Box::new(
87                runledger_postgres::Error::from_query_sqlx_with_context(
88                    "begin exact job catalog definition sync",
89                    error,
90                ),
91            ))
92        })?;
93
94        let scope_job_types = scope.job_types_for_storage();
95        let definitions = self.catalog_definitions();
96        let report = sync_catalog_job_definitions_exact_tx(&mut tx, &definitions, &scope_job_types)
97            .await
98            .map_err(CatalogError::from_definition_catalog_sync_error)?;
99
100        tx.commit().await.map_err(CatalogError::CommitFailure)?;
101
102        Ok(JobCatalogExactSyncReport {
103            disabled_absent_job_types: report.disabled_absent_job_types,
104            disabled_catalog_job_types: report.disabled_catalog_job_types,
105        })
106    }
107
108    fn catalog_definitions(&self) -> Vec<runledger_postgres::jobs::JobDefinitionUpsert<'static>> {
109        self.jobs
110            .values()
111            .map(|entry| self.materialize_definition(entry))
112            .collect()
113    }
114}