Jobs are structs; the queue is a setting.

A job is a serialisable struct with its name, queue and retry count as constants. Dispatch it, and a worker runs it on whichever driver the environment chose.

What you get

  • Drivers: sync, memory, database, Redis, SQS and Kafka.
  • The kubernetes driver runs a job as its own batch/v1 Job pod when Job::kubernetes() returns a spec.
  • Every failed attempt is a JobAttemptFailed event, with final_attempt set on the last.
  • QueueManager::fake() records dispatches for tests: assert_pushed::<NotifyAuthor>().
  • Honest limit: the Redis queue can lose a job it has accepted.

In the docs

src/app/jobs/notify_author.rs
#[derive(Debug, Serialize, Deserialize)]
pub struct NotifyAuthor {
    pub post_id: u64,
}

#[async_trait]
impl Job for NotifyAuthor {
    const NAME: &'static str = "app.notify-author";
    const QUEUE: &'static str = "mail";
    const TRIES: u32 = 5;

    async fn handle(&self, context: &JobContext) -> Result<()> {
        let posts = context.resolve::<PostRepository>()?;
        let users = context.resolve::<UserRepository>()?;

        let Some(post) = posts.find(self.post_id.into()).await? else {
            return Ok(()); // deleted since it was queued: nothing to say
        };
        let Some(author) = users.find(post.author_id.into()).await? else {
            return Ok(());
        };
        context.resolve::<Notifier>()?.send(&author, &PostLive { post }).await?;
        Ok(())
    }
}
src/app/providers/app_provider.rs
events.listen(|event: Arc<PostPublished>| async move {
    Queue::instance().dispatch(NotifyAuthor { post_id: event.post.id }).await?;
    Ok(())
});
src/app/jobs/transcode_video.rs
use rainier_framework::queue::KubernetesJobSpec;

#[async_trait]
impl Job for TranscodeVideo {
    const NAME: &'static str = "media.transcode";
    const QUEUE: &'static str = "transcodes";

    // Return a spec and the `kubernetes` queue driver runs this job as its
    // own `batch/v1` Job pod, sized for it. `None` (the default) runs it
    // on an ordinary worker like any other job.
    fn kubernetes(&self) -> Option<KubernetesJobSpec> {
        Some(KubernetesJobSpec::new("4Gi", "6Gi", "250m", "1").with_tmpfs_scratch())
    }

    async fn handle(&self, context: &JobContext) -> Result<()> {
        /* … */
        Ok(())
    }
}

Up close

Select an image to see it full size.

A job declares its stable wire name, its queue and its retry budget as constants; failed() runs once, after the last attempt.
A job dispatched during a request carries that request trace, so the worker span joins the same trace minutes later.
The dispatch is part of the request that caused it: Lookout shows the job, and what it did, on the request timeline.
All features

Type to search 2.0.0 docs.