Publish your first message
Send a canonical Lily message from a separate publisher and interpret broker confirms correctly.
To see the Consumer process a message, you need a producer using the same contract. This page creates a separate Cargo application that publishes JSON to the orders exchange with the orders.created routing key.
Prepare topology first#
Start your first Consumer and wait for queue registration to complete. Neither QueueClientService initialization nor publish_json creates exchanges, queues, or bindings. The orders exchange and orders.created binding must exist when publishing.
If deployment requires the publisher to create topology, use the explicit RabbitMqTopologyBootstrap::from_client(...).run(...) operational step; named publishers use from_cell. The topology guide covers that separate decision. This workflow keeps topology ownership with the Consumer.
Prepare the publisher project#
cargo new orders-publisher
cd orders-publisher[package]
name = "orders-publisher"
version = "0.1.0"
edition = "2024"
rust-version = "1.96.1"
[dependencies]
lilyrs = { version = "0.1.0", features = ["queue-client", "injection"] }
serde_json = "1"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }This process uses queue-client for publishing and injection because it constructs its own container. Keep the Consumer project's manifest unchanged: the two applications have separate feature profiles.
Download publisher.toml below and place it at the publisher project's root. It is separate from the Consumer's lily.toml:
[queue_client]
mode = "single"
hostname = "127.0.0.1"
port = 5672
username = "guest"
password = "guest"
vhost = "/"
use_tls = false
pool_size = 1
persistence_enabled = truePublish the message#
Save the complete publisher file as src/main.rs:
use lilyrs::{
injection::ApplicationContainer,
queue_client::{PublishSchemaVersion, QueueClientService},
};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let container = ApplicationContainer::build().await?;
let publish = async {
let publisher = container.resolve::<QueueClientService>(None).await?;
publisher.publish_json(
"orders", "orders.created", PublishSchemaVersion::V1,
serde_json::json!({"order_id": "order-42"}),
).await?;
Ok::<(), Box<dyn std::error::Error>>(())
}.await;
let cleanup = container.close().await;
// Always await container cleanup, including after publish failure.
if let Err(error) = &cleanup { eprintln!("Publisher cleanup failed: {error}"); }
publish?;
cleanup?;
Ok(())
}PublishSchemaVersion::V1 is the application message version. publish_json sets body encoding, the json content kind, and the JSON MIME type together, and creates a fresh canonical event ID. The first handler accepts the same queue/version/content contract.
LILY_CONFIG_PATH=./publisher.toml LILY_CONFIG_MODE=development cargo runOn successful delivery, the Consumer terminal prints order-42. A publisher Ok result means the mandatory message was not returned as unroutable and a broker confirm ACK was received. It does not establish that the handler completed or a database transaction committed.
Manage resources and repeated publication#
The example awaits container cleanup even when publication fails, and reports cleanup errors. If you resolve a publisher from an HTTP or WebSocket handler, keep container shutdown with the host that owns it.
Use a fresh event ID for a new business event. When resending the same event, retain its identity through PublishMetadata::try_new(stored_id, version) and a publish method that accepts metadata. Generating a new ID on every attempt breaks the deduplication chain. Transactional outbox provides a separate model for durable database-backed publication.
Sources for this page v0.1.0
This guide is based on the reviewed framework commit.