Skip to main content

azums_core/backend/
mod.rs

1pub mod memory;
2pub mod mock;
3pub mod stream;
4
5pub use memory::{MemoryAttempt, MemoryBackend};
6pub use mock::{CallRecord, MockBackend};
7pub use stream::StreamBackend;
8
9use crate::model::{Job, JobListItem, NewJob};
10use async_trait::async_trait;
11use chrono::{DateTime, Utc};
12use std::pin::Pin;
13use uuid::Uuid;
14
15/// Type alias for asynchronous notification event streams produced by [`StorageBackend::subscribe`].
16pub type NotificationStream = Pin<Box<dyn futures_core::Stream<Item = ()> + Send>>;
17
18/// Async, backend-agnostic storage interface for job queue operations.
19///
20/// Implementations of `StorageBackend` manage job persistence, leasing, retry scheduling,
21/// Dead-Letter Queue (DLQ) routing, maintenance archiving, and health probes.
22#[async_trait]
23pub trait StorageBackend: Send + Sync {
24    /// Returns reference to StreamBackend if supported by this storage implementation.
25    fn as_stream(&self) -> Option<&dyn StreamBackend> {
26        None
27    }
28
29    /// Executes backend schema migrations or setup steps.
30    async fn run_migrations(&self) -> anyhow::Result<()>;
31
32    /// Performs a health check to verify backend connectivity and readiness.
33    async fn health_check(&self) -> anyhow::Result<()>;
34
35    /// Enqueues a new job into the backend queue.
36    async fn enqueue(&self, job: NewJob) -> anyhow::Result<Uuid>;
37
38    /// Subscribes to job enqueue notification events for a specific queue.
39    async fn subscribe(&self, queue: &str) -> anyhow::Result<NotificationStream>;
40
41    /// Leases up to `batch_size` runnable jobs for a specified worker ID.
42    async fn lease_jobs_batch(
43        &self,
44        queue: &str,
45        worker_id: &str,
46        lease_seconds: i64,
47        batch_size: i64,
48    ) -> anyhow::Result<Vec<Job>>;
49
50    /// Leases up to `batch_size` runnable jobs with specified queue ordering preference (`QueueOrdering`).
51    async fn lease_jobs_batch_with_ordering(
52        &self,
53        queue: &str,
54        worker_id: &str,
55        lease_seconds: i64,
56        batch_size: i64,
57        ordering: crate::model::QueueOrdering,
58    ) -> anyhow::Result<Vec<Job>> {
59        let _ = ordering;
60        self.lease_jobs_batch(queue, worker_id, lease_seconds, batch_size)
61            .await
62    }
63
64    /// Reaps expired locks from inactive workers, resetting their status back to queued.
65    async fn reap_expired_locks(&self) -> anyhow::Result<u64>;
66
67    /// Starts job execution attempt records, returning `(job_id, attempt_id, attempt_number)` tuples.
68    async fn start_attempts_batch(
69        &self,
70        dataset_ids: &[String],
71        job_ids: &[Uuid],
72        worker_id: &str,
73    ) -> anyhow::Result<Vec<(Uuid, Uuid, i32)>>;
74
75    /// Marks a single job execution attempt as succeeded.
76    async fn mark_succeeded(
77        &self,
78        job_id: Uuid,
79        attempt_id: Uuid,
80        worker_id: &str,
81        latency_ms: i32,
82    ) -> anyhow::Result<()>;
83
84    /// Marks a batch of job execution attempts as succeeded.
85    async fn mark_succeeded_batch(
86        &self,
87        dataset_id: &str,
88        updates: &[(Uuid, Uuid, i32)],
89        worker_id: &str,
90    ) -> anyhow::Result<()>;
91
92    /// Records a failed attempt and reschedules the job for a future retry attempt.
93    #[allow(clippy::too_many_arguments)]
94    async fn reschedule_for_retry(
95        &self,
96        job_id: Uuid,
97        attempt_id: Uuid,
98        worker_id: &str,
99        latency_ms: i32,
100        next_run_at: DateTime<Utc>,
101        error_code: &str,
102        error_message: &str,
103        attempt_no: i32,
104    ) -> anyhow::Result<()>;
105
106    /// Records a failed attempt and transitions the job to the Dead-Letter Queue (DLQ).
107    #[allow(clippy::too_many_arguments)]
108    async fn mark_dlq(
109        &self,
110        job_id: Uuid,
111        attempt_id: Uuid,
112        worker_id: &str,
113        latency_ms: i32,
114        reason_code: &str,
115        error_code: &str,
116        error_message: &str,
117        attempt_no: i32,
118    ) -> anyhow::Result<()>;
119
120    /// Moves succeeded jobs older than `cutoff` into an archive table or storage location.
121    async fn archive_succeeded_older_than(
122        &self,
123        cutoff: DateTime<Utc>,
124        limit: i64,
125    ) -> anyhow::Result<u64>;
126
127    /// Prunes attempt audit logs and decision records for succeeded jobs older than `cutoff`.
128    async fn delete_history_for_succeeded_older_than(
129        &self,
130        cutoff: DateTime<Utc>,
131        limit: i64,
132    ) -> anyhow::Result<(u64, u64)>;
133
134    /// Performs database maintenance (e.g., VACUUM ANALYZE in Postgres, PRAGMA incremental_vacuum in SQLite).
135    async fn perform_maintenance(&self) -> anyhow::Result<()> {
136        Ok(())
137    }
138
139    /// Extends the lease lock expiration for an in-flight running job.
140    /// Returns `true` if the lease was extended, `false` if the job lock was lost or reaped.
141    async fn extend_lease(
142        &self,
143        _job_id: Uuid,
144        _worker_id: &str,
145        _lease_seconds: i64,
146    ) -> anyhow::Result<bool> {
147        Ok(true)
148    }
149
150    /// Fetches a single job record by ID.
151    async fn get_job(&self, job_id: Uuid) -> anyhow::Result<Option<Job>>;
152
153    /// Fetches a list of jobs matching filters with cursor pagination.
154    async fn list_jobs(
155        &self,
156        queue: Option<&str>,
157        status: Option<&str>,
158        limit: i64,
159        cursor_created_at: Option<DateTime<Utc>>,
160        cursor_id: Option<Uuid>,
161    ) -> anyhow::Result<Vec<JobListItem>>;
162
163    /// Atomically replays a job by ID into the queue.
164    async fn replay_job(
165        &self,
166        job_id: Uuid,
167        override_queue: Option<&str>,
168        override_run_at: Option<DateTime<Utc>>,
169    ) -> anyhow::Result<Uuid>;
170
171    /// Dequeues and leases up to `batch_size` runnable jobs (alias for `lease_jobs_batch`).
172    async fn dequeue_and_lease(
173        &self,
174        queue: &str,
175        worker_id: &str,
176        lease_seconds: i64,
177        batch_size: i64,
178    ) -> anyhow::Result<Vec<Job>> {
179        self.lease_jobs_batch(queue, worker_id, lease_seconds, batch_size)
180            .await
181    }
182
183    /// Marks a job attempt as completed (alias for `mark_succeeded`).
184    async fn complete_job(
185        &self,
186        job_id: Uuid,
187        attempt_id: Uuid,
188        worker_id: &str,
189        latency_ms: i32,
190    ) -> anyhow::Result<()> {
191        self.mark_succeeded(job_id, attempt_id, worker_id, latency_ms)
192            .await
193    }
194
195    /// Reschedules a job for retry (alias for `reschedule_for_retry`).
196    #[allow(clippy::too_many_arguments)]
197    async fn retry_job(
198        &self,
199        job_id: Uuid,
200        attempt_id: Uuid,
201        worker_id: &str,
202        latency_ms: i32,
203        next_run_at: DateTime<Utc>,
204        error_code: &str,
205        error_message: &str,
206        attempt_no: i32,
207    ) -> anyhow::Result<()> {
208        self.reschedule_for_retry(
209            job_id,
210            attempt_id,
211            worker_id,
212            latency_ms,
213            next_run_at,
214            error_code,
215            error_message,
216            attempt_no,
217        )
218        .await
219    }
220
221    /// Moves a job to DLQ on failure (alias for `mark_dlq`).
222    #[allow(clippy::too_many_arguments)]
223    async fn fail_job(
224        &self,
225        job_id: Uuid,
226        attempt_id: Uuid,
227        worker_id: &str,
228        latency_ms: i32,
229        reason_code: &str,
230        error_code: &str,
231        error_message: &str,
232        attempt_no: i32,
233    ) -> anyhow::Result<()> {
234        self.mark_dlq(
235            job_id,
236            attempt_id,
237            worker_id,
238            latency_ms,
239            reason_code,
240            error_code,
241            error_message,
242            attempt_no,
243        )
244        .await
245    }
246}