Skip to main content

mz_persist_client/
read.rs

1// Copyright Materialize, Inc. and contributors. All rights reserved.
2//
3// Use of this software is governed by the Business Source License
4// included in the LICENSE file.
5//
6// As of the Change Date specified in that file, in accordance with
7// the Business Source License, use of this software will be governed
8// by the Apache License, Version 2.0.
9
10//! Read capabilities and handles
11
12use async_stream::stream;
13use std::collections::BTreeMap;
14use std::fmt::Debug;
15use std::future::Future;
16use std::sync::Arc;
17use std::time::{Duration, Instant};
18
19use differential_dataflow::Hashable;
20use differential_dataflow::consolidation::consolidate_updates;
21use differential_dataflow::difference::Monoid;
22use differential_dataflow::lattice::Lattice;
23use futures::Stream;
24use futures_util::{StreamExt, stream};
25use mz_dyncfg::{Config, ParameterScope};
26use mz_ore::cast::CastLossy;
27use mz_ore::halt;
28use mz_ore::instrument;
29use mz_ore::process::halt_with_code;
30use mz_ore::task::JoinHandle;
31use mz_persist::location::{Blob, SeqNo};
32use mz_persist_types::columnar::{ColumnDecoder, Schema};
33use mz_persist_types::{Codec, Codec64};
34use proptest_derive::Arbitrary;
35use serde::{Deserialize, Serialize};
36use timely::PartialOrder;
37use timely::order::TotalOrder;
38use timely::progress::{Antichain, Timestamp};
39use tracing::warn;
40use uuid::Uuid;
41
42use crate::batch::BLOB_TARGET_SIZE;
43use crate::cfg::{COMPACTION_MEMORY_BOUND_BYTES, RetryParameters};
44use crate::fetch::FetchConfig;
45use crate::fetch::{FetchBatchFilter, FetchedPart, Lease, LeasedBatchPart, fetch_leased_part};
46use crate::internal::encoding::Schemas;
47use crate::internal::machine::{Machine, next_listen_batch_retry_params};
48use crate::internal::metrics::{Metrics, ReadMetrics, ShardMetrics};
49use crate::internal::state::{HollowBatch, LeasedReaderState, SnapshotErr};
50use crate::internal::watch::{AwaitableState, StateWatch};
51use crate::iter::{Consolidator, StructuredSort};
52use crate::schema::SchemaCache;
53use crate::stats::{SnapshotPartStats, SnapshotPartsStats, SnapshotStats};
54use crate::{GarbageCollector, PersistConfig, ShardId, parse_id};
55
56pub use crate::internal::encoding::LazyPartStats;
57pub use crate::internal::state::Since;
58
59/// An opaque identifier for a reader of a persist durable TVC (aka shard).
60#[derive(
61    Arbitrary,
62    Clone,
63    PartialEq,
64    Eq,
65    PartialOrd,
66    Ord,
67    Hash,
68    Serialize,
69    Deserialize
70)]
71#[serde(try_from = "String", into = "String")]
72pub struct LeasedReaderId(pub(crate) [u8; 16]);
73
74impl std::fmt::Display for LeasedReaderId {
75    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
76        write!(f, "r{}", Uuid::from_bytes(self.0))
77    }
78}
79
80impl std::fmt::Debug for LeasedReaderId {
81    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
82        write!(f, "LeasedReaderId({})", Uuid::from_bytes(self.0))
83    }
84}
85
86impl std::str::FromStr for LeasedReaderId {
87    type Err = String;
88
89    fn from_str(s: &str) -> Result<Self, Self::Err> {
90        parse_id("r", "LeasedReaderId", s).map(LeasedReaderId)
91    }
92}
93
94impl From<LeasedReaderId> for String {
95    fn from(reader_id: LeasedReaderId) -> Self {
96        reader_id.to_string()
97    }
98}
99
100impl TryFrom<String> for LeasedReaderId {
101    type Error = String;
102
103    fn try_from(s: String) -> Result<Self, Self::Error> {
104        s.parse()
105    }
106}
107
108impl LeasedReaderId {
109    pub(crate) fn new() -> Self {
110        LeasedReaderId(*Uuid::new_v4().as_bytes())
111    }
112}
113
114/// Capable of generating a snapshot of all data at `as_of`, followed by a
115/// listen of all updates.
116///
117/// For more details, see [`ReadHandle::snapshot`] and [`Listen`].
118#[derive(Debug)]
119pub struct Subscribe<K: Codec, V: Codec, T, D> {
120    snapshot: Option<Vec<LeasedBatchPart<T>>>,
121    listen: Listen<K, V, T, D>,
122}
123
124impl<K, V, T, D> Subscribe<K, V, T, D>
125where
126    K: Debug + Codec,
127    V: Debug + Codec,
128    T: Timestamp + TotalOrder + Lattice + Codec64 + Sync,
129    D: Monoid + Codec64 + Send + Sync,
130{
131    fn new(snapshot_parts: Vec<LeasedBatchPart<T>>, listen: Listen<K, V, T, D>) -> Self {
132        Subscribe {
133            snapshot: Some(snapshot_parts),
134            listen,
135        }
136    }
137
138    /// Returns a `LeasedBatchPart` enriched with the proper metadata.
139    ///
140    /// First returns snapshot parts, until they're exhausted, at which point
141    /// begins returning listen parts.
142    ///
143    /// The returned `Antichain` represents the subscription progress as it will
144    /// be _after_ the returned parts are fetched.
145    #[instrument(level = "debug", fields(shard = %self.listen.handle.machine.shard_id()))]
146    pub async fn next(
147        &mut self,
148        // If Some, an override for the default listen sleep retry parameters.
149        listen_retry: Option<RetryParameters>,
150    ) -> Vec<ListenEvent<T, LeasedBatchPart<T>>> {
151        match self.snapshot.take() {
152            Some(parts) => vec![ListenEvent::Updates(parts)],
153            None => {
154                let (parts, upper) = self.listen.next(listen_retry).await;
155                vec![ListenEvent::Updates(parts), ListenEvent::Progress(upper)]
156            }
157        }
158    }
159}
160
161impl<K, V, T, D> Subscribe<K, V, T, D>
162where
163    K: Debug + Codec,
164    V: Debug + Codec,
165    T: Timestamp + TotalOrder + Lattice + Codec64 + Sync,
166    D: Monoid + Codec64 + Send + Sync,
167{
168    /// Equivalent to `next`, but rather than returning a [`LeasedBatchPart`],
169    /// fetches and returns the data from within it.
170    #[instrument(level = "debug", fields(shard = %self.listen.handle.machine.shard_id()))]
171    pub async fn fetch_next(&mut self) -> Vec<ListenEvent<T, ((K, V), T, D)>> {
172        let events = self.next(None).await;
173        let new_len = events
174            .iter()
175            .map(|event| match event {
176                ListenEvent::Updates(parts) => parts.len(),
177                ListenEvent::Progress(_) => 1,
178            })
179            .sum();
180        let mut ret = Vec::with_capacity(new_len);
181        for event in events {
182            match event {
183                ListenEvent::Updates(parts) => {
184                    for part in parts {
185                        let fetched_part = self.listen.fetch_batch_part(part).await;
186                        let updates = fetched_part.collect::<Vec<_>>();
187                        if !updates.is_empty() {
188                            ret.push(ListenEvent::Updates(updates));
189                        }
190                    }
191                }
192                ListenEvent::Progress(progress) => ret.push(ListenEvent::Progress(progress)),
193            }
194        }
195        ret
196    }
197
198    /// Fetches the contents of `part` and returns its lease.
199    pub async fn fetch_batch_part(&mut self, part: LeasedBatchPart<T>) -> FetchedPart<K, V, T, D> {
200        self.listen.fetch_batch_part(part).await
201    }
202}
203
204impl<K, V, T, D> Subscribe<K, V, T, D>
205where
206    K: Debug + Codec,
207    V: Debug + Codec,
208    T: Timestamp + TotalOrder + Lattice + Codec64 + Sync,
209    D: Monoid + Codec64 + Send + Sync,
210{
211    /// Politely expires this subscribe, releasing its lease.
212    ///
213    /// There is a best-effort impl in Drop to expire the
214    /// [`ReadHandle`] held by the subscribe that wasn't explicitly expired
215    /// with this method. When possible, explicit expiry is still preferred
216    /// because it also ensures that the background task is complete.
217    pub async fn expire(mut self) {
218        let _ = self.snapshot.take(); // Drop all leased parts.
219        self.listen.expire().await;
220    }
221}
222
223/// Data and progress events of a shard subscription.
224///
225/// TODO: Unify this with [timely::dataflow::operators::capture::event::Event].
226#[derive(Debug, PartialEq)]
227pub enum ListenEvent<T, D> {
228    /// Progress of the shard.
229    Progress(Antichain<T>),
230    /// Data of the shard.
231    Updates(Vec<D>),
232}
233
234/// An ongoing subscription of updates to a shard.
235#[derive(Debug)]
236pub struct Listen<K: Codec, V: Codec, T, D> {
237    handle: ReadHandle<K, V, T, D>,
238    as_of: Antichain<T>,
239    since: Antichain<T>,
240    frontier: Antichain<T>,
241}
242
243impl<K, V, T, D> Listen<K, V, T, D>
244where
245    K: Debug + Codec,
246    V: Debug + Codec,
247    T: Timestamp + TotalOrder + Lattice + Codec64 + Sync,
248    D: Monoid + Codec64 + Send + Sync,
249{
250    async fn new(
251        mut handle: ReadHandle<K, V, T, D>,
252        as_of: Antichain<T>,
253    ) -> Result<Self, Since<T>> {
254        let () = handle.machine.verify_listen(&as_of)?;
255
256        let since = as_of.clone();
257        if !PartialOrder::less_equal(handle.since(), &since) {
258            // We can't guarantee that the as-of will be available by the time we start reading,
259            // since our handle is already downgraded too far.
260            return Err(Since(handle.since().clone()));
261        }
262        // This listen only needs to distinguish things after its frontier
263        // (initially as_of although the frontier is inclusive and the as_of
264        // isn't). Be a good citizen and downgrade early.
265        handle.downgrade_since(&since).await;
266        Ok(Listen {
267            handle,
268            since,
269            frontier: as_of.clone(),
270            as_of,
271        })
272    }
273
274    /// An exclusive upper bound on the progress of this Listen.
275    pub fn frontier(&self) -> &Antichain<T> {
276        &self.frontier
277    }
278
279    /// Attempt to pull out the next values of this subscription.
280    ///
281    /// The returned [`LeasedBatchPart`] is appropriate to use with
282    /// `crate::fetch::fetch_leased_part`.
283    ///
284    /// The returned `Antichain` represents the subscription progress as it will
285    /// be _after_ the returned parts are fetched.
286    pub async fn next(
287        &mut self,
288        // If Some, an override for the default listen sleep retry parameters.
289        retry: Option<RetryParameters>,
290    ) -> (Vec<LeasedBatchPart<T>>, Antichain<T>) {
291        // Wait until the upper is past our frontier - ie. there is another batch for us to process.
292        let retry = retry
293            .unwrap_or_else(|| next_listen_batch_retry_params(&self.handle.machine.applier.cfg));
294        self.handle
295            .machine
296            .wait_for_upper_past(
297                &self.frontier,
298                &mut self.handle.watch,
299                Some(&self.handle.reader_id),
300                &self.handle.metrics.retries.next_listen_batch,
301                retry,
302            )
303            .await;
304
305        // Obtain a lease before grabbing the upcoming batch from state.
306        let lease = self.handle.lease_seqno().await;
307        let batch = match self
308            .handle
309            .machine
310            .applier
311            .next_listen_batch(&self.frontier)
312        {
313            Ok(batch) => batch,
314            Err(seqno) => {
315                panic!(
316                    "waited for upper past {frontier:?}, but no listen batch was available at {seqno:?}!",
317                    frontier = self.frontier.elements()
318                );
319            }
320        };
321
322        // A lot of things across mz have to line up to hold the following
323        // invariant and violations only show up as subtle correctness errors,
324        // so explicitly validate it here. Better to panic and roll back a
325        // release than be incorrect (also potentially corrupting a sink).
326        //
327        // Note that the since check is intentionally less_than, not less_equal.
328        // If a batch's since is X, that means we can no longer distinguish X
329        // (beyond self.frontier) from X-1 (not beyond self.frontier) to keep
330        // former and filter out the latter.
331        let acceptable_desc = PartialOrder::less_than(batch.desc.since(), &self.frontier)
332            // Special case when the frontier == the as_of (i.e. the first
333            // time this is called on a new Listen). Because as_of is
334            // _exclusive_, we don't need to be able to distinguish X from
335            // X-1.
336            || (self.frontier == self.as_of
337            && PartialOrder::less_equal(batch.desc.since(), &self.frontier));
338        if !acceptable_desc {
339            let lease_state = self
340                .handle
341                .machine
342                .applier
343                .reader_lease(self.handle.reader_id.clone());
344            if let Some(lease) = lease_state {
345                panic!(
346                    "Listen on {} received a batch {:?} advanced past the listen frontier {:?}, but the lease has not expired: {:?}",
347                    self.handle.machine.shard_id(),
348                    batch.desc,
349                    self.frontier,
350                    lease
351                )
352            } else if self.handle.machine.applier.is_tombstone() {
353                // Finalizing the shard removes every reader, which is not a lost lease.
354                halt!(
355                    "Listen on {} received a batch {:?} advanced past the listen frontier {:?} after the shard was tombstoned",
356                    self.handle.machine.shard_id(),
357                    batch.desc,
358                    self.frontier
359                )
360            } else {
361                // Ideally we'd percolate this error up, so callers could eg. restart a dataflow
362                // instead of restarting a process...
363                halt_with_code(
364                    READER_LEASE_EXPIRED_EXIT_CODE,
365                    format_args!(
366                        "Listen on {} received a batch {:?} advanced past the listen frontier {:?} after the reader has expired. \
367                     This can happen in exceptional cases: a machine goes to sleep or is running out of memory or CPU, for example.",
368                        self.handle.machine.shard_id(),
369                        batch.desc,
370                        self.frontier
371                    ),
372                )
373            }
374        }
375
376        let new_frontier = batch.desc.upper().clone();
377
378        // We will have a new frontier, so this is an opportunity to downgrade our
379        // since capability. Go through `maybe_heartbeat` so we can rate limit
380        // this along with our heartbeats.
381        //
382        // HACK! Everything would be simpler if we could downgrade since to the
383        // new frontier, but we can't. The next call needs to be able to
384        // distinguish between the times T at the frontier (to emit updates with
385        // these times) and T-1 (to filter them). Advancing the since to
386        // frontier would erase the ability to distinguish between them. Ideally
387        // we'd use what is conceptually "batch.upper - 1" (the greatest
388        // elements that are still strictly less than batch.upper, which will be
389        // the new value of self.frontier after this call returns), but the
390        // trait bounds on T don't give us a way to compute that directly.
391        // Instead, we sniff out any elements in self.frontier (the upper of the
392        // batch the last time we called this) that are strictly less_than the
393        // batch upper to compute a new since. For totally ordered times
394        // (currently always the case in mz) self.frontier will always have a
395        // single element and it will be less_than upper. We
396        // could also abuse the fact that every time we actually emit is
397        // guaranteed by definition to be less_than upper to be a bit more
398        // prompt, but this would involve a lot more temporary antichains and
399        // it's unclear if that's worth it.
400        for x in self.frontier.elements().iter() {
401            let less_than_upper = batch.desc.upper().elements().iter().any(|u| x.less_than(u));
402            if less_than_upper {
403                self.since.join_assign(&Antichain::from_elem(x.clone()));
404            }
405        }
406
407        // IMPORTANT! Make sure this `lease_batch_parts` stays before the
408        // `maybe_downgrade_since` call. Otherwise, we might give up our
409        // capability on the batch's SeqNo before we lease it, which could lead
410        // to blobs that it references being GC'd.
411        let filter = FetchBatchFilter::Listen {
412            as_of: self.as_of.clone(),
413            lower: self.frontier.clone(),
414        };
415        let parts = self
416            .handle
417            .lease_batch_parts(lease, batch, filter)
418            .collect()
419            .await;
420
421        self.handle.maybe_downgrade_since(&self.since).await;
422
423        // NB: Keep this after we use self.frontier to join_assign self.since
424        // and also after we construct metadata.
425        self.frontier = new_frontier;
426
427        (parts, self.frontier.clone())
428    }
429}
430
431impl<K, V, T, D> Listen<K, V, T, D>
432where
433    K: Debug + Codec,
434    V: Debug + Codec,
435    T: Timestamp + TotalOrder + Lattice + Codec64 + Sync,
436    D: Monoid + Codec64 + Send + Sync,
437{
438    /// Attempt to pull out the next values of this subscription.
439    ///
440    /// The updates received in [ListenEvent::Updates] should be assumed to be in arbitrary order
441    /// and not necessarily consolidated. However, the timestamp of each individual update will be
442    /// greater than or equal to the last received [ListenEvent::Progress] frontier (or this
443    /// [Listen]'s initial `as_of` frontier if no progress event has been emitted yet) and less
444    /// than the next [ListenEvent::Progress] frontier.
445    ///
446    /// If you have a use for consolidated listen output, given that snapshots can't be
447    /// consolidated, come talk to us!
448    #[instrument(level = "debug", name = "listen::next", fields(shard = %self.handle.machine.shard_id()))]
449    pub async fn fetch_next(&mut self) -> Vec<ListenEvent<T, ((K, V), T, D)>> {
450        let (parts, progress) = self.next(None).await;
451        let mut ret = Vec::with_capacity(parts.len() + 1);
452        for part in parts {
453            let fetched_part = self.fetch_batch_part(part).await;
454            let updates = fetched_part.collect::<Vec<_>>();
455            if !updates.is_empty() {
456                ret.push(ListenEvent::Updates(updates));
457            }
458        }
459        ret.push(ListenEvent::Progress(progress));
460        ret
461    }
462
463    /// Convert listener into futures::Stream
464    pub fn into_stream(mut self) -> impl Stream<Item = ListenEvent<T, ((K, V), T, D)>> {
465        async_stream::stream!({
466            loop {
467                for msg in self.fetch_next().await {
468                    yield msg;
469                }
470            }
471        })
472    }
473
474    /// Test helper to read from the listener until the given frontier is
475    /// reached. Because compaction can arbitrarily combine batches, we only
476    /// return the final progress info.
477    #[cfg(test)]
478    #[track_caller]
479    pub async fn read_until(&mut self, ts: &T) -> (Vec<((K, V), T, D)>, Antichain<T>) {
480        let mut updates = Vec::new();
481        let mut frontier = Antichain::from_elem(T::minimum());
482        while self.frontier.less_than(ts) {
483            for event in self.fetch_next().await {
484                match event {
485                    ListenEvent::Updates(mut x) => updates.append(&mut x),
486                    ListenEvent::Progress(x) => frontier = x,
487                }
488            }
489        }
490        // Unlike most tests, intentionally don't consolidate updates here
491        // because Listen replays them at the original fidelity.
492        (updates, frontier)
493    }
494}
495
496impl<K, V, T, D> Listen<K, V, T, D>
497where
498    K: Debug + Codec,
499    V: Debug + Codec,
500    T: Timestamp + TotalOrder + Lattice + Codec64 + Sync,
501    D: Monoid + Codec64 + Send + Sync,
502{
503    /// Fetches the contents of `part` and returns its lease.
504    ///
505    /// This is broken out into its own function to provide a trivial means for
506    /// [`Subscribe`], which contains a [`Listen`], to fetch batches.
507    async fn fetch_batch_part(&mut self, part: LeasedBatchPart<T>) -> FetchedPart<K, V, T, D> {
508        let fetched_part = fetch_leased_part(
509            &self.handle.cfg,
510            &part,
511            self.handle.blob.as_ref(),
512            Arc::clone(&self.handle.metrics),
513            &self.handle.metrics.read.listen,
514            &self.handle.machine.applier.shard_metrics,
515            &self.handle.reader_id,
516            self.handle.read_schemas.clone(),
517            &mut self.handle.schema_cache,
518        )
519        .await;
520        fetched_part
521    }
522
523    /// Politely expires this listen, releasing its lease.
524    ///
525    /// There is a best-effort impl in to expire the
526    /// [`ReadHandle`] held by the listen that wasn't explicitly expired with
527    /// this method. When possible, explicit expiry is still preferred because
528    /// it also ensures that the background task is complete.
529    pub async fn expire(self) {
530        self.handle.expire().await
531    }
532}
533
534/// The state for read holds shared between the heartbeat task and the main client,
535/// including the since frontier and the seqno hold.
536#[derive(Debug)]
537pub(crate) struct ReadHolds<T> {
538    /// The frontier we should hold the time-based lease back to.
539    held_since: Antichain<T>,
540    /// The since hold we've actually committed to state. Should always be <=
541    /// the since hold.
542    applied_since: Antichain<T>,
543    /// The largest seqno we've observed in the state.
544    recent_seqno: SeqNo,
545    /// The set of active leases. We hold back the seqno to the minimum lease or
546    /// the recent_seqno, whichever is earlier.
547    leases: BTreeMap<SeqNo, Lease>,
548    /// True iff this state is expired.
549    expired: bool,
550    /// Used to trigger the background task to heartbeat state, instead of waiting for
551    /// the next tick.
552    request_sync: bool,
553}
554
555impl<T> ReadHolds<T>
556where
557    T: Timestamp + TotalOrder + Lattice + Codec64 + Sync,
558{
559    pub fn downgrade_since(&mut self, since: &Antichain<T>) {
560        self.held_since.join_assign(since);
561    }
562
563    pub fn observe_seqno(&mut self, seqno: SeqNo) {
564        self.recent_seqno = seqno.max(self.recent_seqno);
565    }
566
567    pub fn lease_seqno(&mut self) -> Lease {
568        let seqno = self.recent_seqno;
569        let lease = self
570            .leases
571            .entry(seqno)
572            .or_insert_with(|| Lease::new(seqno));
573        lease.clone()
574    }
575
576    pub fn outstanding_seqno(&mut self) -> SeqNo {
577        while let Some(first) = self.leases.first_entry() {
578            if first.get().count() <= 1 {
579                first.remove();
580            } else {
581                return *first.key();
582            }
583        }
584        self.recent_seqno
585    }
586}
587
588/// A "capability" granting the ability to read the state of some shard at times
589/// greater or equal to `self.since()`.
590///
591/// Production users should call [Self::expire] before dropping a ReadHandle so
592/// that it can expire its leases. If/when rust gets AsyncDrop, this will be
593/// done automatically.
594///
595/// All async methods on ReadHandle retry for as long as they are able, but the
596/// returned [std::future::Future]s implement "cancel on drop" semantics. This
597/// means that callers can add a timeout using [tokio::time::timeout] or
598/// [tokio::time::timeout_at].
599///
600/// ```rust,no_run
601/// # let mut read: mz_persist_client::read::ReadHandle<String, String, u64, i64> = unimplemented!();
602/// # let timeout: std::time::Duration = unimplemented!();
603/// # let new_since: timely::progress::Antichain<u64> = unimplemented!();
604/// # async {
605/// tokio::time::timeout(timeout, read.downgrade_since(&new_since)).await
606/// # };
607/// ```
608#[derive(Debug)]
609pub struct ReadHandle<K: Codec, V: Codec, T, D> {
610    pub(crate) cfg: PersistConfig,
611    pub(crate) metrics: Arc<Metrics>,
612    pub(crate) machine: Machine<K, V, T, D>,
613    pub(crate) gc: GarbageCollector<K, V, T, D>,
614    pub(crate) blob: Arc<dyn Blob>,
615    watch: StateWatch<K, V, T, D>,
616
617    pub(crate) reader_id: LeasedReaderId,
618    pub(crate) read_schemas: Schemas<K, V>,
619    pub(crate) schema_cache: SchemaCache<K, V, T, D>,
620
621    since: Antichain<T>,
622    pub(crate) hold_state: AwaitableState<ReadHolds<T>>,
623    pub(crate) unexpired_state: Option<UnexpiredReadHandleState>,
624}
625
626/// The exit code of a process halted because a live reader lost its lease.
627///
628/// Distinct from [`halt!`]'s 166 so alerting can tell a lost lease from a
629/// routine restart; cloud alerting matches on it. Under Kubernetes the pod is
630/// restarted (with crash backoff), as for any non-zero exit.
631pub const READER_LEASE_EXPIRED_EXIT_CODE: i32 = 168;
632
633/// Length of time after a reader's last operation after which the reader may be
634/// expired.
635pub(crate) const READER_LEASE_DURATION: Config<Duration> = Config::new(
636    "persist_reader_lease_duration",
637    Duration::from_secs(60 * 15),
638    "The time after which we'll clean up stale read leases",
639    ParameterScope::Environment,
640);
641
642impl<K, V, T, D> ReadHandle<K, V, T, D>
643where
644    K: Debug + Codec,
645    V: Debug + Codec,
646    T: Timestamp + TotalOrder + Lattice + Codec64 + Sync,
647    D: Monoid + Codec64 + Send + Sync,
648{
649    #[allow(clippy::unused_async)]
650    pub(crate) async fn new(
651        cfg: PersistConfig,
652        metrics: Arc<Metrics>,
653        machine: Machine<K, V, T, D>,
654        gc: GarbageCollector<K, V, T, D>,
655        blob: Arc<dyn Blob>,
656        reader_id: LeasedReaderId,
657        read_schemas: Schemas<K, V>,
658        state: LeasedReaderState<T>,
659    ) -> Self {
660        let schema_cache = machine.applier.schema_cache();
661        let hold_state = AwaitableState::new(ReadHolds {
662            held_since: state.since.clone(),
663            applied_since: state.since.clone(),
664            recent_seqno: state.seqno,
665            leases: Default::default(),
666            expired: false,
667            request_sync: false,
668        });
669        ReadHandle {
670            cfg,
671            metrics: Arc::clone(&metrics),
672            machine: machine.clone(),
673            gc: gc.clone(),
674            blob,
675            watch: machine.applier.watch(),
676            reader_id: reader_id.clone(),
677            read_schemas,
678            schema_cache,
679            since: state.since,
680            hold_state: hold_state.clone(),
681            unexpired_state: Some(UnexpiredReadHandleState {
682                heartbeat_task: Self::start_reader_heartbeat_task(
683                    machine, reader_id, gc, hold_state,
684                ),
685            }),
686        }
687    }
688
689    fn start_reader_heartbeat_task(
690        machine: Machine<K, V, T, D>,
691        reader_id: LeasedReaderId,
692        gc: GarbageCollector<K, V, T, D>,
693        leased_seqnos: AwaitableState<ReadHolds<T>>,
694    ) -> JoinHandle<()> {
695        let metrics = Arc::clone(&machine.applier.metrics);
696        let name = format!(
697            "persist::heartbeat_read({},{})",
698            machine.shard_id(),
699            reader_id
700        );
701        mz_ore::task::spawn(|| name, {
702            metrics.tasks.heartbeat_read.instrument_task(async move {
703                Self::reader_heartbeat_task(machine, reader_id, gc, leased_seqnos).await
704            })
705        })
706    }
707
708    async fn reader_heartbeat_task(
709        machine: Machine<K, V, T, D>,
710        reader_id: LeasedReaderId,
711        gc: GarbageCollector<K, V, T, D>,
712        leased_seqnos: AwaitableState<ReadHolds<T>>,
713    ) {
714        let sleep_duration = READER_LEASE_DURATION.get(&machine.applier.cfg) / 4;
715        // Jitter the first tick to avoid a thundering herd when many readers are started around
716        // the same instant, like during deploys.
717        let jitter: f64 = f64::cast_lossy(reader_id.hashed()) / f64::cast_lossy(u64::MAX);
718        let mut interval = tokio::time::interval_at(
719            tokio::time::Instant::now() + sleep_duration.mul_f64(jitter),
720            sleep_duration,
721        );
722        let mut held_since = leased_seqnos.read(|s| s.held_since.clone());
723        loop {
724            let before_sleep = Instant::now();
725            let _woke_by_tick = tokio::select! {
726                _tick = interval.tick() => {
727                    true
728                }
729                _whatever = leased_seqnos.wait_while(|s| !s.request_sync) => {
730                    false
731                }
732            };
733
734            let elapsed_since_before_sleeping = before_sleep.elapsed();
735            if elapsed_since_before_sleeping > sleep_duration + Duration::from_secs(60) {
736                warn!(
737                    "reader ({}) of shard ({}) went {}s between heartbeats",
738                    reader_id,
739                    machine.shard_id(),
740                    elapsed_since_before_sleeping.as_secs_f64()
741                );
742            }
743
744            let before_heartbeat = Instant::now();
745            let current_seqno = machine.seqno();
746            let result = leased_seqnos.modify(|s| {
747                if s.expired {
748                    Err(())
749                } else {
750                    s.observe_seqno(current_seqno);
751                    s.request_sync = false;
752                    held_since.join_assign(&s.held_since);
753                    Ok(s.outstanding_seqno())
754                }
755            });
756            let actual_since = match result {
757                Ok(held_seqno) => {
758                    let (seqno, actual_since, maintenance) = machine
759                        .downgrade_since(&reader_id, held_seqno, &held_since)
760                        .await;
761                    leased_seqnos.modify(|s| {
762                        s.applied_since.clone_from(&actual_since.0);
763                        s.observe_seqno(seqno)
764                    });
765                    maintenance.start_performing(&machine, &gc);
766                    actual_since
767                }
768                Err(()) => {
769                    let (seqno, maintenance) = machine.expire_leased_reader(&reader_id).await;
770                    leased_seqnos.modify(|s| s.observe_seqno(seqno));
771                    maintenance.start_performing(&machine, &gc);
772                    break;
773                }
774            };
775
776            let elapsed_since_heartbeat = before_heartbeat.elapsed();
777            if elapsed_since_heartbeat > Duration::from_secs(60) {
778                warn!(
779                    "reader ({}) of shard ({}) heartbeat call took {}s",
780                    reader_id,
781                    machine.shard_id(),
782                    elapsed_since_heartbeat.as_secs_f64(),
783                );
784            }
785
786            if PartialOrder::less_than(&held_since, &actual_since.0) {
787                // If the read handle was intentionally expired, this task
788                // *should* be aborted before it observes the expiration. So if
789                // we get here, this task somehow failed to keep the read lease
790                // alive. Warn loudly, because there's now a live read handle to
791                // an expired shard that will panic if used, but don't panic,
792                // just in case there is some edge case that results in this
793                // task observing the intentional expiration of a read handle.
794                warn!(
795                    "heartbeat task for reader ({}) of shard ({}) exiting due to expired lease \
796                     while read handle is live",
797                    reader_id,
798                    machine.shard_id(),
799                );
800                return;
801            }
802        }
803    }
804
805    /// This handle's shard id.
806    pub fn shard_id(&self) -> ShardId {
807        self.machine.shard_id()
808    }
809
810    /// This handle's `since` frontier.
811    ///
812    /// This will always be greater or equal to the shard-global `since`.
813    pub fn since(&self) -> &Antichain<T> {
814        &self.since
815    }
816
817    /// A less-stale cached version of the shard-global `upper` frontier.
818    ///
819    /// This is the most recently known upper for this shard process-wide. It
820    /// requires a mutex and a clone. This will always be less or equal to the
821    /// shard-global `upper`.
822    pub fn shared_upper(&self) -> Antichain<T> {
823        self.machine.applier.clone_upper()
824    }
825
826    #[cfg(test)]
827    fn outstanding_seqno(&self) -> SeqNo {
828        let current_seqno = self.machine.seqno();
829        self.hold_state.modify(|s| {
830            s.observe_seqno(current_seqno);
831            s.outstanding_seqno()
832        })
833    }
834
835    /// Forwards the since frontier of this handle, giving up the ability to
836    /// read at times not greater or equal to `new_since`.
837    ///
838    /// This may trigger (asynchronous) compaction and consolidation in the
839    /// system. A `new_since` of the empty antichain "finishes" this shard,
840    /// promising that no more data will ever be read by this handle.
841    #[instrument(level = "debug", fields(shard = %self.machine.shard_id()))]
842    pub async fn downgrade_since(&mut self, new_since: &Antichain<T>) {
843        self.since = new_since.clone();
844        self.hold_state.modify(|s| {
845            s.downgrade_since(new_since);
846            s.request_sync = true;
847        });
848        self.hold_state
849            .wait_while(|s| PartialOrder::less_than(&s.applied_since, new_since))
850            .await;
851    }
852
853    /// Returns an ongoing subscription of updates to a shard.
854    ///
855    /// The stream includes all data at times greater than `as_of`. Combined
856    /// with [Self::snapshot] it will produce exactly correct results: the
857    /// snapshot is the TVCs contents at `as_of` and all subsequent updates
858    /// occur at exactly their indicated time. The recipient should only
859    /// downgrade their read capability when they are certain they have all data
860    /// through the frontier they would downgrade to.
861    ///
862    /// This takes ownership of the ReadHandle so the Listen can use it to
863    /// [Self::downgrade_since] as it progresses. If you need to keep this
864    /// handle, then [Self::clone] it before calling listen.
865    ///
866    /// The `Since` error indicates that the requested `as_of` cannot be served
867    /// (the caller has out of date information) and includes the smallest
868    /// `as_of` that would have been accepted.
869    #[instrument(level = "debug", fields(shard = %self.machine.shard_id()))]
870    pub async fn listen(self, as_of: Antichain<T>) -> Result<Listen<K, V, T, D>, Since<T>> {
871        Listen::new(self, as_of).await
872    }
873
874    async fn snapshot_batches(
875        &mut self,
876        as_of: Antichain<T>,
877    ) -> Result<(Lease, Vec<HollowBatch<T>>), Since<T>> {
878        self.machine
879            .wait_for_upper_past(
880                &as_of,
881                &mut self.watch,
882                Some(&self.reader_id),
883                &self.metrics.retries.snapshot,
884                RetryParameters::persist_defaults(),
885            )
886            .await;
887        let lease = self.lease_seqno().await;
888        let batches = match self.machine.applier.snapshot(&as_of) {
889            Ok(data) => data,
890            Err(SnapshotErr::AsOfHistoricalDistinctionsLost(since)) => return Err(since),
891            Err(SnapshotErr::AsOfNotYetAvailable(seqno, upper)) => {
892                panic!(
893                    "waited for upper past {as_of:?}, but at latest seqno {seqno:?} the frontier was only {upper:?}",
894                    as_of = as_of.elements(),
895                    upper = upper.0.elements(),
896                )
897            }
898        };
899        Ok((lease, batches))
900    }
901
902    /// Returns all of the contents of the shard TVC at `as_of` broken up into
903    /// [`LeasedBatchPart`]es. These parts can be "turned in" via
904    /// `crate::fetch::fetch_batch_part` to receive the data they contain.
905    ///
906    /// This command returns the contents of this shard as of `as_of` once they
907    /// are known. This may "block" (in an async-friendly way) if `as_of` is
908    /// greater or equal to the current `upper` of the shard. The recipient
909    /// should only downgrade their read capability when they are certain they
910    /// have all data through the frontier they would downgrade to.
911    ///
912    /// The `Since` error indicates that the requested `as_of` cannot be served
913    /// (the caller has out of date information) and includes the smallest
914    /// `as_of` that would have been accepted.
915    #[instrument(level = "trace", fields(shard = %self.machine.shard_id()))]
916    pub async fn snapshot(
917        &mut self,
918        as_of: Antichain<T>,
919    ) -> Result<Vec<LeasedBatchPart<T>>, Since<T>> {
920        let (lease, batches) = self.snapshot_batches(as_of.clone()).await?;
921
922        if !PartialOrder::less_equal(self.since(), &as_of) {
923            return Err(Since(self.since().clone()));
924        }
925
926        let filter = FetchBatchFilter::Snapshot { as_of };
927        let mut leased_parts = Vec::new();
928        for batch in batches {
929            // Flatten the HollowBatch into one LeasedBatchPart per key. Each key
930            // corresponds to a "part" or s3 object. This allows persist_source
931            // to distribute work by parts (smallish, more even size) instead of
932            // batches (arbitrarily large).
933            leased_parts.extend(
934                self.lease_batch_parts(lease.clone(), batch, filter.clone())
935                    .collect::<Vec<_>>()
936                    .await,
937            );
938        }
939        Ok(leased_parts)
940    }
941
942    /// Returns a snapshot of all of a shard's data using `as_of`, followed by
943    /// listening to any future updates.
944    ///
945    /// For more details on this operation's semantics, see [Self::snapshot] and
946    /// [Self::listen].
947    #[instrument(level = "debug", fields(shard = %self.machine.shard_id()))]
948    pub async fn subscribe(
949        mut self,
950        as_of: Antichain<T>,
951    ) -> Result<Subscribe<K, V, T, D>, Since<T>> {
952        let snapshot_parts = self.snapshot(as_of.clone()).await?;
953        let listen = self.listen(as_of.clone()).await?;
954        Ok(Subscribe::new(snapshot_parts, listen))
955    }
956
957    fn lease_batch_parts(
958        &mut self,
959        lease: Lease,
960        batch: HollowBatch<T>,
961        filter: FetchBatchFilter<T>,
962    ) -> impl Stream<Item = LeasedBatchPart<T>> + '_ {
963        stream! {
964            let blob = Arc::clone(&self.blob);
965            let metrics = Arc::clone(&self.metrics);
966            let desc = batch.desc.clone();
967            let bounds_truncated = batch.runs().any(|(meta, _)| meta.bounds_truncated());
968            for await part in batch.part_stream(self.shard_id(), &*blob, &*metrics) {
969                yield LeasedBatchPart {
970                    metrics: Arc::clone(&self.metrics),
971                    shard_id: self.machine.shard_id(),
972                    filter: filter.clone(),
973                    desc: desc.clone(),
974                    part: part.expect("leased part").into_owned(),
975                    lease: lease.clone(),
976                    reader_id: self.reader_id.clone(),
977                    filter_pushdown_audit: false,
978                    bounds_truncated,
979                }
980            }
981        }
982    }
983
984    /// Tracks that the `ReadHandle`'s machine's current `SeqNo` is being
985    /// "leased out" to a `LeasedBatchPart`. This ensures that until the reader is expired or the
986    /// [Lease] is dropped, we will hold onto all versions of the state with this sequence number
987    /// or larger. In particular, this means that any batches that are present in the state at
988    /// the leased seqno _or in any future state_ will be preserved as long as the lease is active.
989    ///
990    /// Note that this doesn't present batches from earlier versions of state from being GCed,
991    /// even if they were observed just before the lease call. Callers should generally take a lease
992    /// first, and then inspect state to find any batches to return or process.
993    async fn lease_seqno(&mut self) -> Lease {
994        let current_seqno = self.machine.seqno();
995        let lease = self.hold_state.modify(|s| {
996            s.observe_seqno(current_seqno);
997            s.lease_seqno()
998        });
999        // The seqno we've leased may be the seqno observed by our heartbeat task, which could be
1000        // ahead of the last state we saw. Ensure we only observe states in the future of our hold.
1001        // (Since these are backed by the same state in the same process, this should all be pretty
1002        // fast.)
1003        self.watch.wait_for_seqno_ge(lease.seqno()).await;
1004        lease
1005    }
1006
1007    /// Returns an independent [ReadHandle] with a new [LeasedReaderId] but the
1008    /// same `since`.
1009    #[instrument(level = "debug", fields(shard = %self.machine.shard_id()))]
1010    pub async fn clone(&self, purpose: &str) -> Self {
1011        let new_reader_id = LeasedReaderId::new();
1012        let machine = self.machine.clone();
1013        let gc = self.gc.clone();
1014        let (reader_state, maintenance) = machine
1015            .register_leased_reader(
1016                &new_reader_id,
1017                purpose,
1018                READER_LEASE_DURATION.get(&self.cfg),
1019                false,
1020            )
1021            .await;
1022        maintenance.start_performing(&machine, &gc);
1023        // The point of clone is that you're guaranteed to have the same (or
1024        // greater) since capability, verify that.
1025        // TODO: better if it's the same since capability exactly.
1026        assert!(PartialOrder::less_equal(&reader_state.since, &self.since));
1027        let new_reader = ReadHandle::new(
1028            self.cfg.clone(),
1029            Arc::clone(&self.metrics),
1030            machine,
1031            gc,
1032            Arc::clone(&self.blob),
1033            new_reader_id,
1034            self.read_schemas.clone(),
1035            reader_state,
1036        )
1037        .await;
1038        new_reader
1039    }
1040
1041    /// A rate-limited version of [Self::downgrade_since].
1042    ///
1043    /// Users can call this as frequently as they wish; changes will be periodically
1044    /// flushed to state by the heartbeat task.
1045    #[allow(clippy::unused_async)]
1046    pub async fn maybe_downgrade_since(&mut self, new_since: &Antichain<T>) {
1047        self.since = new_since.clone();
1048        self.hold_state.modify(|s| {
1049            s.downgrade_since(new_since);
1050        });
1051    }
1052
1053    /// Politely expires this reader, releasing its lease.
1054    ///
1055    /// There is a best-effort impl in Drop to expire a reader that wasn't
1056    /// explictly expired with this method. When possible, explicit expiry is
1057    /// still preferred because it also ensures that the background task is complete.
1058    #[instrument(level = "debug", fields(shard = %self.machine.shard_id()))]
1059    pub async fn expire(mut self) {
1060        self.hold_state.modify(|s| {
1061            s.expired = true;
1062            s.request_sync = true;
1063        });
1064        let Some(unexpired_state) = self.unexpired_state.take() else {
1065            return;
1066        };
1067        unexpired_state.heartbeat_task.await;
1068    }
1069
1070    /// Test helper for a [Self::listen] call that is expected to succeed.
1071    #[cfg(test)]
1072    #[track_caller]
1073    pub async fn expect_listen(self, as_of: T) -> Listen<K, V, T, D> {
1074        self.listen(Antichain::from_elem(as_of))
1075            .await
1076            .expect("cannot serve requested as_of")
1077    }
1078}
1079
1080/// State for a read handle that has not been explicitly expired.
1081#[derive(Debug)]
1082pub(crate) struct UnexpiredReadHandleState {
1083    pub(crate) heartbeat_task: JoinHandle<()>,
1084}
1085
1086/// An incremental cursor through a particular shard, returned from [ReadHandle::snapshot_cursor].
1087///
1088/// To read an entire dataset, the
1089/// client should call `next` until it returns `None`, which signals all data has been returned...
1090/// but it's also free to abandon the instance at any time if it eg. only needs a few entries.
1091#[derive(Debug)]
1092pub struct Cursor<K: Codec, V: Codec, T: Timestamp + Codec64, D: Codec64, L = Lease> {
1093    consolidator: Consolidator<T, D, StructuredSort<K, V, T, D>>,
1094    max_len: usize,
1095    max_bytes: usize,
1096    _lease: L,
1097    read_schemas: Schemas<K, V>,
1098}
1099
1100impl<K: Codec, V: Codec, T: Timestamp + Codec64, D: Codec64, L> Cursor<K, V, T, D, L> {
1101    /// Extracts and returns the lease from the cursor. Allowing the caller to
1102    /// do any necessary cleanup associated with the lease.
1103    pub fn into_lease(self: Self) -> L {
1104        self._lease
1105    }
1106}
1107
1108impl<K, V, T, D, L> Cursor<K, V, T, D, L>
1109where
1110    K: Debug + Codec + Ord,
1111    V: Debug + Codec + Ord,
1112    T: Timestamp + Lattice + Codec64 + Sync,
1113    D: Monoid + Ord + Codec64 + Send + Sync,
1114{
1115    /// Grab the next batch of consolidated data.
1116    pub async fn next(&mut self) -> Option<impl Iterator<Item = ((K, V), T, D)> + '_> {
1117        let Self {
1118            consolidator,
1119            max_len,
1120            max_bytes,
1121            _lease,
1122            read_schemas: _,
1123        } = self;
1124
1125        let part = consolidator
1126            .next_chunk(*max_len, *max_bytes)
1127            .await
1128            .expect("fetching a leased part")?;
1129        let key_decoder = self
1130            .read_schemas
1131            .key
1132            .decoder_any(part.key.as_ref())
1133            .expect("ok");
1134        let val_decoder = self
1135            .read_schemas
1136            .val
1137            .decoder_any(part.val.as_ref())
1138            .expect("ok");
1139        let iter = (0..part.len()).map(move |i| {
1140            let mut k = K::default();
1141            let mut v = V::default();
1142            key_decoder.decode(i, &mut k);
1143            val_decoder.decode(i, &mut v);
1144            let t = T::decode(part.time.value(i).to_le_bytes());
1145            let d = D::decode(part.diff.value(i).to_le_bytes());
1146            ((k, v), t, d)
1147        });
1148
1149        Some(iter)
1150    }
1151}
1152
1153impl<K, V, T, D> ReadHandle<K, V, T, D>
1154where
1155    K: Debug + Codec + Ord,
1156    V: Debug + Codec + Ord,
1157    T: Timestamp + TotalOrder + Lattice + Codec64 + Sync,
1158    D: Monoid + Ord + Codec64 + Send + Sync,
1159{
1160    /// Generates a [Self::snapshot], and fetches all of the batches it
1161    /// contains.
1162    ///
1163    /// The output is consolidated. Furthermore, to keep memory usage down when
1164    /// reading a snapshot that consolidates well, this consolidates as it goes.
1165    ///
1166    /// Potential future improvements (if necessary):
1167    /// - Accept something like a `F: Fn(K,V) -> (K,V)` argument, which looks
1168    ///   like an MFP you might be pushing down. Reason being that if you are
1169    ///   projecting or transforming in a way that allows further consolidation,
1170    ///   amazing.
1171    /// - Reuse any code we write to streaming-merge consolidate in
1172    ///   persist_source here.
1173    pub async fn snapshot_and_fetch(
1174        &mut self,
1175        as_of: Antichain<T>,
1176    ) -> Result<Vec<((K, V), T, D)>, Since<T>> {
1177        let mut cursor = self.snapshot_cursor(as_of, |_| true).await?;
1178        let mut contents = Vec::new();
1179        while let Some(iter) = cursor.next().await {
1180            contents.extend(iter);
1181        }
1182
1183        // We don't currently guarantee that encoding is one-to-one, so we still need to
1184        // consolidate the decoded outputs. However, let's report if this isn't a noop.
1185        let old_len = contents.len();
1186        consolidate_updates(&mut contents);
1187        if old_len != contents.len() {
1188            // TODO(bkirwi): do we need more / finer-grained metrics for this?
1189            self.machine
1190                .applier
1191                .shard_metrics
1192                .unconsolidated_snapshot
1193                .inc();
1194        }
1195
1196        Ok(contents)
1197    }
1198
1199    /// Generates a [Self::snapshot], and fetches all of the batches it
1200    /// contains.
1201    ///
1202    /// To keep memory usage down when reading a snapshot that consolidates well, this consolidates
1203    /// as it goes. However, note that only the serialized data is consolidated: the deserialized
1204    /// data will only be consolidated if your K/V codecs are one-to-one.
1205    pub async fn snapshot_cursor(
1206        &mut self,
1207        as_of: Antichain<T>,
1208        should_fetch_part: impl for<'a> Fn(Option<&'a LazyPartStats>) -> bool,
1209    ) -> Result<Cursor<K, V, T, D>, Since<T>> {
1210        let (lease, batches) = self.snapshot_batches(as_of.clone()).await?;
1211
1212        Self::read_batches_consolidated(
1213            &self.cfg,
1214            Arc::clone(&self.metrics),
1215            Arc::clone(&self.machine.applier.shard_metrics),
1216            self.metrics.read.snapshot.clone(),
1217            Arc::clone(&self.blob),
1218            self.shard_id(),
1219            as_of,
1220            self.read_schemas.clone(),
1221            &batches,
1222            lease,
1223            should_fetch_part,
1224            COMPACTION_MEMORY_BOUND_BYTES.get(&self.cfg),
1225        )
1226    }
1227
1228    pub(crate) fn read_batches_consolidated<L>(
1229        persist_cfg: &PersistConfig,
1230        metrics: Arc<Metrics>,
1231        shard_metrics: Arc<ShardMetrics>,
1232        read_metrics: ReadMetrics,
1233        blob: Arc<dyn Blob>,
1234        shard_id: ShardId,
1235        as_of: Antichain<T>,
1236        schemas: Schemas<K, V>,
1237        batches: &[HollowBatch<T>],
1238        lease: L,
1239        should_fetch_part: impl for<'a> Fn(Option<&'a LazyPartStats>) -> bool,
1240        memory_budget_bytes: usize,
1241    ) -> Result<Cursor<K, V, T, D, L>, Since<T>> {
1242        let context = format!("{}[as_of={:?}]", shard_id, as_of.elements());
1243        let filter = FetchBatchFilter::Snapshot {
1244            as_of: as_of.clone(),
1245        };
1246
1247        let mut consolidator = Consolidator::new(
1248            context,
1249            FetchConfig::from_persist_config(persist_cfg),
1250            shard_id,
1251            StructuredSort::new(schemas.clone()),
1252            blob,
1253            metrics,
1254            shard_metrics,
1255            read_metrics,
1256            filter,
1257            None,
1258            memory_budget_bytes,
1259        );
1260        for batch in batches {
1261            for (meta, run) in batch.runs() {
1262                consolidator.enqueue_run(
1263                    &batch.desc,
1264                    meta,
1265                    run.into_iter()
1266                        .filter(|p| should_fetch_part(p.stats()))
1267                        .cloned(),
1268                );
1269            }
1270        }
1271        // This default may end up consolidating more records than previously
1272        // for cases like fast-path peeks, where only the first few entries are used.
1273        // If this is a noticeable performance impact, thread the max-len in from the caller.
1274        let max_len = persist_cfg.compaction_yield_after_n_updates;
1275        let max_bytes = BLOB_TARGET_SIZE.get(persist_cfg).max(1);
1276
1277        Ok(Cursor {
1278            consolidator,
1279            max_len,
1280            max_bytes,
1281            _lease: lease,
1282            read_schemas: schemas,
1283        })
1284    }
1285
1286    /// Returns aggregate statistics about the contents of the shard TVC at the
1287    /// given frontier.
1288    ///
1289    /// This command returns the contents of this shard as of `as_of` once they
1290    /// are known. This may "block" (in an async-friendly way) if `as_of` is
1291    /// greater or equal to the current `upper` of the shard. If `None` is given
1292    /// for `as_of`, then the latest stats known by this process are used.
1293    ///
1294    /// The `Since` error indicates that the requested `as_of` cannot be served
1295    /// (the caller has out of date information) and includes the smallest
1296    /// `as_of` that would have been accepted.
1297    pub fn snapshot_stats(
1298        &self,
1299        as_of: Option<Antichain<T>>,
1300    ) -> impl Future<Output = Result<SnapshotStats, Since<T>>> + Send + 'static {
1301        let machine = self.machine.clone();
1302        async move {
1303            let batches = match as_of {
1304                Some(as_of) => machine.unleased_snapshot(&as_of).await?,
1305                None => machine.applier.all_batches(),
1306            };
1307            let num_updates = batches.iter().map(|b| b.len).sum();
1308            Ok(SnapshotStats {
1309                shard_id: machine.shard_id(),
1310                num_updates,
1311            })
1312        }
1313    }
1314
1315    /// Returns aggregate statistics about the contents of the shard TVC at the
1316    /// given frontier.
1317    ///
1318    /// This command returns the contents of this shard as of `as_of` once they
1319    /// are known. This may "block" (in an async-friendly way) if `as_of` is
1320    /// greater or equal to the current `upper` of the shard.
1321    ///
1322    /// The `Since` error indicates that the requested `as_of` cannot be served
1323    /// (the caller has out of date information) and includes the smallest
1324    /// `as_of` that would have been accepted.
1325    pub async fn snapshot_parts_stats(
1326        &self,
1327        as_of: Antichain<T>,
1328    ) -> Result<SnapshotPartsStats, Since<T>> {
1329        let batches = self.machine.unleased_snapshot(&as_of).await?;
1330        let parts = stream::iter(&batches)
1331            .flat_map(|b| b.part_stream(self.shard_id(), &*self.blob, &*self.metrics))
1332            .map(|p| {
1333                let p = p.expect("live batch");
1334                SnapshotPartStats {
1335                    encoded_size_bytes: p.encoded_size_bytes(),
1336                    stats: p.stats().cloned(),
1337                }
1338            })
1339            .collect()
1340            .await;
1341        Ok(SnapshotPartsStats {
1342            metrics: Arc::clone(&self.machine.applier.metrics),
1343            shard_id: self.machine.shard_id(),
1344            parts,
1345        })
1346    }
1347}
1348
1349impl<K, V, T, D> ReadHandle<K, V, T, D>
1350where
1351    K: Debug + Codec + Ord,
1352    V: Debug + Codec + Ord,
1353    T: Timestamp + TotalOrder + Lattice + Codec64 + Sync,
1354    D: Monoid + Codec64 + Send + Sync,
1355{
1356    /// Generates a [Self::snapshot], and streams out all of the updates
1357    /// it contains in bounded memory.
1358    ///
1359    /// The output is not consolidated.
1360    pub async fn snapshot_and_stream(
1361        &mut self,
1362        as_of: Antichain<T>,
1363    ) -> Result<impl Stream<Item = ((K, V), T, D)> + use<K, V, T, D>, Since<T>> {
1364        let snap = self.snapshot(as_of).await?;
1365
1366        let blob = Arc::clone(&self.blob);
1367        let metrics = Arc::clone(&self.metrics);
1368        let snapshot_metrics = self.metrics.read.snapshot.clone();
1369        let shard_metrics = Arc::clone(&self.machine.applier.shard_metrics);
1370        let reader_id = self.reader_id.clone();
1371        let schemas = self.read_schemas.clone();
1372        let mut schema_cache = self.schema_cache.clone();
1373        let persist_cfg = self.cfg.clone();
1374        let stream = async_stream::stream! {
1375            for part in snap {
1376                let mut fetched_part = fetch_leased_part(
1377                    &persist_cfg,
1378                    &part,
1379                    blob.as_ref(),
1380                    Arc::clone(&metrics),
1381                    &snapshot_metrics,
1382                    &shard_metrics,
1383                    &reader_id,
1384                    schemas.clone(),
1385                    &mut schema_cache,
1386                )
1387                .await;
1388
1389                while let Some(next) = fetched_part.next() {
1390                    yield next;
1391                }
1392            }
1393        };
1394
1395        Ok(stream)
1396    }
1397}
1398
1399impl<K, V, T, D> ReadHandle<K, V, T, D>
1400where
1401    K: Debug + Codec + Ord,
1402    V: Debug + Codec + Ord,
1403    T: Timestamp + TotalOrder + Lattice + Codec64 + Sync,
1404    D: Monoid + Ord + Codec64 + Send + Sync,
1405{
1406    /// Test helper to generate a [Self::snapshot] call that is expected to
1407    /// succeed, process its batches, and then return its data sorted.
1408    #[cfg(test)]
1409    #[track_caller]
1410    pub async fn expect_snapshot_and_fetch(&mut self, as_of: T) -> Vec<((K, V), T, D)> {
1411        let mut ret = self
1412            .snapshot_and_fetch(Antichain::from_elem(as_of))
1413            .await
1414            .expect("cannot serve requested as_of");
1415
1416        ret.sort();
1417        ret
1418    }
1419}
1420
1421impl<K: Codec, V: Codec, T, D> Drop for ReadHandle<K, V, T, D> {
1422    fn drop(&mut self) {
1423        self.hold_state.modify(|s| {
1424            s.expired = true;
1425            s.request_sync = true;
1426        });
1427    }
1428}
1429
1430#[cfg(test)]
1431mod tests {
1432    use std::pin;
1433    use std::str::FromStr;
1434
1435    use mz_dyncfg::ConfigUpdates;
1436    use mz_ore::cast::CastFrom;
1437    use mz_ore::metrics::MetricsRegistry;
1438    use mz_persist::mem::{MemBlob, MemBlobConfig, MemConsensus};
1439    use mz_persist::unreliable::{UnreliableConsensus, UnreliableHandle};
1440    use serde::{Deserialize, Serialize};
1441    use serde_json::json;
1442    use tokio_stream::StreamExt;
1443
1444    use crate::async_runtime::IsolatedRuntime;
1445    use crate::batch::BLOB_TARGET_SIZE;
1446    use crate::cache::StateCache;
1447    use crate::internal::metrics::Metrics;
1448    use crate::rpc::NoopPubSubSender;
1449    use crate::tests::{all_ok, new_test_client};
1450    use crate::{Diagnostics, PersistClient, PersistConfig, ShardId};
1451
1452    use super::*;
1453
1454    // Verifies `Subscribe` can be dropped while holding snapshot batches.
1455    #[mz_persist_proc::test(tokio::test)]
1456    #[cfg_attr(miri, ignore)] // unsupported operation: returning ready events from epoll_wait is not yet implemented
1457    async fn drop_unused_subscribe(dyncfgs: ConfigUpdates) {
1458        let data = [
1459            (("0".to_owned(), "zero".to_owned()), 0, 1),
1460            (("1".to_owned(), "one".to_owned()), 1, 1),
1461            (("2".to_owned(), "two".to_owned()), 2, 1),
1462        ];
1463
1464        let (mut write, read) = new_test_client(&dyncfgs)
1465            .await
1466            .expect_open::<String, String, u64, i64>(crate::ShardId::new())
1467            .await;
1468
1469        write.expect_compare_and_append(&data[0..1], 0, 1).await;
1470        write.expect_compare_and_append(&data[1..2], 1, 2).await;
1471        write.expect_compare_and_append(&data[2..3], 2, 3).await;
1472
1473        let subscribe = read
1474            .subscribe(timely::progress::Antichain::from_elem(2))
1475            .await
1476            .unwrap();
1477        assert!(
1478            !subscribe.snapshot.as_ref().unwrap().is_empty(),
1479            "snapshot must have batches for test to be meaningful"
1480        );
1481        drop(subscribe);
1482    }
1483
1484    // The ignored-data fetch optimization substitutes a part's write-time
1485    // diffs_sum for a real fetch, but a truncated batch's parts may hold
1486    // updates that a real fetch would filter out, so it must not fire for
1487    // them.
1488    #[mz_ore::test(tokio::test)]
1489    #[cfg_attr(miri, ignore)] // unsupported operation: returning ready events from epoll_wait is not yet implemented
1490    async fn maybe_optimize_ignores_truncated_batches() {
1491        use arrow::array::{ArrayRef, StringArray};
1492
1493        use crate::batch::{INLINE_WRITES_SINGLE_MAX_BYTES, INLINE_WRITES_TOTAL_MAX_BYTES};
1494        use crate::cache::PersistClientCache;
1495
1496        let mut cache = PersistClientCache::new_no_metrics();
1497        // Compaction rewrites truncated batches with exact bounds, which
1498        // would undo the setup below.
1499        cache.cfg.compaction_enabled = false;
1500        // The optimization only applies to hollow parts.
1501        cache.cfg.set_config(&INLINE_WRITES_SINGLE_MAX_BYTES, 0);
1502        cache.cfg.set_config(&INLINE_WRITES_TOTAL_MAX_BYTES, 0);
1503        let client = cache
1504            .open(crate::PersistLocation::new_in_mem())
1505            .await
1506            .expect("client construction failed");
1507        let (mut write, mut read) = client
1508            .expect_open::<String, String, u64, i64>(ShardId::new())
1509            .await;
1510
1511        write
1512            .expect_compare_and_append(&[(("0".to_owned(), "zero".to_owned()), 0, 1)], 0, 1)
1513            .await;
1514
1515        // Write a batch over [0, 3) but append it under [1, 3): the update
1516        // at 0 stays in the part while every read filters it out.
1517        let mut batch = write
1518            .expect_batch(
1519                &[
1520                    (("stale".to_owned(), "stale".to_owned()), 0, 1),
1521                    (("1".to_owned(), "one".to_owned()), 1, 1),
1522                    (("2".to_owned(), "two".to_owned()), 2, 1),
1523                ],
1524                0,
1525                3,
1526            )
1527            .await;
1528        write
1529            .compare_and_append_batch(
1530                &mut [&mut batch],
1531                Antichain::from_elem(1),
1532                Antichain::from_elem(3),
1533                true,
1534            )
1535            .await
1536            .expect("invalid usage")
1537            .expect("unexpected upper");
1538
1539        // Advance the upper so a snapshot as_of 3 is available.
1540        write
1541            .expect_compare_and_append(&[(("3".to_owned(), "three".to_owned()), 3, 1)], 3, 4)
1542            .await;
1543
1544        let parts = read
1545            .snapshot(Antichain::from_elem(3))
1546            .await
1547            .expect("as_of unavailable");
1548
1549        let mut faked = 0;
1550        let mut kept_truncated = 0;
1551        for mut part in parts {
1552            let key: ArrayRef = Arc::new(StringArray::from(vec!["k"]));
1553            let val: ArrayRef = Arc::new(StringArray::from(vec!["v"]));
1554            part.maybe_optimize(&cache.cfg, key, val);
1555            match part.desc.lower().elements() {
1556                [0] => {
1557                    // Control: an untruncated batch below the as_of gets its
1558                    // fetch optimized away.
1559                    assert!(!part.bounds_truncated);
1560                    assert!(part.part.is_inline(), "expected a faked part");
1561                    faked += 1;
1562                }
1563                [1] => {
1564                    // The truncated batch's statistics count the update at 0,
1565                    // so the fetch must happen for real.
1566                    assert!(part.bounds_truncated);
1567                    assert!(
1568                        !part.part.is_inline(),
1569                        "must not substitute statistics for a truncated part"
1570                    );
1571                    kept_truncated += 1;
1572                }
1573                [3] => (),
1574                x => panic!("unexpected part lower {:?}", x),
1575            }
1576        }
1577        assert_eq!(faked, 1);
1578        assert_eq!(kept_truncated, 1);
1579    }
1580
1581    // Verifies that we streaming-consolidate away identical key-values in the same batch.
1582    #[mz_persist_proc::test(tokio::test)]
1583    #[cfg_attr(miri, ignore)] // unsupported operation: returning ready events from epoll_wait is not yet implemented
1584    async fn streaming_consolidate(dyncfgs: ConfigUpdates) {
1585        let data = &[
1586            // Identical records should sum together...
1587            (("k".to_owned(), "v".to_owned()), 0, 1),
1588            (("k".to_owned(), "v".to_owned()), 1, 1),
1589            (("k".to_owned(), "v".to_owned()), 2, 1),
1590            // ...and when they cancel out entirely they should be omitted.
1591            (("k2".to_owned(), "v".to_owned()), 0, 1),
1592            (("k2".to_owned(), "v".to_owned()), 1, -1),
1593        ];
1594
1595        let (mut write, read) = {
1596            let client = new_test_client(&dyncfgs).await;
1597            client.cfg.set_config(&BLOB_TARGET_SIZE, 1000); // So our batch stays together!
1598            client
1599                .expect_open::<String, String, u64, i64>(crate::ShardId::new())
1600                .await
1601        };
1602
1603        write.expect_compare_and_append(data, 0, 5).await;
1604
1605        let mut snapshot = read
1606            .subscribe(timely::progress::Antichain::from_elem(4))
1607            .await
1608            .unwrap();
1609
1610        let mut updates = vec![];
1611        'outer: loop {
1612            for event in snapshot.fetch_next().await {
1613                match event {
1614                    ListenEvent::Progress(t) => {
1615                        if !t.less_than(&4) {
1616                            break 'outer;
1617                        }
1618                    }
1619                    ListenEvent::Updates(data) => {
1620                        updates.extend(data);
1621                    }
1622                }
1623            }
1624        }
1625        assert_eq!(updates, &[(("k".to_owned(), "v".to_owned()), 4u64, 3i64)],)
1626    }
1627
1628    #[mz_persist_proc::test(tokio::test)]
1629    #[cfg_attr(miri, ignore)] // unsupported operation: returning ready events from epoll_wait is not yet implemented
1630    async fn snapshot_and_stream(dyncfgs: ConfigUpdates) {
1631        let data = &mut [
1632            (("k1".to_owned(), "v1".to_owned()), 0, 1),
1633            (("k2".to_owned(), "v2".to_owned()), 1, 1),
1634            (("k3".to_owned(), "v3".to_owned()), 2, 1),
1635            (("k4".to_owned(), "v4".to_owned()), 2, 1),
1636            (("k5".to_owned(), "v5".to_owned()), 3, 1),
1637        ];
1638
1639        let (mut write, mut read) = {
1640            let client = new_test_client(&dyncfgs).await;
1641            client.cfg.set_config(&BLOB_TARGET_SIZE, 0); // split batches across multiple parts
1642            client
1643                .expect_open::<String, String, u64, i64>(crate::ShardId::new())
1644                .await
1645        };
1646
1647        write.expect_compare_and_append(&data[0..2], 0, 2).await;
1648        write.expect_compare_and_append(&data[2..4], 2, 3).await;
1649        write.expect_compare_and_append(&data[4..], 3, 4).await;
1650
1651        let as_of = Antichain::from_elem(3);
1652        let mut snapshot = pin::pin!(read.snapshot_and_stream(as_of.clone()).await.unwrap());
1653
1654        let mut snapshot_rows = vec![];
1655        while let Some(((k, v), t, d)) = snapshot.next().await {
1656            snapshot_rows.push(((k, v), t, d));
1657        }
1658
1659        for ((_k, _v), t, _d) in data.as_mut_slice() {
1660            t.advance_by(as_of.borrow());
1661        }
1662
1663        assert_eq!(data.as_slice(), snapshot_rows.as_slice());
1664    }
1665
1666    // Verifies the semantics of `SeqNo` leases + checks dropping `LeasedBatchPart` semantics.
1667    #[mz_persist_proc::test(tokio::test)]
1668    #[cfg_attr(miri, ignore)] // https://github.com/MaterializeInc/database-issues/issues/5964
1669    async fn seqno_leases(dyncfgs: ConfigUpdates) {
1670        let mut data = vec![];
1671        for i in 0..20 {
1672            data.push(((i.to_string(), i.to_string()), i, 1))
1673        }
1674
1675        let shard_id = ShardId::new();
1676
1677        let client = new_test_client(&dyncfgs).await;
1678        let (mut write, read) = client
1679            .expect_open::<String, String, u64, i64>(shard_id)
1680            .await;
1681
1682        // Seed with some values
1683        let mut offset = 0;
1684        let mut width = 2;
1685
1686        for i in offset..offset + width {
1687            write
1688                .expect_compare_and_append(
1689                    &data[i..i + 1],
1690                    u64::cast_from(i),
1691                    u64::cast_from(i) + 1,
1692                )
1693                .await;
1694        }
1695        offset += width;
1696
1697        // Create machinery for subscribe + fetch
1698        let mut fetcher = client
1699            .create_batch_fetcher::<String, String, u64, i64>(
1700                shard_id,
1701                Default::default(),
1702                Default::default(),
1703                false,
1704                Diagnostics::for_tests(),
1705            )
1706            .await
1707            .unwrap();
1708
1709        let mut subscribe = read
1710            .subscribe(timely::progress::Antichain::from_elem(1))
1711            .await
1712            .expect("cannot serve requested as_of");
1713
1714        // Determine the sequence number held by our subscribe.
1715        let original_seqno_since = subscribe.listen.handle.outstanding_seqno();
1716        if let Some(snapshot) = &subscribe.snapshot {
1717            for part in snapshot {
1718                assert!(
1719                    part.lease.seqno() >= original_seqno_since,
1720                    "our seqno hold must cover all parts"
1721                );
1722            }
1723        }
1724
1725        let mut parts = vec![];
1726
1727        width = 4;
1728        // Collect parts while continuing to write values
1729        for i in offset..offset + width {
1730            for event in subscribe.next(None).await {
1731                if let ListenEvent::Updates(mut new_parts) = event {
1732                    parts.append(&mut new_parts);
1733                    // Here and elsewhere we "cheat" and immediately downgrade the since
1734                    // to demonstrate the effects of SeqNo leases immediately.
1735                    subscribe
1736                        .listen
1737                        .handle
1738                        .downgrade_since(&subscribe.listen.since)
1739                        .await;
1740                }
1741            }
1742
1743            write
1744                .expect_compare_and_append(
1745                    &data[i..i + 1],
1746                    u64::cast_from(i),
1747                    u64::cast_from(i) + 1,
1748                )
1749                .await;
1750
1751            // SeqNo is not downgraded
1752            assert_eq!(
1753                subscribe.listen.handle.machine.applier.seqno_since(),
1754                original_seqno_since
1755            );
1756        }
1757
1758        offset += width;
1759
1760        let mut seqno_since = subscribe.listen.handle.machine.applier.seqno_since();
1761
1762        // We're starting out with the original, non-downgraded SeqNo
1763        assert_eq!(seqno_since, original_seqno_since);
1764
1765        // We have to handle the parts we generate during the next loop to
1766        // ensure they don't panic.
1767        let mut subsequent_parts = vec![];
1768
1769        // Ensure monotonicity of seqnos we're processing, otherwise the
1770        // invariant we're testing (returning the last part of a seqno will
1771        // downgrade its since) will not hold.
1772        let mut this_seqno = SeqNo::minimum();
1773
1774        // Repeat the same process as above, more or less, while fetching + returning parts
1775        for (mut i, part) in parts.into_iter().enumerate() {
1776            let part_seqno = part.lease.seqno();
1777            let last_seqno = this_seqno;
1778            this_seqno = part_seqno;
1779            assert!(this_seqno >= last_seqno);
1780
1781            let (part, lease) = part.into_exchangeable_part();
1782            let _ = fetcher.fetch_leased_part(part).await;
1783            drop(lease);
1784
1785            // Simulates an exchange
1786            for event in subscribe.next(None).await {
1787                if let ListenEvent::Updates(parts) = event {
1788                    for part in parts {
1789                        let (_, lease) = part.into_exchangeable_part();
1790                        subsequent_parts.push(lease);
1791                    }
1792                }
1793            }
1794
1795            subscribe
1796                .listen
1797                .handle
1798                .downgrade_since(&subscribe.listen.since)
1799                .await;
1800
1801            // Write more new values
1802            i += offset;
1803            write
1804                .expect_compare_and_append(
1805                    &data[i..i + 1],
1806                    u64::cast_from(i),
1807                    u64::cast_from(i) + 1,
1808                )
1809                .await;
1810
1811            // We should expect the SeqNo to be downgraded if this part's SeqNo
1812            // is no longer leased to any other parts, either.
1813            let expect_downgrade = subscribe.listen.handle.outstanding_seqno() > part_seqno;
1814
1815            let new_seqno_since = subscribe.listen.handle.machine.applier.seqno_since();
1816            if expect_downgrade {
1817                assert!(new_seqno_since > seqno_since);
1818            } else {
1819                assert_eq!(new_seqno_since, seqno_since);
1820            }
1821            seqno_since = new_seqno_since;
1822        }
1823
1824        // SeqNo since was downgraded
1825        assert!(seqno_since > original_seqno_since);
1826
1827        // Return any outstanding parts, to prevent a panic!
1828        drop(subsequent_parts);
1829        drop(subscribe);
1830    }
1831
1832    #[mz_ore::test]
1833    fn reader_id_human_readable_serde() {
1834        #[derive(Debug, Serialize, Deserialize)]
1835        struct Container {
1836            reader_id: LeasedReaderId,
1837        }
1838
1839        // roundtrip through json
1840        let id =
1841            LeasedReaderId::from_str("r00000000-1234-5678-0000-000000000000").expect("valid id");
1842        assert_eq!(
1843            id,
1844            serde_json::from_value(serde_json::to_value(id.clone()).expect("serializable"))
1845                .expect("deserializable")
1846        );
1847
1848        // deserialize a serialized string directly
1849        assert_eq!(
1850            id,
1851            serde_json::from_str("\"r00000000-1234-5678-0000-000000000000\"")
1852                .expect("deserializable")
1853        );
1854
1855        // roundtrip id through a container type
1856        let json = json!({ "reader_id": id });
1857        assert_eq!(
1858            "{\"reader_id\":\"r00000000-1234-5678-0000-000000000000\"}",
1859            &json.to_string()
1860        );
1861        let container: Container = serde_json::from_value(json).expect("deserializable");
1862        assert_eq!(container.reader_id, id);
1863    }
1864
1865    // Verifies performance optimizations where a Listener doesn't fetch the
1866    // latest Consensus state if the one it currently has can serve the next
1867    // request.
1868    #[mz_ore::test(tokio::test)]
1869    #[cfg_attr(miri, ignore)] // too slow
1870    async fn skip_consensus_fetch_optimization() {
1871        let data = vec![
1872            (("0".to_owned(), "zero".to_owned()), 0, 1),
1873            (("1".to_owned(), "one".to_owned()), 1, 1),
1874            (("2".to_owned(), "two".to_owned()), 2, 1),
1875        ];
1876
1877        let cfg = PersistConfig::new_for_tests();
1878        let blob = Arc::new(MemBlob::open(MemBlobConfig::default()));
1879        let consensus = Arc::new(MemConsensus::default());
1880        let unreliable = UnreliableHandle::default();
1881        unreliable.totally_available();
1882        let consensus = Arc::new(UnreliableConsensus::new(consensus, unreliable.clone()));
1883        let metrics = Arc::new(Metrics::new(&cfg, &MetricsRegistry::new()));
1884        let pubsub_sender = Arc::new(NoopPubSubSender);
1885        let (mut write, mut read) = PersistClient::new(
1886            cfg,
1887            blob,
1888            consensus,
1889            metrics,
1890            Arc::new(IsolatedRuntime::new_for_tests()),
1891            Arc::new(StateCache::new_no_metrics()),
1892            pubsub_sender,
1893        )
1894        .expect("client construction failed")
1895        .expect_open::<String, String, u64, i64>(ShardId::new())
1896        .await;
1897
1898        write.expect_compare_and_append(&data[0..1], 0, 1).await;
1899        write.expect_compare_and_append(&data[1..2], 1, 2).await;
1900        write.expect_compare_and_append(&data[2..3], 2, 3).await;
1901
1902        let snapshot = read.expect_snapshot_and_fetch(2).await;
1903        let mut listen = read.expect_listen(0).await;
1904
1905        // Manually advance the listener's machine so that it has the latest
1906        // state by fetching the first events from next. This is awkward but
1907        // only necessary because we're about to do some weird things with
1908        // unreliable.
1909        let listen_actual = listen.fetch_next().await;
1910        let expected_events = vec![ListenEvent::Progress(Antichain::from_elem(1))];
1911        assert_eq!(listen_actual, expected_events);
1912
1913        // At this point, the snapshot and listen's state should have all the
1914        // writes. Test this by making consensus completely unavailable.
1915        unreliable.totally_unavailable();
1916        assert_eq!(snapshot, all_ok(&data, 2));
1917        assert_eq!(
1918            listen.read_until(&3).await,
1919            (all_ok(&data[1..], 1), Antichain::from_elem(3))
1920        );
1921    }
1922}