Skip to main content

sui_rpc_api/subscription/
mod.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use crate::metrics::SubscriptionMetrics;
5use futures::{StreamExt, stream::FuturesUnordered};
6use std::sync::Arc;
7use std::sync::atomic::AtomicUsize;
8use std::sync::atomic::Ordering;
9use std::time::Duration;
10use sui_inverted_index::BitmapQuery;
11use sui_types::full_checkpoint_content::Checkpoint;
12use tokio::sync::broadcast;
13use tokio::sync::mpsc;
14use tokio::sync::oneshot;
15use tokio::time::Instant;
16use tokio::time::sleep;
17use tracing::info;
18use tracing::trace;
19use tracing::warn;
20
21mod matcher;
22
23const CHECKPOINT_MAILBOX_SIZE: usize = 1024;
24/// Amortizes admission select/receive overhead while bounding how many
25/// requests can delay the next checkpoint poll.
26const ADMISSION_TURN_LIMIT: usize = 128;
27const SUBSCRIPTION_CHANNEL_SIZE: usize = 256;
28const DEFAULT_MAX_SUBSCRIBERS: usize = 1024;
29/// Bound on each shard task's mailbox (registrations, checkpoint fan-out,
30/// and lag teardowns from the dispatcher).
31const SHARD_MAILBOX_SIZE: usize = 64;
32
33/// Default for [`SubscriptionService::build`]'s `watermark_interval`: ~5
34/// seconds at mainnet checkpoint cadence.
35const DEFAULT_WATERMARK_INTERVAL: u32 = 25;
36
37/// Default for [`SubscriptionService::build`]'s `shards`: the host's
38/// available parallelism, floor 1.
39fn default_shards() -> u32 {
40    std::thread::available_parallelism()
41        .map(|n| n.get() as u32)
42        .unwrap_or(1)
43}
44
45/// Poll interval while waiting for the index to catch up to a checkpoint
46/// before delivering it (see [`IndexedCheckpointFn`]).
47const INDEX_WAIT_POLL_INTERVAL: Duration = Duration::from_millis(10);
48/// Upper bound on how long delivery of a single checkpoint waits for the
49/// index. A healthy index catches up in milliseconds; the bound just keeps
50/// a stalled indexer from wedging the subscription stream forever.
51const INDEX_WAIT_TIMEOUT: Duration = Duration::from_secs(10);
52
53/// Reads the highest checkpoint the index has committed (the embedded
54/// rpc-store's live-cohort watermark), or `None` if it has indexed nothing
55/// yet. When supplied to [`SubscriptionService::build`], the service holds a
56/// checkpoint back until the index has committed it, so a client that
57/// observes a checkpoint (e.g. via `execute_transaction_and_wait_for_checkpoint`)
58/// can immediately read that checkpoint's indexed state.
59pub type IndexedCheckpointFn = Arc<dyn Fn() -> Option<u64> + Send + Sync>;
60
61/// Which item stream a subscriber asked for.
62#[derive(Clone, Copy, Debug, PartialEq, Eq)]
63pub enum SubscriptionKind {
64    Checkpoints,
65    Transactions,
66    Events,
67}
68
69impl SubscriptionKind {
70    pub(crate) fn metric_label(self) -> &'static str {
71        match self {
72            Self::Checkpoints => "checkpoint",
73            Self::Transactions => "transaction",
74            Self::Events => "event",
75        }
76    }
77}
78
79#[derive(Clone, Copy)]
80pub(crate) enum SubscriptionTerminationReason {
81    ClientClosed,
82    SlowConsumer,
83    SourceLag,
84    ServiceShutdown,
85}
86
87impl SubscriptionTerminationReason {
88    fn metric_label(self) -> &'static str {
89        match self {
90            Self::ClientClosed => "client_closed",
91            Self::SlowConsumer => "slow_consumer",
92            Self::SourceLag => "source_lag",
93            Self::ServiceShutdown => "service_shutdown",
94        }
95    }
96}
97
98pub(crate) struct SubscriptionLifecycleGuard {
99    kind: SubscriptionKind,
100    filtered: bool,
101    reservation: SubscriberReservation,
102    inflight_subscribers: prometheus::IntGauge,
103    terminations_total: prometheus::IntCounterVec,
104    termination_reason: SubscriptionTerminationReason,
105}
106
107impl SubscriptionLifecycleGuard {
108    pub(crate) fn new(
109        kind: SubscriptionKind,
110        filtered: bool,
111        reservation: SubscriberReservation,
112        metrics: &SubscriptionMetrics,
113    ) -> Self {
114        reservation.increment_resident_counts(kind, filtered);
115        let inflight_subscribers = metrics
116            .inflight_subscribers
117            .with_label_values(&[kind.metric_label(), if filtered { "true" } else { "false" }]);
118        inflight_subscribers.inc();
119
120        Self {
121            kind,
122            filtered,
123            reservation,
124            inflight_subscribers,
125            terminations_total: metrics.terminations_total.clone(),
126            termination_reason: SubscriptionTerminationReason::ServiceShutdown,
127        }
128    }
129
130    /// Finalizes the subscription now, recording `reason` instead of the
131    /// default `service_shutdown`: consuming `self` runs `Drop`, which
132    /// decrements the counts/gauge and increments the termination counter.
133    pub(crate) fn terminate(mut self, reason: SubscriptionTerminationReason) {
134        self.termination_reason = reason;
135    }
136}
137
138impl Drop for SubscriptionLifecycleGuard {
139    fn drop(&mut self) {
140        self.reservation
141            .decrement_resident_counts(self.kind, self.filtered);
142        self.inflight_subscribers.dec();
143        self.terminations_total
144            .with_label_values(&[
145                self.kind.metric_label(),
146                self.termination_reason.metric_label(),
147            ])
148            .inc();
149    }
150}
151
152/// What a subscriber asked for. `query: None` = unfiltered (stream
153/// everything).
154pub struct SubscriptionSpec {
155    pub kind: SubscriptionKind,
156    pub query: Option<BitmapQuery>,
157}
158
159/// Updates delivered to a subscriber while processing checkpoints. An initial
160/// progress frame may precede a matched frame for the entry checkpoint.
161pub enum SubscriptionUpdate {
162    Matched(MatchedCheckpoint),
163    /// An initial tick identifies the safe position immediately before
164    /// subscription entry: `checkpoint` is the entry checkpoint minus one and
165    /// `tx_hi` is the entry checkpoint's transaction lower bound. A periodic
166    /// tick identifies a fully processed `checkpoint`; `tx_hi` is its
167    /// `network_total_transactions` (the exclusive transaction upper bound).
168    WatermarkTick {
169        checkpoint: u64,
170        tx_hi: u64,
171    },
172}
173
174pub struct MatchedCheckpoint {
175    pub checkpoint: Arc<Checkpoint>,
176    pub matches: SubscriptionMatches,
177}
178
179/// Kind-specific match payload. Indices are within-checkpoint and ascending.
180pub enum SubscriptionMatches {
181    /// Checkpoint subscription: the checkpoint matched (some tx satisfied the
182    /// filter, or the subscription is unfiltered).
183    Checkpoint,
184    /// Transaction subscription: matched transaction indices, run-length
185    /// encoded as half-open ranges -- ascending, non-overlapping, and
186    /// maximally coalesced. Bounds the payload at O(runs) for
187    /// densely-matching filters (e.g. unanchored negation).
188    Transactions(Vec<std::ops::Range<u32>>),
189    /// Transaction subscription without a filter: every transaction in the
190    /// checkpoint matched. O(1) representation of "all"; never constructed
191    /// for a checkpoint with no transactions.
192    AllTransactions,
193    /// Event subscription: per matched transaction index, matched event
194    /// indices.
195    Events(Vec<(u32, Vec<u32>)>),
196    /// Event subscription without a filter: every event in the checkpoint
197    /// matched. O(1) representation of "all"; never constructed for a
198    /// checkpoint with no events.
199    AllEvents,
200}
201
202impl SubscriptionMatches {
203    /// Matched transaction indices of a transaction-subscription payload,
204    /// ascending; `None` for other payload kinds. `tx_count` is the
205    /// checkpoint's transaction count, used to expand
206    /// [`Self::AllTransactions`].
207    pub fn transaction_indices(
208        &self,
209        tx_count: u32,
210    ) -> Option<Box<dyn Iterator<Item = u32> + Send + '_>> {
211        match self {
212            Self::Transactions(ranges) => Some(Box::new(ranges.iter().flat_map(Clone::clone))),
213            Self::AllTransactions => Some(Box::new(0..tx_count)),
214            _ => None,
215        }
216    }
217
218    /// Matched `(transaction index, event index)` pairs of an
219    /// event-subscription payload, ascending; `None` for other payload
220    /// kinds. `checkpoint` is used to expand [`Self::AllEvents`].
221    pub fn event_indices<'a>(
222        &'a self,
223        checkpoint: &'a Checkpoint,
224    ) -> Option<Box<dyn Iterator<Item = (u32, u32)> + Send + 'a>> {
225        match self {
226            Self::Events(txs) => Some(Box::new(
227                txs.iter()
228                    .flat_map(|(tx, evs)| evs.iter().map(move |&ev| (*tx, ev))),
229            )),
230            Self::AllEvents => Some(Box::new(
231                checkpoint
232                    .transactions
233                    .iter()
234                    .enumerate()
235                    .flat_map(|(tx_idx, tx)| {
236                        let event_count =
237                            tx.events.as_ref().map(|e| e.data.len()).unwrap_or(0) as u32;
238                        (0..event_count).map(move |ev| (tx_idx as u32, ev))
239                    }),
240            )),
241            _ => None,
242        }
243    }
244}
245
246struct SubscriptionRequest {
247    spec: SubscriptionSpec,
248    response_sender: oneshot::Sender<mpsc::Receiver<SubscriptionUpdate>>,
249    reservation: SubscriberReservation,
250}
251
252/// Every request owns a reservation, so pending plus resident subscriptions
253/// remain bounded by the configured subscriber limit without a transport bound.
254#[allow(clippy::disallowed_methods)]
255fn subscription_admission_channel() -> (
256    mpsc::UnboundedSender<SubscriptionRequest>,
257    mpsc::UnboundedReceiver<SubscriptionRequest>,
258) {
259    mpsc::unbounded_channel()
260}
261
262enum AdmissionState {
263    Accepting,
264    WaitingForShard(SubscriptionRequest),
265    Closed,
266}
267
268enum SubscriptionServiceEvent {
269    Checkpoint(Result<Arc<Checkpoint>, broadcast::error::RecvError>),
270    AdmissionRequest(SubscriptionRequest),
271    AdmissionClosed,
272    ShardCapacity {
273        shard: usize,
274        permit: mpsc::OwnedPermit<ShardMsg>,
275    },
276    AdmissionCanceled,
277}
278
279#[derive(Clone)]
280pub struct SubscriptionServiceHandle {
281    admission_sender: mpsc::UnboundedSender<SubscriptionRequest>,
282    counters: Arc<SubscriberCounts>,
283    metrics: SubscriptionMetrics,
284}
285
286impl SubscriptionServiceHandle {
287    pub async fn register_subscription(
288        &self,
289        spec: SubscriptionSpec,
290    ) -> Option<mpsc::Receiver<SubscriptionUpdate>> {
291        let reservation = match self.counters.try_reserve() {
292            Some(reservation) => reservation,
293            None => {
294                trace!(
295                    "failed to register new subscriber: hit maximum number of subscribers {}",
296                    self.counters.limit
297                );
298                return None;
299            }
300        };
301
302        let (response_sender, response_receiver) = oneshot::channel();
303        let request = SubscriptionRequest {
304            spec,
305            response_sender,
306            reservation,
307        };
308        self.admission_sender.send(request).ok()?;
309
310        response_receiver.await.ok()
311    }
312
313    pub(crate) fn stream_metrics(
314        &self,
315        kind: SubscriptionKind,
316    ) -> crate::metrics::SubscriptionStreamMetrics {
317        self.metrics.stream_metrics(kind.metric_label())
318    }
319}
320
321/// Shared subscription admission and lifecycle accounting.
322///
323/// `reserved` is the admission authority and counts pending, in-flight, and
324/// resident subscriptions. `total` and the filtered counters track resident
325/// subscriptions through their lifecycle.
326pub(crate) struct SubscriberCounts {
327    limit: usize,
328    reserved: AtomicUsize,
329    total: AtomicUsize,
330    filtered_tx: AtomicUsize,
331    filtered_event: AtomicUsize,
332}
333
334impl SubscriberCounts {
335    fn new(limit: usize) -> Self {
336        Self {
337            limit,
338            reserved: AtomicUsize::new(0),
339            total: AtomicUsize::new(0),
340            filtered_tx: AtomicUsize::new(0),
341            filtered_event: AtomicUsize::new(0),
342        }
343    }
344
345    fn try_reserve(self: &Arc<Self>) -> Option<SubscriberReservation> {
346        self.reserved
347            .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |reserved| {
348                (reserved < self.limit).then_some(reserved + 1)
349            })
350            .ok()?;
351        Some(SubscriberReservation {
352            counters: Arc::clone(self),
353        })
354    }
355
356    #[cfg(test)]
357    fn reserved(&self) -> usize {
358        self.reserved.load(Ordering::Relaxed)
359    }
360}
361
362pub(crate) struct SubscriberReservation {
363    counters: Arc<SubscriberCounts>,
364}
365
366impl SubscriberReservation {
367    fn increment_resident_counts(&self, kind: SubscriptionKind, filtered: bool) {
368        self.counters.total.fetch_add(1, Ordering::Relaxed);
369        if filtered {
370            match kind {
371                SubscriptionKind::Checkpoints | SubscriptionKind::Transactions => {
372                    self.counters.filtered_tx.fetch_add(1, Ordering::Relaxed);
373                }
374                SubscriptionKind::Events => {
375                    self.counters.filtered_event.fetch_add(1, Ordering::Relaxed);
376                }
377            }
378        }
379    }
380
381    fn decrement_resident_counts(&self, kind: SubscriptionKind, filtered: bool) {
382        self.counters.total.fetch_sub(1, Ordering::Relaxed);
383        if filtered {
384            match kind {
385                SubscriptionKind::Checkpoints | SubscriptionKind::Transactions => {
386                    self.counters.filtered_tx.fetch_sub(1, Ordering::Relaxed);
387                }
388                SubscriptionKind::Events => {
389                    self.counters.filtered_event.fetch_sub(1, Ordering::Relaxed);
390                }
391            }
392        }
393    }
394}
395
396impl Drop for SubscriberReservation {
397    fn drop(&mut self) {
398        let previous = self.counters.reserved.fetch_sub(1, Ordering::Relaxed);
399        debug_assert!(previous > 0, "subscriber reservation count underflow");
400    }
401}
402
403enum ShardMsg {
404    /// A checkpoint plus its pre-extracted dimension keys.
405    Checkpoint(Arc<Checkpoint>, Arc<matcher::CheckpointKeys>),
406    /// An admitted subscriber (handshake already completed by the dispatcher).
407    Register {
408        spec: SubscriptionSpec,
409        sender: mpsc::Sender<SubscriptionUpdate>,
410        guard: SubscriptionLifecycleGuard,
411    },
412    /// Drop every subscriber on this shard with the supplied bounded reason.
413    Clear(SubscriptionTerminationReason),
414}
415
416/// One worker task owning a partition of the subscribers: it evaluates their
417/// filters against pre-extracted checkpoint keys and delivers their updates.
418struct SubscriptionShard {
419    mailbox: mpsc::Receiver<ShardMsg>,
420    matcher: matcher::SubscriptionMatcher,
421    /// Checkpoints a subscriber may go without any frame before a standalone
422    /// watermark tick is delivered (see `RpcConfig::subscription_watermark_interval`).
423    watermark_interval: u32,
424}
425
426impl SubscriptionShard {
427    async fn run(mut self) {
428        while let Some(msg) = self.mailbox.recv().await {
429            self.handle_msg(msg);
430        }
431        self.matcher
432            .clear(SubscriptionTerminationReason::ServiceShutdown);
433    }
434
435    fn handle_msg(&mut self, msg: ShardMsg) {
436        match msg {
437            ShardMsg::Register {
438                spec,
439                sender,
440                guard,
441            } => {
442                self.matcher.insert(spec, sender, guard);
443            }
444            ShardMsg::Checkpoint(checkpoint, keys) => {
445                self.matcher
446                    .dispatch_with_keys(&checkpoint, &keys, self.watermark_interval);
447            }
448            ShardMsg::Clear(reason) => {
449                self.matcher.clear(reason);
450            }
451        }
452    }
453
454    /// Synchronously process every message already in the mailbox: a
455    /// test-only stand-in for the spawned `run` loop that keeps the actor
456    /// tests deterministic.
457    #[cfg(test)]
458    fn drain(&mut self) {
459        while let Ok(msg) = self.mailbox.try_recv() {
460            self.handle_msg(msg);
461        }
462    }
463}
464
465pub struct SubscriptionService {
466    // Broadcast receiver for `Checkpoint`s published by the Checkpoint Executor.
467    //
468    // The executor publishes non-blocking, so a slow service can fall behind
469    // and observe `RecvError::Lagged`; checkpoints delivered between lags arrive
470    // in-order.
471    checkpoint_mailbox: broadcast::Receiver<Arc<Checkpoint>>,
472    admission_mailbox: mpsc::UnboundedReceiver<SubscriptionRequest>,
473    /// Registration targets: one mailbox per shard task, each owning a
474    /// partition of the subscribers.
475    shards: Vec<mpsc::Sender<ShardMsg>>,
476    /// Rotating tie-break cursor for shards with equal free mailbox capacity.
477    next_shard: usize,
478    /// Filtered-subscriber counts per key space, shared with the shards;
479    /// gates per-checkpoint key extraction.
480    counters: Arc<SubscriberCounts>,
481
482    // When set, delivery of a checkpoint waits until the index has committed
483    // it (see [`IndexedCheckpointFn`]). `None` preserves the immediate-delivery
484    // behavior used with the legacy synchronously-committed index.
485    indexed_checkpoint: Option<IndexedCheckpointFn>,
486
487    metrics: SubscriptionMetrics,
488}
489
490impl SubscriptionService {
491    /// `None` defaults `watermark_interval` to 25 checkpoints,
492    /// `max_subscribers` to 1024, and `shards` to the host's available
493    /// parallelism, with a minimum of one shard.
494    pub fn build(
495        registry: &prometheus::Registry,
496        indexed_checkpoint: Option<IndexedCheckpointFn>,
497        watermark_interval: Option<u32>,
498        max_subscribers: Option<usize>,
499        shards: Option<u32>,
500    ) -> (
501        broadcast::Sender<Arc<Checkpoint>>,
502        SubscriptionServiceHandle,
503    ) {
504        let metrics = SubscriptionMetrics::new(registry);
505        let max_subscribers = max_subscribers.unwrap_or(DEFAULT_MAX_SUBSCRIBERS);
506        let (checkpoint_sender, checkpoint_mailbox) = broadcast::channel(CHECKPOINT_MAILBOX_SIZE);
507        let (admission_sender, admission_mailbox) = subscription_admission_channel();
508        let counters = Arc::new(SubscriberCounts::new(max_subscribers));
509        let handle = SubscriptionServiceHandle {
510            admission_sender,
511            counters: Arc::clone(&counters),
512            metrics: metrics.clone(),
513        };
514
515        let watermark_interval = watermark_interval
516            .unwrap_or(DEFAULT_WATERMARK_INTERVAL)
517            .max(1);
518        let shards = shards.unwrap_or_else(default_shards).max(1);
519        let mut shard_senders = Vec::with_capacity(shards as usize);
520        for _ in 0..shards {
521            let (sender, shard_mailbox) = mpsc::channel(SHARD_MAILBOX_SIZE);
522            tokio::spawn(
523                SubscriptionShard {
524                    mailbox: shard_mailbox,
525                    matcher: matcher::SubscriptionMatcher::default(),
526                    watermark_interval,
527                }
528                .run(),
529            );
530            shard_senders.push(sender);
531        }
532
533        tokio::spawn(
534            Self {
535                checkpoint_mailbox,
536                admission_mailbox,
537                shards: shard_senders,
538                next_shard: 0,
539                counters,
540                indexed_checkpoint,
541                metrics,
542            }
543            .start(),
544        );
545
546        (checkpoint_sender, handle)
547    }
548
549    async fn start(mut self) {
550        let mut admission_state = AdmissionState::Accepting;
551        loop {
552            let event = match &mut admission_state {
553                AdmissionState::Accepting => {
554                    tokio::select! {
555                        biased;
556
557                        result = self.checkpoint_mailbox.recv() => {
558                            SubscriptionServiceEvent::Checkpoint(result)
559                        },
560                        request = self.admission_mailbox.recv() => {
561                            match request {
562                                Some(request) => {
563                                    SubscriptionServiceEvent::AdmissionRequest(request)
564                                }
565                                None => SubscriptionServiceEvent::AdmissionClosed,
566                            }
567                        },
568                    }
569                }
570                AdmissionState::WaitingForShard(request) => {
571                    let shards: Vec<_> = (0..self.shards.len())
572                        .map(|offset| {
573                            let shard = (self.next_shard + offset) % self.shards.len();
574                            (shard, self.shards[shard].clone())
575                        })
576                        .collect();
577
578                    tokio::select! {
579                        biased;
580
581                        result = self.checkpoint_mailbox.recv() => {
582                            SubscriptionServiceEvent::Checkpoint(result)
583                        },
584                        _ = request.response_sender.closed() => {
585                            SubscriptionServiceEvent::AdmissionCanceled
586                        },
587                        (shard, permit) = Self::reserve_first_available_shard(shards) => {
588                            SubscriptionServiceEvent::ShardCapacity { shard, permit }
589                        },
590                    }
591                }
592                AdmissionState::Closed => {
593                    SubscriptionServiceEvent::Checkpoint(self.checkpoint_mailbox.recv().await)
594                }
595            };
596
597            match event {
598                SubscriptionServiceEvent::Checkpoint(Ok(checkpoint)) => {
599                    self.handle_checkpoint(checkpoint).await;
600                }
601                SubscriptionServiceEvent::Checkpoint(Err(broadcast::error::RecvError::Lagged(
602                    skipped,
603                ))) => {
604                    self.handle_lag(skipped).await;
605                }
606                // Once the executor drops the sender this yields `Closed`
607                // and we can terminate the event loop.
608                SubscriptionServiceEvent::Checkpoint(Err(broadcast::error::RecvError::Closed)) => {
609                    break;
610                }
611                SubscriptionServiceEvent::AdmissionRequest(request) => {
612                    admission_state = self.admit_ready_requests(request);
613                }
614                SubscriptionServiceEvent::AdmissionClosed => {
615                    // Established subscribers remain live until the checkpoint
616                    // source closes.
617                    admission_state = AdmissionState::Closed;
618                }
619                SubscriptionServiceEvent::ShardCapacity { shard, permit } => {
620                    let AdmissionState::WaitingForShard(request) = admission_state else {
621                        unreachable!("shard capacity requires a waiting admission");
622                    };
623                    self.complete_admission(shard, permit, request);
624                    admission_state = AdmissionState::Accepting;
625                }
626                SubscriptionServiceEvent::AdmissionCanceled => {
627                    let AdmissionState::WaitingForShard(_) = admission_state else {
628                        unreachable!("admission cancellation requires a waiting admission");
629                    };
630                    admission_state = AdmissionState::Accepting;
631                }
632            }
633        }
634
635        for shard in &self.shards {
636            shard
637                .send(ShardMsg::Clear(
638                    SubscriptionTerminationReason::ServiceShutdown,
639                ))
640                .await
641                .expect("subscription shard terminated unexpectedly");
642        }
643
644        info!("RPC Subscription Services ended");
645    }
646
647    async fn handle_checkpoint(&mut self, checkpoint: Arc<Checkpoint>) {
648        // Check that we recieved checkpoints in-order. The broadcast stream
649        // preserves send order, and any gap surfaces separately as `Lagged`
650        // (see `handle_lag`), so reaching here out-of-order indicates an
651        // executor bug.
652        {
653            let last_sequence_number = self.metrics.last_recieved_checkpoint.get();
654            let sequence_number = *checkpoint.summary.sequence_number() as i64;
655
656            if last_sequence_number != 0 && (last_sequence_number + 1) != sequence_number {
657                panic!(
658                    "recieved checkpoint out-of-order. expected checkpoint {}, recieved {}",
659                    last_sequence_number + 1,
660                    sequence_number
661                );
662            }
663
664            // Update the metric marking the latest checkpoint we've seen
665            self.metrics.last_recieved_checkpoint.set(sequence_number);
666        }
667
668        // Hold the checkpoint back until the index has committed it, so a
669        // client that observes this checkpoint can immediately read its
670        // indexed state. No-op unless an index gate was configured.
671        self.wait_until_indexed(*checkpoint.summary.sequence_number())
672            .await;
673
674        // No live subscriber or admitted registration pending in a shard:
675        // skip fan-out. Requests merely queued in the admission lane are not
676        // counted and may establish their stream boundary after this checkpoint.
677        if self.counters.total.load(Ordering::Relaxed) == 0 {
678            return;
679        }
680
681        // Extract the checkpoint's dimension keys once, only for the key
682        // spaces with at least one filtered subscriber, then fan the
683        // checkpoint out to every shard. The bounded sends give backpressure:
684        // one slow shard stalls the dispatcher, which lags the broadcast
685        // receiver and tears everything down (see `handle_lag`).
686        let keys = Arc::new(matcher::extract_checkpoint_keys(
687            &checkpoint,
688            self.counters.filtered_tx.load(Ordering::Relaxed) > 0,
689            self.counters.filtered_event.load(Ordering::Relaxed) > 0,
690        ));
691        for shard in &self.shards {
692            shard
693                .send(ShardMsg::Checkpoint(
694                    Arc::clone(&checkpoint),
695                    Arc::clone(&keys),
696                ))
697                .await
698                .expect("subscription shard terminated unexpectedly");
699        }
700    }
701
702    /// Block until the index has committed `sequence_number`, polling the
703    /// configured [`IndexedCheckpointFn`]. Returns immediately when no gate is
704    /// configured or the index is already caught up. Gives up after
705    /// [`INDEX_WAIT_TIMEOUT`] -- a stalled indexer should not wedge delivery
706    /// forever -- delivering a (possibly not-yet-indexed) checkpoint rather
707    /// than stalling the stream.
708    async fn wait_until_indexed(&self, sequence_number: u64) {
709        let Some(indexed) = &self.indexed_checkpoint else {
710            return;
711        };
712
713        if indexed().is_some_and(|hi| hi >= sequence_number) {
714            return;
715        }
716
717        let wait_started = Instant::now();
718        let deadline = wait_started + INDEX_WAIT_TIMEOUT;
719        loop {
720            sleep(INDEX_WAIT_POLL_INTERVAL).await;
721            if indexed().is_some_and(|hi| hi >= sequence_number) {
722                self.metrics
723                    .index_wait_seconds
724                    .observe(wait_started.elapsed().as_secs_f64());
725                return;
726            }
727            if Instant::now() >= deadline {
728                self.metrics.index_wait_timeouts_total.inc();
729                self.metrics
730                    .index_wait_seconds
731                    .observe(wait_started.elapsed().as_secs_f64());
732                warn!(
733                    checkpoint = sequence_number,
734                    "index did not catch up within {INDEX_WAIT_TIMEOUT:?}; \
735                     delivering checkpoint anyway"
736                );
737                return;
738            }
739        }
740    }
741
742    /// Drop every in-progress subscription after the service fell behind the
743    /// broadcast stream. Having missed `skipped` checkpoints we can no longer
744    /// deliver an in-order, gap-free stream to any subscriber, and clients
745    /// already tolerate connection breaks and reconnect, so tearing them all
746    /// down is cheaper than trying to resynchronize each one.
747    async fn handle_lag(&mut self, skipped: u64) {
748        warn!(
749            skipped,
750            "subscription service lagged behind the checkpoint stream; \
751             dropping all in-progress subscriptions"
752        );
753        // Per-shard FIFO ordering guarantees no shard delivers a post-gap
754        // checkpoint to a pre-gap subscriber: Clear is enqueued behind all
755        // pre-gap checkpoints and ahead of all post-gap ones.
756        for shard in &self.shards {
757            shard
758                .send(ShardMsg::Clear(SubscriptionTerminationReason::SourceLag))
759                .await
760                .expect("subscription shard terminated unexpectedly");
761        }
762        // The next delivered checkpoint jumps ahead by `skipped`; reset the
763        // in-order tracker so it is not mistaken for an out-of-order delivery.
764        self.metrics.last_recieved_checkpoint.set(0);
765    }
766
767    fn try_reserve_least_backlogged_shard(&self) -> Option<(usize, mpsc::OwnedPermit<ShardMsg>)> {
768        let mut selected_shard = None;
769        let mut greatest_capacity = 0;
770
771        for offset in 0..self.shards.len() {
772            let shard = (self.next_shard + offset) % self.shards.len();
773            let sender = &self.shards[shard];
774            if sender.is_closed() {
775                panic!("subscription shard terminated unexpectedly");
776            }
777
778            let capacity = sender.capacity();
779            if selected_shard.is_none() || capacity > greatest_capacity {
780                selected_shard = Some(shard);
781                greatest_capacity = capacity;
782            }
783        }
784
785        let shard = selected_shard.expect("subscription service requires at least one shard");
786        match self.shards[shard].clone().try_reserve_owned() {
787            Ok(permit) => Some((shard, permit)),
788            Err(mpsc::error::TrySendError::Full(_)) => None,
789            Err(mpsc::error::TrySendError::Closed(_)) => {
790                panic!("subscription shard terminated unexpectedly")
791            }
792        }
793    }
794
795    async fn reserve_first_available_shard(
796        shards: Vec<(usize, mpsc::Sender<ShardMsg>)>,
797    ) -> (usize, mpsc::OwnedPermit<ShardMsg>) {
798        assert!(
799            !shards.is_empty(),
800            "subscription service requires at least one shard"
801        );
802        for (_, sender) in &shards {
803            if sender.is_closed() {
804                panic!("subscription shard terminated unexpectedly");
805            }
806        }
807
808        let mut reservations = FuturesUnordered::new();
809        for (shard, sender) in shards {
810            reservations.push(async move { (shard, sender.reserve_owned().await) });
811        }
812
813        match reservations
814            .next()
815            .await
816            .expect("subscription service requires at least one shard")
817        {
818            (shard, Ok(permit)) => (shard, permit),
819            (_, Err(_)) => panic!("subscription shard terminated unexpectedly"),
820        }
821    }
822
823    fn complete_admission(
824        &mut self,
825        shard: usize,
826        permit: mpsc::OwnedPermit<ShardMsg>,
827        request: SubscriptionRequest,
828    ) {
829        if request.response_sender.is_closed() {
830            trace!("failed to register new subscriber: request was cancelled");
831            return;
832        }
833
834        let (sender, receiver) = mpsc::channel(SUBSCRIPTION_CHANNEL_SIZE);
835        if request.response_sender.send(receiver).is_err() {
836            trace!("failed to register new subscriber: request was cancelled");
837            return;
838        }
839
840        trace!("successfully registered new subscriber");
841        let kind = request.spec.kind;
842        let filtered = request.spec.query.is_some();
843        let guard =
844            SubscriptionLifecycleGuard::new(kind, filtered, request.reservation, &self.metrics);
845        permit.send(ShardMsg::Register {
846            spec: request.spec,
847            sender,
848            guard,
849        });
850        self.next_shard = (shard + 1) % self.shards.len();
851    }
852
853    fn try_admit(&mut self, request: SubscriptionRequest) -> AdmissionState {
854        if request.response_sender.is_closed() {
855            trace!("failed to register new subscriber: request was cancelled");
856            return AdmissionState::Accepting;
857        }
858
859        let Some((shard, permit)) = self.try_reserve_least_backlogged_shard() else {
860            trace!("waiting for a subscription shard to have capacity");
861            return AdmissionState::WaitingForShard(request);
862        };
863
864        self.complete_admission(shard, permit, request);
865        AdmissionState::Accepting
866    }
867
868    fn admit_ready_requests(&mut self, first_request: SubscriptionRequest) -> AdmissionState {
869        let mut request = first_request;
870        let mut remaining_attempts = ADMISSION_TURN_LIMIT;
871
872        loop {
873            match self.try_admit(request) {
874                AdmissionState::Accepting => {}
875                state => return state,
876            }
877
878            remaining_attempts -= 1;
879            if remaining_attempts == 0 {
880                return AdmissionState::Accepting;
881            }
882
883            request = match self.admission_mailbox.try_recv() {
884                Ok(request) => request,
885                Err(mpsc::error::TryRecvError::Empty) => {
886                    return AdmissionState::Accepting;
887                }
888                Err(mpsc::error::TryRecvError::Disconnected) => {
889                    return AdmissionState::Closed;
890                }
891            };
892        }
893    }
894}
895
896#[cfg(test)]
897mod tests {
898    use std::sync::atomic::AtomicU64;
899
900    use move_core_types::account_address::AccountAddress;
901    use move_core_types::identifier::Identifier;
902    use move_core_types::language_storage::StructTag;
903    use sui_rpc::proto::sui::rpc::v2 as proto;
904    use sui_types::base_types::ObjectID;
905    use sui_types::base_types::SuiAddress;
906    use sui_types::event::Event;
907    use sui_types::test_checkpoint_data_builder::TestCheckpointBuilder;
908
909    use crate::ledger_history::filter::event_filter_to_query;
910    use crate::ledger_history::filter::transaction_filter_to_query;
911
912    use super::*;
913
914    /// An unspawned dispatcher plus its shards, driven synchronously via
915    /// [`drain`] so the actor tests are fully deterministic.
916    fn test_service(shard_count: usize) -> (SubscriptionService, Vec<SubscriptionShard>) {
917        test_service_with(shard_count, 25, None)
918    }
919
920    fn test_service_with(
921        shard_count: usize,
922        watermark_interval: u32,
923        indexed_checkpoint: Option<IndexedCheckpointFn>,
924    ) -> (SubscriptionService, Vec<SubscriptionShard>) {
925        let (service, _checkpoint_sender, _request_sender, shards) = actor_service_with(
926            shard_count,
927            watermark_interval,
928            indexed_checkpoint,
929            16,
930            DEFAULT_MAX_SUBSCRIBERS,
931        );
932        (service, shards)
933    }
934
935    fn actor_service_with(
936        shard_count: usize,
937        watermark_interval: u32,
938        indexed_checkpoint: Option<IndexedCheckpointFn>,
939        checkpoint_capacity: usize,
940        max_subscribers: usize,
941    ) -> (
942        SubscriptionService,
943        broadcast::Sender<Arc<Checkpoint>>,
944        mpsc::UnboundedSender<SubscriptionRequest>,
945        Vec<SubscriptionShard>,
946    ) {
947        let (checkpoint_sender, checkpoint_mailbox) = broadcast::channel(checkpoint_capacity);
948        let (request_sender, admission_mailbox) = subscription_admission_channel();
949        let metrics = SubscriptionMetrics::new(&prometheus::Registry::new());
950        let counters = Arc::new(SubscriberCounts::new(max_subscribers));
951
952        let mut shard_senders = Vec::with_capacity(shard_count);
953        let mut shards = Vec::with_capacity(shard_count);
954        for _ in 0..shard_count {
955            let (sender, shard_mailbox) = mpsc::channel(SHARD_MAILBOX_SIZE);
956            shard_senders.push(sender);
957            shards.push(SubscriptionShard {
958                mailbox: shard_mailbox,
959                matcher: matcher::SubscriptionMatcher::default(),
960                watermark_interval,
961            });
962        }
963
964        let service = SubscriptionService {
965            checkpoint_mailbox,
966            admission_mailbox,
967            shards: shard_senders,
968            next_shard: 0,
969            counters,
970            indexed_checkpoint,
971            metrics,
972        };
973        (service, checkpoint_sender, request_sender, shards)
974    }
975
976    fn checkpoint(sequence_number: u64) -> Arc<Checkpoint> {
977        Arc::new(TestCheckpointBuilder::new(sequence_number).build_checkpoint())
978    }
979
980    /// One checkpoint with one transaction per sender index, in order.
981    fn checkpoint_with_senders(seq: u64, senders: &[u8]) -> Arc<Checkpoint> {
982        let mut builder = TestCheckpointBuilder::new(seq);
983        for &sender in senders {
984            builder = builder.start_transaction(sender).finish_transaction();
985        }
986        Arc::new(builder.build_checkpoint())
987    }
988
989    /// One checkpoint where tx 0 carries no events and tx 1 carries two.
990    fn checkpoint_with_events(seq: u64) -> Arc<Checkpoint> {
991        let package = AccountAddress::random();
992        let event = |name: &str| Event {
993            package_id: ObjectID::from(package),
994            transaction_module: Identifier::new("emitter").unwrap(),
995            sender: addr(1),
996            type_: StructTag {
997                address: package,
998                module: Identifier::new("mod_t").unwrap(),
999                name: Identifier::new(name).unwrap(),
1000                type_params: vec![],
1001            },
1002            contents: vec![],
1003        };
1004        let mut builder = TestCheckpointBuilder::new(seq);
1005        builder = builder.start_transaction(0).finish_transaction();
1006        builder = builder
1007            .start_transaction(1)
1008            .with_events(vec![event("EventA"), event("EventB")])
1009            .finish_transaction();
1010        Arc::new(builder.build_checkpoint())
1011    }
1012
1013    fn addr(idx: u8) -> SuiAddress {
1014        TestCheckpointBuilder::derive_address(idx)
1015    }
1016
1017    fn sender_query(address: SuiAddress, negated: bool) -> BitmapQuery {
1018        let mut sender = proto::SenderFilter::default();
1019        sender.address = Some(address.to_string());
1020        let mut literal = proto::TransactionLiteral::default();
1021        literal.predicate = Some(proto::transaction_literal::Predicate::Sender(sender));
1022        literal.negated = negated;
1023        let mut term = proto::TransactionTerm::default();
1024        term.literals = vec![literal];
1025        let mut filter = proto::TransactionFilter::default();
1026        filter.terms = vec![term];
1027        transaction_filter_to_query(&filter, 16).unwrap()
1028    }
1029
1030    fn event_type_query(type_str: &str) -> BitmapQuery {
1031        let mut event_type = proto::EventTypeFilter::default();
1032        event_type.event_type = Some(type_str.to_owned());
1033        let mut literal = proto::EventLiteral::default();
1034        literal.predicate = Some(proto::event_literal::Predicate::EventType(event_type));
1035        let mut term = proto::EventTerm::default();
1036        term.literals = vec![literal];
1037        let mut filter = proto::EventFilter::default();
1038        filter.terms = vec![term];
1039        event_filter_to_query(&filter, 16).unwrap()
1040    }
1041
1042    fn unfiltered() -> SubscriptionSpec {
1043        SubscriptionSpec {
1044            kind: SubscriptionKind::Checkpoints,
1045            query: None,
1046        }
1047    }
1048
1049    fn subscription_request(
1050        counters: &Arc<SubscriberCounts>,
1051        spec: SubscriptionSpec,
1052        response_sender: oneshot::Sender<mpsc::Receiver<SubscriptionUpdate>>,
1053    ) -> SubscriptionRequest {
1054        let reservation = counters
1055            .try_reserve()
1056            .expect("test request requires subscriber capacity");
1057        SubscriptionRequest {
1058            spec,
1059            response_sender,
1060            reservation,
1061        }
1062    }
1063
1064    /// Register a subscriber through the real admission path (cap check,
1065    /// gauge/space-counter increments, shard selection, and Register enqueue),
1066    /// returning the receiving half a client would hold, or `None` when the
1067    /// dispatcher rejected the registration.
1068    async fn register(
1069        service: &mut SubscriptionService,
1070        spec: SubscriptionSpec,
1071    ) -> Option<mpsc::Receiver<SubscriptionUpdate>> {
1072        let reservation = service.counters.try_reserve()?;
1073        let (response_sender, response_receiver) = oneshot::channel();
1074        assert!(matches!(
1075            service.try_admit(SubscriptionRequest {
1076                spec,
1077                response_sender,
1078                reservation,
1079            }),
1080            AdmissionState::Accepting
1081        ));
1082        response_receiver.await.ok()
1083    }
1084    fn inflight_subscribers(metrics: &SubscriptionMetrics) -> i64 {
1085        ["checkpoint", "transaction", "event"]
1086            .into_iter()
1087            .flat_map(|kind| {
1088                ["true", "false"].into_iter().map(move |filtered| {
1089                    metrics
1090                        .inflight_subscribers
1091                        .with_label_values(&[kind, filtered])
1092                        .get()
1093                })
1094            })
1095            .sum()
1096    }
1097
1098    fn terminations(
1099        metrics: &SubscriptionMetrics,
1100        kind: &'static str,
1101        reason: &'static str,
1102    ) -> u64 {
1103        metrics
1104            .terminations_total
1105            .with_label_values(&[kind, reason])
1106            .get()
1107    }
1108
1109    /// Synchronously run every shard's pending mailbox messages.
1110    fn drain(shards: &mut [SubscriptionShard]) {
1111        for shard in shards {
1112            shard.drain();
1113        }
1114    }
1115
1116    fn matched_sequence_number(update: SubscriptionUpdate) -> u64 {
1117        match update {
1118            SubscriptionUpdate::Matched(matched) => {
1119                assert!(matches!(matched.matches, SubscriptionMatches::Checkpoint));
1120                *matched.checkpoint.summary.sequence_number()
1121            }
1122            SubscriptionUpdate::WatermarkTick { .. } => {
1123                panic!("expected a matched checkpoint, got a watermark tick")
1124            }
1125        }
1126    }
1127
1128    fn matched_transactions(update: SubscriptionUpdate) -> Vec<u32> {
1129        match update {
1130            SubscriptionUpdate::Matched(matched) => match matched.matches {
1131                SubscriptionMatches::Transactions(ranges) => ranges.into_iter().flatten().collect(),
1132                _ => panic!("expected transaction matches"),
1133            },
1134            SubscriptionUpdate::WatermarkTick { .. } => {
1135                panic!("expected a matched checkpoint, got a watermark tick")
1136            }
1137        }
1138    }
1139
1140    #[test]
1141    fn subscriber_reservations_are_bounded_and_released() {
1142        let counters = Arc::new(SubscriberCounts::new(8));
1143        let attempted = Arc::new(std::sync::Barrier::new(65));
1144        let release = Arc::new(std::sync::Barrier::new(65));
1145
1146        std::thread::scope(|scope| {
1147            let mut handles = Vec::with_capacity(64);
1148            for _ in 0..64 {
1149                let counters = Arc::clone(&counters);
1150                let attempted = Arc::clone(&attempted);
1151                let release = Arc::clone(&release);
1152                handles.push(scope.spawn(move || {
1153                    let reservation = counters.try_reserve();
1154                    attempted.wait();
1155                    release.wait();
1156                    reservation.is_some()
1157                }));
1158            }
1159
1160            attempted.wait();
1161            assert_eq!(counters.reserved(), 8);
1162            release.wait();
1163
1164            let successful_reservations = handles
1165                .into_iter()
1166                .map(|handle| handle.join().unwrap())
1167                .filter(|success| *success)
1168                .count();
1169            assert_eq!(successful_reservations, 8);
1170        });
1171
1172        assert_eq!(counters.reserved(), 0);
1173    }
1174
1175    #[tokio::test]
1176    async fn pending_request_reserves_final_subscriber_slot() {
1177        let counters = Arc::new(SubscriberCounts::new(3));
1178        let active_reservations = [
1179            counters.try_reserve().unwrap(),
1180            counters.try_reserve().unwrap(),
1181        ];
1182        let (admission_sender, mailbox) = subscription_admission_channel();
1183        let handle = SubscriptionServiceHandle {
1184            admission_sender,
1185            counters: Arc::clone(&counters),
1186            metrics: SubscriptionMetrics::new(&prometheus::Registry::new()),
1187        };
1188
1189        let pending_handle = handle.clone();
1190        let pending_registration =
1191            tokio::spawn(async move { pending_handle.register_subscription(unfiltered()).await });
1192        tokio::time::timeout(Duration::from_secs(1), async {
1193            while mailbox.len() != 1 {
1194                tokio::task::yield_now().await;
1195            }
1196        })
1197        .await
1198        .expect("pending registration did not enter admission");
1199
1200        assert_eq!(counters.reserved(), 3);
1201        let rejected = tokio::time::timeout(
1202            Duration::from_secs(1),
1203            handle.register_subscription(unfiltered()),
1204        )
1205        .await
1206        .expect("the final subscriber slot is already reserved");
1207        assert!(rejected.is_none());
1208        assert_eq!(mailbox.len(), 1);
1209
1210        drop(mailbox);
1211        assert!(pending_registration.await.unwrap().is_none());
1212        assert_eq!(counters.reserved(), 2);
1213
1214        drop(active_reservations);
1215        assert_eq!(counters.reserved(), 0);
1216    }
1217
1218    #[tokio::test]
1219    async fn zero_subscriber_limit_rejects_before_ingress() {
1220        let (_checkpoint_sender, handle) =
1221            SubscriptionService::build(&prometheus::Registry::new(), None, None, Some(0), Some(1));
1222        assert!(handle.counters.try_reserve().is_none());
1223        assert!(handle.register_subscription(unfiltered()).await.is_none());
1224        assert_eq!(handle.counters.reserved(), 0);
1225    }
1226
1227    #[tokio::test]
1228    async fn public_admission_rejects_before_queue_at_limit() {
1229        let counters = Arc::new(SubscriberCounts::new(1));
1230        let held_reservation = counters.try_reserve().unwrap();
1231        let (admission_sender, mut mailbox) = subscription_admission_channel();
1232        let handle = SubscriptionServiceHandle {
1233            admission_sender,
1234            counters: Arc::clone(&counters),
1235            metrics: SubscriptionMetrics::new(&prometheus::Registry::new()),
1236        };
1237
1238        let result = tokio::time::timeout(
1239            Duration::from_secs(1),
1240            handle.register_subscription(unfiltered()),
1241        )
1242        .await
1243        .expect("a saturated service must reject before queueing");
1244        assert!(result.is_none());
1245        assert!(matches!(
1246            mailbox.try_recv(),
1247            Err(mpsc::error::TryRecvError::Empty)
1248        ));
1249
1250        drop(held_reservation);
1251        assert_eq!(counters.reserved(), 0);
1252    }
1253
1254    #[tokio::test]
1255    async fn closed_public_admission_queue_releases_reservation() {
1256        let counters = Arc::new(SubscriberCounts::new(1));
1257        let (admission_sender, mailbox) = subscription_admission_channel();
1258        drop(mailbox);
1259        let handle = SubscriptionServiceHandle {
1260            admission_sender,
1261            counters: Arc::clone(&counters),
1262            metrics: SubscriptionMetrics::new(&prometheus::Registry::new()),
1263        };
1264
1265        let result = handle.register_subscription(unfiltered()).await;
1266        assert!(result.is_none());
1267        assert_eq!(counters.reserved(), 0);
1268    }
1269
1270    #[test]
1271    fn admission_turn_is_bounded_and_drains_ready_requests() {
1272        let (mut service, _checkpoint_sender, request_sender, _shards) =
1273            actor_service_with(3, 25, None, 4, DEFAULT_MAX_SUBSCRIBERS);
1274        let mut response_receivers = Vec::with_capacity(ADMISSION_TURN_LIMIT + 1);
1275
1276        for _ in 0..=ADMISSION_TURN_LIMIT {
1277            let (response_sender, response_receiver) = oneshot::channel();
1278            assert!(
1279                request_sender
1280                    .send(subscription_request(
1281                        &service.counters,
1282                        unfiltered(),
1283                        response_sender,
1284                    ))
1285                    .is_ok()
1286            );
1287            response_receivers.push(response_receiver);
1288        }
1289
1290        let first_request = service.admission_mailbox.try_recv().unwrap();
1291        assert!(matches!(
1292            service.admit_ready_requests(first_request),
1293            AdmissionState::Accepting
1294        ));
1295        assert_eq!(service.admission_mailbox.len(), 1);
1296
1297        for response_receiver in response_receivers.iter_mut().take(ADMISSION_TURN_LIMIT) {
1298            response_receiver
1299                .try_recv()
1300                .expect("request should be admitted");
1301        }
1302        assert!(matches!(
1303            response_receivers.last_mut().unwrap().try_recv(),
1304            Err(oneshot::error::TryRecvError::Empty)
1305        ));
1306
1307        let final_request = service.admission_mailbox.try_recv().unwrap();
1308        assert!(matches!(
1309            service.admit_ready_requests(final_request),
1310            AdmissionState::Accepting
1311        ));
1312        response_receivers
1313            .last_mut()
1314            .unwrap()
1315            .try_recv()
1316            .expect("final request should be admitted");
1317    }
1318
1319    #[test]
1320    fn canceled_ready_admission_releases_reservation() {
1321        let (mut service, _checkpoint_sender, _request_sender, _shards) =
1322            actor_service_with(1, 25, None, 4, 1);
1323        let (response_sender, response_receiver) = oneshot::channel();
1324        let request = subscription_request(&service.counters, unfiltered(), response_sender);
1325        assert_eq!(service.counters.reserved(), 1);
1326
1327        drop(response_receiver);
1328        assert!(matches!(
1329            service.try_admit(request),
1330            AdmissionState::Accepting
1331        ));
1332        assert_eq!(service.counters.reserved(), 0);
1333    }
1334
1335    #[tokio::test]
1336    async fn checkpoint_backlog_drains_before_registration() {
1337        // Queue one admission and two checkpoints before starting the actor,
1338        // making both select branches ready on its first poll.
1339        let (service, checkpoint_sender, request_sender, mut shards) =
1340            actor_service_with(1, 25, None, 4, DEFAULT_MAX_SUBSCRIBERS);
1341        let metrics = service.metrics.clone();
1342        let (reply_sender, reply_receiver) = oneshot::channel();
1343        assert!(
1344            request_sender
1345                .send(subscription_request(
1346                    &service.counters,
1347                    unfiltered(),
1348                    reply_sender,
1349                ))
1350                .is_ok()
1351        );
1352        assert_eq!(checkpoint_sender.send(checkpoint(1)).unwrap(), 1);
1353        assert_eq!(checkpoint_sender.send(checkpoint(2)).unwrap(), 1);
1354
1355        // The biased select must process both checkpoints before acknowledging
1356        // the registration.
1357        let actor = tokio::spawn(service.start());
1358        let mut receiver = tokio::time::timeout(Duration::from_secs(1), reply_receiver)
1359            .await
1360            .expect("admission remained blocked after the checkpoint backlog drained")
1361            .unwrap();
1362        assert_eq!(metrics.last_recieved_checkpoint.get(), 2);
1363
1364        // The subscriber was admitted after checkpoints 1 and 2, so its first
1365        // deliverable checkpoint is 3.
1366        assert_eq!(checkpoint_sender.send(checkpoint(3)).unwrap(), 1);
1367        drop(checkpoint_sender);
1368        actor.await.unwrap();
1369
1370        drain(&mut shards);
1371        assert_eq!(matched_sequence_number(receiver.recv().await.unwrap()), 3);
1372        assert!(receiver.recv().await.is_none());
1373    }
1374
1375    #[tokio::test]
1376    async fn queued_admission_releases_reservation_when_actor_shuts_down() {
1377        let (service, checkpoint_sender, request_sender, _shards) =
1378            actor_service_with(1, 25, None, 4, 1);
1379        let counters = Arc::clone(&service.counters);
1380        let (response_sender, response_receiver) = oneshot::channel();
1381        assert!(
1382            request_sender
1383                .send(subscription_request(
1384                    &counters,
1385                    unfiltered(),
1386                    response_sender,
1387                ))
1388                .is_ok()
1389        );
1390        assert_eq!(counters.reserved(), 1);
1391
1392        drop(checkpoint_sender);
1393        service.start().await;
1394
1395        assert!(response_receiver.await.is_err());
1396        assert_eq!(counters.reserved(), 0);
1397    }
1398
1399    #[tokio::test]
1400    async fn admission_prefers_least_backlogged_shard() {
1401        // Seed mailbox depths of two, one, and zero. Immediate admission should
1402        // choose shard 2 because it has the greatest remaining capacity.
1403        let (mut service, mut shards) = test_service(3);
1404        for _ in 0..2 {
1405            assert!(
1406                service.shards[0]
1407                    .try_send(ShardMsg::Clear(
1408                        SubscriptionTerminationReason::ServiceShutdown
1409                    ))
1410                    .is_ok()
1411            );
1412        }
1413        assert!(
1414            service.shards[1]
1415                .try_send(ShardMsg::Clear(
1416                    SubscriptionTerminationReason::ServiceShutdown
1417                ))
1418                .is_ok()
1419        );
1420
1421        // Processing the selected registration also verifies that admission
1422        // installed its lifecycle accounting.
1423        let receiver = register(&mut service, unfiltered()).await.unwrap();
1424        let registration = shards[2].mailbox.try_recv().unwrap();
1425        assert!(matches!(registration, ShardMsg::Register { .. }));
1426        shards[2].handle_msg(registration);
1427        assert_eq!(service.counters.total.load(Ordering::Relaxed), 1);
1428        assert_eq!(inflight_subscribers(&service.metrics), 1);
1429        drop(receiver);
1430    }
1431
1432    #[tokio::test]
1433    async fn saturated_admission_waits_for_any_shard() {
1434        // Fill every shard mailbox so the request enters WaitingForShard rather
1435        // than taking the immediate admission path.
1436        let (service, _checkpoint_sender, request_sender, mut shards) =
1437            actor_service_with(2, 25, None, 4, DEFAULT_MAX_SUBSCRIBERS);
1438        for shard in &service.shards {
1439            for _ in 0..SHARD_MAILBOX_SIZE {
1440                assert!(
1441                    shard
1442                        .try_send(ShardMsg::Clear(
1443                            SubscriptionTerminationReason::ServiceShutdown
1444                        ))
1445                        .is_ok()
1446                );
1447            }
1448        }
1449
1450        // Polling once consumes the request and reaches the pending
1451        // shard-capacity wait before a mailbox slot is released.
1452        let (reply_sender, reply_receiver) = oneshot::channel();
1453        assert!(
1454            request_sender
1455                .send(subscription_request(
1456                    &service.counters,
1457                    unfiltered(),
1458                    reply_sender,
1459                ))
1460                .is_ok()
1461        );
1462        let mut actor = Box::pin(service.start());
1463        assert!(futures::poll!(&mut actor).is_pending());
1464        let actor = tokio::spawn(actor);
1465
1466        // Free only shard 1. Waiting on all shard permits must wake and route
1467        // the registration there even though the cursor starts at shard 0.
1468        assert!(matches!(
1469            shards[1].mailbox.try_recv(),
1470            Ok(ShardMsg::Clear(_))
1471        ));
1472        let _receiver = tokio::time::timeout(Duration::from_secs(1), reply_receiver)
1473            .await
1474            .expect("admission did not wake when shard capacity became available")
1475            .unwrap();
1476
1477        // The registration follows the existing shard 1 messages, while shard
1478        // 0 remains full and untouched.
1479        for _ in 1..SHARD_MAILBOX_SIZE {
1480            assert!(matches!(
1481                shards[1].mailbox.try_recv(),
1482                Ok(ShardMsg::Clear(_))
1483            ));
1484        }
1485        assert!(matches!(
1486            shards[1].mailbox.try_recv(),
1487            Ok(ShardMsg::Register { .. })
1488        ));
1489        assert_eq!(shards[0].mailbox.len(), SHARD_MAILBOX_SIZE);
1490
1491        actor.abort();
1492        let _ = actor.await;
1493    }
1494
1495    #[tokio::test]
1496    async fn saturated_head_blocks_newer_request_until_canceled() {
1497        // Fill every shard, then queue a head request followed by a newer one.
1498        let (service, _checkpoint_sender, request_sender, mut shards) =
1499            actor_service_with(2, 25, None, 4, DEFAULT_MAX_SUBSCRIBERS);
1500        let counters = Arc::clone(&service.counters);
1501        for shard in &service.shards {
1502            for _ in 0..SHARD_MAILBOX_SIZE {
1503                assert!(
1504                    shard
1505                        .try_send(ShardMsg::Clear(
1506                            SubscriptionTerminationReason::ServiceShutdown
1507                        ))
1508                        .is_ok()
1509                );
1510            }
1511        }
1512
1513        let (first_sender, first_reply) = oneshot::channel();
1514        let (second_sender, second_reply) = oneshot::channel();
1515        assert!(
1516            request_sender
1517                .send(subscription_request(
1518                    &service.counters,
1519                    unfiltered(),
1520                    first_sender,
1521                ))
1522                .is_ok()
1523        );
1524        assert!(
1525            request_sender
1526                .send(subscription_request(
1527                    &service.counters,
1528                    unfiltered(),
1529                    second_sender,
1530                ))
1531                .is_ok()
1532        );
1533        assert_eq!(counters.reserved(), 2);
1534
1535        // One poll consumes only the FIFO head and reaches the pending
1536        // shard-capacity wait, leaving the newer request queued.
1537        let mut actor = Box::pin(service.start());
1538        assert!(futures::poll!(&mut actor).is_pending());
1539
1540        drop(first_reply);
1541        // The next poll drops the canceled head and moves the newer request
1542        // into the shard-capacity wait.
1543        assert!(futures::poll!(&mut actor).is_pending());
1544        assert_eq!(counters.reserved(), 1);
1545        let actor = tokio::spawn(actor);
1546
1547        // Free one shard slot so the newer request can complete admission.
1548        assert!(matches!(
1549            shards[0].mailbox.try_recv(),
1550            Ok(ShardMsg::Clear(_))
1551        ));
1552        let _receiver = tokio::time::timeout(Duration::from_secs(1), second_reply)
1553            .await
1554            .expect("newer admission remained blocked after canceling the waiting request")
1555            .unwrap();
1556        assert_eq!(counters.reserved(), 1);
1557
1558        actor.abort();
1559        let _ = actor.await;
1560        drop(shards);
1561        assert_eq!(counters.reserved(), 0);
1562    }
1563
1564    #[tokio::test]
1565    async fn run_loop_lag_clears_subscribers_and_resets_sequence_tracker() {
1566        // Register a subscriber, then overflow a one-slot checkpoint broadcast.
1567        // The actor first observes Lagged and then the retained checkpoint 2.
1568        let (mut service, checkpoint_sender, _request_sender, mut shards) =
1569            actor_service_with(1, 25, None, 1, DEFAULT_MAX_SUBSCRIBERS);
1570        let mut receiver = register(&mut service, unfiltered()).await.unwrap();
1571        let counters = Arc::clone(&service.counters);
1572        let metrics = service.metrics.clone();
1573        // A deliberately incompatible prior sequence proves lag handling resets
1574        // the tracker before checkpoint 2 is processed.
1575        metrics.last_recieved_checkpoint.set(99);
1576
1577        assert_eq!(checkpoint_sender.send(checkpoint(1)).unwrap(), 1);
1578        assert_eq!(checkpoint_sender.send(checkpoint(2)).unwrap(), 1);
1579        let actor = tokio::spawn(service.start());
1580        drop(checkpoint_sender);
1581        actor.await.unwrap();
1582
1583        // Draining applies the queued registration, source-lag clear, retained
1584        // checkpoint, and shutdown clear in shard FIFO order.
1585        drain(&mut shards);
1586        assert!(receiver.recv().await.is_none());
1587        assert_eq!(counters.total.load(Ordering::Relaxed), 0);
1588        assert_eq!(inflight_subscribers(&metrics), 0);
1589        assert_eq!(terminations(&metrics, "checkpoint", "source_lag"), 1);
1590        // The retained checkpoint can be accepted only because the Lagged
1591        // branch reset the deliberately incompatible prior sequence number.
1592        assert_eq!(metrics.last_recieved_checkpoint.get(), 2);
1593    }
1594
1595    #[tokio::test]
1596    async fn labeled_inflight_gauge_covers_all_types_and_filter_states() {
1597        let (mut service, mut shards) = test_service(1);
1598        let kinds = [
1599            SubscriptionKind::Checkpoints,
1600            SubscriptionKind::Transactions,
1601            SubscriptionKind::Events,
1602        ];
1603        let mut receivers = Vec::new();
1604
1605        for kind in kinds {
1606            for filtered in [false, true] {
1607                let query = filtered.then(|| match kind {
1608                    SubscriptionKind::Checkpoints | SubscriptionKind::Transactions => {
1609                        sender_query(addr(0), false)
1610                    }
1611                    SubscriptionKind::Events => event_type_query(
1612                        "0x0000000000000000000000000000000000000000000000000000000000000002::coin::CoinEvent",
1613                    ),
1614                });
1615                receivers.push(
1616                    register(&mut service, SubscriptionSpec { kind, query })
1617                        .await
1618                        .unwrap(),
1619                );
1620            }
1621        }
1622        drain(&mut shards);
1623
1624        for kind in kinds {
1625            for filtered in ["false", "true"] {
1626                assert_eq!(
1627                    service
1628                        .metrics
1629                        .inflight_subscribers
1630                        .with_label_values(&[kind.metric_label(), filtered])
1631                        .get(),
1632                    1
1633                );
1634            }
1635        }
1636
1637        drop(receivers);
1638        service.handle_checkpoint(checkpoint(1)).await;
1639        drain(&mut shards);
1640        assert_eq!(inflight_subscribers(&service.metrics), 0);
1641        assert_eq!(service.counters.total.load(Ordering::Relaxed), 0);
1642
1643        shards[0].handle_msg(ShardMsg::Clear(
1644            SubscriptionTerminationReason::ServiceShutdown,
1645        ));
1646        assert_eq!(inflight_subscribers(&service.metrics), 0);
1647        assert_eq!(service.counters.total.load(Ordering::Relaxed), 0);
1648        for kind in ["checkpoint", "transaction", "event"] {
1649            assert_eq!(terminations(&service.metrics, kind, "client_closed"), 2);
1650        }
1651    }
1652
1653    #[tokio::test]
1654    async fn handle_checkpoint_fans_out_in_order() {
1655        let (mut service, mut shards) = test_service(1);
1656        let mut receiver = register(&mut service, unfiltered()).await.unwrap();
1657
1658        service.handle_checkpoint(checkpoint(1)).await;
1659        service.handle_checkpoint(checkpoint(2)).await;
1660        drain(&mut shards);
1661
1662        assert_eq!(matched_sequence_number(receiver.recv().await.unwrap()), 1);
1663        assert_eq!(matched_sequence_number(receiver.recv().await.unwrap()), 2);
1664        assert_eq!(shards[0].matcher.len(), 1);
1665    }
1666
1667    #[tokio::test]
1668    async fn handle_checkpoint_drops_departed_subscriber() {
1669        let (mut service, mut shards) = test_service(1);
1670        let receiver = register(&mut service, unfiltered()).await.unwrap();
1671        drop(receiver);
1672
1673        service.handle_checkpoint(checkpoint(1)).await;
1674        drain(&mut shards);
1675
1676        assert!(shards[0].matcher.is_empty());
1677        assert_eq!(inflight_subscribers(&service.metrics), 0);
1678        assert_eq!(service.counters.total.load(Ordering::Relaxed), 0);
1679        assert_eq!(
1680            terminations(&service.metrics, "checkpoint", "client_closed"),
1681            1
1682        );
1683    }
1684
1685    #[tokio::test(start_paused = true)]
1686    async fn handle_checkpoint_waits_for_index_before_delivering() {
1687        // The first checkpoint is already indexed and does not count as a wait.
1688        let indexed = Arc::new(AtomicU64::new(4));
1689        let gate = indexed.clone();
1690        let (mut service, mut shards) = test_service_with(
1691            1,
1692            25,
1693            Some(Arc::new(move || Some(gate.load(Ordering::SeqCst)))),
1694        );
1695        let mut receiver = register(&mut service, unfiltered()).await.unwrap();
1696
1697        service.handle_checkpoint(checkpoint(4)).await;
1698        drain(&mut shards);
1699        assert_eq!(matched_sequence_number(receiver.recv().await.unwrap()), 4);
1700        assert_eq!(service.metrics.index_wait_seconds.get_sample_count(), 0);
1701        assert_eq!(service.metrics.index_wait_timeouts_total.get(), 0);
1702
1703        // Delivery of checkpoint 5 blocks until the index catches up to it.
1704        {
1705            let mut deliver = std::pin::pin!(service.handle_checkpoint(checkpoint(5)));
1706            assert!(
1707                futures::poll!(&mut deliver).is_pending(),
1708                "delivery should block while checkpoint 5 is unindexed"
1709            );
1710            assert!(receiver.try_recv().is_err());
1711
1712            indexed.store(5, Ordering::SeqCst);
1713            tokio::time::advance(INDEX_WAIT_POLL_INTERVAL).await;
1714            deliver.await;
1715        }
1716        drain(&mut shards);
1717
1718        assert_eq!(matched_sequence_number(receiver.recv().await.unwrap()), 5);
1719        assert_eq!(service.metrics.index_wait_seconds.get_sample_count(), 1);
1720        assert!(
1721            service.metrics.index_wait_seconds.get_sample_sum()
1722                >= INDEX_WAIT_POLL_INTERVAL.as_secs_f64()
1723        );
1724        assert_eq!(service.metrics.index_wait_timeouts_total.get(), 0);
1725    }
1726
1727    #[tokio::test(start_paused = true)]
1728    async fn index_wait_timeout_records_and_delivers_with_paused_time() {
1729        let indexed = Arc::new(AtomicU64::new(4));
1730        let gate = indexed.clone();
1731        let (mut service, mut shards) = test_service_with(
1732            1,
1733            25,
1734            Some(Arc::new(move || Some(gate.load(Ordering::SeqCst)))),
1735        );
1736        let mut receiver = register(&mut service, unfiltered()).await.unwrap();
1737
1738        {
1739            let mut deliver = std::pin::pin!(service.handle_checkpoint(checkpoint(5)));
1740            assert!(
1741                futures::poll!(&mut deliver).is_pending(),
1742                "delivery should block while checkpoint 5 is unindexed"
1743            );
1744            tokio::time::advance(INDEX_WAIT_TIMEOUT).await;
1745            deliver.await;
1746        }
1747        drain(&mut shards);
1748
1749        assert_eq!(matched_sequence_number(receiver.recv().await.unwrap()), 5);
1750        assert_eq!(service.metrics.index_wait_seconds.get_sample_count(), 1);
1751        assert!(
1752            service.metrics.index_wait_seconds.get_sample_sum() >= INDEX_WAIT_TIMEOUT.as_secs_f64()
1753        );
1754        assert_eq!(service.metrics.index_wait_timeouts_total.get(), 1);
1755    }
1756
1757    #[tokio::test]
1758    async fn handle_lag_drops_all_subscribers_and_resets_tracker() {
1759        let (mut service, mut shards) = test_service(2);
1760        let mut receiver_1 = register(&mut service, unfiltered()).await.unwrap();
1761        let mut receiver_2 = register(&mut service, unfiltered()).await.unwrap();
1762
1763        service.handle_checkpoint(checkpoint(5)).await;
1764        drain(&mut shards);
1765        assert_eq!(service.metrics.last_recieved_checkpoint.get(), 5);
1766
1767        service.handle_lag(10).await;
1768        drain(&mut shards);
1769
1770        // Clear empties every shard.
1771        assert!(shards[0].matcher.is_empty());
1772        assert!(shards[1].matcher.is_empty());
1773        assert_eq!(inflight_subscribers(&service.metrics), 0);
1774        assert_eq!(service.counters.total.load(Ordering::Relaxed), 0);
1775        // Both subscriptions are torn down, so the client streams close.
1776        assert!(receiver_1.recv().await.is_some()); // checkpoint 5, then closed
1777        assert!(receiver_1.recv().await.is_none());
1778        assert!(receiver_2.recv().await.is_some());
1779        assert!(receiver_2.recv().await.is_none());
1780        // The tracker is reset so the next, jumped-ahead checkpoint is not
1781        // mistaken for an out-of-order delivery (which would panic).
1782        assert_eq!(service.metrics.last_recieved_checkpoint.get(), 0);
1783        assert_eq!(
1784            terminations(&service.metrics, "checkpoint", "source_lag"),
1785            2
1786        );
1787        service.handle_lag(1).await;
1788        drain(&mut shards);
1789        assert_eq!(
1790            terminations(&service.metrics, "checkpoint", "source_lag"),
1791            2
1792        );
1793
1794        let mut receiver_3 = register(&mut service, unfiltered()).await.unwrap();
1795        service.handle_checkpoint(checkpoint(100)).await;
1796        drain(&mut shards);
1797        assert_eq!(
1798            matched_sequence_number(receiver_3.recv().await.unwrap()),
1799            100
1800        );
1801    }
1802
1803    #[tokio::test]
1804    async fn service_shutdown_records_each_subscription_once() {
1805        let (mut service, mut shards) = test_service(2);
1806        let receiver_explicit = register(&mut service, unfiltered()).await.unwrap();
1807        let receiver_fallback = register(&mut service, unfiltered()).await.unwrap();
1808        drain(&mut shards);
1809        let metrics = service.metrics.clone();
1810        let counters = Arc::clone(&service.counters);
1811        assert_eq!(counters.reserved(), 2);
1812
1813        service.shards[0]
1814            .send(ShardMsg::Clear(
1815                SubscriptionTerminationReason::ServiceShutdown,
1816            ))
1817            .await
1818            .unwrap();
1819        shards[0].drain();
1820        assert_eq!(terminations(&metrics, "checkpoint", "service_shutdown"), 1);
1821        assert_eq!(counters.reserved(), 1);
1822
1823        let fallback_shard = shards.pop().unwrap();
1824        drop(service);
1825        fallback_shard.run().await;
1826        assert!(receiver_explicit.is_closed());
1827        assert!(receiver_fallback.is_closed());
1828        assert_eq!(inflight_subscribers(&metrics), 0);
1829        assert_eq!(counters.total.load(Ordering::Relaxed), 0);
1830        assert_eq!(counters.reserved(), 0);
1831        assert_eq!(counters.filtered_tx.load(Ordering::Relaxed), 0);
1832        assert_eq!(counters.filtered_event.load(Ordering::Relaxed), 0);
1833        assert_eq!(terminations(&metrics, "checkpoint", "service_shutdown"), 2);
1834
1835        drop(shards);
1836        assert_eq!(terminations(&metrics, "checkpoint", "service_shutdown"), 2);
1837    }
1838
1839    #[tokio::test]
1840    async fn subscribers_on_every_shard_receive_a_matched_checkpoint() {
1841        let (mut service, mut shards) = test_service(2);
1842        let mut receivers = Vec::new();
1843        for _ in 0..4 {
1844            receivers.push(register(&mut service, unfiltered()).await.unwrap());
1845        }
1846        drain(&mut shards);
1847        // Equal-capacity tie-breaking spreads subscribers across the shards.
1848        assert_eq!(shards[0].matcher.len(), 2);
1849        assert_eq!(shards[1].matcher.len(), 2);
1850
1851        service.handle_checkpoint(checkpoint(1)).await;
1852        drain(&mut shards);
1853        for receiver in &mut receivers {
1854            assert_eq!(matched_sequence_number(receiver.recv().await.unwrap()), 1);
1855        }
1856    }
1857
1858    #[tokio::test]
1859    async fn filtered_subscribers_match_via_dispatcher_extracted_keys() {
1860        let (mut service, mut shards) = test_service(2);
1861        // Include filter on sender 0...
1862        let mut include_rx = register(
1863            &mut service,
1864            SubscriptionSpec {
1865                kind: SubscriptionKind::Transactions,
1866                query: Some(sender_query(addr(0), false)),
1867            },
1868        )
1869        .await
1870        .unwrap();
1871        // ...and an exclude-only filter, anchored on the synthetic
1872        // `TxUniverse` key that dispatcher-side extraction must insert into
1873        // every transaction's key set.
1874        let mut exclude_rx = register(
1875            &mut service,
1876            SubscriptionSpec {
1877                kind: SubscriptionKind::Transactions,
1878                query: Some(sender_query(addr(0), true)),
1879            },
1880        )
1881        .await
1882        .unwrap();
1883        assert_eq!(service.counters.filtered_tx.load(Ordering::Relaxed), 2);
1884
1885        let checkpoint = checkpoint_with_senders(1, &[0, 1]);
1886        let tx_lo = checkpoint.summary.data().network_total_transactions
1887            - checkpoint.transactions.len() as u64;
1888        service.handle_checkpoint(checkpoint).await;
1889        drain(&mut shards);
1890
1891        assert!(matches!(
1892            include_rx.recv().await.unwrap(),
1893            SubscriptionUpdate::WatermarkTick {
1894                checkpoint: 0,
1895                tx_hi
1896            } if tx_hi == tx_lo
1897        ));
1898        assert!(matches!(
1899            exclude_rx.recv().await.unwrap(),
1900            SubscriptionUpdate::WatermarkTick {
1901                checkpoint: 0,
1902                tx_hi
1903            } if tx_hi == tx_lo
1904        ));
1905
1906        assert_eq!(
1907            matched_transactions(include_rx.recv().await.unwrap()),
1908            vec![0]
1909        );
1910        assert_eq!(
1911            matched_transactions(exclude_rx.recv().await.unwrap()),
1912            vec![1]
1913        );
1914    }
1915
1916    #[tokio::test]
1917    async fn configured_cap_is_enforced_globally_across_shards() {
1918        let max_subscribers = 3;
1919        let (mut service, _, _, mut shards) = actor_service_with(2, 25, None, 16, max_subscribers);
1920        let mut receivers = Vec::with_capacity(max_subscribers);
1921        for _ in 0..max_subscribers {
1922            receivers.push(register(&mut service, unfiltered()).await.unwrap());
1923        }
1924        drain(&mut shards);
1925        assert_eq!(
1926            service.counters.total.load(Ordering::Relaxed),
1927            max_subscribers
1928        );
1929        assert_eq!(service.counters.reserved(), max_subscribers);
1930        assert_eq!(
1931            inflight_subscribers(&service.metrics),
1932            max_subscribers as i64
1933        );
1934        assert!(!shards[0].matcher.is_empty());
1935        assert!(!shards[1].matcher.is_empty());
1936
1937        // Reservation failure occurs before constructing an admission request,
1938        // so the cap cannot add queued work or lifecycle accounting.
1939        assert!(register(&mut service, unfiltered()).await.is_none());
1940        assert_eq!(
1941            service.counters.total.load(Ordering::Relaxed),
1942            max_subscribers
1943        );
1944        assert_eq!(service.counters.reserved(), max_subscribers);
1945        assert_eq!(
1946            inflight_subscribers(&service.metrics),
1947            max_subscribers as i64
1948        );
1949        assert_eq!(
1950            terminations(&service.metrics, "checkpoint", "service_shutdown"),
1951            0
1952        );
1953
1954        drop(receivers);
1955        service.handle_checkpoint(checkpoint(1)).await;
1956        drain(&mut shards);
1957        assert_eq!(service.counters.total.load(Ordering::Relaxed), 0);
1958        assert_eq!(service.counters.reserved(), 0);
1959        assert_eq!(inflight_subscribers(&service.metrics), 0);
1960
1961        let replacement = register(&mut service, unfiltered()).await.unwrap();
1962        drain(&mut shards);
1963        assert_eq!(service.counters.total.load(Ordering::Relaxed), 1);
1964        assert_eq!(service.counters.reserved(), 1);
1965
1966        drop(replacement);
1967        service.handle_checkpoint(checkpoint(2)).await;
1968        drain(&mut shards);
1969        assert_eq!(service.counters.total.load(Ordering::Relaxed), 0);
1970        assert_eq!(service.counters.reserved(), 0);
1971    }
1972
1973    #[tokio::test]
1974    async fn unprocessed_register_finalizes_guard_when_shard_drops() {
1975        let (mut service, shards) = test_service(1);
1976        let _receiver = register(&mut service, unfiltered()).await.unwrap();
1977        assert_eq!(service.counters.total.load(Ordering::Relaxed), 1);
1978        assert_eq!(inflight_subscribers(&service.metrics), 1);
1979        assert_eq!(service.counters.reserved(), 1);
1980
1981        // Drop the shard with the Register message still queued in its
1982        // mailbox: the guard travelling inside the message must finalize
1983        // with the default service_shutdown reason and rebalance the
1984        // counts and gauge exactly once.
1985        drop(shards);
1986        assert_eq!(service.counters.total.load(Ordering::Relaxed), 0);
1987        assert_eq!(service.counters.reserved(), 0);
1988        assert_eq!(inflight_subscribers(&service.metrics), 0);
1989        assert_eq!(
1990            terminations(&service.metrics, "checkpoint", "service_shutdown"),
1991            1
1992        );
1993    }
1994
1995    #[tokio::test]
1996    async fn registration_racing_dispatch_lands_at_the_next_checkpoint() {
1997        let (mut service, mut shards) = test_service(1);
1998        let mut receiver_a = register(&mut service, unfiltered()).await.unwrap();
1999        service.handle_checkpoint(checkpoint(1)).await;
2000        let mut receiver_b = register(&mut service, unfiltered()).await.unwrap();
2001        service.handle_checkpoint(checkpoint(2)).await;
2002        drain(&mut shards);
2003
2004        // Per-shard FIFO: B's Register is queued behind checkpoint 1, so B
2005        // sees only checkpoint 2.
2006        assert_eq!(matched_sequence_number(receiver_a.recv().await.unwrap()), 1);
2007        assert_eq!(matched_sequence_number(receiver_a.recv().await.unwrap()), 2);
2008        assert_eq!(matched_sequence_number(receiver_b.recv().await.unwrap()), 2);
2009        assert!(receiver_b.try_recv().is_err());
2010    }
2011
2012    #[tokio::test]
2013    async fn space_counters_track_filtered_subscribers() {
2014        let (mut service, mut shards) = test_service_with(2, 1, None);
2015        let tx_rx = register(
2016            &mut service,
2017            SubscriptionSpec {
2018                kind: SubscriptionKind::Transactions,
2019                query: Some(sender_query(addr(0), false)),
2020            },
2021        )
2022        .await
2023        .unwrap();
2024        assert_eq!(service.counters.filtered_tx.load(Ordering::Relaxed), 1);
2025        let event_rx = register(
2026            &mut service,
2027            SubscriptionSpec {
2028                kind: SubscriptionKind::Events,
2029                query: Some(event_type_query(
2030                    "0x0000000000000000000000000000000000000000000000000000000000000002::coin::CoinEvent",
2031                )),
2032            },
2033        )
2034        .await
2035        .unwrap();
2036        assert_eq!(service.counters.filtered_event.load(Ordering::Relaxed), 1);
2037
2038        // Departed clients are finalized by their shard-owned lifecycle
2039        // guards when the next dispatch observes the closed channels.
2040        drop(tx_rx);
2041        drop(event_rx);
2042        service.handle_checkpoint(checkpoint(1)).await;
2043        drain(&mut shards);
2044
2045        assert_eq!(service.counters.filtered_tx.load(Ordering::Relaxed), 0);
2046        assert_eq!(service.counters.filtered_event.load(Ordering::Relaxed), 0);
2047        assert_eq!(inflight_subscribers(&service.metrics), 0);
2048    }
2049
2050    #[tokio::test]
2051    async fn unfiltered_subscribers_receive_all_matches() {
2052        let (mut service, mut shards) = test_service(1);
2053        let mut tx_rx = register(
2054            &mut service,
2055            SubscriptionSpec {
2056                kind: SubscriptionKind::Transactions,
2057                query: None,
2058            },
2059        )
2060        .await
2061        .unwrap();
2062        let mut ev_rx = register(
2063            &mut service,
2064            SubscriptionSpec {
2065                kind: SubscriptionKind::Events,
2066                query: None,
2067            },
2068        )
2069        .await
2070        .unwrap();
2071
2072        let event_checkpoint = checkpoint_with_events(1);
2073        let tx_lo = event_checkpoint.summary.data().network_total_transactions
2074            - event_checkpoint.transactions.len() as u64;
2075        service.handle_checkpoint(event_checkpoint).await;
2076        drain(&mut shards);
2077
2078        assert!(matches!(
2079            tx_rx.recv().await.unwrap(),
2080            SubscriptionUpdate::WatermarkTick {
2081                checkpoint: 0,
2082                tx_hi
2083            } if tx_hi == tx_lo
2084        ));
2085        assert!(matches!(
2086            ev_rx.recv().await.unwrap(),
2087            SubscriptionUpdate::WatermarkTick {
2088                checkpoint: 0,
2089                tx_hi
2090            } if tx_hi == tx_lo
2091        ));
2092
2093        // Unfiltered matches arrive as O(1) "all" payloads that the index
2094        // accessors expand to every transaction / event.
2095        let matched = match tx_rx.recv().await.unwrap() {
2096            SubscriptionUpdate::Matched(matched) => matched,
2097            SubscriptionUpdate::WatermarkTick { .. } => panic!("expected a match, got a tick"),
2098        };
2099        assert!(matches!(
2100            matched.matches,
2101            SubscriptionMatches::AllTransactions
2102        ));
2103        let tx_count = matched.checkpoint.transactions.len() as u32;
2104        assert_eq!(
2105            matched
2106                .matches
2107                .transaction_indices(tx_count)
2108                .unwrap()
2109                .collect::<Vec<_>>(),
2110            vec![0, 1]
2111        );
2112
2113        let matched = match ev_rx.recv().await.unwrap() {
2114            SubscriptionUpdate::Matched(matched) => matched,
2115            SubscriptionUpdate::WatermarkTick { .. } => panic!("expected a match, got a tick"),
2116        };
2117        assert!(matches!(matched.matches, SubscriptionMatches::AllEvents));
2118        assert_eq!(
2119            matched
2120                .matches
2121                .event_indices(&matched.checkpoint)
2122                .unwrap()
2123                .collect::<Vec<_>>(),
2124            vec![(1, 0), (1, 1)]
2125        );
2126
2127        // A checkpoint with no transactions yields no frame for either
2128        // subscriber: empty match sets stay on the watermark-tick path.
2129        service.handle_checkpoint(checkpoint(2)).await;
2130        drain(&mut shards);
2131        assert!(tx_rx.try_recv().is_err());
2132        assert!(ev_rx.try_recv().is_err());
2133    }
2134
2135    #[test]
2136    fn event_indices_flatten_sparse_matches_in_order() {
2137        let matches = SubscriptionMatches::Events(vec![(0, vec![1, 2]), (3, vec![0])]);
2138        let checkpoint = checkpoint(1);
2139        assert_eq!(
2140            matches
2141                .event_indices(&checkpoint)
2142                .unwrap()
2143                .collect::<Vec<_>>(),
2144            vec![(0, 1), (0, 2), (3, 0)]
2145        );
2146    }
2147}