1pub mod memory;
2pub mod mock;
3pub mod observability;
4pub mod stream;
5
6pub use memory::{MemoryAttempt, MemoryBackend};
7pub use mock::{CallRecord, MockBackend};
8pub use observability::{JobExplanation, JobObservationEvent, ObservabilityBackend, QueueMetrics};
9pub use stream::StreamBackend;
10
11use crate::model::{BackendCapabilities, BackendSemanticCapabilities, Job, JobListItem, NewJob};
12use async_trait::async_trait;
13use chrono::{DateTime, Utc};
14use std::pin::Pin;
15use uuid::Uuid;
16
17pub type NotificationStream = Pin<Box<dyn futures_core::Stream<Item = ()> + Send>>;
19
20#[async_trait]
25pub trait StorageBackend: Send + Sync {
26 fn capabilities(&self) -> BackendCapabilities;
28
29 fn semantic_capabilities(&self) -> Option<BackendSemanticCapabilities> {
34 self.capabilities().semantics()
35 }
36
37 fn as_stream(&self) -> Option<&dyn StreamBackend> {
39 None
40 }
41
42 fn as_observability(&self) -> Option<&dyn ObservabilityBackend> {
44 None
45 }
46
47 async fn run_migrations(&self) -> anyhow::Result<()>;
49
50 async fn health_check(&self) -> anyhow::Result<()>;
52
53 async fn enqueue(&self, job: NewJob) -> anyhow::Result<Uuid>;
55
56 async fn subscribe(&self, queue: &str) -> anyhow::Result<NotificationStream>;
58
59 async fn lease_jobs_batch(
61 &self,
62 queue: &str,
63 worker_id: &str,
64 lease_seconds: i64,
65 batch_size: i64,
66 ) -> anyhow::Result<Vec<Job>>;
67
68 async fn lease_jobs_batch_with_ordering(
70 &self,
71 queue: &str,
72 worker_id: &str,
73 lease_seconds: i64,
74 batch_size: i64,
75 ordering: crate::model::QueueOrdering,
76 ) -> anyhow::Result<Vec<Job>> {
77 let _ = ordering;
78 self.lease_jobs_batch(queue, worker_id, lease_seconds, batch_size)
79 .await
80 }
81
82 async fn reap_expired_locks(&self) -> anyhow::Result<u64>;
84
85 async fn start_attempts_batch(
87 &self,
88 dataset_ids: &[String],
89 job_ids: &[Uuid],
90 worker_id: &str,
91 ) -> anyhow::Result<Vec<(Uuid, Uuid, i32)>>;
92
93 async fn mark_succeeded(
95 &self,
96 job_id: Uuid,
97 attempt_id: Uuid,
98 worker_id: &str,
99 latency_ms: i32,
100 ) -> anyhow::Result<()>;
101
102 async fn mark_succeeded_batch(
104 &self,
105 dataset_id: &str,
106 updates: &[(Uuid, Uuid, i32)],
107 worker_id: &str,
108 ) -> anyhow::Result<()>;
109
110 #[allow(clippy::too_many_arguments)]
112 async fn reschedule_for_retry(
113 &self,
114 job_id: Uuid,
115 attempt_id: Uuid,
116 worker_id: &str,
117 latency_ms: i32,
118 next_run_at: DateTime<Utc>,
119 error_code: &str,
120 error_message: &str,
121 attempt_no: i32,
122 ) -> anyhow::Result<()>;
123
124 #[allow(clippy::too_many_arguments)]
126 async fn mark_dlq(
127 &self,
128 job_id: Uuid,
129 attempt_id: Uuid,
130 worker_id: &str,
131 latency_ms: i32,
132 reason_code: &str,
133 error_code: &str,
134 error_message: &str,
135 attempt_no: i32,
136 ) -> anyhow::Result<()>;
137
138 async fn archive_succeeded_older_than(
140 &self,
141 cutoff: DateTime<Utc>,
142 limit: i64,
143 ) -> anyhow::Result<u64>;
144
145 async fn delete_history_for_succeeded_older_than(
147 &self,
148 cutoff: DateTime<Utc>,
149 limit: i64,
150 ) -> anyhow::Result<(u64, u64)>;
151
152 async fn perform_maintenance(&self) -> anyhow::Result<()> {
154 Ok(())
155 }
156
157 async fn extend_lease(
160 &self,
161 _job_id: Uuid,
162 _worker_id: &str,
163 _lease_seconds: i64,
164 ) -> anyhow::Result<bool> {
165 Ok(true)
166 }
167
168 async fn cancel_job(&self, job_id: Uuid, worker_id: Option<&str>) -> anyhow::Result<()>;
174
175 async fn get_job(&self, job_id: Uuid) -> anyhow::Result<Option<Job>>;
177
178 async fn list_jobs(
180 &self,
181 queue: Option<&str>,
182 status: Option<&str>,
183 limit: i64,
184 cursor_created_at: Option<DateTime<Utc>>,
185 cursor_id: Option<Uuid>,
186 ) -> anyhow::Result<Vec<JobListItem>>;
187
188 async fn replay_job(
190 &self,
191 job_id: Uuid,
192 override_queue: Option<&str>,
193 override_run_at: Option<DateTime<Utc>>,
194 ) -> anyhow::Result<Uuid>;
195
196 async fn dequeue_and_lease(
198 &self,
199 queue: &str,
200 worker_id: &str,
201 lease_seconds: i64,
202 batch_size: i64,
203 ) -> anyhow::Result<Vec<Job>> {
204 self.lease_jobs_batch(queue, worker_id, lease_seconds, batch_size)
205 .await
206 }
207
208 async fn complete_job(
210 &self,
211 job_id: Uuid,
212 attempt_id: Uuid,
213 worker_id: &str,
214 latency_ms: i32,
215 ) -> anyhow::Result<()> {
216 self.mark_succeeded(job_id, attempt_id, worker_id, latency_ms)
217 .await
218 }
219
220 #[allow(clippy::too_many_arguments)]
222 async fn retry_job(
223 &self,
224 job_id: Uuid,
225 attempt_id: Uuid,
226 worker_id: &str,
227 latency_ms: i32,
228 next_run_at: DateTime<Utc>,
229 error_code: &str,
230 error_message: &str,
231 attempt_no: i32,
232 ) -> anyhow::Result<()> {
233 self.reschedule_for_retry(
234 job_id,
235 attempt_id,
236 worker_id,
237 latency_ms,
238 next_run_at,
239 error_code,
240 error_message,
241 attempt_no,
242 )
243 .await
244 }
245
246 #[allow(clippy::too_many_arguments)]
248 async fn fail_job(
249 &self,
250 job_id: Uuid,
251 attempt_id: Uuid,
252 worker_id: &str,
253 latency_ms: i32,
254 reason_code: &str,
255 error_code: &str,
256 error_message: &str,
257 attempt_no: i32,
258 ) -> anyhow::Result<()> {
259 self.mark_dlq(
260 job_id,
261 attempt_id,
262 worker_id,
263 latency_ms,
264 reason_code,
265 error_code,
266 error_message,
267 attempt_no,
268 )
269 .await
270 }
271}