Skip to main content

krabka_client_streams/store/
suppress_store.rs

1//! Suppress buffer as a registered, changelog-backed state store.
2//!
3//! The suppress store does NOT sit over the pluggable `ByteKeyValueStore`
4//! backend, unlike the other typed stores
5//! [`WindowBytesStore`](crate::store::window) and
6//! [`SessionBytesStore`](crate::store::session). A `KTable.suppress(...)` buffer
7//! evicts by `buffer_time`, not by key order. So its internal storage is a
8//! time-ordered `BTreeMap` keyed by `(buffer_time, seq)`, with a side `index`
9//! from `key_bytes` to slot for replace-by-key. The store keeps serialized keys
10//! and values, tracks byte-size limits, and writes a JVM-compatible changelog.
11//!
12//! The changelog KEY is the serialized record-key bytes. The changelog VALUE is
13//! the JVM `BufferValue` payload. A buffer eviction logs a `(key_bytes, None)`
14//! tombstone.
15use std::{
16    any::Any,
17    collections::{BTreeMap, HashMap},
18};
19
20use async_trait::async_trait;
21use bytes::{BufMut, Bytes};
22use krabka_units::prelude::*;
23
24use crate::{
25    dsl::processors::change::Change,
26    processor::serde::Serde,
27    store::{
28        api::StateStore,
29        snapshot::{SnapshotReader, put_sized, store_payload},
30        suppress_bufval::{
31            SuppressRecordCtx, deserialize_buffer_change, serialize_buffer_change,
32            try_deserialize_buffer_change,
33        },
34    },
35};
36
37/// Typed, time-ordered suppress buffer.
38///
39/// `put` buffers a [`Change<V>`] under a `buffer_time` and replaces by key.
40/// `evict_while` and `evict_oldest` drain the earliest-closing entries and log
41/// tombstones, so the processor can forward them.
42// `pub(crate)`: the trait surfaces `Change<V>` (a crate-internal type) and the
43// suppress store is a built-in DSL mechanism the suppress processor reaches via
44// `ctx.get_suppress_store` — never a user-facing custom-processor store.
45#[async_trait]
46pub(crate) trait SuppressStore<K: Send + Sync, V: Send>: StateStore {
47    /// Buffers or replaces the pending change for `key` at `buffer_time`.
48    ///
49    /// `ctx` is the source record context. The store carries it into the
50    /// changelog VALUE.
51    async fn put(&mut self, key: K, buffer_time: i64, change: Change<V>, ctx: SuppressRecordCtx);
52    /// Pops every entry with `buffer_time <= threshold`.
53    ///
54    /// The entries come back in `(buffer_time, seq)` order as
55    /// `(key, change, record_ts)`. This method logs a tombstone for each popped
56    /// entry.
57    async fn evict_while(&mut self, threshold: i64) -> Vec<(K, Change<V>, i64)>;
58    /// Pops the single lowest-`(buffer_time, seq)` entry for emit-early overflow.
59    ///
60    /// Returns `None` if the buffer is empty. This method logs a tombstone for
61    /// the popped entry.
62    async fn evict_oldest(&mut self) -> Option<(K, Change<V>, i64)>;
63    fn len(&self) -> usize;
64    /// Paired with [`len`](Self::len) for `clippy::len_without_is_empty`.
65    ///
66    /// The processor reads `len` and `byte_size` for the caps, never emptiness.
67    /// Only the store's own tests call this method.
68    #[allow(dead_code)]
69    fn is_empty(&self) -> bool;
70    /// Total buffered size, the JVM `maxBytes`-cap accounting unit.
71    ///
72    /// Each entry counts `key_bytes.len() + new_bytes.len()`.
73    fn byte_size(&self) -> ByteSize;
74}
75
76/// One buffered entry.
77///
78/// `new_bytes` and `old_bytes` are the serializable sides of the buffered
79/// `Change`. The store recovers them on eviction. `record_ts` is the forwarded
80/// timestamp. The store builds the changelog VALUE once at `put` and drains it on
81/// evict, so it does not keep the `prior` and `ctx` that VALUE needs per entry.
82struct Entry {
83    key_bytes: Bytes,
84    new_bytes: Option<Bytes>,
85    old_bytes: Option<Bytes>,
86    record_ts: i64,
87}
88
89/// Per-entry `maxBytes` accounting unit: serialized key + serialized new value.
90fn entry_size(key_bytes: &Bytes, new_bytes: Option<&Bytes>) -> usize {
91    key_bytes.len() + new_bytes.map_or(0, Bytes::len)
92}
93
94/// Typed suppress store that holds the key serde and the value serde.
95///
96/// The task registers this store in its state-store registry, and the suppress
97/// processor looks it up there.
98pub struct SuppressBytesStore<K, V> {
99    name: String,
100    changelog_topic: String,
101    logging: bool,
102    key_serde: Box<dyn Serde<K>>,
103    value_serde: Box<dyn Serde<V>>,
104    /// Ordered by `(buffer_time, seq)`. `seq` separates equal buffer times.
105    entries: BTreeMap<(i64, u64), Entry>,
106    /// `key_bytes -> slot`, for replace-by-key.
107    index: HashMap<Bytes, (i64, u64)>,
108    seq: u64,
109    byte_size: usize,
110    changelog: Vec<(Bytes, Option<Bytes>)>,
111}
112
113impl<K: 'static, V: 'static> SuppressBytesStore<K, V> {
114    #[must_use]
115    pub(crate) fn new(
116        name: String,
117        key_serde: Box<dyn Serde<K>>,
118        value_serde: Box<dyn Serde<V>>,
119        changelog_topic: String,
120    ) -> Self {
121        Self {
122            name,
123            changelog_topic,
124            logging: true,
125            key_serde,
126            value_serde,
127            entries: BTreeMap::new(),
128            index: HashMap::new(),
129            seq: 0,
130            byte_size: 0,
131            changelog: Vec::new(),
132        }
133    }
134
135    /// The public ctor that the tests and the DSL use.
136    ///
137    /// Suppress has no pluggable backend, so `in_memory` and `new` have the same
138    /// body.
139    #[must_use]
140    pub fn in_memory(
141        name: String,
142        key_serde: Box<dyn Serde<K>>,
143        value_serde: Box<dyn Serde<V>>,
144        changelog_topic: String,
145    ) -> Self {
146        Self::new(name, key_serde, value_serde, changelog_topic)
147    }
148
149    /// Removes the current slot for a key, if there is one, and subtracts its
150    /// size.
151    ///
152    /// Returns the `new_bytes` of the removed entry. That is the value the store
153    /// buffered for the key before. It is the `prior` for the JVM `-2` alias rule.
154    fn remove_existing(&mut self, key_bytes: &Bytes) -> Option<Bytes> {
155        let slot = self.index.remove(key_bytes)?;
156        let entry = self.entries.remove(&slot).expect("indexed slot present");
157        self.byte_size -= entry_size(&entry.key_bytes, entry.new_bytes.as_ref());
158        entry.new_bytes
159    }
160
161    /// Inserts a fresh slot at `(buffer_time, seq)` and accounts its size.
162    fn insert_slot(&mut self, buffer_time: i64, entry: Entry) {
163        let slot = (buffer_time, self.seq);
164        self.seq += 1;
165        self.index.insert(entry.key_bytes.clone(), slot);
166        self.byte_size += entry_size(&entry.key_bytes, entry.new_bytes.as_ref());
167        self.entries.insert(slot, entry);
168    }
169
170    /// Pops one slot.
171    ///
172    /// This method drops the slot from `entries` and `index`, subtracts its size,
173    /// logs a tombstone if logging is on, and rebuilds the typed
174    /// `(K, Change<V>, record_ts)`.
175    fn pop_slot(&mut self, slot: (i64, u64)) -> (K, Change<V>, i64) {
176        let entry = self.entries.remove(&slot).expect("slot present");
177        self.index.remove(&entry.key_bytes);
178        self.byte_size -= entry_size(&entry.key_bytes, entry.new_bytes.as_ref());
179        if self.logging {
180            self.changelog.push((entry.key_bytes.clone(), None));
181        }
182        let key = self
183            .key_serde
184            .deserialize(&self.changelog_topic, &entry.key_bytes)
185            .expect("suppress key deserialize");
186        let change = Change {
187            old: entry.old_bytes.map(|b| {
188                self.value_serde
189                    .deserialize(&self.changelog_topic, &b)
190                    .expect("old value deserialize")
191            }),
192            new: entry.new_bytes.map(|b| {
193                self.value_serde
194                    .deserialize(&self.changelog_topic, &b)
195                    .expect("new value deserialize")
196            }),
197        };
198        (key, change, entry.record_ts)
199    }
200}
201
202#[async_trait]
203impl<K: 'static, V: 'static> StateStore for SuppressBytesStore<K, V> {
204    fn name(&self) -> &str {
205        &self.name
206    }
207    async fn flush(&mut self) {}
208    fn close(&mut self) {}
209    fn as_any_mut(&mut self) -> &mut dyn Any {
210        self
211    }
212    fn changelog_topic(&self) -> &str {
213        &self.changelog_topic
214    }
215    fn take_changelog(&mut self) -> Vec<(Bytes, Option<Bytes>)> {
216        std::mem::take(&mut self.changelog)
217    }
218    async fn apply_changelog(&mut self, key: Bytes, value: Option<Bytes>) {
219        // Restore is silent: re-build the buffer WITHOUT pushing to `self.changelog`.
220        match value {
221            Some(v) => {
222                let d = deserialize_buffer_change(&v);
223                // Replace-by-key: drop any prior slot for this key (the changelog
224                // KEY == the serialized key bytes) before inserting the fresh slot.
225                self.remove_existing(&key);
226                let entry = Entry {
227                    key_bytes: key,
228                    new_bytes: d.new.map(Bytes::from),
229                    old_bytes: d.old.map(Bytes::from),
230                    record_ts: d.ctx.timestamp,
231                };
232                self.insert_slot(d.buffer_time, entry);
233            }
234            None => {
235                self.remove_existing(&key);
236            }
237        }
238    }
239    fn set_logging(&mut self, on: bool) {
240        self.logging = on;
241    }
242    async fn clear(&mut self) {
243        // No pluggable backend: wipe the in-memory time-ordered buffer + index +
244        // accounting + the changelog buffer (re-restore replays the committed log).
245        self.entries.clear();
246        self.index.clear();
247        self.seq = 0;
248        self.byte_size = 0;
249        self.changelog.clear();
250    }
251    async fn snapshot(&mut self) -> Bytes {
252        // The buffer is in-process, so the payload carries the two accounting
253        // scalars as well as the entries. Each entry keeps its own `(buffer_time,
254        // seq)` slot, which is what orders eviction, and its value reuses the
255        // JVM-exact `BufferValue` codec. `prior` is not part of an entry, so the
256        // payload writes none and the reader gets `old` back verbatim.
257        let mut buffer = store_payload();
258        buffer.put_u64(self.seq);
259        buffer.put_u64(self.byte_size as u64);
260        buffer.put_u32(u32::try_from(self.entries.len()).expect("entry count fits u32"));
261        for ((buffer_time, seq), entry) in &self.entries {
262            buffer.put_i64(*buffer_time);
263            buffer.put_u64(*seq);
264            put_sized(&mut buffer, &entry.key_bytes);
265            let ctx = SuppressRecordCtx {
266                topic: self.changelog_topic.clone(),
267                partition: 0,
268                offset: 0,
269                timestamp: entry.record_ts,
270            };
271            let value = serialize_buffer_change(
272                &ctx,
273                None,
274                entry.old_bytes.as_deref(),
275                entry.new_bytes.as_deref(),
276                *buffer_time,
277            );
278            put_sized(&mut buffer, &value);
279        }
280        buffer.freeze()
281    }
282    async fn restore_snapshot(
283        &mut self,
284        data: Bytes,
285    ) -> Result<(), crate::error::StreamsClientError> {
286        let mut reader = SnapshotReader::new(&data);
287        reader.store_version()?;
288        let seq = reader.u64()?;
289        let byte_size = usize::try_from(reader.u64()?).map_err(|_error| {
290            crate::error::StreamsClientError::Snapshot(
291                "suppress buffer byte size does not fit this platform".to_owned(),
292            )
293        })?;
294        let count = reader.u32()?;
295        let mut entries = BTreeMap::new();
296        let mut index = HashMap::new();
297        for _ in 0..count {
298            let buffer_time = reader.i64()?;
299            let slot = (buffer_time, reader.u64()?);
300            let key_bytes = reader.sized()?;
301            let value = reader.sized()?;
302            let decoded = try_deserialize_buffer_change(&value).ok_or_else(|| {
303                crate::error::StreamsClientError::Snapshot(
304                    "malformed suppress buffer value in snapshot".to_owned(),
305                )
306            })?;
307            index.insert(key_bytes.clone(), slot);
308            entries.insert(
309                slot,
310                Entry {
311                    key_bytes,
312                    new_bytes: decoded.new.map(Bytes::from),
313                    old_bytes: decoded.old.map(Bytes::from),
314                    record_ts: decoded.ctx.timestamp,
315                },
316            );
317        }
318        reader.finish()?;
319        self.entries = entries;
320        self.index = index;
321        self.seq = seq;
322        self.byte_size = byte_size;
323        self.changelog.clear();
324        Ok(())
325    }
326}
327
328#[async_trait]
329impl<K: Send + Sync + 'static, V: Send + 'static> SuppressStore<K, V> for SuppressBytesStore<K, V> {
330    async fn put(&mut self, key: K, buffer_time: i64, change: Change<V>, ctx: SuppressRecordCtx) {
331        let kb = self.key_serde.serialize(&self.changelog_topic, &key);
332        let new_bytes = change
333            .new
334            .as_ref()
335            .map(|v| self.value_serde.serialize(&self.changelog_topic, v));
336        let old_bytes = change
337            .old
338            .as_ref()
339            .map(|v| self.value_serde.serialize(&self.changelog_topic, v));
340        // prior = the value previously buffered for this key; on first buffering
341        // there is none, so prior IS the incoming change's `old` (matching the JVM
342        // reference-identity rule where the first put's prior == oldValue, which
343        // makes the `-2` "old same as prior" changelog sentinel fire).
344        let existing_new = self.remove_existing(&kb);
345        let prior_bytes = existing_new.or_else(|| old_bytes.clone());
346
347        let entry = Entry {
348            key_bytes: kb.clone(),
349            new_bytes: new_bytes.clone(),
350            old_bytes: old_bytes.clone(),
351            record_ts: ctx.timestamp,
352        };
353        self.insert_slot(buffer_time, entry);
354
355        if self.logging {
356            let value = serialize_buffer_change(
357                &ctx,
358                prior_bytes.as_deref(),
359                old_bytes.as_deref(),
360                new_bytes.as_deref(),
361                buffer_time,
362            );
363            self.changelog.push((kb, Some(value)));
364        }
365    }
366
367    async fn evict_while(&mut self, threshold: i64) -> Vec<(K, Change<V>, i64)> {
368        let mut out = Vec::new();
369        while let Some((&slot, _)) = self.entries.iter().next() {
370            if slot.0 > threshold {
371                break;
372            }
373            out.push(self.pop_slot(slot));
374        }
375        out
376    }
377
378    async fn evict_oldest(&mut self) -> Option<(K, Change<V>, i64)> {
379        let (&slot, _) = self.entries.iter().next()?;
380        Some(self.pop_slot(slot))
381    }
382
383    fn len(&self) -> usize {
384        self.entries.len()
385    }
386
387    fn is_empty(&self) -> bool {
388        self.entries.is_empty()
389    }
390
391    fn byte_size(&self) -> ByteSize {
392        // The running total is kept as a `usize` so the incremental add/subtract
393        // stays exact; the dimension is put back on at the accessor.
394        ByteSize::from_bytes(self.byte_size.try_into().unwrap_or(u64::MAX))
395    }
396}
397
398#[cfg(test)]
399mod tests {
400    use assert2::check;
401
402    use super::*;
403    use crate::processor::serde::{I64Serde, StringSerde};
404
405    fn store() -> SuppressBytesStore<String, i64> {
406        SuppressBytesStore::<String, i64>::in_memory(
407            "sup".into(),
408            Box::new(StringSerde),
409            Box::new(I64Serde),
410            "app-sup-changelog".into(),
411        )
412    }
413
414    fn ctx(timestamp: i64) -> SuppressRecordCtx {
415        SuppressRecordCtx {
416            topic: "in".into(),
417            partition: 0,
418            offset: 0,
419            timestamp,
420        }
421    }
422
423    /// The suppress store does not override the cache/IQ `StateStore` defaults,
424    /// so it exercises the default trait-method bodies in `store::api`:
425    /// `as_iq` → None, `enable_cache_erased`/`is_cached_erased` → false,
426    /// `flush_cache_into` → no-op, and `take_changelog_ts` wraps `take_changelog`
427    /// with a `None` timestamp.
428    #[tokio::test]
429    async fn uses_statestore_defaults_for_cache_and_iq() {
430        use std::sync::{Arc, Mutex};
431
432        use crate::store::cache::named::NamedCache;
433        let mut s = store();
434
435        // Not interactively queryable, not cache-aware.
436        assert!(s.as_iq().is_none());
437        assert!(!s.is_cached_erased());
438        let cache = Arc::new(Mutex::new(NamedCache::new("sup".into())));
439        assert!(
440            !s.enable_cache_erased(cache),
441            "suppress store is not cache-aware"
442        );
443        assert!(
444            !s.is_cached_erased(),
445            "still not cached after the no-op enable"
446        );
447
448        // The default flush_cache_into forwards nothing even with a staged entry.
449        s.put("a".into(), 30, Change::update(None, 1), ctx(30))
450            .await;
451        let mut buffer = std::collections::VecDeque::new();
452        s.flush_cache_into(&mut buffer, &[0]).await;
453        assert!(buffer.is_empty(), "no record cache → no forwarded change");
454
455        // take_changelog_ts wraps each changelog entry with a None timestamp.
456        let cl_ts = s.take_changelog_ts();
457        assert_eq!(cl_ts.len(), 1);
458        assert!(cl_ts[0].2.is_none(), "default timestamp is None");
459        // The wrapped take drained the buffer.
460        assert!(s.take_changelog().is_empty());
461
462        // set_record_context default is a no-op (must not panic).
463        s.set_record_context(crate::processor::record::RecordContext {
464            topic: "t".into(),
465            partition: 0,
466            offset: 0,
467            timestamp: 0,
468        });
469    }
470
471    #[tokio::test]
472    async fn put_then_evict_while_in_buffer_time_order() {
473        let mut s = store();
474        s.put("a".into(), 30, Change::update(None, 1), ctx(30))
475            .await;
476        s.put("b".into(), 10, Change::update(None, 2), ctx(10))
477            .await;
478        check!(s.len() == 2);
479        check!(!s.is_empty());
480        check!(s.byte_size() > ByteSize::ZERO);
481        // threshold 30 drains both, earliest buffer_time first.
482        let out = s.evict_while(30).await;
483        check!(out.len() == 2);
484        check!(out[0].0 == "b" && out[0].1.new == Some(2) && out[0].2 == 10);
485        check!(out[1].0 == "a" && out[1].1.new == Some(1) && out[1].2 == 30);
486        check!(s.len() == 0);
487        check!(s.is_empty());
488        check!(s.byte_size() == ByteSize::ZERO);
489    }
490
491    #[tokio::test]
492    async fn replace_by_key_keeps_one_entry_with_latest_change() {
493        let mut s = store();
494        s.put("k".into(), 10, Change::update(None, 1), ctx(10))
495            .await;
496        s.put("k".into(), 10, Change::update(Some(1), 2), ctx(12))
497            .await;
498        check!(s.len() == 1);
499        let out = s.evict_while(10).await;
500        check!(out.len() == 1);
501        check!(out[0].1.new == Some(2));
502        check!(out[0].1.old == Some(1));
503    }
504
505    #[tokio::test]
506    async fn evict_oldest_pops_lowest_buffer_time_then_none() {
507        let mut s = store();
508        s.put("a".into(), 30, Change::update(None, 1), ctx(30))
509            .await;
510        s.put("b".into(), 10, Change::update(None, 2), ctx(10))
511            .await;
512        let first = s.evict_oldest().await.unwrap();
513        check!(first.0 == "b" && first.1.new == Some(2));
514        let second = s.evict_oldest().await.unwrap();
515        check!(second.0 == "a" && second.1.new == Some(1));
516        check!(s.evict_oldest().await.is_none());
517    }
518
519    #[tokio::test]
520    async fn changelog_records_puts_then_tombstones_on_evict() {
521        let mut s = store();
522        s.put("a".into(), 10, Change::update(None, 1), ctx(10))
523            .await;
524        s.put("b".into(), 20, Change::update(None, 2), ctx(20))
525            .await;
526        let cl = s.take_changelog();
527        check!(cl.len() == 2);
528        check!(cl.iter().all(|(_, v)| v.is_some()));
529        // evicting both logs a None tombstone keyed by the serialized key bytes.
530        let _ = s.evict_while(20).await;
531        let cl = s.take_changelog();
532        check!(cl.len() == 2);
533        check!(cl.iter().all(|(_, v)| v.is_none()));
534        check!(cl[0].0.as_ref() == b"a");
535        check!(cl[1].0.as_ref() == b"b");
536    }
537
538    #[tokio::test]
539    async fn apply_changelog_round_trips_then_tombstone_removes() {
540        // Produce a put changelog entry from a source store.
541        let mut src = store();
542        src.put("k".into(), 42, Change::update(Some(7), 9), ctx(42))
543            .await;
544        let cl = src.take_changelog();
545        check!(cl.len() == 1);
546        let (key, value) = cl.into_iter().next().unwrap();
547
548        // Restore into a FRESH store; apply_changelog must be silent.
549        let mut dst = store();
550        dst.apply_changelog(key.clone(), value).await;
551        check!(dst.len() == 1);
552        check!(dst.take_changelog().is_empty()); // restore re-emits nothing
553        let out = dst.evict_while(42).await;
554        check!(out.len() == 1);
555        check!(out[0].0 == "k");
556        check!(out[0].1.new == Some(9));
557        check!(out[0].1.old == Some(7));
558        check!(out[0].2 == 42);
559
560        // Re-buffer, then a None tombstone removes it.
561        dst.apply_changelog(
562            key.clone(),
563            Some(serialize_buffer_change(
564                &ctx(42),
565                None,
566                Some(&7i64.to_be_bytes()),
567                Some(&9i64.to_be_bytes()),
568                42,
569            )),
570        )
571        .await;
572        check!(dst.len() == 1);
573        dst.apply_changelog(key, None).await;
574        check!(dst.is_empty());
575    }
576
577    #[tokio::test]
578    async fn logging_off_suppresses_changelog() {
579        let mut s = store();
580        s.set_logging(false);
581        s.put("k".into(), 10, Change::update(None, 1), ctx(10))
582            .await;
583        check!(s.take_changelog().is_empty());
584        let _ = s.evict_while(10).await;
585        check!(s.take_changelog().is_empty());
586    }
587
588    /// Restart-restore at the golden's scenario.
589    ///
590    /// A windowed-key suppress buffer survives `take_changelog` → fresh store →
591    /// `apply_changelog`. The buffer has the `until_window_closes` shape and
592    /// `Windowed<String>` keys from `TimeWindowedSerde`. The restored buffered
593    /// windows still emit their final value when stream-time closes them. Task
594    /// restore drives exactly this drain and replay over the registered store.
595    #[tokio::test]
596    async fn windowed_buffer_restores_and_emits_on_close() {
597        use crate::dsl::windows::{TimeWindowedSerde, Window, Windowed};
598
599        fn win_store() -> SuppressBytesStore<Windowed<String>, i64> {
600            SuppressBytesStore::<Windowed<String>, i64>::in_memory(
601                "sup".into(),
602                Box::new(TimeWindowedSerde::new(StringSerde, millis(10))),
603                Box::new(I64Serde),
604                "app-sup-changelog".into(),
605            )
606        }
607        fn wk(key: &str, start: i64) -> Windowed<String> {
608            Windowed {
609                key: key.into(),
610                window: Window {
611                    start,
612                    end: start + 10,
613                },
614            }
615        }
616
617        // Source store buffers two windows' final values (buffer_time = window end).
618        let mut src = win_store();
619        src.put(wk("a", 0), 10, Change::update(None, 2), ctx(5))
620            .await;
621        src.put(wk("b", 20), 30, Change::update(None, 7), ctx(25))
622            .await;
623        let changelog = src.take_changelog();
624        check!(changelog.len() == 2);
625
626        // Restart: a FRESH store replays the changelog (silently — no re-emit).
627        let mut restored = win_store();
628        for (key, value) in changelog {
629            restored.apply_changelog(key, value).await;
630        }
631        check!(restored.len() == 2);
632        check!(restored.take_changelog().is_empty());
633
634        // Closing window [0,10) (threshold 10) emits a@[0,10)'s final value (2);
635        // b@[20,30) stays buffered until its own close.
636        let closed = restored.evict_while(10).await;
637        check!(closed.len() == 1);
638        check!(closed[0].0 == wk("a", 0));
639        check!(closed[0].1.new == Some(2));
640        check!(restored.len() == 1);
641        // Raising the threshold closes b@[20,30) too.
642        let rest = restored.evict_while(30).await;
643        check!(rest.len() == 1);
644        check!(rest[0].0 == wk("b", 20));
645        check!(rest[0].1.new == Some(7));
646    }
647}