Skip to main content

ruststream/runtime/
context.rs

1//! Per-delivery [`Context`], generic over the broker's typed per-delivery context `C` and the
2//! application's typed shared state `S`.
3//!
4//! A `Context` is built for each delivery and threaded (as `&mut`) through the middleware chain
5//! into the handler. It carries the channel the message arrived on, a working copy of the
6//! headers (middleware may enrich them), the typed shared application state ([`Context::state`]),
7//! and the broker's typed per-delivery context read by key ([`Context::context`] /
8//! [`Context::set`]). Both steps are lazy: the delivery is asked for its header map the first time
9//! anything reads it, and the map is borrowed until the first
10//! [`headers_mut`](Context::headers_mut), so a delivery whose handler and middleware never touch
11//! the headers pays neither the broker's accessor nor a clone.
12
13use std::cell::OnceCell;
14use std::future::Future;
15use std::pin::Pin;
16#[cfg(feature = "testing")]
17use std::sync::Arc;
18
19use bytes::BytesMut;
20
21#[cfg(feature = "testing")]
22use crate::testing::coordinator::RecordedValue;
23use crate::{Field, FieldMut, HeaderMap, IncomingMessage};
24
25use super::dispatch::Delivery;
26use super::failure::FailurePolicy;
27use super::handler::{HandlerOutcome, HandlerResult};
28use super::main_runtime::MainRuntime;
29use super::shutdown::Shutdown;
30
31/// Where one delivery's header map comes from.
32///
33/// The map is a broker accessor, and answering it is work on most transports: a shadow buffer
34/// over the record's header block, a parse, and the buffer's destruction. Most deliveries never
35/// read the answer, so the context holds the source rather than the map and asks it at most once.
36pub(crate) trait HeaderSource {
37    /// This delivery's headers as the broker reports them.
38    fn headers(&self) -> &HeaderMap;
39}
40
41/// A map that is already built: the batch path, which carries no single delivery's headers, and
42/// the tests that drive a context directly.
43impl HeaderSource for HeaderMap {
44    fn headers(&self) -> &HeaderMap {
45        self
46    }
47}
48
49/// A delivery's headers as a publish reads them: a map already in hand, or the source that
50/// answers it, asked only when a transform reads it.
51///
52/// The broker's accessor is a parse and a buffer on most transports, so a reply whose transforms
53/// read no header never reaches it.
54#[derive(Clone, Copy)]
55pub(crate) enum DeliveryHeaders<'a> {
56    /// The map is already resolved: the handler read or modified it, or there is no delivery.
57    Ready(&'a HeaderMap),
58    /// Nothing has asked yet; the broker answers on the first read.
59    Source(&'a (dyn HeaderSource + Sync)),
60}
61
62impl<'a> DeliveryHeaders<'a> {
63    /// The map, asking the source where nothing has yet.
64    pub(crate) fn get(self) -> &'a HeaderMap {
65        match self {
66            Self::Ready(headers) => headers,
67            Self::Source(source) => source.headers(),
68        }
69    }
70}
71
72impl<'a> From<&'a HeaderMap> for DeliveryHeaders<'a> {
73    fn from(headers: &'a HeaderMap) -> Self {
74        Self::Ready(headers)
75    }
76}
77
78/// The delivery itself, which is what the dispatch path hands the context.
79pub(crate) struct FromDelivery<'m, M>(pub(crate) &'m M);
80
81impl<M: IncomingMessage> HeaderSource for FromDelivery<'_, M> {
82    fn headers(&self) -> &HeaderMap {
83        self.0.headers()
84    }
85}
86
87/// A post-settle continuation: a boxed `Send` future the dispatcher runs after the message (or
88/// batch) has been settled.
89type Continuation = Pin<Box<dyn Future<Output = ()> + Send + 'static>>;
90
91/// The settlement kind a post-settle hook is gated on. Drop and retry are distinct settlements
92/// (`nack` without vs with requeue), so they gate separately: [`HandlerResult::drop`] gates on
93/// [`Drop`](Self::Drop), [`HandlerResult::retry`] on [`Retry`](Self::Retry), and
94/// [`HandlerResult::retry_after`] on [`RetryAfter`](Self::RetryAfter) regardless of the delay
95/// value.
96#[derive(Debug, Clone, Copy, PartialEq, Eq)]
97enum OutcomeKind {
98    Ack,
99    Drop,
100    Retry,
101    RetryAfter,
102}
103
104impl OutcomeKind {
105    /// The settlement kind of `outcome`: the `requeue` flag splits a `Nack` into drop vs retry; the
106    /// `NackAfter` delay value is discarded.
107    fn of(outcome: HandlerResult) -> Self {
108        match outcome {
109            HandlerResult::Ack => Self::Ack,
110            HandlerResult::Nack { requeue: false } => Self::Drop,
111            HandlerResult::Nack { requeue: true } => Self::Retry,
112            HandlerResult::NackAfter { .. } => Self::RetryAfter,
113        }
114    }
115}
116
117/// One registered post-settle hook: the future to run, and the outcome variant it is gated on
118/// (`None` means it runs regardless of how the message settled).
119struct AfterHook {
120    gate: Option<OutcomeKind>,
121    fut: Continuation,
122}
123
124/// Per-delivery context, threaded through middleware and into the handler.
125///
126/// Carries the channel ([`name`](Self::name)), a working copy of the message
127/// [`headers`](Self::headers) (middleware may enrich them for the handler; the broker message
128/// itself is untouched), the typed shared application [state](Self::state) (where a publisher to
129/// publish from a handler lives), and the broker's typed per-delivery context read by key
130/// ([`context`](Self::context) / [`set`](Self::set)). The delivery's map is taken off the broker on
131/// the first read of it, and the working copy is made on the first
132/// [`headers_mut`](Self::headers_mut) call. Outgoing messages do not inherit it: replies and manual
133/// publishes start from fresh headers, shaped by the publish pipeline.
134pub struct Context<'a, C = (), S = ()> {
135    name: &'a str,
136    source: &'a (dyn HeaderSource + Sync),
137    /// The delivery's own map, resolved off [`Context::source`] the first time anything reads it.
138    original: OnceCell<&'a HeaderMap>,
139    modified: Option<HeaderMap>,
140    state: &'a S,
141    cx: C,
142    delivery: &'a Delivery<C>,
143    after: Vec<AfterHook>,
144    failfast: Option<&'a Shutdown>,
145    /// The dispatch loop's encode buffer, lent to this delivery's reply. `None` where the
146    /// context was built without one, and then the reply encodes into a buffer of its own.
147    encode: Option<&'a mut BytesMut>,
148    /// The subscriber's materialization policy, set by the dispatcher from the definition's
149    /// failure policies. It reaches the handler body because a `Headers` contract is parsed
150    /// there rather than in the decode adapter.
151    decode: FailurePolicy,
152    /// Set by the [`Typed`](super::typed::Typed) decode adapter when the payload fails to decode,
153    /// so the dispatcher can record the outcome as a decode failure (otherwise indistinguishable
154    /// from a handler drop). Present under the `testing` feature (harness classification) and the
155    /// `otel` feature (the consume layer's decode-failure counter).
156    #[cfg(any(feature = "testing", feature = "otel"))]
157    decode_failed: bool,
158    /// The value the delivery lent the handler on the carried lane, cloned for the harness's
159    /// record; `None` everywhere else, and always outside a harness run.
160    #[cfg(feature = "testing")]
161    carried: Option<RecordedValue>,
162}
163
164impl<C, S> std::fmt::Debug for Context<'_, C, S> {
165    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
166        f.debug_struct("Context")
167            .field("name", &self.name)
168            .field("after_hooks", &self.after.len())
169            .finish_non_exhaustive()
170    }
171}
172
173impl<'a, C, S> Context<'a, C, S> {
174    /// Creates a context for one delivery, taking the headers off `source` when something first
175    /// asks for them and carrying the typed per-delivery context `cx` (built by
176    /// [`BuildContext`](crate::BuildContext) from the broker message).
177    pub(crate) fn new(
178        name: &'a str,
179        source: &'a (dyn HeaderSource + Sync),
180        state: &'a S,
181        cx: C,
182        delivery: &'a Delivery<C>,
183    ) -> Self {
184        Self {
185            name,
186            source,
187            original: OnceCell::new(),
188            modified: None,
189            state,
190            cx,
191            delivery,
192            after: Vec::new(),
193            failfast: None,
194            encode: None,
195            decode: FailurePolicy::Drop,
196            #[cfg(any(feature = "testing", feature = "otel"))]
197            decode_failed: false,
198            #[cfg(feature = "testing")]
199            carried: None,
200        }
201    }
202
203    /// Records that the payload failed to decode for this delivery. Called by the decode adapter so
204    /// the harness and the otel consume layer can classify the outcome as a decode failure.
205    #[cfg(any(feature = "testing", feature = "otel"))]
206    pub(crate) fn mark_decode_failed(&mut self) {
207        self.decode_failed = true;
208    }
209
210    /// Reads the decode-failure flag without clearing it. The otel consume layer runs inside the
211    /// dispatch (before the harness's clearing [`took_decode_failed`](Self::took_decode_failed)
212    /// read), so its read must leave the flag in place.
213    #[cfg(feature = "otel")]
214    pub(crate) fn decode_failed(&self) -> bool {
215        self.decode_failed
216    }
217
218    /// Returns and clears the decode-failure flag for this delivery.
219    #[cfg(feature = "testing")]
220    pub(crate) fn took_decode_failed(&mut self) -> bool {
221        std::mem::take(&mut self.decode_failed)
222    }
223
224    /// Keeps a clone of the value the delivery lent the handler, for the harness to record: the
225    /// carried value never was bytes, so the payload the record keeps cannot stand for it.
226    /// Nothing is cloned outside a harness run.
227    #[cfg(feature = "testing")]
228    pub(crate) fn record_carried<T: Clone + Send + Sync + 'static>(&mut self, value: &T) {
229        if self.delivery.hooks.coordinator().is_some() {
230            self.carried = Some(Arc::new(value.clone()));
231        }
232    }
233
234    /// Takes the value [`record_carried`](Self::record_carried) kept for this delivery.
235    #[cfg(feature = "testing")]
236    pub(crate) fn take_carried(&mut self) -> Option<RecordedValue> {
237        self.carried.take()
238    }
239
240    /// Attaches the runtime's error-shutdown handle, so a fail-fast decode policy can tear the
241    /// service down from inside the handler. The dispatch loop sets this; contexts built in tests
242    /// leave it unset (a fail-fast there logs but cannot reach the run loop).
243    #[must_use]
244    pub(crate) fn with_failfast(mut self, failfast: &'a Shutdown) -> Self {
245        self.failfast = Some(failfast);
246        self
247    }
248
249    /// Lends this delivery the dispatch loop's encode buffer.
250    ///
251    /// One buffer per sequential loop, per keyed lane and per pooled worker task, so a reply
252    /// encoded into it allocates only while it is growing - which it stops doing after the
253    /// subscription's first message. What the buffer is worth to the publish depends on the
254    /// broker: a publisher that reads the payload is lent it, and one that keeps the payload is
255    /// handed a buffer of its own regardless.
256    pub(crate) fn with_encode_buffer(mut self, buf: &'a mut BytesMut) -> Self {
257        self.encode = Some(buf);
258        self
259    }
260
261    /// Takes the loop's encode buffer out for this delivery's reply.
262    ///
263    /// Taken rather than borrowed, so the rest of the context stays readable while the reply is
264    /// being built: what the publish needs of the context (the subscription's name, the
265    /// delivery's headers) is read next to the buffer, not through it.
266    pub(crate) fn take_encode_buffer(&mut self) -> Option<&'a mut BytesMut> {
267        self.encode.take()
268    }
269
270    /// Attaches the subscriber's effective materialization policy, so a handler-side contract
271    /// (a [`Headers`](super::Headers) parameter) settles by the same policy as the
272    /// payload codec no matter where the policy was named - in the attribute, or on the builder
273    /// at the mount site.
274    pub(crate) fn with_decode_policy(mut self, decode: FailurePolicy) -> Self {
275        self.decode = decode;
276        self
277    }
278
279    /// The materialization policy in force for this delivery: what happens to a payload that
280    /// does not decode, or a header contract that does not parse.
281    ///
282    /// The `#[subscriber]` expansion reads it to settle a failed
283    /// [`Headers`](super::Headers) extraction; a hand-written handler can read it for the
284    /// same reason.
285    ///
286    /// # Examples
287    ///
288    /// ```
289    /// use std::str;
290    ///
291    /// use ruststream::IncomingMessage;
292    /// use ruststream::runtime::{Context, FailurePolicy, HandlerOutcome};
293    ///
294    /// /// A hand-written handler reads the payload as text itself, and settles one that is not the
295    /// /// way the registration's decode policy says.
296    /// async fn handle<M: IncomingMessage>(msg: &M, ctx: &mut Context<'_>) -> HandlerOutcome {
297    ///     let Ok(line) = str::from_utf8(msg.payload()) else {
298    ///         return match ctx.decode_policy() {
299    ///             FailurePolicy::Retry => HandlerOutcome::retry(),
300    ///             FailurePolicy::RetryAfter(delay) => HandlerOutcome::retry_after(delay),
301    ///             FailurePolicy::Skip => HandlerOutcome::ack(),
302    ///             _ => HandlerOutcome::drop(),
303    ///         };
304    ///     };
305    ///     tracing::info!(line, "handled");
306    ///     HandlerOutcome::ack()
307    /// }
308    /// ```
309    #[must_use]
310    pub fn decode_policy(&self) -> FailurePolicy {
311        self.decode
312    }
313
314    /// Triggers a fail-fast shutdown for `reason` if a handle is attached, naming this delivery's
315    /// subscription. Used by the decode path when its policy is
316    /// [`FailFast`](super::FailurePolicy::FailFast).
317    pub(crate) fn fail_fast(&self, reason: &str) {
318        if let Some(failfast) = self.failfast {
319            failfast.signal(self.name, reason);
320        }
321    }
322
323    /// The channel (name / subject) the message arrived on.
324    #[must_use]
325    pub fn name(&self) -> &str {
326        self.name
327    }
328
329    /// The runtime the app runs on: the one explicit way from a handler on dedicated threads
330    /// (`threads(n)`) to send work to the app's runtime (see [`MainRuntime`]). On any other
331    /// placement the handler already runs there.
332    ///
333    /// # Examples
334    ///
335    /// ```
336    /// use ruststream::IncomingMessage;
337    /// use ruststream::runtime::{Context, HandlerOutcome};
338    ///
339    /// async fn notify(_id: u64) {}
340    ///
341    /// async fn handle<M: IncomingMessage>(_msg: &M, ctx: &mut Context<'_>) -> HandlerOutcome {
342    ///     // The notification leaves the handler's thread; the handler does not wait for it.
343    ///     ctx.main_runtime().spawn(notify(7));
344    ///     HandlerOutcome::ack()
345    /// }
346    /// ```
347    #[must_use]
348    pub fn main_runtime(&self) -> &MainRuntime {
349        &self.delivery.runtime
350    }
351
352    /// The delivery's own headers, taken off the broker on the first call and kept for the rest
353    /// of the delivery.
354    fn original(&self) -> &'a HeaderMap {
355        self.original.get_or_init(|| self.source.headers())
356    }
357
358    /// The working copy of the message headers.
359    #[must_use]
360    pub fn headers(&self) -> &HeaderMap {
361        self.modified
362            .as_ref()
363            .map_or_else(|| self.original(), |modified| modified)
364    }
365
366    /// The headers a publish made from inside this delivery reads, without asking the broker
367    /// for them until a transform does.
368    pub(crate) fn delivery_headers(&self) -> DeliveryHeaders<'_> {
369        if let Some(modified) = &self.modified {
370            return DeliveryHeaders::Ready(modified);
371        }
372        self.original
373            .get()
374            .map_or(DeliveryHeaders::Source(self.source), |original| {
375                DeliveryHeaders::Ready(original)
376            })
377    }
378
379    /// The working copy of the message headers, mutably. The first call clones the message
380    /// headers; later calls return the same copy.
381    pub fn headers_mut(&mut self) -> &mut HeaderMap {
382        let original = self.original();
383        self.modified.get_or_insert_with(|| original.clone())
384    }
385
386    /// Returns the shared application state: the typed `S` the app's `on_startup` produced (or
387    /// `()` when the app declares none), borrowed for the delivery. Read its fields directly.
388    ///
389    /// # Examples
390    ///
391    /// ```
392    /// # #[cfg(all(feature = "macros", feature = "json"))]
393    /// # mod demo {
394    /// use ruststream::prelude::*;
395    /// use serde::Deserialize;
396    ///
397    /// #[derive(Deserialize)]
398    /// struct Order {
399    ///     id: u64,
400    /// }
401    ///
402    /// /// What `on_startup` produced, shared by every delivery.
403    /// struct Config {
404    ///     region: String,
405    /// }
406    ///
407    /// #[subscriber("orders")]
408    /// async fn handle(order: &Order, ctx: &mut Context<'_, (), Config>) -> HandlerOutcome {
409    ///     tracing::info!(order.id, region = ctx.state().region, "accepted");
410    ///     HandlerOutcome::ack()
411    /// }
412    /// # }
413    /// # fn main() {}
414    /// ```
415    #[must_use]
416    pub fn state(&self) -> &S {
417        self.state
418    }
419
420    /// Reads a broker-supplied per-delivery field off the typed context by compile-time `key`.
421    ///
422    /// The key is a zero-sized selector the broker exports; resolution is a direct field read off
423    /// the typed context (no hashing, boxing, or downcasting). The default `()` context carries no
424    /// fields, so keys exist only for brokers that expose a context type. For shared app state use
425    /// [`state`](Self::state) instead.
426    ///
427    /// # Examples
428    ///
429    /// ```
430    /// use ruststream::runtime::{Context, HandlerOutcome};
431    /// use ruststream::{Field, IncomingMessage};
432    ///
433    /// /// A broker's per-delivery context with one field, and the key that reads it.
434    /// struct Delivery {
435    ///     offset: u64,
436    /// }
437    ///
438    /// #[derive(Clone, Copy)]
439    /// struct Offset;
440    ///
441    /// impl Field<Delivery> for Offset {
442    ///     type Value<'a> = u64;
443    ///     fn get(self, d: &Delivery) -> u64 {
444    ///         d.offset
445    ///     }
446    /// }
447    ///
448    /// async fn handle<M: IncomingMessage>(
449    ///     _msg: &M,
450    ///     ctx: &mut Context<'_, Delivery>,
451    /// ) -> HandlerOutcome {
452    ///     tracing::info!(offset = ctx.context(Offset), "handled");
453    ///     HandlerOutcome::ack()
454    /// }
455    /// ```
456    pub fn context<K: Field<C>>(&self, key: K) -> K::Value<'_> {
457        key.get(&self.cx)
458    }
459
460    /// The broker's per-delivery context, borrowed for the publish path.
461    ///
462    /// A reply published from this handler carries the delivery's typed context to its static
463    /// [`PublishTransform`](super::PublishTransform) as a [`PublishContext`](super::PublishContext); this is
464    /// the accessor the runtime uses to build that read-only view.
465    pub(crate) fn cx_ref(&self) -> &C {
466        &self.cx
467    }
468
469    /// Writes a per-delivery scratch value downstream handlers read by `key`.
470    ///
471    /// Middleware uses this to hand typed data to downstream handlers (an authenticated user, a
472    /// correlation id) without serializing it into the headers, when the context type exposes a
473    /// writable ([`FieldMut`](crate::FieldMut)) key.
474    ///
475    /// # Examples
476    ///
477    /// ```
478    /// use ruststream::runtime::{Context, HandlerOutcome};
479    /// use ruststream::{Field, FieldMut, IncomingMessage};
480    ///
481    /// /// A broker's per-delivery context, with a slot for the authenticated user.
482    /// #[derive(Default)]
483    /// struct Scratch {
484    ///     user: Option<u64>,
485    /// }
486    ///
487    /// #[derive(Clone, Copy)]
488    /// struct User;
489    ///
490    /// impl Field<Scratch> for User {
491    ///     type Value<'a> = Option<&'a u64>;
492    ///     fn get(self, s: &Scratch) -> Option<&u64> {
493    ///         s.user.as_ref()
494    ///     }
495    /// }
496    ///
497    /// impl FieldMut<Scratch> for User {
498    ///     type Owned = u64;
499    ///     fn set(self, s: &mut Scratch, value: u64) {
500    ///         s.user = Some(value);
501    ///     }
502    /// }
503    ///
504    /// # /// The identity provider's check: the user a valid token was issued to.
505    /// # fn verify_token(_token: &str) -> Option<u64> {
506    /// #     None
507    /// # }
508    /// /// Verifies the delivery's bearer token and records whose it is, for every later stage
509    /// /// that reads `User`.
510    /// async fn authenticate<M: IncomingMessage>(
511    ///     _msg: &M,
512    ///     ctx: &mut Context<'_, Scratch>,
513    /// ) -> HandlerOutcome {
514    ///     let Some(user) = ctx
515    ///         .headers()
516    ///         .get_str("authorization")
517    ///         .and_then(|value| value.strip_prefix("Bearer "))
518    ///         .and_then(verify_token)
519    ///     else {
520    ///         return HandlerOutcome::drop();
521    ///     };
522    ///     ctx.set(User, user);
523    ///     HandlerOutcome::ack()
524    /// }
525    /// ```
526    pub fn set<K: FieldMut<C>>(&mut self, key: K, value: K::Owned) {
527        key.set(&mut self.cx, value);
528    }
529
530    /// Begins registering a post-settle hook gated on `outcome`.
531    ///
532    /// The returned builder's [`then`](After::then) registers a future that the dispatcher runs
533    /// once the message has been settled, but only if the actual settlement matches `outcome` by
534    /// kind. The four kinds are distinct: [`HandlerOutcome::ack`], [`HandlerOutcome::drop`] (nack
535    /// without requeue), [`HandlerOutcome::retry`] (nack with requeue), and
536    /// [`HandlerOutcome::retry_after`] (which matches regardless of the delay). So a hook gated
537    /// on `drop()` does not fire on a `retry()` settlement, and vice versa. Multiple hooks
538    /// accumulate and every matching one runs. Any continuation already attached to the gate
539    /// value itself is ignored: the hook future is what `then` registers.
540    ///
541    /// The hook is scoped to the whole delivery. On the batch path a `Context` is one per batch,
542    /// so a hook registered here runs after the entire batch settles; because a batch has
543    /// per-element outcomes, the outcome gate is ignored there and only [`after_settle`](Self::after_settle)
544    /// hooks (which run regardless) fire (see that method).
545    ///
546    /// # Cancel safety
547    ///
548    /// Post-settle hooks are at-most-once: the message is already settled before any hook runs, so
549    /// a hook that panics, or that is lost when the process crashes, never causes a redelivery and
550    /// never blocks the next delivery. A graceful shutdown drains in-flight hooks (bounded by the
551    /// app's [`shutdown_timeout`](super::RustStream::shutdown_timeout)); an aborted shutdown may
552    /// drop them.
553    ///
554    /// # Examples
555    ///
556    /// ```
557    /// # #[cfg(all(feature = "macros", feature = "json"))]
558    /// # mod demo {
559    /// use ruststream::prelude::*;
560    /// use serde::Deserialize;
561    ///
562    /// #[derive(Deserialize)]
563    /// struct Order {
564    ///     id: u64,
565    /// }
566    ///
567    /// async fn notify_shipping(_id: u64) {}
568    ///
569    /// #[subscriber("orders")]
570    /// async fn accept(order: &Order, ctx: &mut Context<'_>) -> HandlerOutcome {
571    ///     let id = order.id;
572    ///     // Shipping hears of the order only once it is acked, never for one that comes back.
573    ///     ctx.after(HandlerOutcome::ack())
574    ///         .then(async move { notify_shipping(id).await });
575    ///     HandlerOutcome::ack()
576    /// }
577    /// # }
578    /// # fn main() {}
579    /// ```
580    // The gate is a just-built outcome token (`after(HandlerOutcome::ack())`); a reference
581    // parameter would force `&` noise at every call site.
582    #[allow(clippy::needless_pass_by_value)]
583    pub fn after(&mut self, outcome: HandlerOutcome) -> After<'_, 'a, C, S> {
584        After {
585            ctx: self,
586            gate: Some(OutcomeKind::of(outcome.outcome())),
587        }
588    }
589
590    /// Registers a post-settle hook that runs only after the message is acked.
591    ///
592    /// Sugar for `self.after(HandlerOutcome::ack()).then(fut)`; see [`after`](Self::after) for
593    /// the gating and cancel-safety semantics.
594    ///
595    /// # Cancel safety
596    ///
597    /// At-most-once, as for [`after`](Self::after): the ack has already happened, so a lost hook
598    /// never redelivers.
599    ///
600    /// # Examples
601    ///
602    /// ```
603    /// # #[cfg(all(feature = "macros", feature = "json"))]
604    /// # mod demo {
605    /// use ruststream::prelude::*;
606    /// use serde::Deserialize;
607    ///
608    /// #[derive(Deserialize)]
609    /// struct Order {
610    ///     id: u64,
611    /// }
612    ///
613    /// async fn warm_cache(_id: u64) {}
614    ///
615    /// #[subscriber("orders")]
616    /// async fn accept(order: &Order, ctx: &mut Context<'_>) -> HandlerOutcome {
617    ///     let id = order.id;
618    ///     ctx.after_ack(async move { warm_cache(id).await });
619    ///     HandlerOutcome::ack()
620    /// }
621    /// # }
622    /// # fn main() {}
623    /// ```
624    pub fn after_ack(&mut self, fut: impl Future<Output = ()> + Send + 'static) {
625        self.after.push(AfterHook {
626            gate: Some(OutcomeKind::Ack),
627            fut: Box::pin(fut),
628        });
629    }
630
631    /// Registers a post-settle hook that runs after the message settles, whatever the outcome.
632    ///
633    /// Unlike [`after`](Self::after) this has no outcome gate, so it fires on `Ack`, `Drop`,
634    /// `Retry`, and `RetryAfter` alike. It is the only post-settle form honoured on the batch path, where the
635    /// per-element outcomes make an outcome gate ill-defined; there it runs once after the whole
636    /// batch has been settled.
637    ///
638    /// # Cancel safety
639    ///
640    /// At-most-once, as for [`after`](Self::after).
641    ///
642    /// # Examples
643    ///
644    /// ```
645    /// # #[cfg(all(feature = "macros", feature = "json"))]
646    /// # mod demo {
647    /// use std::time::Instant;
648    ///
649    /// use ruststream::prelude::*;
650    /// use serde::Deserialize;
651    ///
652    /// #[derive(Deserialize)]
653    /// struct Order {
654    ///     id: u64,
655    /// }
656    ///
657    /// #[subscriber("orders")]
658    /// async fn accept(order: &Order, ctx: &mut Context<'_>) -> HandlerOutcome {
659    ///     let started = Instant::now();
660    ///     // Recorded whatever the outcome: an ack, a drop or a retry.
661    ///     ctx.after_settle(async move {
662    ///         tracing::info!(elapsed = ?started.elapsed(), "settled");
663    ///     });
664    ///     if order.id == 0 {
665    ///         return HandlerOutcome::drop();
666    ///     }
667    ///     HandlerOutcome::ack()
668    /// }
669    /// # }
670    /// # fn main() {}
671    /// ```
672    pub fn after_settle(&mut self, fut: impl Future<Output = ()> + Send + 'static) {
673        self.after.push(AfterHook {
674            gate: None,
675            fut: Box::pin(fut),
676        });
677    }
678
679    /// Whether this delivery registered any post-settle hook at all. The dispatch path reads it
680    /// first so a delivery that registered none never builds the drained list.
681    pub(crate) fn has_hooks(&self) -> bool {
682        !self.after.is_empty()
683    }
684
685    /// Drains the registered hooks whose gate matches `outcome` (and the ungated ones), in
686    /// registration order. Used by the single-message dispatch path after it settles the message.
687    pub(crate) fn take_hooks_for(&mut self, outcome: HandlerResult) -> Vec<Continuation> {
688        let kind = OutcomeKind::of(outcome);
689        let mut runnable = Vec::new();
690        let mut kept = Vec::new();
691        for hook in self.after.drain(..) {
692            if hook.gate.is_none_or(|gate| gate == kind) {
693                runnable.push(hook.fut);
694            } else {
695                kept.push(hook);
696            }
697        }
698        self.after = kept;
699        runnable
700    }
701
702    /// Drains the ungated hooks (registered via [`after_settle`](Self::after_settle)), in
703    /// registration order. Used by the batch dispatch path, where per-element outcomes make the
704    /// outcome gate ill-defined, so only ungated hooks run.
705    pub(crate) fn take_settle_hooks(&mut self) -> Vec<Continuation> {
706        let mut runnable = Vec::new();
707        let mut kept = Vec::new();
708        for hook in self.after.drain(..) {
709            if hook.gate.is_none() {
710                runnable.push(hook.fut);
711            } else {
712                kept.push(hook);
713            }
714        }
715        self.after = kept;
716        runnable
717    }
718
719    /// Returns the subscription's delivery context: the task tracker post-settle
720    /// [`HandlerResult::and_after`] continuations are spawned onto, and the publisher and address
721    /// the `retry_after` fallback re-publishes through.
722    ///
723    /// Borrowed for the scope's lifetime rather than the context's, so the batch path can settle
724    /// through it after handing `&mut self` to the handler - and so no borrow of the context is
725    /// held across an await, which would demand `Context: Sync`.
726    pub(crate) fn delivery(&self) -> &'a Delivery<C> {
727        self.delivery
728    }
729
730    /// The subscription's name, borrowed for the scope's lifetime rather than the context's.
731    ///
732    /// [`name`](Self::name) borrows through `&self`, so a batch path that hands `&mut self` to
733    /// the handler and still has to name the subscription afterwards had to copy the name into a
734    /// `String` once per batch. The name outlives every delivery of the subscription, so the
735    /// longer borrow is what it always was.
736    pub(crate) fn subscription(&self) -> &'a str {
737        self.name
738    }
739}
740
741/// A builder for an outcome-gated post-settle hook, returned by [`Context::after`].
742///
743/// Call [`then`](Self::then) to register the continuation. Holding it without calling `then`
744/// registers nothing.
745#[must_use = "call `.then(fut)` to register the post-settle hook"]
746pub struct After<'ctx, 'a, C = (), S = ()> {
747    ctx: &'ctx mut Context<'a, C, S>,
748    gate: Option<OutcomeKind>,
749}
750
751impl<C, S> std::fmt::Debug for After<'_, '_, C, S> {
752    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
753        f.debug_struct("After").field("gate", &self.gate).finish()
754    }
755}
756
757impl<C, S> After<'_, '_, C, S> {
758    /// Registers `fut` to run after the message settles, if the settlement matches the gate this
759    /// builder was created with (see [`Context::after`]).
760    ///
761    /// # Examples
762    ///
763    /// ```
764    /// # #[cfg(all(feature = "macros", feature = "json"))]
765    /// # mod demo {
766    /// use ruststream::prelude::*;
767    /// use serde::Deserialize;
768    ///
769    /// #[derive(Deserialize)]
770    /// struct Order {
771    ///     id: u64,
772    /// }
773    ///
774    /// async fn alert(_id: u64) {}
775    ///
776    /// #[subscriber("orders")]
777    /// async fn accept(order: &Order, ctx: &mut Context<'_>) -> HandlerOutcome {
778    ///     let id = order.id;
779    ///     // Someone hears of an order that was dropped for good, and only of that one.
780    ///     ctx.after(HandlerOutcome::drop())
781    ///         .then(async move { alert(id).await });
782    ///     if id == 0 {
783    ///         return HandlerOutcome::drop();
784    ///     }
785    ///     HandlerOutcome::ack()
786    /// }
787    /// # }
788    /// # fn main() {}
789    /// ```
790    pub fn then(self, fut: impl Future<Output = ()> + Send + 'static) {
791        self.ctx.after.push(AfterHook {
792            gate: self.gate,
793            fut: Box::pin(fut),
794        });
795    }
796}
797
798#[cfg(test)]
799mod tests;