Background Job Queue
Requires the jobs feature:
[dependencies]rust-web-server = { version = "17", features = ["jobs"] }There are two queue types:
JobQueue— in-memory worker pool. Simple, no extra dependencies. Queued and retrying jobs are lost if the process crashes or restarts.PersistentJobQueue— backed by the model layer (model-sqlite/model-postgres/model-mysql). Jobs are written to arws_jobstable before being acknowledged, so pending and retrying jobs survive a restart.
JobQueue quick start
use rust_web_server::jobs::JobQueue;
let queue = JobQueue::new(4); // 4 worker threads
// In a request handler, after committing the signup:let to = "new-user@example.com".to_string();queue.submit(move || { // send_welcome_email(&to) — return Err(msg) to trigger a retry Ok(())});submit returns immediately; the job runs on one of the queue’s worker threads.
Retry with backoff
A job that returns Err is retried on the same worker thread with exponential
backoff. Configure the policy with .max_retries() / .backoff() (defaults:
3 retries, 500 ms initial backoff, 2x multiplier):
use rust_web_server::jobs::JobQueue;use std::time::Duration;
let queue = JobQueue::new(4) .max_retries(5) .backoff(Duration::from_millis(200), 2);max_retries counts retries after the first attempt — max_retries(2)
allows 3 total attempts (1 initial + 2 retries) before giving up. A job that
exhausts its retries is logged and dropped.
Named jobs
Implement the Job trait directly for jobs that need state or a stable name
in log lines, instead of a closure:
use rust_web_server::jobs::Job;
struct SendWelcomeEmail { to: String,}
impl Job for SendWelcomeEmail { fn run(&self) -> Result<(), String> { // send_welcome_email(&self.to) Ok(()) }
fn name(&self) -> &str { "send_welcome_email" // used in retry/failure log lines; defaults to "job" }}
queue.submit(SendWelcomeEmail { to: "new-user@example.com".to_string() });Shutdown
queue.join(); // stop accepting new jobs; wait for in-flight jobs (and their retries) to finishjoin() is mainly useful in tests and at process shutdown — most long-running
servers simply let the queue live for the process lifetime.
PersistentJobQueue
Additionally requires a model-* feature:
[dependencies]rust-web-server = { version = "17", features = ["jobs", "model-sqlite"] }Because a closure can’t be serialized to disk, persisted jobs are (job_type, payload) string pairs, dispatched at execution time to a handler registered
by name. Register every job_type your process enqueues before starting
workers — this includes the process that comes back up after a crash, since
recovery works by replaying rows from the rws_jobs table, not by replaying
in-memory closures.
# async fn example() -> Result<(), rust_web_server::model::DbError> {use rust_web_server::jobs::PersistentJobQueue;use rust_web_server::model::DbPool;use std::sync::Arc;
let pool = DbPool::from_env().await?;let queue = Arc::new(PersistentJobQueue::new(pool).await?);
queue.register("send_welcome_email", |payload| { // send_welcome_email(payload) — payload is whatever string you enqueued Ok(())});
let _worker_handles = Arc::clone(&queue).start(4); // 4 polling worker threads
// From a request handler:queue.enqueue("send_welcome_email", "new-user@example.com").await?;# Ok(())# }Crash safety
PersistentJobQueue::new() creates the rws_jobs table if it doesn’t exist,
then resets any row left status = 'running' back to pending — those rows
were being processed when the previous instance crashed or was killed, so they
get picked up again on the next poll.
Completed jobs are deleted from the table. A job that exhausts its retries is
left with status = 'failed' and its last error in last_error, for
inspection, instead of being deleted.
Configuration
# async fn example() -> Result<(), rust_web_server::model::DbError> {use rust_web_server::jobs::PersistentJobQueue;use rust_web_server::model::DbPool;use std::time::Duration;
let pool = DbPool::from_env().await?;let queue = PersistentJobQueue::new(pool).await? .max_retries(5) // default: 3 (retries after the first attempt) .backoff(Duration::from_millis(500), 2) // default: 500ms initial, 2x multiplier .poll_interval(Duration::from_millis(250)); // how often idle workers check for due jobs (default: 500ms)# Ok(())# }Use enqueue_with_retries(job_type, payload, max_retries) to override the
retry count for a single job instead of the queue-wide default.
Testing without a background poll loop
tick() runs a single poll-claim-execute cycle and returns whether a job was
claimed — useful in tests, or for embedding the queue in a caller-owned loop
instead of start()’s dedicated worker threads:
# async fn example() -> Result<(), rust_web_server::model::DbError> {use rust_web_server::jobs::PersistentJobQueue;use rust_web_server::model::DbPool;
let pool = DbPool::memory().await?;let queue = PersistentJobQueue::new(pool).await?;queue.register("noop", |_payload| Ok(()));queue.enqueue("noop", "").await?;
assert_eq!(true, queue.tick().await?); // ran the jobassert_eq!(false, queue.tick().await?); // nothing left to do# Ok(())# }