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;