Versioning and rollouts
Run compatible handlers together with exact version/content dispatch and plan replica rollouts.
The dispatch key is the tuple of physical queue name, positive schema version and exact content kind. The version belongs to the application’s message contract, not the framework. One physical queue can dispatch to V1 and V2 handlers in the same binary without defining a second subscription.
Add old and new handlers together#
This module registers two JSON contracts for the same queue. If it lives in a separate file, include it with mod orders; in the application module tree; TOML still contains one orders.created entry.
use lilyrs::consumer::{Injectable, ServiceTrait};
use lilyrs::queue::{Json, QueueHandlerError, queue, queue_service};
use serde::Deserialize;
#[derive(Deserialize)]
struct OrderV1 { order_id: String }
#[derive(Deserialize)]
struct OrderV2 { order_id: String, source: String }
#[derive(Injectable)]
#[service(lifetime = "Singleton")]
struct OrderWorker;
impl ServiceTrait for OrderWorker {}
#[queue_service]
impl OrderWorker {
#[queue("orders.created", version = 1, content = "json")]
async fn v1(&self, Json(event): Json<OrderV1>) -> Result<(), QueueHandlerError> {
apply_order(event.order_id, "legacy".into()).await
}
#[queue("orders.created", version = 2, content = "json")]
async fn v2(&self, Json(event): Json<OrderV2>) -> Result<(), QueueHandlerError> {
apply_order(event.order_id, event.source).await
}
}
async fn apply_order(id: String, source: String) -> Result<(), QueueHandlerError> {
if id.is_empty() || source.is_empty() {
return Err(QueueHandlerError::permanent("ORDER_INVALID"));
}
Ok(())
}The application converts V1 into its shared model here. Lily does not automatically upcast, infer a JSON shape or choose the nearest version. version must be a literal in 1–65535; content is required, and a trailing comma after the final macro argument is rejected.
What happens to unknown contracts?#
| Incoming contract | Outcome |
|---|---|
| Registered version/content | Corresponding handler plan |
| Version not registered | Permanent QUEUE_SCHEMA_VERSION_UNSUPPORTED |
| Version exists but content does not | Permanent QUEUE_CONTENT_KIND_UNSUPPORTED |
| Same tuple linked twice | Startup configuration error |
Unknown version/content is rejected before a scope opens or middleware runs. Application middleware therefore cannot observe every poison message. Linked duplicate checks apply even when that queue is inactive in the current configuration.
Sequence the replica rollout#
- Prepare a binary that accepts both V1 and V2 and deploy it to every Consumer replica.
- Confirm that no V1-only replicas remain and check queue readiness.
- Switch publishers to producing V2.
- Remove V1 support only after accounting for broker backlog, retry/DLQ replay and producer migration.
RabbitMQ does not select a compatible binary using the Lily version header when choosing a competing consumer. Mixing V1-only and V2-only replicas on one queue can cause the wrong replica to reject a message permanently. Use separate physical queues and bindings for deliberately incompatible processes.
Identity and durable records#
Preserve the stored event ID when resending the same event; generating a new identity for every send changes deduplication behavior. With transactional inbox, handler identity/name and the retention horizon also form part of the rollout decision. Apply the migrations and retention guide alongside this one.
Sources for this page v0.1.0
This guide is based on the reviewed framework commit.