protocol_droid/domain/stream/event_ring.rs
1//! A bounded ring of stream events, replayable from a client's last-seen id.
2//!
3//! # The shape of the storage, and why it is a `Vec` with a cursor
4//!
5//! A `VecDeque` is the obvious structure and it is the wrong one here, for a
6//! reason about the *return type* rather than about the cost. [`Replay`] hands
7//! back a borrowed slice of the events after a watermark, and `VecDeque::get`
8//! with a range returns `None` unless the range happens to be contiguous — so
9//! on a wrapped ring the answer would depend on the push history rather than on
10//! the watermark. Making it contiguous on demand is `make_contiguous`, which
11//! needs `&mut self` for what is a read, so a reconnect would mutate the ring
12//! in order to look at it and the port owning the ring would have to hand out a
13//! mutable borrow.
14//!
15//! A `Vec` plus a `start` cursor is always contiguous, keeps
16//! [`EventRing::replay_from`] on `&self`, and costs one amortised compaction:
17//! once the dead prefix reaches the capacity it is drained and the cursor
18//! returns to zero. That bounds the allocation at roughly twice the capacity in
19//! slots and makes every push amortised O(1), which is what a push on a busy
20//! notification stream needs.
21//!
22//! # Why eviction is recorded rather than inferred
23//!
24//! The oldest retained event is not always in the buffer. After a compaction it
25//! is gone, and a `Gap` still has to be reported for a client that asks from
26//! before the last drop. So `evicted_through` is a field rather than something
27//! read off the front of the ring, and it is never reset: the newest id that has
28//! ever been dropped is the only thing needed to tell "still contiguous" from
29//! "a hole is in front of you".
30
31use core::fmt;
32use core::num::NonZeroUsize;
33
34/// A stream event's position, as a client quotes it back.
35///
36/// Monotonic from 1, and 0 is reserved: [`EventId::WATERMARK`] is "before
37/// anything was sent", which is what a client means by a `Last-Event-ID` of
38/// `0` and what an empty ring has as its last id. Reserving it is what lets
39/// every comparison in [`EventRing::replay_from`] be a plain `u64` comparison
40/// with no `Option` threaded through the arithmetic.
41#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
42pub struct EventId(u64);
43
44impl EventId {
45 /// The watermark before the first event.
46 pub const WATERMARK: Self = Self(0);
47
48 /// An id, for a caller that computed one. `0` is [`WATERMARK`](Self::WATERMARK).
49 #[must_use]
50 pub const fn new(value: u64) -> Self {
51 Self(value)
52 }
53
54 /// The id as a number.
55 #[must_use]
56 pub const fn get(self) -> u64 {
57 self.0
58 }
59
60 /// Whether this is the reserved watermark.
61 #[must_use]
62 pub const fn is_watermark(self) -> bool {
63 self.0 == 0
64 }
65}
66
67impl fmt::Display for EventId {
68 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
69 write!(formatter, "{}", self.0)
70 }
71}
72
73/// One event, with the id a client would quote to resume after it.
74#[derive(Debug, Clone, PartialEq, Eq)]
75pub struct StreamEvent<T> {
76 id: EventId,
77 payload: T,
78}
79
80impl<T> StreamEvent<T> {
81 /// An event at a known position.
82 #[must_use]
83 pub const fn new(id: EventId, payload: T) -> Self {
84 Self { id, payload }
85 }
86
87 /// The position.
88 #[must_use]
89 pub const fn id(&self) -> EventId {
90 self.id
91 }
92
93 /// The payload.
94 #[must_use]
95 pub const fn payload(&self) -> &T {
96 &self.payload
97 }
98
99 /// Unwraps into the parts.
100 #[must_use]
101 pub fn into_parts(self) -> (EventId, T) {
102 (self.id, self.payload)
103 }
104
105 /// Borrows the payload mutably, for a payload that is an in-place buffer.
106 #[must_use]
107 pub const fn payload_mut(&mut self) -> &mut T {
108 &mut self.payload
109 }
110}
111
112/// What a client asking to resume actually gets.
113///
114/// Four arms, and the distinction that matters is between the first two and the
115/// last two. `UpToDate` and `Buffered` are success: nothing was lost. `Gap` and
116/// `Unknown` are refusals, and a caller that collapses them into an empty list
117/// has just reported a lost event as a success — which is the specific failure
118/// this enum is shaped to make awkward.
119#[derive(Debug, Clone, PartialEq, Eq)]
120pub enum Replay<'a, T> {
121 /// The client is already current. There is genuinely nothing to send.
122 UpToDate {
123 /// The newest event the stream ever issued.
124 last: EventId,
125 },
126
127 /// Everything after the client's watermark is still held. Send all of it.
128 Buffered {
129 /// The events, oldest first, contiguous from the watermark.
130 events: &'a [StreamEvent<T>],
131 /// The newest event the stream ever issued.
132 last: EventId,
133 },
134
135 /// The client is behind the point where events started being dropped, so a
136 /// resume would silently omit what it missed. The client must be told, and
137 /// must re-issue whatever it has not seen.
138 Gap {
139 /// The watermark the client asked from.
140 requested: EventId,
141 /// The newest event the ring has dropped. Anything at or before this is
142 /// gone.
143 evicted_through: EventId,
144 /// The newest event the stream ever issued.
145 last: EventId,
146 },
147
148 /// The client quoted an id the stream never issued.
149 ///
150 /// Separate from [`Gap`](Self::Gap) on purpose. A gap means the client is
151 /// behind and something was lost; this means the client's id is ahead of
152 /// anything that exists, which is a client that is out of step with the
153 /// server — a different fault with a different fix, and one that a
154 /// "resume from the beginning" fallback would paper over.
155 Unknown {
156 /// The id the client quoted.
157 requested: EventId,
158 /// The newest event the stream ever issued.
159 last: EventId,
160 },
161}
162
163impl<T> Replay<'_, T> {
164 /// Whether the client's request can be honoured without losing an event.
165 ///
166 /// The one question an adapter has to answer before writing a response
167 /// body, and it is [`false`] for exactly the two arms where a naive
168 /// implementation would send a partial stream and call it a success.
169 #[must_use]
170 pub const fn is_complete(&self) -> bool {
171 matches!(self, Self::UpToDate { .. } | Self::Buffered { .. })
172 }
173
174 /// The events to write, if any.
175 ///
176 /// Empty for both refusals, which is why it is not the thing to branch on:
177 /// use [`is_complete`](Self::is_complete) for that. Provided for the write
178 /// loop, which has nothing to do after being told about a gap.
179 #[must_use]
180 pub const fn events(&self) -> &[StreamEvent<T>] {
181 match self {
182 Self::Buffered { events, .. } => events,
183 Self::UpToDate { .. } | Self::Gap { .. } | Self::Unknown { .. } => &[],
184 }
185 }
186}
187
188/// A capacity for a stream whose payload size the caller has not measured.
189///
190/// Exists so [`EventRing::new`] can take a `NonZeroUsize` — a zero-capacity ring
191/// would accept a push and drop it immediately, which is a livelock dressed up
192/// as a buffer — without forcing every construction site to reach for
193/// `NonZeroUsize::new`. A caller that knows its own bound should pass one
194/// directly; this is the one to use when it does not.
195#[derive(Debug, Clone, Copy, PartialEq, Eq)]
196pub struct RecommendedCapacity(NonZeroUsize);
197
198impl RecommendedCapacity {
199 /// The count.
200 #[must_use]
201 pub const fn get(self) -> usize {
202 self.0.get()
203 }
204}
205
206impl From<RecommendedCapacity> for NonZeroUsize {
207 fn from(value: RecommendedCapacity) -> Self {
208 value.0
209 }
210}
211
212/// Events to retain for a stream whose payload size is unmeasured.
213///
214/// 1024, and the number is a guess with a stated basis rather than a constant
215/// someone liked. Two observations set it:
216///
217/// - It is a *reconnect* window, not a session window. A client that has been
218/// gone for longer than the window is expected to resume from the beginning
219/// of what is retained, or to be told there is a gap.
220/// - It is sized for the notification traffic the transport actually carries
221/// on a stream — progress and message notifications for the request they
222/// relate to, not `resources/read` bodies, which in `2025-11-25` and earlier
223/// flow on the response stream of their own request.
224///
225/// At a notional one progress notification per second that is roughly seventeen
226/// minutes of disconnection. The memory cost is 1024 slots, and it is the
227/// *payload* that decides whether that is cheap, which is exactly why an
228/// adapter streaming large bodies should pass a smaller number of its own
229/// choosing. Nothing in the specification requires any particular bound.
230///
231/// `recommended_capacity_covers_a_reconnect`
232/// is the test that holds this reasoning to the number.
233pub const RECOMMENDED_CAPACITY: RecommendedCapacity =
234 RecommendedCapacity(match NonZeroUsize::new(1024) {
235 Some(value) => value,
236 // Unreachable: 1024 is not zero. Written as a `match` rather than
237 // `unwrap` because the crate denies that, and the alternative — a
238 // `LazyLock` or a hard-coded duplicate of the number — would be worse
239 // for a case the compiler already rules out.
240 None => NonZeroUsize::MIN,
241 });
242
243/// A bounded, replayable buffer of stream events.
244///
245/// Generic over the payload because the ring is transport machinery: it must not
246/// name an MCP type, and a `serde_json::Value` in a domain signature is exactly
247/// the kind of raw JSON the layer rules keep out of here. The adapter supplies
248/// whatever its wire type is.
249#[derive(Debug)]
250pub struct EventRing<T> {
251 capacity: NonZeroUsize,
252 buffer: Vec<StreamEvent<T>>,
253 /// Index of the oldest retained event within `buffer`.
254 start: usize,
255 /// The newest id ever issued, or the watermark if nothing has been.
256 last: EventId,
257 /// The newest id ever dropped, if anything has been.
258 evicted_through: Option<EventId>,
259 /// The newest id belonging to a stream that [`clear`](Self::clear) ended.
260 ///
261 /// Separate from `evicted_through` because the two mean different things to
262 /// a client. An overflow drop means the client is *behind* and could be
263 /// caught up from the drop boundary. A clear means the stream those ids
264 /// belonged to is finished, so there is no continuation of them to catch up
265 /// to — and answering with the retained window would splice two streams
266 /// together as though they were one.
267 retired_through: Option<EventId>,
268}
269
270impl<T> EventRing<T> {
271 /// A ring retaining at most `capacity` events.
272 ///
273 /// A capacity of zero is refused at the type level rather than tolerated:
274 /// such a ring would accept a push, drop it, and report success, which is
275 /// the eviction-as-success bug with no eviction to notice.
276 #[must_use]
277 pub fn new(capacity: NonZeroUsize) -> Self {
278 Self {
279 capacity,
280 buffer: Vec::with_capacity(capacity.get()),
281 start: 0,
282 last: EventId::WATERMARK,
283 evicted_through: None,
284 retired_through: None,
285 }
286 }
287
288 /// Retains [`RECOMMENDED_CAPACITY`] events.
289 #[must_use]
290 pub fn with_recommended_capacity() -> Self {
291 Self::new(RECOMMENDED_CAPACITY.into())
292 }
293
294 /// The most events this ring will hold.
295 #[must_use]
296 pub const fn capacity(&self) -> usize {
297 self.capacity.get()
298 }
299
300 /// The newest id ever issued, or the watermark if nothing has been.
301 #[must_use]
302 pub const fn last(&self) -> EventId {
303 self.last
304 }
305
306 /// How many events are currently retained.
307 #[must_use]
308 pub const fn len(&self) -> usize {
309 self.buffer.len() - self.start
310 }
311
312 /// Whether nothing is retained.
313 #[must_use]
314 pub const fn is_empty(&self) -> bool {
315 self.len() == 0
316 }
317
318 /// The newest event ever dropped, if the ring has dropped anything.
319 ///
320 /// A client at or after this id can still be served contiguously; a client
321 /// before it cannot.
322 #[must_use]
323 pub const fn evicted_through(&self) -> Option<EventId> {
324 self.evicted_through
325 }
326
327 /// Appends an event, dropping the oldest if the ring is full.
328 ///
329 /// Returns the id assigned, which is the value a client will quote to
330 /// resume after this event.
331 pub fn push(&mut self, payload: T) -> EventId {
332 if self.len() == self.capacity.get() {
333 self.evict_one();
334 }
335 self.last = EventId(self.last.get().saturating_add(1));
336 self.buffer.push(StreamEvent::new(self.last, payload));
337 self.last
338 }
339
340 /// The live window, which is the buffer minus its dead prefix.
341 fn window(&self) -> &[StreamEvent<T>] {
342 self.buffer.get(self.start..).unwrap_or(&[])
343 }
344
345 /// Drops the oldest retained event and compacts if that is now worthwhile.
346 fn evict_one(&mut self) {
347 if let Some(dropped) = self.buffer.get(self.start) {
348 self.evicted_through = Some(dropped.id());
349 }
350 self.start += 1;
351 // Compact when the dead prefix is at least half the allocation, so the
352 // `drain` is amortised O(1) against the pushes that caused it, and when
353 // everything is dead so the allocation is reused rather than regrown.
354 if self.start == self.buffer.len() {
355 self.buffer.clear();
356 self.start = 0;
357 } else if self.start >= self.capacity.get() {
358 self.buffer.drain(..self.start);
359 self.start = 0;
360 }
361 }
362
363 /// Answers a client that says where it stopped.
364 ///
365 /// `after` is the client's `Last-Event-ID`, or `None` for a client that has
366 /// never seen this stream and wants everything. The four outcomes are
367 /// described on [`Replay`]; the short version is that a request is only
368 /// [`Replay::is_complete`] when the ring is still holding everything after
369 /// the watermark.
370 ///
371 /// Takes `&self` and hands back a borrow, so an adapter can answer from a
372 /// shared reference without taking the stream's lock exclusively.
373 #[must_use]
374 pub fn replay_from(&self, after: Option<EventId>) -> Replay<'_, T> {
375 let last = self.last;
376 let Some(requested) = after else {
377 // A client with no history cannot be missing anything, so whatever
378 // is retained is a complete answer *for it* — even when the ring has
379 // dropped events that a resuming client would have asked for. This
380 // is why the no-history case is answered before the eviction
381 // watermark is consulted: a fresh subscriber is not behind, and
382 // refusing it over a drop that predates its arrival would lose the
383 // entire stream for no reason.
384 //
385 // Nothing retained answers `UpToDate` rather than an empty
386 // `Buffered`: a client asking a stream that has emitted nothing is
387 // up to date, and handing it a zero-length window instead would
388 // push a branch on "did I get an empty one or the end" into every
389 // caller to distinguish two answers that mean the same thing.
390 if self.is_empty() {
391 return Replay::UpToDate { last };
392 }
393 return Replay::Buffered {
394 events: self.window(),
395 last,
396 };
397 };
398 if requested == last {
399 return Replay::UpToDate { last };
400 }
401 if requested > last {
402 // A client quoting an id past the end is not merely behind: it is
403 // out of step, and resuming from the top would send it events it
404 // believes it already has.
405 return Replay::Unknown { requested, last };
406 }
407 if let Some(retired) = self.retired_through
408 && requested <= retired
409 {
410 // Ids from before an explicit `clear` belong to a stream that is
411 // over. They are neither behind nor in step: there is no
412 // continuation of them, so this is the same answer as an id past
413 // the end rather than a gap the client could recover across.
414 return Replay::Unknown { requested, last };
415 }
416 // `requested < last`, so at least one event is outstanding. Whether it
417 // can be served is decided by the last drop, not by the current front.
418 //
419 // `replay_from(Some(id))` means "I have seen through `id`, send me what
420 // follows", so a client quoting exactly `evicted_through` is asking for
421 // `evicted_through + 1` onward — every one of which is still here. The
422 // test is therefore `requested < evicted_through`, exclusive.
423 //
424 // An inclusive test here is a trap with no good recovery: a caller that
425 // backs its cursor off to `evicted_through` to recover is told it is
426 // still behind and gets nothing but gap notices forever, and a caller
427 // that instead moves to `evicted_through + 1` skips the one surviving
428 // event. Both readings lose, which is what makes the exclusive test the
429 // only one a caller can act on.
430 if let Some(evicted_through) = self.evicted_through
431 && requested < evicted_through
432 {
433 return Replay::Gap {
434 requested,
435 evicted_through,
436 last,
437 };
438 }
439 // The watermark is at or after the last drop, so every event after it
440 // is still here. `self.len()` is non-zero because `requested < last`.
441 // `behind` counts events back from the newest, so subtracting it from
442 // the retained count gives the offset *within the live window* — which
443 // is `buffer[start..]`, not `buffer`. Adding `self.start` is what turns
444 // it into a real index, and omitting it silently resends the dead
445 // prefix.
446 let offset = usize::try_from(last.get() - requested.get())
447 .ok()
448 .and_then(|behind| self.len().checked_sub(behind))
449 .map(|offset| self.start + offset);
450 offset
451 .and_then(|start| self.buffer.get(start..))
452 .map_or_else(
453 // Unreachable given the arithmetic above: `behind` is the
454 // number of events after the watermark and cannot exceed what
455 // is retained once the gap check has passed. Returning a gap
456 // rather than an empty success is the safe direction to be
457 // wrong in, because an adapter that sees a gap re-issues and
458 // one that sees an empty success silently drops the stream.
459 || Replay::Gap {
460 requested,
461 evicted_through: self.evicted_through.unwrap_or(EventId::WATERMARK),
462 last,
463 },
464 |events| Replay::Buffered { events, last },
465 )
466 }
467
468 /// Empties the ring, keeping the id sequence going.
469 ///
470 /// A new stream, not a continuing one: ids stay monotonic so a client that
471 /// reconnects across the reset is refused with
472 /// [`Replay::Unknown`](Replay::Unknown) rather than being served events
473 /// from a stream that no longer exists.
474 pub fn clear(&mut self) {
475 self.buffer.clear();
476 self.start = 0;
477 // The ids do not restart, so a client quoting one from the finished
478 // stream can be told it is out of step rather than served the new
479 // stream's opening events as though they followed its own.
480 self.retired_through = Some(self.last);
481 self.evicted_through = None;
482 }
483}
484
485impl<T> Default for EventRing<T> {
486 /// Retains [`RECOMMENDED_CAPACITY`], so `EventRing::default()` is the
487 /// unmeasured-payload case and never a zero-capacity ring.
488 fn default() -> Self {
489 Self::with_recommended_capacity()
490 }
491}
492
493#[cfg(test)]
494mod tests {
495 use super::{EventId, EventRing, RECOMMENDED_CAPACITY, Replay, StreamEvent};
496 use core::num::NonZeroUsize;
497 use std::sync::Arc;
498 use std::thread;
499
500 /// Generic over the payload so each test states the type it is about.
501 /// Most push a `&'static str`; the id-monotonicity tests push the index
502 /// they are tracking, and the borrow test pushes an `Arc` it expects to
503 /// see come back without a copy. Pinning one payload here would make those
504 /// tests about a type they are not exercising.
505 fn ring<T>(capacity: usize) -> Result<EventRing<T>, Failure> {
506 let capacity = NonZeroUsize::new(capacity).ok_or(Failure::ZeroCapacity)?;
507 Ok(EventRing::new(capacity))
508 }
509
510 #[derive(Debug, PartialEq, Eq)]
511 enum Failure {
512 ZeroCapacity,
513 WrongIds(Vec<u64>),
514 WrongPayloads(Vec<String>),
515 Outcome(&'static str),
516 Concurrent,
517 }
518
519 /// The payloads a replay would deliver, as owned strings so a borrow from
520 /// the ring does not have to outlive the assertion.
521 fn payloads<T: Clone + core::fmt::Debug>(replay: &Replay<'_, T>) -> Vec<String> {
522 replay
523 .events()
524 .iter()
525 .map(|event| format!("{:?}", event.payload()))
526 .collect()
527 }
528
529 fn ids<T>(replay: &Replay<'_, T>) -> Vec<u64> {
530 replay
531 .events()
532 .iter()
533 .map(|event| event.id().get())
534 .collect()
535 }
536
537 /// A client that has seen nothing gets the whole ring, in order, with no
538 /// duplicates and no reordering.
539 #[test]
540 fn first_continuation_returns_everything_in_order() -> Result<(), Failure> {
541 let mut ring = ring(8)?;
542 let mut assigned = Vec::new();
543 for payload in ["a", "b", "c"] {
544 assigned.push(ring.push(payload).get());
545 }
546 let replay = ring.replay_from(None);
547 if !replay.is_complete() {
548 return Err(Failure::Outcome("a fresh client must be served whole"));
549 }
550 if ids(&replay) != assigned {
551 return Err(Failure::WrongIds(assigned));
552 }
553 if payloads(&replay) != vec!["\"a\"", "\"b\"", "\"c\""] {
554 return Err(Failure::WrongPayloads(payloads(&replay)));
555 }
556 Ok(())
557 }
558
559 /// Resuming from the last thing the client saw sends only what came after
560 /// it. A duplicate here is the failure the ring exists to make impossible.
561 #[test]
562 fn resume_sends_only_what_follows() -> Result<(), Failure> {
563 let mut ring = ring(8)?;
564 let first = ring.push("a");
565 let second = ring.push("b");
566 let replay = ring.replay_from(Some(first));
567 if ids(&replay) != vec![second.get()] {
568 return Err(Failure::WrongIds(ids(&replay)));
569 }
570 if payloads(&replay) != vec!["\"b\""] {
571 return Err(Failure::WrongPayloads(payloads(&replay)));
572 }
573 Ok(())
574 }
575
576 /// A client already caught up gets an explicit "nothing to send" rather than
577 /// an empty slice it has to interpret.
578 #[test]
579 fn a_current_client_is_told_it_is_current() -> Result<(), Failure> {
580 let mut ring = ring(8)?;
581 ring.push("a");
582 match ring.replay_from(Some(ring.last())) {
583 Replay::UpToDate { last } if last.get() == 1 => Ok(()),
584 other => Err(Failure::Outcome(match other {
585 Replay::UpToDate { .. } => "wrong last id",
586 Replay::Buffered { .. } => "a current client was sent events",
587 Replay::Gap { .. } => "reported a gap with nothing dropped",
588 Replay::Unknown { .. } => "reported an unknown id for a current client",
589 })),
590 }
591 }
592
593 /// **The eviction case.** Once the ring has dropped an event, a client
594 /// asking from before the drop is told there is a hole — the outcome is
595 /// named `Gap`, it is not [`Replay::is_complete`], and it carries the id
596 /// the client would have to resume from instead. This is the test that fails
597 /// if a `replay_from` ever returns an empty success for a forgotten event.
598 #[test]
599 fn replay_from_a_forgotten_event_is_a_named_gap_not_an_empty_success() -> Result<(), Failure> {
600 let mut ring = ring(3)?;
601 let first = ring.push("a");
602 ring.push("b");
603 ring.push("c");
604 let fourth = ring.push("d");
605
606 // `first` was evicted, but quoting it is the client saying it *received*
607 // `first`, which is a cursor the ring can serve. The gap is one id
608 // lower — a client that has seen less than the oldest thing ever issued.
609 let asked = EventId::WATERMARK;
610 let _ = first;
611 match ring.replay_from(Some(asked)) {
612 Replay::Gap {
613 requested,
614 evicted_through,
615 last,
616 } => {
617 if requested != asked {
618 return Err(Failure::WrongIds(vec![requested.get()]));
619 }
620 if last != fourth {
621 return Err(Failure::WrongIds(vec![last.get()]));
622 }
623 // The gap has to name the boundary that put the client behind
624 // it, and that boundary is strictly _after_ the id it asked
625 // for — a client holding `evicted_through` itself can still be
626 // served everything after it. It also has to name an event that
627 // really was dropped, so a client told to resync somewhere it
628 // already is not stranded.
629 if evicted_through.get() <= requested.get() {
630 return Err(Failure::WrongIds(vec![evicted_through.get()]));
631 }
632 if evicted_through.get() > last.get() {
633 return Err(Failure::WrongIds(vec![evicted_through.get()]));
634 }
635 Ok(())
636 }
637 Replay::UpToDate { .. } => Err(Failure::Outcome(
638 "a dropped event was reported as up to date",
639 )),
640 Replay::Buffered { .. } => {
641 Err(Failure::Outcome("a dropped event was reported as buffered"))
642 }
643 Replay::Unknown { .. } => Err(Failure::Outcome(
644 "a dropped event was reported as an unknown id",
645 )),
646 }
647 }
648
649 /// A client asking from the dropped event itself is still contiguous: the
650 /// drop *was* that event, and everything after it is held. Only asking from
651 /// before it is a gap.
652 #[test]
653 fn resume_from_the_dropped_event_is_still_contiguous() -> Result<(), Failure> {
654 let mut ring = ring(2)?;
655 ring.push("a");
656 let second = ring.push("b");
657 let third = ring.push("c");
658 let replay = ring.replay_from(Some(second));
659 if !replay.is_complete() {
660 return Err(Failure::Outcome(
661 "resuming at the drop point should be complete",
662 ));
663 }
664 if ids(&replay) != vec![third.get()] {
665 return Err(Failure::WrongIds(ids(&replay)));
666 }
667 Ok(())
668 }
669
670 /// A client quoting an id the stream never issued is refused distinctly
671 /// from a client that is behind. Falling back to "send everything" here
672 /// would re-deliver a stream the client has already seen.
673 #[test]
674 fn an_id_from_the_future_is_unknown_not_a_gap() -> Result<(), Failure> {
675 let mut ring = ring(4)?;
676 ring.push("a");
677 match ring.replay_from(Some(EventId::new(99))) {
678 Replay::Unknown { requested, last } => {
679 if requested.get() != 99 || last.get() != 1 {
680 return Err(Failure::WrongIds(vec![requested.get(), last.get()]));
681 }
682 Ok(())
683 }
684 _ => Err(Failure::Outcome("a future id was not reported as unknown")),
685 }
686 }
687
688 /// A gap on a ring that has never dropped anything is impossible, and the
689 /// unreachable branch of `replay_from` is held to that by construction: a
690 /// small ring that fits everything never reports a gap.
691 #[test]
692 fn a_ring_that_fits_everything_never_reports_a_gap() -> Result<(), Failure> {
693 let mut ring = EventRing::new(NonZeroUsize::new(16).ok_or(Failure::ZeroCapacity)?);
694 let mut last = EventId::WATERMARK;
695 for index in 0..15 {
696 last = ring.push(index);
697 }
698 for watermark in [EventId::WATERMARK, EventId::new(1), EventId::new(7), last] {
699 if !ring.replay_from(Some(watermark)).is_complete() {
700 return Err(Failure::Outcome("a ring holding everything reported a gap"));
701 }
702 }
703 Ok(())
704 }
705
706 /// Eviction must not disturb the ids. Ids are a sequence, not a window, so
707 /// a client reconnecting across an eviction still recognises them — that is
708 /// the whole basis for a `Gap` being reportable at all.
709 #[test]
710 fn ids_stay_monotonic_across_eviction() -> Result<(), Failure> {
711 let mut ring = ring(2)?;
712 let mut assigned = Vec::new();
713 for index in 0..10 {
714 assigned.push(ring.push(index).get());
715 }
716 // Strictly increasing by one, whatever was dropped.
717 let expected: Vec<u64> = (1..=10).collect();
718 if assigned != expected {
719 return Err(Failure::WrongIds(assigned));
720 }
721 if ring.last().get() != 10 {
722 return Err(Failure::WrongIds(vec![ring.last().get()]));
723 }
724 if ring.len() != 2 {
725 return Err(Failure::WrongIds(vec![
726 ring.len().try_into().unwrap_or(u64::MAX),
727 ]));
728 }
729 Ok(())
730 }
731
732 /// The dead prefix must actually be reclaimed, or the ring's memory grows
733 /// without bound and the `capacity` it advertises is a lie.
734 #[test]
735 fn compaction_keeps_the_buffer_from_growing() -> Result<(), Failure> {
736 let mut ring = ring(4)?;
737 for index in 0..1_000 {
738 ring.push(index);
739 }
740 if ring.len() != 4 {
741 return Err(Failure::WrongIds(vec![
742 ring.len().try_into().unwrap_or(u64::MAX),
743 ]));
744 }
745 // At most twice the live count: the compaction threshold is the live
746 // length, and the buffer is drained as soon as the dead prefix reaches
747 // the capacity.
748 let allocated = ring.buffer.len();
749 if allocated > ring.capacity() * 2 {
750 return Err(Failure::WrongIds(vec![
751 allocated.try_into().unwrap_or(u64::MAX),
752 ]));
753 }
754 Ok(())
755 }
756
757 /// The recommended capacity is the number the module docs justify, and the
758 /// justification is checkable: the window has to survive a reconnect long
759 /// enough to be worth having. Seventeen minutes at one event a second is
760 /// the claim, so the test holds the count to that arithmetic rather than to
761 /// a literal.
762 #[test]
763 fn recommended_capacity_covers_a_reconnect() -> Result<(), Failure> {
764 // Seventeen minutes at one notification a second, which is the claim
765 // the module docs make.
766 const RECONNECT_SECONDS: usize = 1_020;
767 if RECOMMENDED_CAPACITY.get() < RECONNECT_SECONDS {
768 return Err(Failure::WrongIds(vec![
769 RECOMMENDED_CAPACITY.get().try_into().unwrap_or(u64::MAX),
770 ]));
771 }
772 // ...and it is not unbounded: the same reasoning caps it at a size whose
773 // slots are a plausible allocation for one stream.
774 if RECOMMENDED_CAPACITY.get() > 65_536 {
775 return Err(Failure::WrongIds(vec![
776 RECOMMENDED_CAPACITY.get().try_into().unwrap_or(u64::MAX),
777 ]));
778 }
779 Ok(())
780 }
781
782 /// Clearing is a new stream, and a client that reconnects across the reset
783 /// is told its id is unknown rather than being served from a stream that no
784 /// longer exists.
785 #[test]
786 fn clear_starts_a_new_stream_and_refuses_the_old_ids() -> Result<(), Failure> {
787 let mut ring = ring(4)?;
788 let before = ring.push("a");
789 ring.push("b");
790 ring.clear();
791 if !ring.is_empty() {
792 return Err(Failure::Outcome("clear left events behind"));
793 }
794 if !matches!(ring.replay_from(Some(before)), Replay::Unknown { .. }) {
795 return Err(Failure::Outcome("a cleared stream served a pre-reset id"));
796 }
797 // Ids keep going up rather than restarting, which is what makes the
798 // refusal above possible.
799 let after = ring.push("c");
800 if after.get() != 3 {
801 return Err(Failure::WrongIds(vec![after.get()]));
802 }
803 Ok(())
804 }
805
806 /// Quoting a dropped id is servable; quoting an *older* one is a gap.
807 ///
808 /// The distinction is the whole contract of the resume argument, and it
809 /// runs against the intuition. `replay_from(Some(id))` means "I have seen
810 /// through `id`, send me what follows", so a client quoting an id the ring
811 /// no longer holds is a client that received it — the drop happened after
812 /// that client was last seen. Everything after the drop is contiguous, so
813 /// the answer is `Buffered` of the survivors.
814 ///
815 /// Reading the same situation inclusively leaves a caller no move that
816 /// does not lose. A cursor at `evicted_through` is refused again, and a
817 /// cursor at `evicted_through + 1` skips the one surviving event. The
818 /// exclusive test is the only one a caller can act on, which is why this
819 /// test exists in this shape rather than as a single-arm assertion.
820 #[test]
821 fn quoting_a_dropped_id_is_servable_but_older_is_not() -> Result<(), Failure> {
822 let mut ring = ring(3)?;
823 let first = ring.push("a");
824 ring.push("b");
825 ring.push("c");
826 ring.push("d");
827 // `first` is the evicted id.
828 let held = match ring.replay_from(Some(first)) {
829 Replay::Buffered { events, .. } => events,
830 Replay::UpToDate { .. } => {
831 return Err(Failure::Outcome("a held id was reported as up to date"));
832 }
833 Replay::Gap { .. } => {
834 return Err(Failure::Outcome(
835 "a client holding the dropped id was told it was behind",
836 ));
837 }
838 Replay::Unknown { .. } => {
839 return Err(Failure::Outcome(
840 "a client holding the dropped id was told it was unknown",
841 ));
842 }
843 };
844 // Everything after the drop, in order, starting at the first survivor.
845 let ids: Vec<u64> = held.iter().map(|event| event.id().get()).collect();
846 if ids != vec![2, 3, 4] {
847 return Err(Failure::WrongIds(ids));
848 }
849 // One id lower, and the client genuinely is behind.
850 if !matches!(
851 ring.replay_from(Some(EventId::WATERMARK)),
852 Replay::Gap { .. }
853 ) {
854 return Err(Failure::Outcome("a cursor below the drop was not a gap"));
855 }
856 Ok(())
857 }
858
859 /// A ring of one is the smallest that can lose anything, and it must still
860 /// report the loss rather than serve a stream with a hole in it.
861 #[test]
862 fn a_single_slot_ring_still_reports_eviction() -> Result<(), Failure> {
863 let mut ring = ring(1)?;
864 let first = ring.push("a");
865 ring.push("b");
866 // Resuming from *below* the drop is the gap case. A one-slot ring
867 // holding only `b` evicted `first`, so a client that has seen less than
868 // `first` has lost something.
869 if !matches!(
870 ring.replay_from(Some(EventId::WATERMARK)),
871 Replay::Gap { .. }
872 ) {
873 return Err(Failure::Outcome(
874 "a one-slot ring did not report its eviction",
875 ));
876 }
877 // ...and resuming from the dropped id itself is not a gap, because
878 // that quote *is* the client saying it received it. What follows is all
879 // still here. See `quoting_a_dropped_id_is_servable_but_older_is_not`.
880 if !matches!(ring.replay_from(Some(first)), Replay::Buffered { .. }) {
881 return Err(Failure::Outcome(
882 "a client holding the dropped id was refused its own successor",
883 ));
884 }
885 Ok(())
886 }
887
888 /// An empty ring answers a watermark with "current" and anything else with
889 /// "unknown", so a stream that has sent nothing does not invent a gap.
890 #[test]
891 fn an_empty_ring_has_no_gap() -> Result<(), Failure> {
892 let ring = ring::<&str>(4)?;
893 if !matches!(ring.replay_from(None), Replay::UpToDate { .. }) {
894 return Err(Failure::Outcome("an empty ring refused a fresh client"));
895 }
896 if !matches!(
897 ring.replay_from(Some(EventId::WATERMARK)),
898 Replay::UpToDate { .. }
899 ) {
900 return Err(Failure::Outcome("an empty ring refused the watermark"));
901 }
902 if !matches!(
903 ring.replay_from(Some(EventId::new(1))),
904 Replay::Unknown { .. }
905 ) {
906 return Err(Failure::Outcome("an empty ring invented an event"));
907 }
908 Ok(())
909 }
910
911 /// The `is_complete` answer is the one an adapter branches on, so it is
912 /// checked against every arm rather than left to the individual tests to
913 /// imply.
914 #[test]
915 fn completeness_agrees_with_the_arm() -> Result<(), Failure> {
916 let mut ring = ring(2)?;
917 ring.push("a");
918 ring.push("b");
919 ring.push("c");
920 let cases = [
921 (ring.replay_from(None), true),
922 (ring.replay_from(Some(ring.last())), true),
923 // The only thing incomplete is a gap, so this case has to be a real
924 // one: a cursor below the eviction point, not the dropped id itself.
925 (ring.replay_from(Some(EventId::WATERMARK)), false),
926 (ring.replay_from(Some(EventId::new(77))), false),
927 ];
928 for (replay, expected) in cases {
929 if replay.is_complete() != expected {
930 return Err(Failure::Outcome("is_complete disagreed with the arm"));
931 }
932 }
933 Ok(())
934 }
935
936 /// Replay hands back a borrow rather than a copy, so a caller can answer
937 /// from a shared reference while the producing task keeps the ring. Held
938 /// here as a compile-time claim: two overlapping borrows, one of them
939 /// `&self`, both live at once.
940 #[test]
941 fn a_replay_borrows_rather_than_copying() -> Result<(), Failure> {
942 let mut ring = ring::<Arc<str>>(4)?;
943 ring.push(Arc::<str>::from("a"));
944 ring.push(Arc::<str>::from("b"));
945 // The `Replay` is bound before the borrow is taken, because `events()`
946 // hands out a reference *into* it and a temporary would die at the end
947 // of the statement that produced the reference.
948 let replay = ring.replay_from(None);
949 let borrowed: &[StreamEvent<Arc<str>>] = replay.events();
950 // `ring` is still usable while `borrowed` lives, and the payloads are
951 // the caller's own `Arc`s, not copies of them.
952 if ring.len() != 2 {
953 return Err(Failure::WrongIds(vec![
954 ring.len().try_into().unwrap_or(u64::MAX),
955 ]));
956 }
957 let first = borrowed
958 .first()
959 .ok_or(Failure::Outcome("no event borrowed"))?;
960 if Arc::strong_count(first.payload()) < 1 {
961 return Err(Failure::Outcome("payload was cloned rather than borrowed"));
962 }
963 Ok(())
964 }
965
966 /// The ring is `Send` and `Sync` on its payload's account alone, which is
967 /// what lets a stream live in axum's shared state while a reconnect handler
968 /// reads it. A `Mutex` inside the ring would make this false and force every
969 /// reader to take a lock; the type is checked to stay that way.
970 #[test]
971 fn is_shareable_across_threads() -> Result<(), Failure> {
972 fn assert_send_sync<T: Send + Sync>() {}
973 assert_send_sync::<EventRing<u64>>();
974 assert_send_sync::<Replay<'static, u64>>();
975
976 // And the ring really does work when several threads read it while one
977 // writes, which is the shape a stream has in a server.
978 let ring = Arc::new(std::sync::Mutex::new(EventRing::new(
979 NonZeroUsize::new(64).ok_or(Failure::ZeroCapacity)?,
980 )));
981 let writer = {
982 let ring = Arc::clone(&ring);
983 thread::spawn(move || {
984 let mut held = ring.lock().map_err(|_| Failure::Concurrent)?;
985 for value in 0..1_000_u64 {
986 held.push(value);
987 }
988 // The lock is not needed to return, and holding it across the
989 // join would serialise the reader behind this thread.
990 drop(held);
991 Ok::<(), Failure>(())
992 })
993 };
994 let reader = {
995 let ring = Arc::clone(&ring);
996 thread::spawn(move || {
997 // The reader resumes from a watermark rather than from the
998 // beginning, because a reader with no history can *legitimately*
999 // be told "up to date" by an empty ring — that is the writer
1000 // not having started yet, not a lost event, and an assertion
1001 // that reads it as a loss is a race against thread start-up
1002 // rather than a claim about the ring.
1003 let mut watermark = EventId::WATERMARK;
1004 for _ in 0..1_000 {
1005 let held = ring.lock().map_err(|_| Failure::Concurrent)?;
1006 let replay = held.replay_from(Some(watermark));
1007 // The claim: once the writer has produced something this
1008 // reader has not seen, a complete answer carries at least
1009 // one event. A gap is excluded because the ring is then
1010 // saying it cannot serve the reader contiguously, which is
1011 // a refusal rather than a loss.
1012 if replay.is_complete()
1013 && watermark.get() < held.last().get()
1014 && replay.events().is_empty()
1015 {
1016 return Err(Failure::Outcome("a complete replay was empty"));
1017 }
1018 for event in replay.events() {
1019 if event.id().get() <= watermark.get() {
1020 return Err(Failure::Outcome("an event was replayed twice"));
1021 }
1022 watermark = event.id();
1023 }
1024 // Released here rather than at the end of the body so the
1025 // next iteration does not queue behind this lock.
1026 drop(held);
1027 }
1028 Ok::<(), Failure>(())
1029 })
1030 };
1031 if writer.join().map_err(|_| Failure::Concurrent)? != Ok(()) {
1032 return Err(Failure::Outcome("the writer thread failed"));
1033 }
1034 if reader.join().map_err(|_| Failure::Concurrent)? != Ok(()) {
1035 return Err(Failure::Outcome("the reader thread failed"));
1036 }
1037 Ok(())
1038 }
1039}