Jobs
A job is any type that is Serialize + DeserializeOwned + Send + Sync + 'static and derives Job. The derive binds it to one variant of one queue
set, and that binding is what makes the rest of the API type-safe.
#[derive(Debug, Serialize, Deserialize, Job)]#[job(queue = AppQueues::Emails, retry(max_attempts = 5))]struct SendEmail { to: String, body: String,}The attributes
Section titled “The attributes”| Key | Default | Meaning |
|---|---|---|
queue = AppQueues::Emails |
required | The variant this job lives on. Everything before the last segment is the queue set type |
name = "emails.send" |
module_path!() + "::" + type name |
The job type name carried in every envelope and used to route to a handler |
retry(...) |
inherit from the queue | Retry policy override for this job type |
crate = "path" |
auto | Same as on #[queues]; normally unnecessary |
The queue path is doing double duty: its last segment becomes the constant
Job::QUEUE, and everything before it becomes the associated type
Job::Queue. That is why it needs at least two segments —
#[job(queue = Emails)] alone cannot say which queue set Emails belongs
to, and is a compile error.
Full ranges and error messages are in Job attributes.
What the derive generates
Section titled “What the derive generates”pub trait Job: Serialize + DeserializeOwned + Send + Sync + 'static { type Queue: QueueSet; const NAME: &'static str; const QUEUE: Self::Queue; fn retry_policy() -> Option<RetryPolicy> { None }}NAME is the string that travels in every envelope’s job_type field and is
the key a worker routes on. It defaults to the fully qualified type name, so
two identically named structs in different modules do not collide.
retry_policy() returns None unless the job has its own retry(...), and
None means “use whatever the queue says”. See
Retries and backoff for the precedence rules.
Mix-ups do not compile
Section titled “Mix-ups do not compile”Producer<Q, _> and Worker<Q, _> are generic over the queue set, and every
method that takes a job requires J: Job<Queue = Q>. A job from another
application’s queue set is therefore a type error rather than a message that
quietly lands on the wrong queue:
producer.enqueue(&BuildReport { id: 1 }).await?; // BuildReport belongs to OtherQueueserror[E0271]: type mismatch resolving `<BuildReport as Job>::Queue == AppQueues` --> tests/compile_fail/enqueue_foreign_job.rs:35:22 |35 | producer.enqueue(&BuildReport { id: 1 }).await.unwrap(); | ------- ^^^^^^^^^^^^^^^^^^^^^^ type mismatch resolving `<BuildReport as Job>::Queue == AppQueues` | | | required by a bound introduced by this call |note: expected this to be `AppQueues` --> tests/compile_fail/enqueue_foreign_job.rs:25:15 |25 | #[job(queue = OtherQueues::Reports)] | ^^^^^^^^^^^note: required by a bound in `queuey::Producer::<Q, B>::enqueue` | | pub async fn enqueue<J: Job<Queue = Q>>(&self, job: &J) -> Result<uuid::Uuid> { | ^^^^^^^^^ required by this bound in `Producer::<Q, B>::enqueue`The same bound rejects registering a foreign handler on a worker, and rejects
wrapping a foreign job in FnHandler. All three cases are covered by
compile-fail tests in the repository.
Payloads
Section titled “Payloads”The payload is serialized to JSON and carried in the envelope’s payload
field, so anything serde can represent works: nested structs, enums, Option,
collections, Uuid, chrono types with their serde features on.
Two practical constraints:
Keep payloads small and stable. A message may sit in a queue across a
deploy, so a payload has to be decodable by the next version of your code. A
field added with #[serde(default)] is safe; a field removed or retyped is not.
A body that no longer decodes is dead-lettered with decode error: ... rather
than retried, because retrying it would never succeed.
Reference, do not embed. Put an id in the job and load the row in the handler. That keeps the queue small and means the handler always works on current data rather than on a snapshot from whenever the job was enqueued.
Enums as jobs
Section titled “Enums as jobs”Nothing requires a job to be a struct. Any serde-able type works, including
an enum, which is a compact way to model a small family of related work on one
queue:
#[derive(Debug, Serialize, Deserialize, Job)]#[job(queue = AppQueues::Emails)]enum Notify { Welcome { user_id: u64 }, PasswordReset { user_id: u64, token: String },}One handler then receives all variants. Generic job types are not
supported: #[derive(Job)] on a type with type parameters is a compile error,
because NAME and QUEUE are constants and could not be monomorphised into a
single wire identity.