1#[cfg(feature = "queue-redis")]
29pub mod redis;
31#[cfg(feature = "queue-sqlite")]
32pub mod sqlite;
34pub mod worker;
36
37#[cfg(test)]
38mod lease_tests;
39#[cfg(all(test, feature = "queue-sqlite", not(miri)))]
40mod recovery_tests;
41#[cfg(all(test, feature = "queue-sqlite"))]
42mod tests;
43#[cfg(test)]
44mod worker_tests;
45
46#[cfg(feature = "queue-redis")]
47pub use redis::redis_driver::RedisDriver;
48#[cfg(feature = "queue-sqlite")]
49pub use sqlite::SqliteDriver;
50pub use worker::{JobHandler, Worker, WorkerHandle};
51
52use async_trait::async_trait;
53use serde_json::Value;
54use std::sync::Arc;
55use std::time::{Duration, SystemTime, UNIX_EPOCH};
56use uuid::Uuid;
57
58const MAX_SCHEDULE_DELAY: Duration = Duration::from_secs(366 * 24 * 60 * 60);
59
60#[cfg_attr(
63 not(any(feature = "queue-sqlite", feature = "queue-redis")),
64 allow(dead_code)
65)]
66pub(crate) const DEFAULT_MAX_STALLED_LEASES: u32 = 5;
67
68#[derive(Debug, thiserror::Error)]
72#[non_exhaustive]
73pub enum QueueError {
74 #[error("Queue driver error: {0}")]
76 Driver(String),
77 #[error("Queue serialization error: {0}")]
79 Serialization(String),
80 #[error("No handler registered for job: {0}")]
82 HandlerNotFound(String),
83 #[error("Job execution failed: {0}")]
85 JobFailed(String),
86 #[error("the queue worker requires an active Tokio runtime")]
88 RuntimeUnavailable,
89 #[error("invalid queue worker configuration: {0}")]
91 InvalidConfiguration(String),
92 #[error("job '{job_id}' could not transition via '{operation}': {message}")]
94 StateTransition {
95 job_id: String,
97 operation: &'static str,
99 message: String,
101 },
102 #[error("job '{job_id}' exceeded its {timeout_ms}ms execution timeout")]
104 JobTimedOut {
105 job_id: String,
107 timeout_ms: u64,
109 },
110 #[error("job '{job_id}' handler panicked")]
112 JobPanicked {
113 job_id: String,
115 },
116 #[error("queue worker task failed: {0}")]
118 WorkerTask(String),
119 #[error("queue operation is unsupported: {0}")]
121 Unsupported(String),
122}
123
124#[derive(Debug, Clone)]
128pub struct QueuedJob {
129 pub id: String,
131 pub name: String,
133 pub payload: Value,
135 pub attempts: u32,
137}
138
139#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
141pub struct QueuedJobDetail {
142 pub id: String,
144 pub name: String,
146 pub payload: String,
148 pub status: String,
150 pub error: Option<String>,
152 pub attempts: i32,
154 pub created_at: String,
156 pub updated_at: String,
158}
159
160#[async_trait]
167pub trait QueueDriver: Send + Sync {
168 async fn push(&self, id: &str, job_name: &str, payload: &str) -> Result<(), QueueError>;
170 async fn push_at(
176 &self,
177 id: &str,
178 job_name: &str,
179 payload: &str,
180 available_at: SystemTime,
181 ) -> Result<(), QueueError> {
182 if available_at > SystemTime::now() {
183 Err(QueueError::Unsupported(
184 "this driver cannot persist future scheduled jobs".to_string(),
185 ))
186 } else {
187 self.push(id, job_name, payload).await
188 }
189 }
190 async fn pop(&self) -> Result<Option<QueuedJob>, QueueError>;
197 async fn mark_complete(&self, job_id: &str) -> Result<(), QueueError>;
203 async fn mark_failed(&self, job_id: &str, error: &str) -> Result<(), QueueError>;
205 async fn requeue(&self, _job_id: &str, _reason: &str) -> Result<(), QueueError> {
207 Err(QueueError::Unsupported(
208 "this driver cannot requeue interrupted jobs".to_string(),
209 ))
210 }
211 async fn mark_complete_attempt(&self, job_id: &str, _attempt: u32) -> Result<(), QueueError> {
220 self.mark_complete(job_id).await
221 }
222 async fn mark_failed_attempt(
227 &self,
228 job_id: &str,
229 _attempt: u32,
230 error: &str,
231 ) -> Result<(), QueueError> {
232 self.mark_failed(job_id, error).await
233 }
234 async fn requeue_attempt(
239 &self,
240 job_id: &str,
241 _attempt: u32,
242 reason: &str,
243 ) -> Result<(), QueueError> {
244 self.requeue(job_id, reason).await
245 }
246 async fn requeue_attempt_after(
255 &self,
256 _job_id: &str,
257 _attempt: u32,
258 _reason: &str,
259 _delay: Duration,
260 ) -> Result<(), QueueError> {
261 Err(QueueError::Unsupported(
262 "this driver cannot defer claimed jobs".to_string(),
263 ))
264 }
265 async fn recover_stalled(&self, _stale_after: std::time::Duration) -> Result<u64, QueueError> {
271 Err(QueueError::Unsupported(
272 "this driver cannot recover stalled jobs".to_string(),
273 ))
274 }
275 async fn pending_count(&self) -> Result<u64, QueueError>;
277 async fn list_all_jobs(&self, _limit: u32) -> Result<Vec<QueuedJobDetail>, QueueError> {
279 Err(QueueError::Unsupported(
280 "this queue driver does not expose job inspection".to_string(),
281 ))
282 }
283 async fn retry_failed_job(&self, _job_id: &str) -> Result<(), QueueError> {
285 Err(QueueError::Unsupported(
286 "this queue driver does not expose failed-job retry".to_string(),
287 ))
288 }
289 async fn purge_completed_jobs(&self) -> Result<(), QueueError> {
291 Err(QueueError::Unsupported(
292 "this queue driver does not expose failed-job purge".to_string(),
293 ))
294 }
295 async fn purge_failed_jobs(&self) -> Result<(), QueueError> {
297 self.purge_completed_jobs().await
298 }
299 async fn purge_completed_history(&self) -> Result<(), QueueError> {
301 Err(QueueError::Unsupported(
302 "this queue driver does not retain completed job history".to_string(),
303 ))
304 }
305}
306
307pub struct Queue {
313 driver: Arc<Box<dyn QueueDriver>>,
314}
315
316impl Queue {
317 #[cfg(feature = "queue-sqlite")]
319 pub async fn sqlite(database_url: impl Into<String>) -> Result<Self, QueueError> {
320 let driver = SqliteDriver::new(database_url).await?;
321 Ok(Self {
322 driver: Arc::new(Box::new(driver)),
323 })
324 }
325
326 #[cfg(feature = "queue-sqlite")]
331 pub async fn sqlite_with_completed_history(
332 database_url: impl Into<String>,
333 retained_jobs: usize,
334 ) -> Result<Self, QueueError> {
335 let driver = SqliteDriver::new(database_url)
336 .await?
337 .try_with_completed_history_limit(retained_jobs)?;
338 Ok(Self {
339 driver: Arc::new(Box::new(driver)),
340 })
341 }
342
343 #[cfg(feature = "queue-redis")]
345 #[cfg_attr(mutants, mutants::skip)]
346 pub fn redis(redis_url: impl Into<String>) -> Result<Self, QueueError> {
347 let driver = redis::redis_driver::RedisDriver::new(redis_url)?;
348 Ok(Self {
349 driver: Arc::new(Box::new(driver)),
350 })
351 }
352
353 pub fn custom(driver: Box<dyn QueueDriver>) -> Self {
355 Self {
356 driver: Arc::new(driver),
357 }
358 }
359
360 pub async fn dispatch(&self, job_name: &str, payload: Value) -> Result<String, QueueError> {
362 let id = Uuid::new_v4().to_string();
363 let payload_str = serde_json::to_string(&payload)
364 .map_err(|e| QueueError::Serialization(e.to_string()))?;
365 self.driver.push(&id, job_name, &payload_str).await?;
366 Ok(id)
367 }
368
369 pub async fn dispatch_at(
375 &self,
376 job_name: &str,
377 payload: Value,
378 available_at: SystemTime,
379 ) -> Result<String, QueueError> {
380 if available_at.duration_since(UNIX_EPOCH).is_err() {
381 return Err(QueueError::InvalidConfiguration(
382 "scheduled timestamp predates the Unix epoch".to_string(),
383 ));
384 }
385 let now = SystemTime::now();
386 if available_at
387 .duration_since(now)
388 .is_ok_and(|delay| delay > MAX_SCHEDULE_DELAY)
389 {
390 return Err(QueueError::InvalidConfiguration(
391 "scheduled jobs may be dispatched at most 366 days ahead".to_string(),
392 ));
393 }
394 let id = Uuid::new_v4().to_string();
395 let payload_str = serde_json::to_string(&payload)
396 .map_err(|error| QueueError::Serialization(error.to_string()))?;
397 self.driver
398 .push_at(&id, job_name, &payload_str, available_at)
399 .await?;
400 Ok(id)
401 }
402
403 pub async fn pending_count(&self) -> Result<u64, QueueError> {
405 self.driver.pending_count().await
406 }
407
408 pub async fn list_all_jobs(&self, limit: u32) -> Result<Vec<QueuedJobDetail>, QueueError> {
410 self.driver.list_all_jobs(limit).await
411 }
412
413 #[cfg_attr(mutants, mutants::skip)]
415 pub async fn retry_failed_job(&self, job_id: &str) -> Result<(), QueueError> {
416 self.driver.retry_failed_job(job_id).await
417 }
418
419 pub async fn purge_failed_jobs(&self) -> Result<(), QueueError> {
421 self.driver.purge_failed_jobs().await
422 }
423
424 pub async fn purge_completed_history(&self) -> Result<(), QueueError> {
426 self.driver.purge_completed_history().await
427 }
428
429 #[deprecated(
431 since = "12.0.0",
432 note = "use purge_failed_jobs; this method has always targeted failed rows"
433 )]
434 #[cfg_attr(mutants, mutants::skip)]
435 pub async fn purge_completed_jobs(&self) -> Result<(), QueueError> {
436 self.purge_failed_jobs().await
437 }
438
439 pub(crate) fn driver_ref(&self) -> Arc<Box<dyn QueueDriver>> {
441 Arc::clone(&self.driver)
442 }
443}
444
445#[cfg(any(feature = "queue-sqlite", feature = "queue-redis"))]
446pub(crate) fn unix_timestamp_millis_ceil(timestamp: SystemTime) -> Result<u64, QueueError> {
447 let duration = timestamp.duration_since(UNIX_EPOCH).map_err(|_| {
448 QueueError::InvalidConfiguration("timestamp predates Unix epoch".to_string())
449 })?;
450 let whole_millis = duration.as_millis();
451 let has_fractional_millisecond = duration.subsec_nanos() % 1_000_000 != 0;
452 let rounded_millis = whole_millis
453 .checked_add(u128::from(has_fractional_millisecond))
454 .ok_or_else(|| {
455 QueueError::InvalidConfiguration("timestamp exceeds queue storage range".to_string())
456 })?;
457 u64::try_from(rounded_millis).map_err(|_| {
458 QueueError::InvalidConfiguration("timestamp exceeds queue storage range".to_string())
459 })
460}
461
462#[cfg(feature = "queue-sqlite")]
463pub(crate) fn unix_timestamp_millis_floor(timestamp: SystemTime) -> Result<u64, QueueError> {
464 let duration = timestamp.duration_since(UNIX_EPOCH).map_err(|_| {
465 QueueError::InvalidConfiguration("timestamp predates Unix epoch".to_string())
466 })?;
467 u64::try_from(duration.as_millis()).map_err(|_| {
468 QueueError::InvalidConfiguration("timestamp exceeds queue storage range".to_string())
469 })
470}