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;