Skip to main content

liminal/durability/
counting.rs

1//! A counting seam around any [`DurableStore`].
2//!
3//! Every durable operation liminal makes goes through a [`DurableStore`], so a
4//! wrapper at that seam is where store work per journey can be counted: calls
5//! per method, rows read, rows and bytes written, and flushes. The wrapper is
6//! a mirror, not a filter: each method is forwarded once, explicitly, and its
7//! answer is handed back untouched, so a caller cannot tell it is there.
8//!
9//! `append_all` and `read_at` are forwarded explicitly rather than left to the
10//! trait's defaults. A store may override both (haematite commits a batch as
11//! one write and answers a point read without a range read), and falling back
12//! to the defaults here would turn one batch into one write per row.
13//!
14//! What the seam cannot see is work a store does inside one call: the engine
15//! rows behind a short `read_from`, and the streams a `scan_page` lists. Both
16//! happen below the trait, and an answer carries no record of them.
17//!
18//! Cost, measured once by `the_wrapper_cost_per_call_is_measured_over_a_hundred_thousand_calls`
19//! on 6 October 2026 on Tom's Mac (debug build; Dean's laptop was offline):
20//! median 230 ns per `read_value` wrapped against 140 ns bare, so 90 ns per
21//! call for the seam, over 100,000 calls each. To be re-measured on Dean's
22//! laptop.
23
24use std::fmt;
25use std::sync::{Arc, Mutex, OnceLock};
26use std::sync::atomic::{AtomicU64, Ordering};
27
28use super::{DurabilityError, DurableStore, ScanCursor, ScanPage, StoredEntry};
29
30/// Every counter a [`CountingStore`] keeps, read at one moment.
31///
32/// Counters only grow, so the work between two snapshots is
33/// [`StoreCounts::since`] of the later over the earlier.
34#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
35pub struct StoreCounts {
36    /// Atomic routed batch calls, including refusals.
37    pub apply_routed_batch_calls: u64,
38    /// Conditional replacements in successful routed batches.
39    pub apply_routed_batch_items: u64,
40    /// Routed point-read calls, including errors.
41    pub get_routed_calls: u64,
42    /// Values returned by routed point reads.
43    pub get_routed_rows: u64,
44
45    /// `append` calls.
46    pub append_calls: u64,
47    /// Rows written by `append` calls that succeeded.
48    pub append_rows: u64,
49    /// Payload bytes written by `append` calls that succeeded.
50    pub append_bytes: u64,
51    /// `append_all` calls.
52    pub append_all_calls: u64,
53    /// Rows written by `append_all` calls that succeeded.
54    pub append_all_rows: u64,
55    /// Payload bytes written by `append_all` calls that succeeded.
56    pub append_all_bytes: u64,
57    /// `read_from` calls.
58    pub read_from_calls: u64,
59    /// Rows returned by `read_from`.
60    pub read_from_rows: u64,
61    /// `read_at` calls.
62    pub read_at_calls: u64,
63    /// Rows returned by `read_at`: one per call that found its row.
64    pub read_at_rows: u64,
65    /// `cas` calls.
66    pub cas_calls: u64,
67    /// `read_value` calls.
68    pub read_value_calls: u64,
69    /// `read_stream_next_seq` calls.
70    pub read_stream_next_seq_calls: u64,
71    /// `scan_page` calls.
72    pub scan_page_calls: u64,
73    /// Rows returned by `scan_page`.
74    pub scan_page_rows: u64,
75    /// `flush` calls.
76    pub flushes: u64,
77    /// `truncate_before` calls.
78    pub truncate_calls: u64,
79    /// Rows deleted by `truncate_before` calls that succeeded.
80    pub truncate_rows: u64,
81}
82
83impl StoreCounts {
84    /// The work counted after `earlier` was taken.
85    ///
86    /// `earlier` must be a snapshot of the same store taken before `self`.
87    /// Counters never decrease, so each difference is the work between the
88    /// two; a counter that reads lower in `self` answers zero rather than
89    /// wrapping.
90    #[must_use]
91    pub const fn since(&self, earlier: &Self) -> Self {
92        Self {
93            apply_routed_batch_calls: self
94                .apply_routed_batch_calls
95                .saturating_sub(earlier.apply_routed_batch_calls),
96            apply_routed_batch_items: self
97                .apply_routed_batch_items
98                .saturating_sub(earlier.apply_routed_batch_items),
99            get_routed_calls: self
100                .get_routed_calls
101                .saturating_sub(earlier.get_routed_calls),
102            get_routed_rows: self.get_routed_rows.saturating_sub(earlier.get_routed_rows),
103
104            append_calls: self.append_calls.saturating_sub(earlier.append_calls),
105            append_rows: self.append_rows.saturating_sub(earlier.append_rows),
106            append_bytes: self.append_bytes.saturating_sub(earlier.append_bytes),
107            append_all_calls: self
108                .append_all_calls
109                .saturating_sub(earlier.append_all_calls),
110            append_all_rows: self.append_all_rows.saturating_sub(earlier.append_all_rows),
111            append_all_bytes: self
112                .append_all_bytes
113                .saturating_sub(earlier.append_all_bytes),
114            read_from_calls: self.read_from_calls.saturating_sub(earlier.read_from_calls),
115            read_from_rows: self.read_from_rows.saturating_sub(earlier.read_from_rows),
116            read_at_calls: self.read_at_calls.saturating_sub(earlier.read_at_calls),
117            read_at_rows: self.read_at_rows.saturating_sub(earlier.read_at_rows),
118            cas_calls: self.cas_calls.saturating_sub(earlier.cas_calls),
119            read_value_calls: self
120                .read_value_calls
121                .saturating_sub(earlier.read_value_calls),
122            read_stream_next_seq_calls: self
123                .read_stream_next_seq_calls
124                .saturating_sub(earlier.read_stream_next_seq_calls),
125            scan_page_calls: self.scan_page_calls.saturating_sub(earlier.scan_page_calls),
126            scan_page_rows: self.scan_page_rows.saturating_sub(earlier.scan_page_rows),
127            flushes: self.flushes.saturating_sub(earlier.flushes),
128            truncate_calls: self.truncate_calls.saturating_sub(earlier.truncate_calls),
129            truncate_rows: self.truncate_rows.saturating_sub(earlier.truncate_rows),
130        }
131    }
132
133    /// The sum of two sets of counts, for work spread over two stores.
134    #[must_use]
135    pub const fn plus(&self, other: &Self) -> Self {
136        Self {
137            apply_routed_batch_calls: self
138                .apply_routed_batch_calls
139                .saturating_add(other.apply_routed_batch_calls),
140            apply_routed_batch_items: self
141                .apply_routed_batch_items
142                .saturating_add(other.apply_routed_batch_items),
143            get_routed_calls: self.get_routed_calls.saturating_add(other.get_routed_calls),
144            get_routed_rows: self.get_routed_rows.saturating_add(other.get_routed_rows),
145
146            append_calls: self.append_calls.saturating_add(other.append_calls),
147            append_rows: self.append_rows.saturating_add(other.append_rows),
148            append_bytes: self.append_bytes.saturating_add(other.append_bytes),
149            append_all_calls: self.append_all_calls.saturating_add(other.append_all_calls),
150            append_all_rows: self.append_all_rows.saturating_add(other.append_all_rows),
151            append_all_bytes: self.append_all_bytes.saturating_add(other.append_all_bytes),
152            read_from_calls: self.read_from_calls.saturating_add(other.read_from_calls),
153            read_from_rows: self.read_from_rows.saturating_add(other.read_from_rows),
154            read_at_calls: self.read_at_calls.saturating_add(other.read_at_calls),
155            read_at_rows: self.read_at_rows.saturating_add(other.read_at_rows),
156            cas_calls: self.cas_calls.saturating_add(other.cas_calls),
157            read_value_calls: self.read_value_calls.saturating_add(other.read_value_calls),
158            read_stream_next_seq_calls: self
159                .read_stream_next_seq_calls
160                .saturating_add(other.read_stream_next_seq_calls),
161            scan_page_calls: self.scan_page_calls.saturating_add(other.scan_page_calls),
162            scan_page_rows: self.scan_page_rows.saturating_add(other.scan_page_rows),
163            flushes: self.flushes.saturating_add(other.flushes),
164            truncate_calls: self.truncate_calls.saturating_add(other.truncate_calls),
165            truncate_rows: self.truncate_rows.saturating_add(other.truncate_rows),
166        }
167    }
168
169    /// Every counter with its name, in the one fixed order [`fmt::Display`]
170    /// prints them in.
171    #[must_use]
172    pub const fn named(&self) -> [(&'static str, u64); 22] {
173        [
174            ("append_calls", self.append_calls),
175            ("append_rows", self.append_rows),
176            ("append_bytes", self.append_bytes),
177            ("append_all_calls", self.append_all_calls),
178            ("append_all_rows", self.append_all_rows),
179            ("append_all_bytes", self.append_all_bytes),
180            ("read_from_calls", self.read_from_calls),
181            ("read_from_rows", self.read_from_rows),
182            ("read_at_calls", self.read_at_calls),
183            ("read_at_rows", self.read_at_rows),
184            ("cas_calls", self.cas_calls),
185            ("read_value_calls", self.read_value_calls),
186            (
187                "read_stream_next_seq_calls",
188                self.read_stream_next_seq_calls,
189            ),
190            ("scan_page_calls", self.scan_page_calls),
191            ("scan_page_rows", self.scan_page_rows),
192            ("flushes", self.flushes),
193            ("truncate_calls", self.truncate_calls),
194            ("truncate_rows", self.truncate_rows),
195            ("apply_routed_batch_calls", self.apply_routed_batch_calls),
196            ("apply_routed_batch_items", self.apply_routed_batch_items),
197            ("get_routed_calls", self.get_routed_calls),
198            ("get_routed_rows", self.get_routed_rows),
199        ]
200    }
201}
202
203/// One counter per line, `name value`, in the order of [`StoreCounts::named`],
204/// so two runs compare line by line.
205impl fmt::Display for StoreCounts {
206    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
207        for (name, value) in self.named() {
208            writeln!(formatter, "{name} {value}")?;
209        }
210        Ok(())
211    }
212}
213
214/// Receives counter increments and explicit flush calls at the counting boundary.
215///
216/// The callback must not inspect history or perform work for ordinary increments.
217/// Flush observations describe the boundary call, including a call that fails.
218pub trait StoreCountObserver: fmt::Debug + Send + Sync {
219    /// Called after an actual counter increment, with no publication origin.
220    fn changed(&self);
221
222    /// Called once per explicit flush; a row count is absent unless the boundary knows it.
223    fn flushed(&self, store: &str, batch_rows: Option<u64>);
224}
225
226#[derive(Debug)]
227struct ObserverBinding {
228    name: String,
229    observer: Arc<dyn StoreCountObserver>,
230}
231
232static OBSERVER_INSTALL: Mutex<()> = Mutex::new(());
233
234/// The live counters behind a [`CountingStore`].
235#[derive(Debug, Default)]
236struct Counters {
237    apply_routed_batch_calls: AtomicU64,
238    apply_routed_batch_items: AtomicU64,
239    get_routed_calls: AtomicU64,
240    get_routed_rows: AtomicU64,
241
242    append_calls: AtomicU64,
243    append_rows: AtomicU64,
244    append_bytes: AtomicU64,
245    append_all_calls: AtomicU64,
246    append_all_rows: AtomicU64,
247    append_all_bytes: AtomicU64,
248    read_from_calls: AtomicU64,
249    read_from_rows: AtomicU64,
250    read_at_calls: AtomicU64,
251    read_at_rows: AtomicU64,
252    cas_calls: AtomicU64,
253    read_value_calls: AtomicU64,
254    read_stream_next_seq_calls: AtomicU64,
255    scan_page_calls: AtomicU64,
256    scan_page_rows: AtomicU64,
257    flushes: AtomicU64,
258    truncate_calls: AtomicU64,
259    truncate_rows: AtomicU64,
260    observer: OnceLock<ObserverBinding>,
261}
262
263impl Counters {
264    fn add(&self, counter: &AtomicU64, amount: u64) {
265        if amount == 0 {
266            return;
267        }
268        counter.fetch_add(amount, Ordering::Relaxed);
269        if let Some(binding) = self.observer.get() {
270            binding.observer.changed();
271        }
272    }
273
274    fn flushed(&self) {
275        if let Some(binding) = self.observer.get() {
276            binding.observer.flushed(&binding.name, None);
277        }
278    }
279
280    /// Every counter as it stands now, each read once without a lock.
281    fn snapshot(&self) -> StoreCounts {
282        let read = |counter: &AtomicU64| counter.load(Ordering::Relaxed);
283        StoreCounts {
284            apply_routed_batch_calls: read(&self.apply_routed_batch_calls),
285            apply_routed_batch_items: read(&self.apply_routed_batch_items),
286            get_routed_calls: read(&self.get_routed_calls),
287            get_routed_rows: read(&self.get_routed_rows),
288            append_calls: read(&self.append_calls),
289            append_rows: read(&self.append_rows),
290            append_bytes: read(&self.append_bytes),
291            append_all_calls: read(&self.append_all_calls),
292            append_all_rows: read(&self.append_all_rows),
293            append_all_bytes: read(&self.append_all_bytes),
294            read_from_calls: read(&self.read_from_calls),
295            read_from_rows: read(&self.read_from_rows),
296            read_at_calls: read(&self.read_at_calls),
297            read_at_rows: read(&self.read_at_rows),
298            cas_calls: read(&self.cas_calls),
299            read_value_calls: read(&self.read_value_calls),
300            read_stream_next_seq_calls: read(&self.read_stream_next_seq_calls),
301            scan_page_calls: read(&self.scan_page_calls),
302            scan_page_rows: read(&self.scan_page_rows),
303            flushes: read(&self.flushes),
304            truncate_calls: read(&self.truncate_calls),
305            truncate_rows: read(&self.truncate_rows),
306        }
307    }
308}
309
310/// A cloneable reader of one or more [`CountingStore`]s' counters.
311///
312/// Handed out where a store is built, beside the store itself, so a test can
313/// take a snapshot of a store it only holds as `Arc<dyn DurableStore>`. A
314/// handle over several stores answers their sum: the work of one journey,
315/// wherever it landed.
316#[derive(Clone, Debug, Default)]
317pub struct StoreCountsHandle {
318    stores: Vec<Arc<Counters>>,
319}
320
321impl StoreCountsHandle {
322    /// The number of independently counted stores read by this handle.
323    #[must_use]
324    pub fn store_count(&self) -> usize {
325        self.stores.len()
326    }
327
328    /// Installs one observer with one nonempty name for each counted store.
329    ///
330    /// # Errors
331    /// Refuses absent stores, a name count mismatch, empty names, duplicate stores,
332    /// an already installed observer, or a poisoned installation lock. Refusals
333    /// detected during validation leave every store unchanged.
334    pub fn install_observer(
335        &self,
336        names: &[&str],
337        observer: Arc<dyn StoreCountObserver>,
338    ) -> Result<(), DurabilityError> {
339        if self.stores.is_empty() || names.len() != self.stores.len() {
340            return Err(observer_error(
341                "one name is required for each counted store",
342            ));
343        }
344        if names.iter().any(|name| name.trim().is_empty()) {
345            return Err(observer_error("store name is empty"));
346        }
347        for (index, counters) in self.stores.iter().enumerate() {
348            if self.stores[..index]
349                .iter()
350                .any(|earlier| Arc::ptr_eq(earlier, counters))
351            {
352                return Err(observer_error(
353                    "the handle contains a duplicate counted store",
354                ));
355            }
356        }
357        let installation = OBSERVER_INSTALL
358            .lock()
359            .map_err(|_| observer_error("installation lock poisoned"))?;
360        if self
361            .stores
362            .iter()
363            .any(|counters| counters.observer.get().is_some())
364        {
365            return Err(observer_error("observer already installed"));
366        }
367        for ((counters, name), observer) in self
368            .stores
369            .iter()
370            .zip(names)
371            .zip(std::iter::repeat_n(observer, self.stores.len()))
372        {
373            counters
374                .observer
375                .set(ObserverBinding {
376                    name: (*name).to_owned(),
377                    observer,
378                })
379                .map_err(|_| observer_error("observer installation invariant failed"))?;
380        }
381        drop(installation);
382        Ok(())
383    }
384
385    /// One handle answering the sum of `self` and `other`.
386    #[must_use]
387    pub fn with(mut self, other: &Self) -> Self {
388        self.stores.extend(other.stores.iter().cloned());
389        self
390    }
391
392    /// Every counter, summed over the stores this handle reads, as it stands
393    /// now. See [`CountingStore::snapshot`] for what a snapshot taken during
394    /// a call may miss.
395    #[must_use]
396    pub fn snapshot(&self) -> StoreCounts {
397        self.stores
398            .iter()
399            .fold(StoreCounts::default(), |total, counters| {
400                total.plus(&counters.snapshot())
401            })
402    }
403}
404
405/// Databases opened or created in this process through liminal's open paths.
406static STORES_OPENED: AtomicU64 = AtomicU64::new(0);
407
408/// How many durable databases this process has opened or created through
409/// liminal's open paths, ephemeral and persistent alike. Only grows.
410#[must_use]
411pub fn stores_opened() -> u64 {
412    STORES_OPENED.load(Ordering::Relaxed)
413}
414
415/// Records one durable database opened or created. Every open path calls it
416/// once, after the database is open, so a failed open is never counted.
417pub fn record_store_opened() {
418    add(&STORES_OPENED, 1);
419}
420
421/// Adds `amount` to `counter`.
422///
423/// Relaxed ordering: each counter is a tally on its own, read only by a
424/// snapshot, and orders nothing else.
425fn add(counter: &AtomicU64, amount: u64) {
426    counter.fetch_add(amount, Ordering::Relaxed);
427}
428
429fn observer_error(message: &str) -> DurabilityError {
430    DurabilityError::ConfigError(format!("store count observer: {message}"))
431}
432
433/// A row or byte total as a counter increment; a `usize` always fits.
434fn widen(amount: usize) -> u64 {
435    u64::try_from(amount).unwrap_or(u64::MAX)
436}
437
438/// A [`DurableStore`] that forwards every method to `S` and counts the work.
439///
440/// Counter increments take no lock and allocate nothing. An installed observer
441/// receives the increment and explicit flush call; every await is the inner store's.
442#[derive(Debug)]
443pub struct CountingStore<S> {
444    inner: S,
445    counters: Arc<Counters>,
446}
447
448impl<S: DurableStore> CountingStore<S> {
449    /// Wraps `inner`, with every counter at zero.
450    #[must_use]
451    pub fn new(inner: S) -> Self {
452        Self {
453            inner,
454            counters: Arc::default(),
455        }
456    }
457
458    /// A handle that reads this store's counters, to be kept beside the store
459    /// once it is held only as `Arc<dyn DurableStore>`.
460    #[must_use]
461    pub fn handle(&self) -> StoreCountsHandle {
462        StoreCountsHandle {
463            stores: vec![Arc::clone(&self.counters)],
464        }
465    }
466
467    /// Every counter as it stands now.
468    ///
469    /// Each counter is read once, without a lock, so a snapshot taken while
470    /// other calls are in flight may count a call whose rows it does not yet
471    /// count. Between two steps of one journey nothing is in flight.
472    #[must_use]
473    pub fn snapshot(&self) -> StoreCounts {
474        self.counters.snapshot()
475    }
476}
477
478#[async_trait::async_trait]
479impl<S: DurableStore> DurableStore for CountingStore<S> {
480    fn reclaim_spine(&self) -> Option<std::sync::Arc<haematite::EventStore>> {
481        self.inner.reclaim_spine()
482    }
483
484    fn max_request_bytes(&self) -> Option<usize> {
485        self.inner.max_request_bytes()
486    }
487
488    async fn apply_routed_batch(
489        &self,
490        route_key: &str,
491        items: Vec<super::RoutedBatchItem>,
492    ) -> Result<(), DurabilityError> {
493        self.counters
494            .add(&self.counters.apply_routed_batch_calls, 1);
495        let count = widen(items.len());
496        self.inner.apply_routed_batch(route_key, items).await?;
497        self.counters
498            .add(&self.counters.apply_routed_batch_items, count);
499        Ok(())
500    }
501
502    async fn get_routed(
503        &self,
504        route_key: &str,
505        key: &[u8],
506    ) -> Result<Option<Vec<u8>>, DurabilityError> {
507        self.counters.add(&self.counters.get_routed_calls, 1);
508        let value = self.inner.get_routed(route_key, key).await?;
509        if value.is_some() {
510            self.counters.add(&self.counters.get_routed_rows, 1);
511        }
512        Ok(value)
513    }
514
515    async fn append(
516        &self,
517        stream_key: &str,
518        payload: Vec<u8>,
519        expected_seq: u64,
520    ) -> Result<u64, DurabilityError> {
521        self.counters.add(&self.counters.append_calls, 1);
522        let bytes = widen(payload.len());
523        let assigned = self.inner.append(stream_key, payload, expected_seq).await?;
524        self.counters.add(&self.counters.append_rows, 1);
525        self.counters.add(&self.counters.append_bytes, bytes);
526        Ok(assigned)
527    }
528
529    async fn read_from(
530        &self,
531        stream_key: &str,
532        offset: u64,
533        limit: usize,
534    ) -> Result<Vec<StoredEntry>, DurabilityError> {
535        self.counters.add(&self.counters.read_from_calls, 1);
536        let entries = self.inner.read_from(stream_key, offset, limit).await?;
537        self.counters
538            .add(&self.counters.read_from_rows, widen(entries.len()));
539        Ok(entries)
540    }
541
542    async fn append_all(
543        &self,
544        stream_key: &str,
545        payloads: Vec<Vec<u8>>,
546        expected_seq: u64,
547    ) -> Result<u64, DurabilityError> {
548        self.counters.add(&self.counters.append_all_calls, 1);
549        let rows = widen(payloads.len());
550        let bytes = payloads
551            .iter()
552            .map(|payload| widen(payload.len()))
553            .fold(0_u64, u64::saturating_add);
554        let next = self
555            .inner
556            .append_all(stream_key, payloads, expected_seq)
557            .await?;
558        self.counters.add(&self.counters.append_all_rows, rows);
559        self.counters.add(&self.counters.append_all_bytes, bytes);
560        Ok(next)
561    }
562
563    async fn read_at(
564        &self,
565        stream_key: &str,
566        sequence: u64,
567    ) -> Result<Option<StoredEntry>, DurabilityError> {
568        self.counters.add(&self.counters.read_at_calls, 1);
569        let entry = self.inner.read_at(stream_key, sequence).await?;
570        if entry.is_some() {
571            self.counters.add(&self.counters.read_at_rows, 1);
572        }
573        Ok(entry)
574    }
575
576    async fn cas(&self, key: &str, old_value: u64, new_value: u64) -> Result<(), DurabilityError> {
577        self.counters.add(&self.counters.cas_calls, 1);
578        self.inner.cas(key, old_value, new_value).await
579    }
580
581    async fn read_stream_next_seq(&self, stream_key: &str) -> Result<Option<u64>, DurabilityError> {
582        self.counters
583            .add(&self.counters.read_stream_next_seq_calls, 1);
584        self.inner.read_stream_next_seq(stream_key).await
585    }
586
587    async fn read_value(&self, key: &str) -> Result<Option<u64>, DurabilityError> {
588        self.counters.add(&self.counters.read_value_calls, 1);
589        self.inner.read_value(key).await
590    }
591
592    async fn scan_page(
593        &self,
594        prefix: &str,
595        cursor: Option<ScanCursor>,
596        limit: usize,
597    ) -> Result<ScanPage, DurabilityError> {
598        self.counters.add(&self.counters.scan_page_calls, 1);
599        let page = self.inner.scan_page(prefix, cursor, limit).await?;
600        self.counters
601            .add(&self.counters.scan_page_rows, widen(page.entries.len()));
602        Ok(page)
603    }
604
605    async fn flush(&self) -> Result<(), DurabilityError> {
606        self.counters.add(&self.counters.flushes, 1);
607        self.counters.flushed();
608        self.inner.flush().await
609    }
610
611    async fn truncate_before(
612        &self,
613        stream_key: &str,
614        before_seq: u64,
615    ) -> Result<u64, DurabilityError> {
616        self.counters.add(&self.counters.truncate_calls, 1);
617        let deleted = self.inner.truncate_before(stream_key, before_seq).await?;
618        self.counters.add(&self.counters.truncate_rows, deleted);
619        Ok(deleted)
620    }
621
622    async fn scan_routed(
623        &self,
624        route_key: &str,
625        from: &[u8],
626        to: &[u8],
627    ) -> Result<Vec<(Vec<u8>, Vec<u8>)>, DurabilityError> {
628        self.inner.scan_routed(route_key, from, to).await
629    }
630
631    async fn remove_routed_range(
632        &self,
633        route_key: &str,
634        from: &[u8],
635        to: &[u8],
636    ) -> Result<u64, DurabilityError> {
637        self.inner.remove_routed_range(route_key, from, to).await
638    }
639
640    fn store_counts(&self) -> Option<StoreCountsHandle> {
641        Some(self.handle())
642    }
643}
644
645#[cfg(test)]
646mod tests;