pub async fn job_worker<AppState: AS>(
app_state: AppState,
registry: impl JobRegistry<AppState>,
sleep_duration: Duration,
max_retries: i32,
shutdown_token: CancellationToken,
config: JobWorkerConfig,
) -> Result<()>Expand description
Start a job worker that processes jobs from the queue.
The worker will continuously poll for jobs and execute them using the provided registry. Jobs are executed with automatic retry logic on failure.
§Arguments
app_state- The application state containing database connection and configurationregistry- The job registry that maps job names to their implementationssleep_duration- How long to sleep when no jobs are available. With an idle poll gate, only permit holders sleep on this interval; permitless workers park at admission.max_retries- Maximum number of times to retry a failed job before moving to the dead letter queue (default: 20)shutdown_token- Cancellation token for graceful shutdown. When cancelled, the worker will stop accepting new jobs and release database locks before exiting.config- Heartbeat interval, reclaim window, local shutdown drain budget, and optional shared idle poll gate
§Retry Behavior
When a job fails:
- The error count is incremented
- The error message and timestamp are recorded
- The job is requeued with exponential backoff: delay =
2^(error_count + 1)seconds (first retry: 2s, second: 4s, third: 8s, fourth: 16s, etc.) - If
error_count>=max_retries, the job is moved to the dead letter queue
§Graceful Shutdown
When the shutdown_token is cancelled:
- The worker stops polling for new jobs
- Any currently executing job gets a bounded drain to complete
- Remaining owned locks are released after the drain without charging an attempt
§Transient Error Resilience
Errors from the worker loop itself (e.g. the database being unreachable while trying to claim the next job, such as a pool acquire timeout during a brief database stall) are NOT fatal. They are logged at ERROR level and the worker retries with exponential backoff (starting at 1 second, doubling up to a 30 second cap, resetting after the next successful tick). This keeps a transient database hiccup from killing the worker task — and with it, apps that join on all worker tasks. Job execution failures are unaffected by this: they continue to use the per-job retry machinery described above.
§Schema Check
Before polling, the worker confirms the jobs and dead_letter_jobs
tables have the columns its queries use, and returns an error if the
database is missing cja migrations. Without this an out-of-date schema
boots fine and then fails every claim silently. A database that is merely
unreachable at startup is not an error; see transient error resilience
above.
§Lease Timing
If a worker crashes or becomes unresponsive while processing a job, the job will remain
locked in the database. JobWorkerConfig defaults to a 30-second heartbeat,
a 120-second reclaim window, and the shutdown budget’s job drain (2 seconds
when unset). An expired lock is charged as a failed attempt.
§Example
use std::time::Duration;
use tokio_util::sync::CancellationToken;
let shutdown_token = CancellationToken::new();
let worker_token = shutdown_token.clone();
// Start worker with the default heartbeat, reclaim, and drain settings
tokio::spawn(async move {
cja::jobs::worker::job_worker(
app_state,
registry,
Duration::from_secs(60), // poll every 60s when idle
20, // max 20 retries
worker_token, // for graceful shutdown
Default::default(),
).await.unwrap();
});
// Later, trigger shutdown
shutdown_token.cancel();