Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Unique Jobs

A unique key prevents duplicate jobs. When a job carries a UniqueKey, the server rejects a new enqueue whose key matches an existing job within the key’s scope — returning the existing job instead of creating a second one.

Note

Unique jobs require a Pro license on the server.

This solves problems like “only one rebuild_search_index job should be queued at a time”, or “don’t email the same user twice for the same event”.

Building a key

The recommended way to build a key from data is to hash it. UniqueKey::tagged_hash_of serialises a value to canonical JSON (object keys sorted, so the result is independent of field ordering) and hashes it with SHA-256. The tag — pass the job type name — is a readable prefix that keeps two job types from colliding on identical data. UniqueKey::hash_of is the same without the tag.

Pass whatever identifies the job: the whole payload (self), a single field (&self.field), or a tuple of fields for a subset:

#![allow(unused)]
fn main() {
use zizq::UniqueKey;
// Whole payload.
let _ =
UniqueKey::tagged_hash_of("send_email", ("alice@example.com", "Welcome!"))
;
}

Note

A tuple of fields serialises to a JSON array, so the order of its elements matters — keep it stable.

If you already have a key string, wrap it verbatim with UniqueKey::raw — no hashing or transformation is applied.

Supplying a key

There are two ways to attach a key to a job.

Per job type — override JobKind::unique_key to derive a key from the payload. Every enqueue of that type then carries it automatically:

#![allow(unused)]
fn main() {
use serde::{Deserialize, Serialize};
use zizq::{JobKind, UniqueKey};

#[derive(Serialize, Deserialize)]
struct SendWelcomeEmail {
    user_id: u64,
}

impl JobKind for SendWelcomeEmail {
    const NAME: &'static str = "send_welcome_email";

    fn unique_key(&self) -> Option<UniqueKey> {
        Some(UniqueKey::tagged_hash_of(Self::NAME, &self.user_id))
    }
}
}

Per enqueue — supply a key for a single call with EnqueueBuilder::unique_key. This overrides whatever the JobKind would derive:

#![allow(unused)]
fn main() {
use serde::{Deserialize, Serialize};
use zizq::{Client, JobKind, UniqueKey};
#[derive(Serialize, Deserialize)]
struct RebuildIndex;
impl JobKind for RebuildIndex { const NAME: &'static str = "rebuild_index"; }
async fn run(client: &Client) -> Result<(), zizq::ZizqError> {
client
    .enqueue(RebuildIndex)
    .unique_key(UniqueKey::raw("rebuild_index"))
    .await?;
Ok(()) }
}

Scope

A UniqueKey has a scope — the lifecycle window during which it blocks duplicates. Set it with UniqueScope:

ScopeA duplicate is rejected while an existing job is…
Queued waiting to run (ready or scheduled) — the default.
Active queued or currently being processed.
Exists present in any state at all, including completed or dead (until reaped by retention).

UniqueKey::raw(key) uses the default Queued scope. To choose a different scope, chain .scope(...):

#![allow(unused)]
fn main() {
use zizq::{UniqueKey, UniqueScope};

let key = UniqueKey::raw("rebuild_index").scope(UniqueScope::Active);
}

Detecting a duplicate

When an enqueue collides with an existing unique job, the call still succeeds — it returns the existing job, with its duplicate field set to Some(true):

#![allow(unused)]
fn main() {
use serde::{Deserialize, Serialize};
use zizq::{Client, JobKind, UniqueKey};
#[derive(Serialize, Deserialize)]
struct RebuildIndex;
impl JobKind for RebuildIndex { const NAME: &'static str = "rebuild_index"; }
async fn run(client: &Client) -> Result<(), zizq::ZizqError> {
let job = client
    .enqueue(RebuildIndex)
    .unique_key(UniqueKey::raw("rebuild_index"))
    .await?;

if job.duplicate == Some(true) {
    println!("a rebuild was already queued — job {}", job.id);
}
Ok(()) }
}