Background Jobs
RustAPI provides a robust background job processing system through the rustapi-jobs crate. This allows you to offload time-consuming tasks (like sending emails, processing images, or generating reports) from the main request/response cycle, keeping your API fast and responsive.
Setup
First, add rustapi-jobs to your Cargo.toml. Since rustapi-jobs is not re-exported by the main crate by default, you must include it explicitly.
[dependencies]
rustapi-rs = "0.1"
rustapi-jobs = "0.1"
serde = { version = "1.0", features = ["derive"] }
async-trait = "0.1"
tokio = { version = "1.0", features = ["full"] }
Defining a Job
A job consists of a data structure (the payload) and an implementation of the Job trait.
#![allow(unused)]
fn main() {
use rustapi_jobs::{Job, JobContext, Result};
use serde::{Deserialize, Serialize};
use async_trait::async_trait;
// 1. Define the job payload
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct WelcomeEmailData {
pub user_id: String,
pub email: String,
}
// 2. Define the job handler struct
#[derive(Clone)]
pub struct WelcomeEmailJob;
// 3. Implement the Job trait
#[async_trait]
impl Job for WelcomeEmailJob {
// Unique name for the job type
const NAME: &'static str = "send_welcome_email";
// The payload type
type Data = WelcomeEmailData;
async fn execute(&self, ctx: JobContext, data: Self::Data) -> Result<()> {
println!("Processing job {} (attempt {})", ctx.job_id, ctx.attempt);
println!("Sending welcome email to {} ({})", data.email, data.user_id);
// Simulate work
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
Ok(())
}
}
}
Registering and Running the Queue
In your main application setup, you need to:
- Initialize the backend (Memory, Redis, or Postgres).
- Create the
JobQueue. - Register your job handlers.
- Start the worker loop in a background task.
- Add the
JobQueueto your application state so handlers can use it.
use rustapi_rs::prelude::*;
use rustapi_jobs::{JobQueue, InMemoryBackend};
// use crate::jobs::{WelcomeEmailJob, WelcomeEmailData}; // Import your job
#[tokio::main]
async fn main() -> std::io::Result<()> {
// 1. Initialize backend
// For production, use Redis or Postgres backend
let backend = InMemoryBackend::new();
// 2. Create queue
let queue = JobQueue::new(backend);
// 3. Register jobs
// You must register an instance of the job handler
queue.register_job(WelcomeEmailJob).await;
// 4. Start worker in background
let queue_for_worker = queue.clone();
tokio::spawn(async move {
if let Err(e) = queue_for_worker.start_worker().await {
eprintln!("Worker failed: {}", e);
}
});
// 5. Build application
RustApi::auto()
.with_state(queue) // Inject queue into state
.serve("127.0.0.1:3000")
.await
}
Enqueueing Jobs
You can now inject the JobQueue into your request handlers using the State extractor and enqueue jobs.
#![allow(unused)]
fn main() {
use rustapi_rs::prelude::*;
use rustapi_jobs::JobQueue;
#[rustapi::post("/register")]
async fn register_user(
State(queue): State<JobQueue>,
Json(payload): Json<RegisterRequest>,
) -> Result<impl IntoResponse, ApiError> {
// ... logic to create user in DB ...
let user_id = "user_123".to_string(); // Simulated ID
// Enqueue the background job
// The queue will handle serialization and persistence
queue.enqueue::<WelcomeEmailJob>(WelcomeEmailData {
user_id,
email: payload.email,
}).await.map_err(|e| ApiError::InternalServerError(e.to_string()))?;
Ok(Json(json!({
"status": "registered",
"message": "Welcome email will be sent shortly"
})))
}
#[derive(Deserialize)]
struct RegisterRequest {
username: String,
email: String,
}
}
Resilience and Retries
rustapi-jobs handles failures automatically. If your execute method returns an Err, the job will be:
- Marked as failed.
- Optionally scheduled for retry with exponential backoff if retries are enabled.
- Retried up to
max_attemptswhen you configure it viaEnqueueOptions.
By default, EnqueueOptions::new() sets max_attempts to 0, so a failed job will not be retried unless you explicitly opt in by calling .max_attempts(...) with a value greater than the current attempts count.
To customize retry behavior, use enqueue_opts:
#![allow(unused)]
fn main() {
use rustapi_jobs::EnqueueOptions;
queue.enqueue_opts::<WelcomeEmailJob>(
data,
EnqueueOptions::new()
.max_attempts(5) // Retry up to 5 times
.delay(std::time::Duration::from_secs(60)) // Initial delay
).await?;
}
Backends
While InMemoryBackend is great for testing and simple apps, production systems should use persistent backends:
- Redis: High performance, good for volatile queues. Enable
redisfeature inrustapi-jobs. - Postgres: Best for reliability and transactional safety. Enable
postgresfeature.
# In Cargo.toml
rustapi-jobs = { version = "0.1", features = ["redis"] }