Publishing¶
The message name is the routing key. The exchange is a property of the publisher: the default
exchange unless .exchange("events") says otherwise. On the default exchange the routing key
addresses the queue with that name, which is why the quickstart works with no topology at all.
Messages are published persistent (delivery mode 2) by default; .persistent(false) opts out
for fire-and-forget traffic where losing messages on a broker restart is acceptable.
Well-known headers map onto native AMQP properties (content-type, correlation-id,
reply-to, message-id); every other header travels in the AMQP header table as a byte string,
so binary values round-trip.
Replying from a handler¶
The framework's publish(..) form works unchanged: the handler returns the reply value and the
runtime encodes and publishes it through the TypedPublisher the mount was given (see the
core publishing guide for the whole surface,
including per-publisher transforms and app-wide publish layers). The
request/reply page shows the RPC variant, where a transform redirects each
reply to the requester's private address.
Three publishers¶
broker.publisher() is fire-and-forget: the publish resolves when the frame is written, with no
broker feedback. Upgrade on the publisher when the guarantee matters:
.confirms()- publisher confirms: every publish resolves only once the broker confirmed it. Transactions buffer client-side and flush on commit. Durable and fast; the recommended transactional publisher..server_tx()- AMQP channel transactions (tx.select/tx.commit/tx.rollback): messages become visible atomically at commit. Slower (a synchronous round trip per commit), but the only option when partial flushes are unacceptable.
// The transactional flavour is picked on the publisher; swap `.confirms()` for
// `.server_tx()` to trade throughput for AMQP server-side atomicity.
let shipments = Shipments {
publisher: broker.publisher().confirms(),
};
The trade-off in one sentence: confirms give per-message durability (a failed commit may leave earlier messages published), server transactions give all-or-nothing visibility.
Transactional fan-out from a handler¶
A publisher is a value like any other shared resource: build it once, wire it into the typed
application state at startup, and let handlers request it by type (State<Shipments> below).
Here an order fans out into per-item shipment commands, published all-or-nothing:
// The application state wires the use-case object once at startup; `#[derive(FromRef)]` makes
// each field injectable into handlers as `State<FieldType>`.
#[derive(Clone, FromRef)]
struct AppState {
shipments: Shipments,
}
#[derive(Clone)]
struct Shipments {
publisher: ConfirmsPublisher,
}
impl Shipments {
/// Publishes one shipment command per item, all-or-nothing: commit resolves only after the
/// broker confirmed every message, and any failure aborts so shipments are never
/// half-visible.
async fn dispatch(&self, order: &Order) -> Result<(), AmqpError> {
self.publisher.begin_transaction().await?;
for item in &order.items {
let command = ItemShipment {
order_id: order.id,
item: item.clone(),
};
let payload = JsonCodec.encode(&command).expect("serializable");
let outgoing = OutgoingMessage::new("shipments", payload.as_ref());
if let Err(err) = self.publisher.publish(outgoing).await {
self.publisher.abort().await.ok();
return Err(err);
}
}
self.publisher.commit().await
}
}
#[subscriber("orders")]
async fn ship(order: &Order, State(shipments): State<Shipments>) -> HandlerResult {
if shipments.dispatch(order).await.is_err() {
// Nothing was committed; ask for redelivery and try the whole fan-out again.
return HandlerResult::retry();
}
HandlerResult::Ack
}
Both transactional publishers implement the framework's TransactionalPublisher, so either
plugs into the same begin_transaction / commit / abort call sites. Clones of a publisher share
the underlying channel and transaction state.