rainier_queue/lib.rs
1//! # rainier-queue
2//!
3//! Deferred work: the [`Job`] contract, the [`Queue`] port and its drivers, and
4//! the [`Worker`] that runs them.
5//!
6//! ```
7//! use rainier_queue::{Job, JobContext, JobRegistry, MemoryQueue, Queue, QueueManager};
8//! use serde::{Deserialize, Serialize};
9//! use std::sync::Arc;
10//!
11//! #[derive(Serialize, Deserialize)]
12//! struct SendWelcomeEmail { user_id: u64 }
13//!
14//! #[async_trait::async_trait]
15//! impl Job for SendWelcomeEmail {
16//! const NAME: &'static str = "mail.welcome";
17//! const QUEUE: &'static str = "mail";
18//!
19//! async fn handle(&self, _context: &JobContext) -> rainier_support::Result<()> {
20//! Ok(())
21//! }
22//! }
23//!
24//! # #[tokio::main] async fn main() -> rainier_support::Result<()> {
25//! let registry = Arc::new(JobRegistry::new().with::<SendWelcomeEmail>());
26//! let queue = Arc::new(MemoryQueue::new());
27//! let dispatcher = QueueManager::new(queue.clone(), registry);
28//!
29//! dispatcher.dispatch(SendWelcomeEmail { user_id: 1 }).await?;
30//! assert_eq!(queue.size("mail").await?, 1);
31//! # Ok(()) }
32//! ```
33//!
34//! ## A job crosses a process boundary
35//!
36//! Everything about the design follows from that. A job is written by a web
37//! request and read, later, by a worker that may be a different process on a
38//! different machine — so a job is a **serialisable payload plus a stable
39//! name**, not a closure, and the worker needs a [`JobRegistry`] to turn the
40//! name back into code. Dependencies cannot be captured either, so a running
41//! job resolves them from the container through its [`JobContext`].
42//!
43//! ## Drivers
44//!
45//! | Driver | Survives a restart | For |
46//! |---|---|---|
47//! | [`SyncQueue`] | — runs inline | development, and tests that want the side effect |
48//! | [`MemoryQueue`] | no | tests, single-process development |
49//! | [`DatabaseQueue`] | yes | production, on the database you already have |
50//! | `SqsQueue` | yes | production, managed — needs the `sqs` feature |
51//!
52//! ## Testing
53//!
54//! [`QueueManager::fake`] records dispatches instead of performing them:
55//!
56//! ```
57//! # use rainier_queue::{Job, JobContext, QueueManager};
58//! # use serde::{Deserialize, Serialize};
59//! # #[derive(Serialize, Deserialize)] struct SendInvoice;
60//! # #[async_trait::async_trait] impl Job for SendInvoice {
61//! # const NAME: &'static str = "billing.invoice";
62//! # async fn handle(&self, _: &JobContext) -> rainier_support::Result<()> { Ok(()) }
63//! # }
64//! # #[tokio::main] async fn main() -> rainier_support::Result<()> {
65//! let queue = QueueManager::fake();
66//! queue.dispatch(SendInvoice).await?;
67//! queue.assert_pushed_times::<SendInvoice>(1);
68//! # Ok(()) }
69//! ```
70
71#![deny(missing_docs)]
72#![deny(rustdoc::broken_intra_doc_links)]
73
74pub mod database;
75pub mod driver;
76pub mod job;
77#[cfg(feature = "kafka")]
78pub mod kafka;
79pub mod manager;
80pub mod queue;
81#[cfg(feature = "redis")]
82pub mod redis;
83#[cfg(feature = "sqs")]
84pub mod sqs;
85pub mod worker;
86
87pub use database::{DatabaseQueue, FailedJobRow, JobRow};
88pub use driver::QueueDriver;
89pub use job::{Job, JobContext, JobRegistry, QueuedJob};
90#[cfg(feature = "kafka")]
91pub use kafka::{require_shared as require_shared_locks, KafkaQueue};
92pub use manager::{PendingDispatch, QueueManager, SyncQueue};
93pub use queue::{FailedJob, MemoryQueue, Queue};
94#[cfg(feature = "redis")]
95pub use redis::{Keys as RedisQueueKeys, RedisQueue};
96#[cfg(feature = "sqs")]
97pub use sqs::SqsQueue;
98pub use worker::{
99 JobFailed, JobProcessed, JobProcessing, JobReleased, Outcome, Worker, WorkerOptions,
100 WorkerStats,
101};
102
103// Re-exported so job implementations get the attribute macro without adding
104// the dependency themselves.
105pub use async_trait::async_trait;