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.
- Macros
- Manual
/// 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 app's state: each field reaches a subscriber as `State<FieldType>`.
#[derive(Clone)]
pub(crate) struct Shop {
pub(crate) gateway: Arc<Gateway>,
pub(crate) mailer: Arc<Mailer>,
}
// What `#[derive(FromRef)]` writes for each field: `State<Arc<Gateway>>` resolves through it.
impl FromRef<Shop> for Arc<Gateway> {
fn from_ref(shop: &Shop) -> Self {
shop.gateway.clone()
}
}
impl FromRef<Shop> for Arc<Mailer> {
fn from_ref(shop: &Shop) -> Self {
shop.mailer.clone()
}
}
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.
- Macros
- Manual
/// 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(¬ice.customer, ¬ice.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()
}
}
}
/// E-mails a notice to the customer.
struct Mail;
// The mailer comes from the app's state; the context names the subscription in the warning.
impl Handle<Notice, (), (), (), Shop> for Mail {
async fn handle(
&self,
notice: &Notice,
_outs: &(),
ctx: &mut Context<'_, (), Shop>,
) -> Result<(), HandlerOutcome> {
let State(mailer) = State::<Arc<Mailer>>::from_context(ctx).await?;
if let Err(error) = mailer.send(¬ice.customer, ¬ice.text).await {
// A refused mailbox stays refused; another try would only repeat the warning.
tracing::warn!(subscription = ctx.name(), %error, "notice not sent");
return Err(HandlerOutcome::drop());
}
Ok(())
}
}
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.