Skip to main content

ruststream/memory/
capability.rs

1//! Native capability-trait implementations for the in-memory broker.
2//!
3//! These run on the broker's own in-process semantics: buffered transactions, greedy batching,
4//! correlated request / reply over an in-process inbox. The conformance harness offers a suite
5//! per capability (see
6//! `conformance::capabilities`), and these implementations are the executable reference for
7//! them.
8
9use std::{
10    fmt,
11    future::{Future, ready},
12    marker::PhantomData,
13    num::{NonZeroU64, NonZeroUsize},
14    sync::{
15        Arc, Mutex,
16        atomic::{AtomicUsize, Ordering},
17    },
18    task::{Poll, ready},
19    time::Duration,
20};
21
22use futures::{Stream, task::AtomicWaker};
23use thiserror::Error;
24use tokio::{sync::mpsc, time::timeout};
25use tracing::warn;
26
27use super::{
28    Bus, Discarding, LogMode, MemoryDelivery, MemoryError, MemoryMessage, MemoryOutbound,
29    MemoryPublisher, MemoryState, MemorySubscriber, Retaining, SubscriptionShared, log,
30};
31#[cfg(feature = "testing")]
32use crate::testing::coordinator::Coordinator;
33use crate::{
34    BatchSubscriber, BytesMut, IncomingMessage, OutgoingMessage, OwnedTransactions, Partitioned,
35    Positioned, Publisher, RequestReply, Seekable, Seeker, Str, Subscriber, Take, Transaction,
36    TransactionalPublisher,
37};
38
39/// The well-known header the [`Partitioned`] implementation reads the partition key from.
40pub const PARTITION_KEY_HEADER: &str = "partition-key";
41
42/// Errors returned by [`MemoryRequester`] operations.
43#[derive(Debug, Error)]
44#[non_exhaustive]
45pub enum RequestError {
46    /// No reply arrived in the request inbox before the timeout elapsed.
47    #[error("no reply to \"{subject}\" within {timeout:?}")]
48    Timeout {
49        /// The subject the request was published to.
50        subject: String,
51        /// How long the requester waited for a reply.
52        timeout: Duration,
53    },
54    /// The operation ran through a handle aliasing a bus that was shut down
55    /// ([`ConnectedBroker::shutdown`](crate::ConnectedBroker::shutdown)) and not revived by a
56    /// sibling clone's [`Broker::connect`](crate::Broker::connect).
57    #[error("the memory broker is shut down")]
58    ShutDown,
59}
60
61/// Publisher with native request / reply support, returned by
62/// [`MemoryBroker::requester`](super::MemoryBroker::requester).
63///
64/// [`request`](RequestReply::request) publishes the message with a unique in-process inbox in
65/// the `reply-to` header and resolves on the first message delivered to that inbox. A responder
66/// reads `reply-to` from the request headers and publishes its reply there. Plain
67/// [`publish`](Publisher::publish) is the usual fire-and-forget fanout.
68///
69/// # Examples
70///
71/// ```
72/// # #[cfg(all(feature = "macros", feature = "memory", feature = "json"))]
73/// # mod demo {
74/// use std::time::Duration;
75///
76/// use ruststream::OutgoingMessage;
77/// use ruststream::memory::prelude::*;
78/// use serde::{Deserialize, Serialize};
79///
80/// #[derive(Deserialize)]
81/// struct Order {
82///     id: u64,
83/// }
84///
85/// #[derive(Serialize)]
86/// struct Ask {
87///     id: u64,
88/// }
89///
90/// #[derive(Deserialize)]
91/// struct Quote {
92///     price: u64,
93/// }
94///
95/// #[derive(OutSlot)]
96/// struct Pricing;
97///
98/// #[subscriber("orders")]
99/// async fn price(order: &Order, Out(pricing): Out<impl RequestReply, Pricing>) -> HandlerOutcome {
100///     let Ok(ask) = serde_json::to_vec(&Ask { id: order.id }) else {
101///         return HandlerOutcome::drop();
102///     };
103///     // The reply comes back on an inbox the request names in its `reply-to` header.
104///     let request = OutgoingMessage::new("quotes.ask", ask.as_slice());
105///     let Ok(reply) = pricing.request(request, Duration::from_secs(2)).await else {
106///         return HandlerOutcome::retry();
107///     };
108///     match serde_json::from_slice::<Quote>(reply.payload()) {
109///         Ok(quote) => {
110///             tracing::info!(order.id, quote.price, "priced");
111///             HandlerOutcome::ack()
112///         }
113///         Err(_) => HandlerOutcome::drop(),
114///     }
115/// }
116///
117/// fn app() -> RustStream {
118///     RustStream::new(AppInfo::new("orders", "0.1.0")).with_broker(MemoryBroker::new(), |b| {
119///         b.include(price).out(Pricing, Request).build();
120///     })
121/// }
122/// # }
123/// # fn main() {}
124/// ```
125#[derive(Clone)]
126pub struct MemoryRequester {
127    state: Arc<MemoryState>,
128}
129
130impl MemoryRequester {
131    pub(super) fn new(state: Arc<MemoryState>) -> Self {
132        Self { state }
133    }
134}
135
136impl fmt::Debug for MemoryRequester {
137    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
138        f.debug_struct("MemoryRequester").finish_non_exhaustive()
139    }
140}
141
142impl Publisher for MemoryRequester {
143    // The same bus, kept the same way: see `MemoryPublisher`.
144    type Payload = Take;
145    type Error = RequestError;
146    type Options = ();
147
148    fn publish(
149        &self,
150        msg: OutgoingMessage<'_, BytesMut>,
151        _options: Option<&Self::Options>,
152    ) -> impl Future<Output = Result<(), Self::Error>> {
153        let (name, payload, headers) = msg.into_parts();
154        ready(
155            self.state
156                .fanout(name, payload.freeze(), headers)
157                .map_err(|_| RequestError::ShutDown),
158        )
159    }
160}
161
162impl RequestReply for MemoryRequester {
163    /// A reply arrives on a single-use inbox rather than a subscription, so it carries no
164    /// position and no seeker whichever broker the requester came from.
165    type Reply = MemoryMessage<Discarding>;
166
167    async fn request(
168        &self,
169        msg: OutgoingMessage<'_, BytesMut>,
170        wait: Duration,
171    ) -> Result<Self::Reply, Self::Error> {
172        let id = self.state.inbox_seq.fetch_add(1, Ordering::Relaxed);
173        let inbox = format!("_inbox.{id}");
174        let (tx, mut rx) = mpsc::unbounded_channel();
175        self.state
176            .register(&inbox, tx.clone())
177            .map_err(|_| RequestError::ShutDown)?;
178
179        // The name is the caller's buffer, so it outlives the message consumed here; the inbox
180        // is written into the request's own map rather than into a copy of it.
181        let (subject, payload, mut headers) = msg.into_parts();
182        headers.insert(Str::from_static("reply-to"), inbox.clone());
183        if self
184            .state
185            .fanout(subject, payload.freeze(), headers)
186            .is_err()
187        {
188            self.state.unregister(&inbox);
189            return Err(RequestError::ShutDown);
190        }
191
192        let outcome = timeout(wait, rx.recv()).await;
193        self.state.unregister(&inbox);
194        match outcome {
195            Ok(Some(reply)) => Ok(MemoryMessage {
196                delivery: Some(reply),
197                subscription: None,
198                // The inbox reply is consumed here, not by a dispatch loop, and its enqueue was not
199                // counted (see `fanout`), so it carries no coordinator.
200                #[cfg(feature = "testing")]
201                coordinator: None,
202                mode: PhantomData,
203            }),
204            // `tx` is held on this stack frame, so the channel cannot report closed.
205            Ok(None) => unreachable!("request inbox closed while its sender is held"),
206            Err(_) => Err(RequestError::Timeout {
207                subject: subject.to_owned(),
208                timeout: wait,
209            }),
210        }
211    }
212}
213
214/// Greedy batching: a batch is the first awaited delivery plus everything already buffered, up to
215/// the size the registration asked for. Partial batches ship immediately, so no deadline timer is
216/// needed - the in-memory transport has nothing to wait for. Each batch is allocated at the
217/// requested size up front, so a partial batch holds the room of a full one.
218impl<Log: LogMode> BatchSubscriber for MemorySubscriber<Log> {
219    type Batch = Vec<MemoryMessage<Log>>;
220
221    fn batches(
222        &mut self,
223        size: NonZeroUsize,
224    ) -> impl Stream<Item = Result<Self::Batch, <Self as Subscriber>::Error>> + Send + '_ {
225        let limit = size.get();
226        #[cfg(feature = "testing")]
227        let coordinator = self.coordinator();
228        // The drain happens inside a single poll, so no batch state is buffered between polls
229        // and the stream stays cancel-safe, like `MemorySubscriber::stream`.
230        futures::stream::poll_fn(move |cx| {
231            // Same ordering as `MemorySubscriber::stream`: register, then apply a pending seek.
232            // Only a seeker wakes this waker, and a discarding subscription mints none.
233            if log::retains::<Log>() {
234                self.subscription.seek.waker.register(cx.waker());
235            }
236            self.apply_pending_seek();
237            let first = loop {
238                match ready!(self.rx.poll_recv(cx)) {
239                    // A stale pre-seek copy (a requeue that raced the seek): drop it, the
240                    // replay already covers everything from the watermark on.
241                    Some(delivery)
242                        if log::retains::<Log>()
243                            && delivery.seq < self.subscription.seek.watermark() =>
244                    {
245                        #[cfg(feature = "testing")]
246                        if let Some(coordinator) = &coordinator {
247                            coordinator.consumed();
248                        }
249                    }
250                    Some(delivery) => break delivery,
251                    None => return Poll::Ready(None),
252                }
253            };
254            // Sized for the batch the registration asked for: a batch grown one push at a time
255            // reallocates six times on its way to 64.
256            let mut batch = Vec::with_capacity(limit);
257            batch.push(MemoryMessage {
258                delivery: Some(first),
259                subscription: Some(Arc::clone(&self.subscription)),
260                #[cfg(feature = "testing")]
261                coordinator: coordinator.clone(),
262                mode: PhantomData,
263            });
264            while batch.len() < limit {
265                match self.rx.poll_recv(cx) {
266                    // The same stale-copy filter as above, off the batch.
267                    Poll::Ready(Some(delivery))
268                        if log::retains::<Log>()
269                            && delivery.seq < self.subscription.seek.watermark() =>
270                    {
271                        #[cfg(feature = "testing")]
272                        if let Some(coordinator) = &coordinator {
273                            coordinator.consumed();
274                        }
275                    }
276                    Poll::Ready(Some(delivery)) => batch.push(MemoryMessage {
277                        delivery: Some(delivery),
278                        subscription: Some(Arc::clone(&self.subscription)),
279                        #[cfg(feature = "testing")]
280                        coordinator: coordinator.clone(),
281                        mode: PhantomData,
282                    }),
283                    // Drained (pending) or closed: ship what we have; a closed channel ends
284                    // the stream on the next poll.
285                    Poll::Ready(None) | Poll::Pending => break,
286                }
287            }
288            Poll::Ready(Some(Ok(batch)))
289        })
290    }
291}
292
293/// In-process transactions: [`begin_transaction`](TransactionalPublisher::begin_transaction)
294/// switches this handle to buffering, [`commit`](TransactionalPublisher::commit) fans out the
295/// buffer in publish order, [`abort`](TransactionalPublisher::abort) discards it.
296///
297/// Misuse errors per the trait contract: a second `begin_transaction` returns
298/// [`MemoryError::TransactionBusy`] (leaving the open transaction untouched), and `commit` /
299/// `abort` without an open transaction return [`MemoryError::NoTransaction`]. Clones of the
300/// handle do not share the transaction (see [`MemoryPublisher`]).
301impl TransactionalPublisher for MemoryPublisher {
302    fn begin_transaction(&self) -> impl Future<Output = Result<(), MemoryError>> {
303        let mut txn = self.txn.lock().expect("memory broker mutex poisoned");
304        if txn.is_some() {
305            return ready(Err(MemoryError::TransactionBusy));
306        }
307        *txn = Some(Vec::new());
308        drop(txn);
309        ready(Ok(()))
310    }
311
312    fn commit(&self) -> impl Future<Output = Result<(), MemoryError>> {
313        let buffered = self
314            .txn
315            .lock()
316            .expect("memory broker mutex poisoned")
317            .take();
318        let Some(buffered) = buffered else {
319            return ready(Err(MemoryError::NoTransaction));
320        };
321        for outbound in buffered {
322            if let Err(err) = self
323                .state
324                .fanout(&outbound.name, outbound.payload, outbound.headers)
325            {
326                return ready(Err(err));
327            }
328        }
329        ready(Ok(()))
330    }
331
332    fn abort(&self) -> impl Future<Output = Result<(), MemoryError>> {
333        ready(
334            self.txn
335                .lock()
336                .expect("memory broker mutex poisoned")
337                .take()
338                .map(|_| ())
339                .ok_or(MemoryError::NoTransaction),
340        )
341    }
342}
343
344/// An owned in-process transaction, opened by
345/// [`transaction`](crate::OwnedTransactions::transaction) on a [`MemoryPublisher`].
346///
347/// A private delivery buffer, fanned out to the bus in publish order on commit and discarded
348/// on abort.
349///
350/// Unlike the handle-level [`TransactionalPublisher`] buffer, any number of these can be open
351/// on one handle at a time, and the handle keeps publishing directly while they are.
352///
353/// # Examples
354///
355/// ```
356/// # #[cfg(all(feature = "macros", feature = "memory", feature = "json"))]
357/// # mod demo {
358/// use ruststream::memory::prelude::*;
359/// use serde::{Deserialize, Serialize};
360///
361/// #[derive(Deserialize)]
362/// struct Order {
363///     id: u64,
364/// }
365///
366/// #[derive(Serialize, Outgoing)]
367/// #[outgoing(name = "ledger.settled")]
368/// struct Settled {
369///     id: u64,
370/// }
371///
372/// #[derive(OutSlot)]
373/// #[publishes(Settled)]
374/// struct Ledger;
375///
376/// #[subscriber("orders")]
377/// async fn settle(
378///     order: &Order,
379///     Out(ledger): Out<impl OwnedTransactions, Ledger, Settled>,
380/// ) -> HandlerOutcome {
381///     // Nothing reaches `ledger.settled` until the commit, and an abort discards it all.
382///     let Ok(mut txn) = ledger.transaction().await else {
383///         return HandlerOutcome::retry();
384///     };
385///     if txn
386///         .message(&Settled { id: order.id })
387///         .publish()
388///         .await
389///         .is_err()
390///         || txn.commit().await.is_err()
391///     {
392///         return HandlerOutcome::retry();
393///     }
394///     HandlerOutcome::ack()
395/// }
396///
397/// fn app() -> RustStream {
398///     RustStream::new(AppInfo::new("ledger", "0.1.0")).with_broker(MemoryBroker::new(), |b| {
399///         b.include(settle).out(Ledger, Publish).build();
400///     })
401/// }
402/// # }
403/// # fn main() {}
404/// ```
405#[must_use = "a transaction does nothing until settled with commit() or abort()"]
406pub struct MemoryTransaction {
407    state: Arc<MemoryState>,
408    buffered: Vec<MemoryOutbound>,
409    settled: bool,
410}
411
412impl fmt::Debug for MemoryTransaction {
413    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
414        f.debug_struct("MemoryTransaction")
415            .field("buffered", &self.buffered.len())
416            .field("settled", &self.settled)
417            .finish_non_exhaustive()
418    }
419}
420
421impl Drop for MemoryTransaction {
422    fn drop(&mut self) {
423        // Destructors cannot run async work, so a drop can only discard the buffer; the warning
424        // marks that as an abort the caller never wrote, mirroring the runtime's
425        // TransactionScope drop.
426        if !self.settled {
427            warn!(
428                target: "ruststream::memory",
429                buffered = self.buffered.len(),
430                "owned transaction dropped without commit or abort; its buffered messages are \
431                 discarded"
432            );
433        }
434    }
435}
436
437impl Transaction for MemoryTransaction {
438    // The buffer holds the messages until the commit fans them out, so it keeps what it is
439    // handed.
440    type Payload = Take;
441    type Error = MemoryError;
442    type Options = ();
443
444    fn publish(
445        &mut self,
446        msg: OutgoingMessage<'_, BytesMut>,
447        _options: Option<&Self::Options>,
448    ) -> impl Future<Output = Result<(), MemoryError>> {
449        // Buffering is local to this value and never touches the bus; a commit against a
450        // shut-down bus is what reports the error.
451        self.buffered.push(MemoryOutbound::from(msg));
452        ready(Ok(()))
453    }
454
455    fn commit(mut self) -> impl Future<Output = Result<(), MemoryError>> {
456        // Settled before the flush: a failed commit has still consumed the transaction (the
457        // buffer is lost per the Transaction contract), so the drop warning must not fire.
458        self.settled = true;
459        for outbound in std::mem::take(&mut self.buffered) {
460            if let Err(err) = self
461                .state
462                .fanout(&outbound.name, outbound.payload, outbound.headers)
463            {
464                return ready(Err(err));
465            }
466        }
467        ready(Ok(()))
468    }
469
470    fn abort(mut self) -> impl Future<Output = Result<(), MemoryError>> {
471        self.settled = true;
472        ready(Ok(()))
473    }
474}
475
476/// Owned transactions: every [`transaction`](OwnedTransactions::transaction) call opens an
477/// independent buffer-owning `MemoryTransaction`, so any number can be open concurrently on one
478/// handle, next to (and unaffected by) the handle-level [`TransactionalPublisher`] transaction.
479impl OwnedTransactions for MemoryPublisher {
480    type Transaction = MemoryTransaction;
481
482    fn transaction(&self) -> impl Future<Output = Result<MemoryTransaction, MemoryError>> {
483        // Opening allocates a buffer and never touches the bus; a shut-down bus surfaces at
484        // commit, the visibility point, like the handle-level begin.
485        ready(Ok(MemoryTransaction {
486            state: Arc::clone(&self.state),
487            buffered: Vec::new(),
488            settled: false,
489        }))
490    }
491}
492
493/// The partition key is carried in the [`PARTITION_KEY_HEADER`] header; producers that want
494/// per-key ordering set it at publish time. Messages without the header return `None` (any
495/// partition will do).
496impl<Log: LogMode> Partitioned for MemoryMessage<Log> {
497    fn partition_key(&self) -> Option<&[u8]> {
498        self.headers().get(PARTITION_KEY_HEADER)
499    }
500}
501
502/// Shared between a subscriber's polling side and the seekers minted off it.
503///
504/// A seek is a handoff, not an in-place mutation: the seeker records the target and wakes the
505/// stream, and the subscriber applies it inside its own poll, where `&mut` access to the
506/// receiver is available (see [`MemorySubscriber::apply_pending_seek`]).
507#[derive(Default)]
508pub(super) struct SeekControl {
509    /// The requested log position, taken by the subscriber inside its next poll.
510    pending: Mutex<Option<usize>>,
511    /// Deliveries stamped below this log position are stale pre-seek copies (a requeue racing
512    /// the seek) and are dropped by the polling side.
513    watermark: AtomicUsize,
514    /// Wakes the subscriber's stream task after `pending` is set.
515    pub(super) waker: AtomicWaker,
516}
517
518impl SeekControl {
519    /// The stale-delivery cutoff, read on the polling side for every delivery.
520    ///
521    /// Acquire pairs with the Release store in [`Seeker::seek`]: a poll that observes a
522    /// delivery enqueued after a seek also observes that seek's watermark.
523    pub(super) fn watermark(&self) -> usize {
524        self.watermark.load(Ordering::Acquire)
525    }
526}
527
528/// A position in one name's publish log on a retaining in-memory broker.
529///
530/// Construct it with [`start`](Self::start), [`sequence`](Self::sequence) or [`end`](Self::end),
531/// or capture the position of a delivered [`MemoryMessage`] with [`Positioned::position`].
532/// Sequence numbers are absolute and stable: the `n`-th message published to a name keeps
533/// position `n` while the broker retains it, whatever the retention bound has evicted before it,
534/// and seeking to a captured position redelivers exactly that message.
535///
536/// The two constructed positions follow the broker's own vocabulary: [`start`](Self::start) is
537/// the oldest message still retained, [`end`](Self::end) is the tip, past everything published so
538/// far. A sequence the retention bound has already evicted is refused with
539/// [`MemoryError::PositionEvicted`]; one past the tip resumes with the next publish.
540///
541/// # Examples
542///
543/// ```
544/// # #[cfg(all(feature = "macros", feature = "memory", feature = "json"))]
545/// # mod demo {
546/// use ruststream::memory::prelude::*;
547/// use serde::Deserialize;
548///
549/// #[derive(Deserialize)]
550/// struct Entry {
551///     id: u64,
552/// }
553///
554/// /// Rebuilds its view from everything the broker still holds, on every start.
555/// #[subscriber("audit", start_at(MemoryPosition::start()))]
556/// async fn rebuild(entry: &Entry) -> HandlerOutcome {
557///     tracing::info!(entry.id, "replayed");
558///     HandlerOutcome::ack()
559/// }
560///
561/// fn app() -> RustStream {
562///     // The last 1024 messages of every name stay replayable.
563///     let broker = MemoryBroker::retaining(Retention::Messages(nonzero!(1024)));
564///     RustStream::new(AppInfo::new("audit", "0.1.0")).with_broker(broker, |b| {
565///         b.include(rebuild);
566///     })
567/// }
568/// # }
569/// # fn main() {}
570/// ```
571#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
572#[must_use]
573pub enum MemoryPosition {
574    /// The oldest message the name still retains.
575    Start,
576    /// The zero-based `n`-th message published to the name.
577    Sequence(usize),
578    /// The tip of the log: everything published so far is behind it.
579    End,
580}
581
582impl MemoryPosition {
583    /// The oldest retained message: seeking here replays everything the broker still holds for
584    /// the name.
585    pub const fn start() -> Self {
586        Self::Start
587    }
588
589    /// The position of the zero-based `n`-th message published to the name.
590    pub const fn sequence(n: usize) -> Self {
591        Self::Sequence(n)
592    }
593
594    /// The end of the log: seeking here skips everything published so far and resumes with
595    /// the next publish (the memory analog of a "latest" position).
596    pub const fn end() -> Self {
597        Self::End
598    }
599}
600
601/// A clonable handle repositioning one [`MemorySubscriber`], minted by
602/// [`Seekable::seeker`] before the subscription's stream is opened.
603///
604/// The scope is that one subscription instance: other subscribers of the same name are
605/// unaffected. After the bus shuts down, [`seek`](Seeker::seek) reports
606/// [`MemoryError::ShutDown`] like any other aliased handle.
607///
608/// # Examples
609///
610/// ```
611/// # #[cfg(all(feature = "macros", feature = "memory", feature = "json"))]
612/// # mod demo {
613/// use ruststream::Seeker;
614/// use ruststream::memory::prelude::*;
615/// use serde::Deserialize;
616///
617/// #[derive(Deserialize)]
618/// struct Job {
619///     id: u64,
620///     poisoned_until: Option<usize>,
621/// }
622///
623/// /// Skips forward past a region the producer marked poisoned.
624/// #[subscriber("jobs")]
625/// async fn work(job: &Job, Ctx(seeker): Ctx<SeekHandle>) -> HandlerOutcome {
626///     if let Some(resume_at) = job.poisoned_until {
627///         if seeker
628///             .seek(MemoryPosition::sequence(resume_at))
629///             .await
630///             .is_err()
631///         {
632///             return HandlerOutcome::retry();
633///         }
634///     }
635///     tracing::info!(job.id, "done");
636///     HandlerOutcome::ack()
637/// }
638///
639/// fn app() -> RustStream {
640///     let broker = MemoryBroker::retaining(Retention::Messages(nonzero!(1024)));
641///     RustStream::new(AppInfo::new("jobs", "0.1.0")).with_broker(broker, |b| {
642///         b.include(work);
643///     })
644/// }
645/// # }
646/// # fn main() {}
647/// ```
648#[derive(Clone)]
649pub struct MemorySeeker {
650    // The subscription's shared block, which every delivery already holds: minting a seeker off
651    // a delivery, as the per-delivery context does, is one reference count.
652    subscription: Arc<SubscriptionShared>,
653}
654
655impl MemorySeeker {
656    /// The seeker of the subscription `msg` came from: the reference count the delivery holds,
657    /// taken once more, so a per-delivery context allocates nothing.
658    ///
659    /// # Panics
660    ///
661    /// Panics on a request-reply inbox message, which has no subscription behind it; no dispatch
662    /// loop, and therefore no context, ever builds off one.
663    fn of(msg: &MemoryMessage<Retaining>) -> Self {
664        Self {
665            subscription: Arc::clone(
666                msg.subscription
667                    .as_ref()
668                    .expect("a seek context builds only off subscription deliveries"),
669            ),
670        }
671    }
672}
673
674impl fmt::Debug for MemorySeeker {
675    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
676        f.debug_struct("MemorySeeker")
677            .field("name", &self.subscription.name)
678            .finish_non_exhaustive()
679    }
680}
681
682/// Seeking is native on a retaining broker: it keeps a bounded publish log per name, so a
683/// subscription can re-enqueue any retained suffix of it. A discarding broker keeps no log, and
684/// its subscriptions carry no seek surface at all.
685impl Seekable for MemorySubscriber<Retaining> {
686    type Seeker = MemorySeeker;
687
688    fn seeker(&self) -> MemorySeeker {
689        MemorySeeker {
690            subscription: Arc::clone(&self.subscription),
691        }
692    }
693}
694
695impl Seeker for MemorySeeker {
696    type Position = MemoryPosition;
697    type Error = MemoryError;
698
699    /// Records the reposition and wakes the subscription; the stream applies it at the top of
700    /// its next poll, so once this resolves the next delivery reflects the new position.
701    ///
702    /// Under a [`TestApp`](crate::testing::TestApp) the reposition counts as in-flight work
703    /// until the subscription applies it, so driving the service to a standstill waits for the
704    /// replay instead of racing the poll that produces it.
705    ///
706    /// # Errors
707    ///
708    /// Returns [`MemoryError::ShutDown`] through a handle aliasing a shut-down bus, and
709    /// [`MemoryError::PositionEvicted`] for a sequence the retention bound has already dropped.
710    fn seek(&self, to: MemoryPosition) -> impl Future<Output = Result<(), MemoryError>> {
711        // The liveness check and the handoff happen under the bus lock (then the log lock,
712        // fanout's order): a seek that returns Ok happened strictly before any shutdown, and
713        // the resolved position is consistent with the log at that instant.
714        let bus = self
715            .subscription
716            .state
717            .subscribers
718            .lock()
719            .expect("memory broker mutex poisoned");
720        if !matches!(&*bus, Bus::Live(_)) {
721            return ready(Err(MemoryError::ShutDown));
722        }
723        let log = self
724            .subscription
725            .state
726            .log
727            .lock()
728            .expect("memory broker mutex poisoned");
729        let (oldest, tip) = log
730            .name(&self.subscription.name)
731            .map_or((0, 0), |name| (name.first_seq(), name.next_seq()));
732        drop(log);
733        let target = match to {
734            MemoryPosition::Start => oldest,
735            MemoryPosition::End => tip,
736            // Past the tip is a position the name has not reached yet: the subscription waits
737            // there rather than failing, and the next publish opens it.
738            MemoryPosition::Sequence(n) if n >= oldest => n.min(tip),
739            MemoryPosition::Sequence(n) => {
740                return ready(Err(MemoryError::PositionEvicted {
741                    requested: n,
742                    oldest,
743                }));
744            }
745        };
746        // Watermark first (Release, paired with the Acquire load in the delivery filter), then
747        // the pending target: a poll that takes the target must see its watermark.
748        self.subscription
749            .seek
750            .watermark
751            .store(target, Ordering::Release);
752        let replaced = self
753            .subscription
754            .seek
755            .pending
756            .lock()
757            .expect("memory broker mutex poisoned")
758            .replace(target);
759        drop(bus);
760        // Under the harness, hold the reaction open from the moment the reposition is recorded
761        // until the stream applies it: the replay it will enqueue is not counted yet, and a
762        // quiescence wait landing in that window would return on an empty in-flight count.
763        // One token per pending reposition, released when the subscription applies it: a second
764        // seek before the first is applied replaces the target and owes nothing more.
765        #[cfg(feature = "testing")]
766        if replaced.is_none()
767            && let Some(coordinator) = self.subscription.state.coordinator()
768        {
769            coordinator.enqueued();
770        }
771        #[cfg(not(feature = "testing"))]
772        let _ = replaced;
773        self.subscription.seek.waker.wake();
774        ready(Ok(()))
775    }
776}
777
778impl<Log: LogMode> MemorySubscriber<Log> {
779    /// Applies a reposition requested through a [`MemorySeeker`], if one is pending.
780    ///
781    /// Runs at the top of the subscriber's own poll (the stream and batch paths both call it),
782    /// which is what makes `&mut` access to the receiver possible: everything queued before
783    /// the seek is drained, then the retained log from the resolved target on is re-enqueued.
784    /// Holding both bus locks makes the swap atomic against `fanout`, which stamps and sends
785    /// under the same locks in the same order.
786    pub(super) fn apply_pending_seek(&mut self) {
787        // A discarding subscription hands out no seeker, so nothing can ever be pending: the
788        // constant drops the whole body, locks included, from its poll.
789        if !log::retains::<Log>() {
790            return;
791        }
792        // Take the target in its own statement so the pending guard is released before the
793        // bus lock is acquired; `Seeker::seek` nests the locks in the other direction.
794        let pending = self
795            .subscription
796            .seek
797            .pending
798            .lock()
799            .expect("memory broker mutex poisoned")
800            .take();
801        let Some(target) = pending else { return };
802        #[cfg(feature = "testing")]
803        let coordinator = self.coordinator();
804        // The seek counted itself in flight when it recorded this target; the token goes back
805        // however this returns, so a quiescence wait cannot observe the gap between the request
806        // and the replay, and a seek that raced shutdown still drains the in-flight count.
807        #[cfg(feature = "testing")]
808        let seek_token = SeekToken(coordinator.clone());
809
810        let bus = self
811            .subscription
812            .state
813            .subscribers
814            .lock()
815            .expect("memory broker mutex poisoned");
816        // A seek that raced shutdown: the subscription is dead, replay nothing.
817        if !matches!(&*bus, Bus::Live(_)) {
818            return;
819        }
820        let log = self
821            .subscription
822            .state
823            .log
824            .lock()
825            .expect("memory broker mutex poisoned");
826        let retained = log.name(self.subscription.name.as_str());
827
828        // The replay is counted in flight BEFORE the drain releases the queued deliveries:
829        // decrementing first would let the in-flight count touch zero mid-swap, and a
830        // concurrent quiescence wait (`TestApp::settle`) could observe that instant and return
831        // before the replayed deliveries were processed.
832        #[cfg(feature = "testing")]
833        if let Some(coordinator) = &coordinator {
834            for _ in retained.into_iter().flat_map(|log| log.replay_from(target)) {
835                coordinator.enqueued();
836            }
837        }
838
839        while self.rx.try_recv().is_ok() {
840            // Every drained delivery was counted in flight when it was enqueued.
841            #[cfg(feature = "testing")]
842            if let Some(coordinator) = &coordinator {
843                coordinator.consumed();
844            }
845        }
846
847        if let Some(retained) = retained {
848            for (seq, entry) in retained.replay_from(target) {
849                let delivery = MemoryDelivery {
850                    // The block the fanout built, name and payload and headers together: a
851                    // replayed delivery is one reference count, not a rebuilt message.
852                    shared: entry.shared(),
853                    seq,
854                    // A replay is a fresh delivery of what the log holds, not a redelivery of
855                    // the copy a handler saw, so its count starts where a fanout's does.
856                    deliveries: NonZeroU64::MIN,
857                };
858                // The send cannot fail: this subscriber holds both ends of its own channel.
859                let _ = self.subscription.requeue.send(delivery);
860            }
861        }
862        drop(log);
863        drop(bus);
864        #[cfg(feature = "testing")]
865        drop(seek_token);
866    }
867}
868
869/// The in-flight token one pending reposition holds, released when the subscription has applied
870/// it (whichever way [`apply_pending_seek`](MemorySubscriber::apply_pending_seek) returns).
871#[cfg(feature = "testing")]
872struct SeekToken(Option<Coordinator>);
873
874#[cfg(feature = "testing")]
875impl Drop for SeekToken {
876    fn drop(&mut self) {
877        if let Some(coordinator) = &self.0 {
878            coordinator.consumed();
879        }
880    }
881}
882
883/// The position is the delivery's absolute sequence number in its name's publish log, assigned
884/// at fanout and preserved across requeues, so a redelivered message reports the same position.
885/// Only a retaining broker's deliveries have one: without a log there is nothing to be a
886/// position in.
887impl Positioned for MemoryMessage<Retaining> {
888    type Position = MemoryPosition;
889
890    fn position(&self) -> MemoryPosition {
891        MemoryPosition::Sequence(self.delivery.as_ref().map_or(0, |d| d.seq))
892    }
893}
894
895/// The in-memory broker's per-delivery context: this delivery's position in its name's publish
896/// log, and the subscription's own seeker.
897///
898/// The runtime builds one per dispatched delivery (see [`BuildContext`](crate::BuildContext)),
899/// and handlers read its fields by key - [`Position`] and [`SeekHandle`] - through the
900/// [`Ctx`](crate::runtime::Ctx) extractor or `ctx.context(..)`. A body that manages a log
901/// cursor names this type as its `C` axis and needs nothing else.
902///
903/// # Examples
904///
905/// ```
906/// # #[cfg(all(feature = "memory", feature = "json"))]
907/// # mod demo {
908/// use ruststream::memory::{MemoryContext, Position, SeekHandle};
909/// use ruststream::prelude::*;
910/// use ruststream::Seeker;
911/// # #[derive(serde::Deserialize, schemars::JsonSchema)]
912/// # struct Job { id: u64 }
913///
914/// struct Replayer;
915///
916/// impl Handle<Job, (), (), MemoryContext> for Replayer {
917///     async fn handle(
918///         &self,
919///         job: &Job,
920///         _outs: &(),
921///         ctx: &mut Context<'_, MemoryContext>,
922///     ) -> Result<(), HandlerOutcome> {
923///         let here = ctx.context(Position);
924///         if job.id == u64::MAX && ctx.context(SeekHandle).seek(here).await.is_err() {
925///             return Err(HandlerOutcome::retry());
926///         }
927///         Ok(())
928///     }
929/// }
930/// # }
931/// ```
932#[derive(Debug)]
933pub struct MemoryContext {
934    position: MemoryPosition,
935    seeker: MemorySeeker,
936}
937
938impl crate::BuildContext<MemoryMessage<Retaining>> for MemoryContext {
939    /// # Panics
940    ///
941    /// Panics on a request-reply inbox message, which is not a subscription delivery; no
942    /// dispatch loop (and therefore no per-delivery context) ever builds off one.
943    fn build(msg: &MemoryMessage<Retaining>) -> Self {
944        Self {
945            position: Positioned::position(msg),
946            seeker: MemorySeeker::of(msg),
947        }
948    }
949}
950
951/// The in-memory broker's subscription-scoped batch context: the subscription's own seeker,
952/// shared by every delivery of the batch.
953///
954/// The runtime builds one per dispatched batch from the batch's first delivery (see
955/// [`BuildBatchContext`](crate::BuildBatchContext)), and a batch body reads it by key -
956/// [`SeekHandle`] - through `ctx.context(..)`. Per-delivery data (a [`Position`]) has no place
957/// here: a batch spans many deliveries, so the position a body reacts to rides the elements
958/// themselves (a `&[Message<H, T>]` batch reads it off each element's header contract), and
959/// keeping this a separate type from [`MemoryContext`] is what rejects a batch body asking for
960/// per-delivery fields at compile time.
961///
962/// # Examples
963///
964/// ```
965/// # #[cfg(all(feature = "memory", feature = "json"))]
966/// # mod demo {
967/// use ruststream::Seeker;
968/// use ruststream::memory::{MemoryBatchContext, MemoryPosition, SeekHandle};
969/// use ruststream::prelude::*;
970/// # #[derive(serde::Deserialize, schemars::JsonSchema)]
971/// # struct Job { id: u64 }
972///
973/// struct Replayer;
974///
975/// impl Handle<[Job], (), (), MemoryBatchContext> for Replayer {
976///     async fn handle(
977///         &self,
978///         batch: &[Job],
979///         _outs: &(),
980///         ctx: &mut Context<'_, MemoryBatchContext>,
981///     ) -> Result<(), Vec<HandlerOutcome>> {
982///         // A batch that saw the rewind marker repositions the whole subscription once it is
983///         // settled; the next batch opens at the target.
984///         if batch.iter().any(|job| job.id == u64::MAX)
985///             && ctx
986///                 .context(SeekHandle)
987///                 .seek(MemoryPosition::start())
988///                 .await
989///                 .is_err()
990///         {
991///             return Err(batch.iter().map(|_| HandlerOutcome::retry()).collect());
992///         }
993///         Ok(())
994///     }
995/// }
996/// # }
997/// ```
998#[derive(Debug)]
999pub struct MemoryBatchContext {
1000    seeker: MemorySeeker,
1001}
1002
1003impl crate::BuildBatchContext<MemoryMessage<Retaining>> for MemoryBatchContext {
1004    /// # Panics
1005    ///
1006    /// Panics on a request-reply inbox message, which is not a subscription delivery; no
1007    /// dispatch loop (and therefore no batch context) ever builds off one.
1008    fn build(first: &MemoryMessage<Retaining>) -> Self {
1009        Self {
1010            seeker: MemorySeeker::of(first),
1011        }
1012    }
1013}
1014
1015impl crate::Field<MemoryBatchContext> for SeekHandle {
1016    type Value<'a> = &'a MemorySeeker;
1017
1018    fn get(self, src: &MemoryBatchContext) -> &MemorySeeker {
1019        &src.seeker
1020    }
1021}
1022
1023/// The key reading this delivery's [`MemoryPosition`] out of [`MemoryContext`].
1024///
1025/// # Examples
1026///
1027/// ```
1028/// # #[cfg(all(feature = "memory", feature = "macros", feature = "json"))]
1029/// # mod demo {
1030/// use ruststream::memory::Position;
1031/// use ruststream::prelude::*;
1032/// use ruststream::subscriber;
1033/// # #[derive(serde::Deserialize)]
1034/// # struct Job { id: u64 }
1035///
1036/// #[subscriber("jobs")]
1037/// async fn audit(job: &Job, Ctx(at): Ctx<Position>) -> HandlerOutcome {
1038///     println!("job {} sits at {at:?}", job.id);
1039///     HandlerOutcome::ack()
1040/// }
1041/// # }
1042/// ```
1043#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
1044pub struct Position;
1045
1046impl crate::ContextField for Position {
1047    type Context = MemoryContext;
1048    type Value = MemoryPosition;
1049
1050    fn read(self, src: &MemoryContext) -> MemoryPosition {
1051        src.position
1052    }
1053}
1054
1055impl crate::Field<MemoryContext> for Position {
1056    type Value<'a> = MemoryPosition;
1057
1058    fn get(self, src: &MemoryContext) -> MemoryPosition {
1059        src.position
1060    }
1061}
1062
1063/// The key reading the subscription's [`MemorySeeker`] out of [`MemoryContext`]: the reposition
1064/// handle, resolved once when the subscription opens and carried by every delivery's context.
1065///
1066/// # Examples
1067///
1068/// ```
1069/// # #[cfg(all(feature = "memory", feature = "macros", feature = "json"))]
1070/// # mod demo {
1071/// use ruststream::memory::{MemoryPosition, SeekHandle};
1072/// use ruststream::prelude::*;
1073/// use ruststream::{Seeker, subscriber};
1074/// # #[derive(serde::Deserialize)]
1075/// # struct Job { id: u64, poisoned_until: Option<usize> }
1076///
1077/// /// Skips forward past a region the producer marked poisoned.
1078/// #[subscriber("jobs")]
1079/// async fn work(job: &Job, Ctx(seeker): Ctx<SeekHandle>) -> HandlerOutcome {
1080///     if let Some(resume_at) = job.poisoned_until {
1081///         if seeker.seek(MemoryPosition::sequence(resume_at)).await.is_err() {
1082///             return HandlerOutcome::retry();
1083///         }
1084///     }
1085///     HandlerOutcome::ack()
1086/// }
1087/// # }
1088/// ```
1089#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
1090pub struct SeekHandle;
1091
1092impl crate::ContextField for SeekHandle {
1093    type Context = MemoryContext;
1094    type Value = MemorySeeker;
1095
1096    fn read(self, src: &MemoryContext) -> MemorySeeker {
1097        src.seeker.clone()
1098    }
1099}
1100
1101impl crate::Field<MemoryContext> for SeekHandle {
1102    type Value<'a> = &'a MemorySeeker;
1103
1104    fn get(self, src: &MemoryContext) -> &MemorySeeker {
1105        &src.seeker
1106    }
1107}
1108
1109#[cfg(test)]
1110mod tests;