lilydocs
HTTP APIWebSocketConsumer
Fundamentals3 min read

Queue services and handlers

Make a DI service own queue handlers and understand which registrations become active.

A queue service is a DI service containing one or more message handlers. DI determines the service lifetime, #[queue] declares the message contract, and configuration defines the physical queue's runtime policy.

Register the worker service#

Rust
use lilyrs::{
    consumer::{Injectable, ServiceTrait},
    queue::{Json, QueueHandlerError, queue, queue_service},
};
use serde::Deserialize;

#[derive(Deserialize)]
struct OrderCreated { order_id: String }

#[derive(Default, Injectable)]
#[service(lifetime = "Singleton")]
struct OrderWorker;
impl ServiceTrait for OrderWorker {}

#[queue_service]
impl OrderWorker {
    #[queue("orders.created", version = 1, content = "json")]
    async fn created(&self, Json(event): Json<OrderCreated>) -> Result<(), QueueHandlerError> {
        println!("{}", event.order_id);
        Ok(())
    }
}

This fragment is the worker portion of the first application; its process entry calls Consumer::run().await. Injectable creates the DI registration, while queue_service supplies handler metadata. When moving a worker to another module, include that module in the compiled Rust module tree.

Macro and signature rules#

Rule Canonical form
Impl Non-generic inherent impl Worker
Outer attribute Argument-free #[queue_service], above service markers
Handler async fn name(&self, ...) -> Result<(), E>; E: Into<QueueHandlerError>
Parameter Owned extractor; at most 16 extractors
Payload At most one terminal payload extractor, placed last
Queue marker Explicit queue, version, content; no trailing argument comma

&mut self, generic handlers, reference extractors, and non-unit success values do not satisfy this contract. #[queue] is not placed on a free function. concurrency, retry, and retention are configuration settings, rather than macro arguments; see configuration.

Multiple contracts on one queue#

Flow
(orders.created, 1, json)   → OrderWorker::created_v1
(orders.created, 2, json)   → OrderWorker::created_v2
(orders.created, 2, binary) → OrderWorker::created_binary_v2

Every queue/version/content tuple must be unique. This example has one physical orders.created queue entry in TOML, with three handlers selected by message contract. Registering the same tuple in another worker causes a startup error; an ambiguous registration remains invalid even when its queue is inactive in the selected configuration.

A valid linked handler whose queue is absent from configuration does not create active consumption. Conversely, a configured physical queue with no handler is an error. Handler selection does not make the broker route headers to different processes; rollout order must account for the versions each replica accepts.

Service lifetime and pipeline#

A Singleton worker can retain shared, thread-safe application state. A Scoped worker resolves within the admitted delivery's scope after middleware and guards. Singleton ownership is not mandatory, but a DI registration is required. Use the Service extractor to obtain business services in handlers.

Service and handler levels can use #[middleware(Type)] and #[guard(Type)] markers. Each marker accepts one type and is processed by the outer #[queue_service]. See middleware and guards for registration order.

Sources for this page v0.1.0

This guide is based on the reviewed framework commit.

QUICK START
Documentation
Source code