Skip to main content

Shared state and context

The payment client and the mailer open once at startup and reach every subscriber as State; the context tells a subscriber which delivery it holds.

A service shares clients, pools and caches between its subscribers. The app opens them in on_startup, before the first subscription opens, and an error there stops the startup. What the hook returns is the app's state, one value for the whole service. A subscriber takes one field of it as a State<T> parameter. The context is the per-delivery half: the subscription's name, the headers, the broker's fields of this delivery. A subscriber takes it as a &mut Context parameter, right after the message.

The state​

Shop holds the payment client and the mailer, each behind an Arc so a clone is cheap. FromRef projects each field out of the state: State<Arc<Gateway>> resolves through it. open is the hook the app passes to on_startup in chapter 5.

state.rs
/// The app's state: each field reaches a subscriber as `State<FieldType>`.
#[derive(Clone, FromRef)]
pub(crate) struct Shop {
pub(crate) gateway: Arc<Gateway>,
pub(crate) mailer: Arc<Mailer>,
}

impl Shop {
/// Opens the clients. The app runs it once, before the first subscription opens; an error
/// stops the startup.
pub(crate) async fn open() -> Result<Self, MailError> {
Ok(Self {
gateway: Arc::new(Gateway::new(CARD_LIMIT_CENTS, CHARGES_PER_SECOND)),
mailer: Arc::new(Mailer::connect("smtp://mail.internal:25").await?),
})
}
}

The manual form writes the two impls the derive generates, one per field.

The state and the context in one subscriber​

send e-mails a notice through the mailer from the state. A mailbox that does not exist stays that way, so a refused notice is dropped, and the warning names the subscription from the context.

notifications.rs
/// E-mails a notice to the customer.
// The mailer comes from the app's state; the context names the subscription in the warning.
#[subscriber("notices")]
async fn send(
notice: &Notice,
ctx: &mut Context<'_, (), Shop>,
State(mailer): State<Arc<Mailer>>,
) -> HandlerOutcome {
match mailer.send(&notice.customer, &notice.text).await {
Ok(()) => HandlerOutcome::ack(),
Err(error) => {
// A refused mailbox stays refused; another try would only repeat the warning.
tracing::warn!(subscription = ctx.name(), %error, "notice not sent");
HandlerOutcome::drop()
}
}
}

In the manual form the state type is the last parameter of Handle, and the mount checks it against the app's. The body reads the field with from_context, which is what the macro does for a State parameter. Custom extractors are types of your own read the same way.