Skip to main content

chronon_core/
store.rs

1//! Scheduler persistence port for jobs, runs, revisions, and coordinator metadata.
2
3use async_trait::async_trait;
4use chrono::{DateTime, Utc};
5
6use crate::error::Result;
7use crate::models::{
8    Job, JobRevision, PartitionAssignment, Run, RunStatus, SchedulerLeader, Script, Worker,
9};
10
11/// Async persistence port for jobs, runs, revisions, and coordinator metadata.
12///
13/// Hosts provide one implementation per storage substrate. The scheduler, executor, and
14/// HTTP API call these methods; implementations must be `Send + Sync` for shared use across
15/// Tokio tasks.
16///
17/// | Adapter | Public crate feature | Topology fit |
18/// |---------|----------------|--------------|
19/// | `InMemorySchedulerStore` | `mem` | Embedded local / tests (not multi-process) |
20/// | `SqliteSchedulerStore` | `sqlite` | Embedded single-host durable |
21/// | `PostgresSchedulerStore` | `postgres` | Coordinator–worker shared durable |
22/// | `PostgresRedisSchedulerStore` | `postgres,redis` | Coordinator–worker production claim path |
23///
24/// Inject via `ChrononBuilder::scheduler_store` (or [`crate::StoreRouter`] +
25/// `scheduler_store_from_global`). Custom adapters implement this trait in a separate crate.
26///
27/// # Contract
28///
29/// - Job rows are keyed by [`Job::job_id`]; [`Job::job_name`] is unique per deployment.
30/// - Run claims must be atomic: at most one worker holds a claimed run at a time.
31/// - Tick claims prevent duplicate enqueue when coordinators race on the same job.
32/// - Leader election uses a singleton row with TTL renewal semantics.
33///
34/// # Examples
35///
36/// Trait-object usage (pass any adapter that implements the port):
37///
38/// ```
39/// use std::sync::Arc;
40/// use chronon_core::{Job, SchedulerStore};
41///
42/// async fn seed(store: Arc<dyn SchedulerStore>) -> chronon_core::Result<()> {
43///     store.upsert_job(&Job::new("demo", "noop")).await?;
44///     assert_eq!(store.list_jobs().await?.len(), 1);
45///     Ok(())
46/// }
47/// ```
48///
49/// Concrete adapters and runnable boots live in `chronon-backend-mem` / `-sqlite` /
50/// `-postgres` / `-redis` and the `uf-chronon` examples (`sqlite_boot`, `postgres_boot`, …).
51#[async_trait]
52pub trait SchedulerStore: Send + Sync {
53    // --- Jobs ---
54
55    /// Insert or replace a job row keyed by [`Job::job_id`].
56    ///
57    /// # Contract
58    ///
59    /// Replaces the full row; callers must send complete job state on update.
60    async fn upsert_job(&self, job: &Job) -> Result<()>;
61
62    /// Look up a job by primary key.
63    async fn get_job(&self, job_id: &str) -> Result<Option<Job>>;
64
65    /// Look up a job by unique [`Job::job_name`].
66    async fn get_job_by_name(&self, job_name: &str) -> Result<Option<Job>>;
67
68    /// Return all jobs (admin / list API).
69    async fn list_jobs(&self) -> Result<Vec<Job>>;
70
71    /// Jobs with `next_run_at <= before` and enabled scheduling (tick discovery).
72    ///
73    /// # Contract
74    ///
75    /// Returns only enabled jobs whose `next_run_at` is set and `<= before`.
76    async fn list_due_jobs(&self, before: DateTime<Utc>) -> Result<Vec<Job>>;
77
78    /// Disable automatic scheduling without deleting the job.
79    async fn pause_job(&self, job_id: &str) -> Result<()>;
80
81    /// Re-enable automatic scheduling after [`Self::pause_job`].
82    async fn resume_job(&self, job_id: &str) -> Result<()>;
83
84    // --- Runs ---
85
86    /// Persist a new run row (typically `RunStatus::Queued`).
87    ///
88    /// # Contract
89    ///
90    /// `run_id` must be unique; duplicate inserts are a backend error.
91    async fn create_run(&self, run: &Run) -> Result<()>;
92
93    /// Replace an existing run row (status transitions, lease renewal, completion).
94    async fn update_run(&self, run: &Run) -> Result<()>;
95
96    /// Look up a run by [`Run::run_id`].
97    async fn get_run(&self, run_id: &str) -> Result<Option<Run>>;
98
99    /// Recent runs for one job, newest first, capped by `limit`.
100    async fn list_runs_for_job(&self, job_id: &str, limit: usize) -> Result<Vec<Run>>;
101
102    /// Paginated run listing with optional job and status filters (HTTP list API).
103    async fn list_runs_filtered(
104        &self,
105        job_id: Option<&str>,
106        status: Option<RunStatus>,
107        offset: usize,
108        limit: usize,
109    ) -> Result<Vec<Run>>;
110
111    /// Claim the next queued run for a worker pool.
112    ///
113    /// # Contract
114    ///
115    /// Atomically selects a `Queued` run matching `pool_id`, sets `claimed_by`, and writes
116    /// `claim_lease_until = now + lease_ttl_secs`. Returns `None` when the pool queue is empty.
117    /// At most one worker may hold a given run claim at a time.
118    async fn claim_next_queued(
119        &self,
120        pool_id: &str,
121        worker_id: &str,
122        now: DateTime<Utc>,
123        lease_ttl_secs: i64,
124    ) -> Result<Option<Run>>;
125
126    /// Claim a specific queued run by id (postgres-redis hybrid hot path).
127    ///
128    /// Returns `None` when the run is not queued, pool/lease checks fail, or the row
129    /// does not exist.
130    async fn claim_run_by_id(
131        &self,
132        run_id: &str,
133        pool_id: &str,
134        worker_id: &str,
135        now: DateTime<Utc>,
136        lease_ttl_secs: i64,
137    ) -> Result<Option<Run>>;
138
139    /// Claim multiple queued runs by id in one round trip when the backend supports it.
140    ///
141    /// Default implementation claims each id sequentially via [`Self::claim_run_by_id`].
142    async fn claim_runs_by_ids(
143        &self,
144        run_ids: &[&str],
145        pool_id: &str,
146        worker_id: &str,
147        now: DateTime<Utc>,
148        lease_ttl_secs: i64,
149    ) -> Result<Vec<Run>> {
150        let mut claimed = Vec::new();
151        for run_id in run_ids {
152            if let Some(run) = self
153                .claim_run_by_id(run_id, pool_id, worker_id, now, lease_ttl_secs)
154                .await?
155            {
156                claimed.push(run);
157            }
158        }
159        Ok(claimed)
160    }
161
162    /// Extend the worker lease on a claimed run if `worker_id` still holds the claim.
163    ///
164    /// Returns `false` when the run is not claimed by this worker or the lease expired.
165    async fn renew_run_lease(
166        &self,
167        run_id: &str,
168        worker_id: &str,
169        now: DateTime<Utc>,
170        lease_ttl_secs: i64,
171    ) -> Result<bool>;
172
173    /// Reset `claimed` / `running` runs whose `claim_lease_until` is at or before `now` to
174    /// [`RunStatus::Queued`].
175    ///
176    /// Clears `claimed_by`, `claim_lease_until`, and `started_at`. Returns the reclaimed
177    /// `run_id` values so hybrid backends can re-enqueue claim queues.
178    async fn reclaim_expired_run_leases(&self, now: DateTime<Utc>) -> Result<Vec<String>>;
179
180    // --- Revisions ---
181
182    /// Append an immutable job revision snapshot (audit / rollback).
183    async fn append_revision(&self, revision: &JobRevision) -> Result<()>;
184
185    /// All revisions for a job, typically oldest-first.
186    async fn list_revisions(&self, job_id: &str) -> Result<Vec<JobRevision>>;
187
188    // --- Scripts ---
189
190    /// Insert or replace script metadata (name, signature hash).
191    async fn upsert_script(&self, script: &Script) -> Result<()>;
192
193    /// Look up persisted script metadata by name.
194    async fn get_script(&self, script_name: &str) -> Result<Option<Script>>;
195
196    // --- Run-once coordinator safety ---
197
198    /// Attempt exclusive claim for a run-once job before enqueueing its single run.
199    ///
200    /// Returns `true` when this `claimed_by` instance acquired the claim; `false` when another
201    /// coordinator already holds it or the job already completed.
202    async fn try_claim_run_once(
203        &self,
204        job_id: &str,
205        claimed_by: &str,
206        now: DateTime<Utc>,
207        claim_ttl_secs: i64,
208    ) -> Result<bool>;
209
210    /// Mark a run-once job as finished so future ticks skip it.
211    async fn mark_run_once_completed(
212        &self,
213        job_id: &str,
214        completed_at: DateTime<Utc>,
215    ) -> Result<()>;
216
217    /// Release a run-once claim when enqueue failed or the coordinator shut down cleanly.
218    async fn release_run_once_claim(
219        &self,
220        job_id: &str,
221        claimed_by: &str,
222        now: DateTime<Utc>,
223    ) -> Result<()>;
224
225    // --- Tick / partition coordinator ---
226
227    /// Due job ids owned by this coordinator's partition slice (distributed tick).
228    async fn find_due_job_ids_in_partitions(
229        &self,
230        owned_partitions: &[u32],
231        due_until: DateTime<Utc>,
232        limit: u32,
233    ) -> Result<Vec<String>>;
234
235    /// Earliest `next_run_at` among jobs in the owned partitions (sleep hint for tick loop).
236    async fn min_next_run_at_in_partitions(
237        &self,
238        owned_partitions: &[u32],
239    ) -> Result<Option<DateTime<Utc>>>;
240
241    /// Exclusive short-lived lease on a job row during tick processing.
242    ///
243    /// Prevents duplicate enqueue when multiple scheduler instances race on the same job.
244    async fn claim_job_for_tick(
245        &self,
246        job_id: &str,
247        claim_id: &str,
248        now: DateTime<Utc>,
249        lease_ttl_secs: i64,
250    ) -> Result<bool>;
251
252    /// Release tick claim after enqueue succeeds or the tick aborts.
253    async fn release_job_tick_claim(&self, job_id: &str) -> Result<()>;
254
255    /// Persist `next_run_at` (and related job fields) after a successful tick.
256    async fn persist_post_tick_job_state(
257        &self,
258        job_id: &str,
259        next_run_at: Option<DateTime<Utc>>,
260    ) -> Result<()>;
261
262    // --- Scheduler leader ---
263
264    /// Attempt to become the active scheduler leader (singleton row + TTL).
265    async fn try_acquire_leader(&self, instance_id: &str, ttl_secs: i64) -> Result<bool>;
266
267    /// Renew the leader lease while this instance remains leader.
268    async fn renew_leader_lease(&self, instance_id: &str, ttl_secs: i64) -> Result<()>;
269
270    /// Current leader row, if any (expired leases may still be returned for diagnostics).
271    async fn get_leader(&self) -> Result<Option<SchedulerLeader>>;
272
273    // --- Partitions / workers ---
274
275    /// Upsert partition ownership for coordinator sharding.
276    async fn upsert_partition_assignment(&self, assignment: &PartitionAssignment) -> Result<()>;
277
278    /// All partition assignments (rebalance / diagnostics).
279    async fn list_partition_assignments(&self) -> Result<Vec<PartitionAssignment>>;
280
281    /// Register or update a worker heartbeat row.
282    async fn register_worker(&self, worker: &Worker) -> Result<()>;
283
284    /// Update `last_heartbeat_at` for an existing worker.
285    async fn heartbeat_worker(&self, worker_id: &str, at: DateTime<Utc>) -> Result<()>;
286}