Skip to main content

mz_persist_client/
lib.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//! An abstraction presenting as a durable time-varying collection (aka shard)
11
12// The `fuzzing` feature re-exports internal types (see `fuzz_exports`) that are
13// intentionally undocumented. Don't require docs/Debug for them in that
14// test-only build. The normal public API is still linted.
15#![cfg_attr(
16    not(feature = "fuzzing"),
17    warn(missing_docs, missing_debug_implementations)
18)]
19// #[track_caller] is currently a no-op on async functions, but that hopefully won't be the case
20// forever. So we already annotate those functions now and ignore the compiler warning until
21// https://github.com/rust-lang/rust/issues/87417 pans out.
22#![allow(ungated_async_fn_track_caller)]
23
24use std::fmt::Debug;
25use std::marker::PhantomData;
26use std::sync::Arc;
27
28use differential_dataflow::difference::Monoid;
29use differential_dataflow::lattice::Lattice;
30use itertools::Itertools;
31use mz_build_info::{BuildInfo, build_info};
32use mz_dyncfg::ConfigSet;
33use mz_ore::instrument;
34use mz_persist::location::{Blob, Consensus, ExternalError};
35use mz_persist_types::schema::SchemaId;
36use mz_persist_types::{Codec, Codec64};
37use mz_proto::{IntoRustIfSome, ProtoType};
38use semver::Version;
39use timely::order::TotalOrder;
40use timely::progress::{Antichain, Timestamp};
41
42use crate::async_runtime::IsolatedRuntime;
43use crate::batch::{BATCH_DELETE_ENABLED, Batch, BatchBuilder, ProtoBatch};
44use crate::cache::{PersistClientCache, StateCache};
45use crate::cfg::PersistConfig;
46use crate::critical::{CriticalReaderId, Opaque, SinceHandle};
47use crate::error::InvalidUsage;
48use crate::fetch::{BatchFetcher, BatchFetcherConfig};
49use crate::internal::compact::{CompactConfig, Compactor};
50use crate::internal::encoding::parse_id;
51use crate::internal::gc::GarbageCollector;
52use crate::internal::machine::{Machine, retry_external};
53use crate::internal::state_versions::StateVersions;
54use crate::metrics::Metrics;
55use crate::read::{
56    Cursor, LazyPartStats, LeasedReaderId, READER_LEASE_DURATION, ReadHandle, Since,
57};
58use crate::rpc::PubSubSender;
59use crate::schema::CaESchema;
60use crate::write::{WriteHandle, WriterId};
61
62pub mod async_runtime;
63pub mod batch;
64pub mod cache;
65pub mod cfg;
66pub mod cli {
67    //! Persist command-line utilities
68    pub mod admin;
69    pub mod args;
70    pub mod bench;
71    pub mod inspect;
72}
73pub mod critical;
74pub mod error;
75pub mod fetch;
76pub mod internals_bench;
77pub mod iter;
78pub mod metrics {
79    //! Utilities related to metrics.
80    pub use crate::internal::metrics::{
81        BackpressureMetrics, Metrics, SinkMetrics, SinkWorkerMetrics, UpdateDelta, encode_ts_metric,
82    };
83}
84pub mod operators {
85    //! [timely] operators for reading and writing persist Shards.
86
87    use mz_dyncfg::{Config, ParameterScope};
88
89    pub mod shard_source;
90
91    // TODO(cfg): Move this next to the use.
92    pub(crate) const STORAGE_SOURCE_DECODE_FUEL: Config<usize> = Config::new(
93        "storage_source_decode_fuel",
94        100_000,
95        "\
96        The maximum amount of work to do in the persist_source mfp_and_decode \
97        operator before yielding.",
98        ParameterScope::Environment,
99    );
100}
101pub mod read;
102pub mod rpc;
103pub mod schema;
104pub mod stats;
105pub mod usage;
106pub mod write;
107
108/// Internal durable-state types re-exported under `cfg(feature = "fuzzing")` so
109/// the fuzz crate can drive their proto round-trips (`ProtoRollup`/`ProtoStateDiff`
110/// are decoded from blob/consensus on every state load). Not part of the public
111/// API.
112#[cfg(feature = "fuzzing")]
113pub mod fuzz_exports {
114    pub use crate::internal::encoding::Rollup;
115    pub use crate::internal::state::{ProtoRollup, ProtoStateDiff};
116    pub use crate::internal::state_diff::StateDiff;
117}
118
119/// An implementation of the public crate interface.
120mod internal {
121    pub mod apply;
122    pub mod cache;
123    pub mod compact;
124    pub mod encoding;
125    pub mod gc;
126    pub mod machine;
127    pub mod maintenance;
128    pub mod merge;
129    pub mod metrics;
130    pub mod paths;
131    pub mod restore;
132    pub mod service;
133    pub mod state;
134    pub mod state_diff;
135    pub mod state_versions;
136    pub mod trace;
137    pub mod watch;
138
139    #[cfg(test)]
140    pub mod datadriven;
141}
142
143/// Persist build information.
144pub const BUILD_INFO: BuildInfo = build_info!();
145
146// Re-export for convenience.
147pub use mz_persist_types::{PersistLocation, ShardId};
148
149pub use crate::internal::encoding::Schemas;
150
151/// Additional diagnostic information used within Persist
152/// e.g. for logging, metric labels, etc.
153#[derive(Clone, Debug)]
154pub struct Diagnostics {
155    /// A user-friendly name for the shard.
156    pub shard_name: String,
157    /// A purpose for the handle.
158    pub handle_purpose: String,
159}
160
161impl Diagnostics {
162    /// Create a new `Diagnostics` from `handle_purpose`.
163    pub fn from_purpose(handle_purpose: &str) -> Self {
164        Self {
165            shard_name: "unknown".to_string(),
166            handle_purpose: handle_purpose.to_string(),
167        }
168    }
169
170    /// Create a new `Diagnostics` for testing.
171    pub fn for_tests() -> Self {
172        Self {
173            shard_name: "test-shard-name".to_string(),
174            handle_purpose: "test-purpose".to_string(),
175        }
176    }
177}
178
179/// A handle for interacting with the set of persist shard made durable at a
180/// single [PersistLocation].
181///
182/// All async methods on PersistClient retry for as long as they are able, but
183/// the returned [std::future::Future]s implement "cancel on drop" semantics.
184/// This means that callers can add a timeout using [tokio::time::timeout] or
185/// [tokio::time::timeout_at].
186///
187/// ```rust,no_run
188/// # use std::sync::Arc;
189/// # use mz_persist_types::codec_impls::StringSchema;
190/// # let client: mz_persist_client::PersistClient = unimplemented!();
191/// # let timeout: std::time::Duration = unimplemented!();
192/// # let id = mz_persist_client::ShardId::new();
193/// # let diagnostics = mz_persist_client::Diagnostics { shard_name: "".into(), handle_purpose: "".into() };
194/// # async {
195/// tokio::time::timeout(timeout, client.open::<String, String, u64, i64>(id,
196///     Arc::new(StringSchema),Arc::new(StringSchema),diagnostics, true)).await
197/// # };
198/// ```
199#[derive(Debug, Clone)]
200pub struct PersistClient {
201    cfg: PersistConfig,
202    blob: Arc<dyn Blob>,
203    consensus: Arc<dyn Consensus>,
204    metrics: Arc<Metrics>,
205    isolated_runtime: Arc<IsolatedRuntime>,
206    shared_states: Arc<StateCache>,
207    pubsub_sender: Arc<dyn PubSubSender>,
208}
209
210impl PersistClient {
211    /// Returns a new client for interfacing with persist shards made durable to
212    /// the given [Blob] and [Consensus].
213    ///
214    /// This is exposed mostly for testing. Persist users likely want
215    /// [crate::cache::PersistClientCache::open].
216    pub fn new(
217        cfg: PersistConfig,
218        blob: Arc<dyn Blob>,
219        consensus: Arc<dyn Consensus>,
220        metrics: Arc<Metrics>,
221        isolated_runtime: Arc<IsolatedRuntime>,
222        shared_states: Arc<StateCache>,
223        pubsub_sender: Arc<dyn PubSubSender>,
224    ) -> Result<Self, ExternalError> {
225        // TODO: Verify somehow that blob matches consensus to prevent
226        // accidental misuse.
227        Ok(PersistClient {
228            cfg,
229            blob,
230            consensus,
231            metrics,
232            isolated_runtime,
233            shared_states,
234            pubsub_sender,
235        })
236    }
237
238    /// Returns a new in-mem [PersistClient] for tests and examples.
239    pub async fn new_for_tests() -> Self {
240        let cache = PersistClientCache::new_no_metrics();
241        cache
242            .open(PersistLocation::new_in_mem())
243            .await
244            .expect("in-mem location is valid")
245    }
246
247    /// Returns persist's [ConfigSet].
248    pub fn dyncfgs(&self) -> &ConfigSet {
249        &self.cfg.configs
250    }
251
252    async fn make_machine<K, V, T, D>(
253        &self,
254        shard_id: ShardId,
255        diagnostics: Diagnostics,
256    ) -> Result<Machine<K, V, T, D>, InvalidUsage<T>>
257    where
258        K: Debug + Codec,
259        V: Debug + Codec,
260        T: Timestamp + Lattice + Codec64 + Sync,
261        D: Monoid + Codec64 + Send + Sync,
262    {
263        let state_versions = StateVersions::new(
264            self.cfg.clone(),
265            Arc::clone(&self.consensus),
266            Arc::clone(&self.blob),
267            Arc::clone(&self.metrics),
268        );
269        let machine = Machine::<K, V, T, D>::new(
270            self.cfg.clone(),
271            shard_id,
272            Arc::clone(&self.metrics),
273            Arc::new(state_versions),
274            Arc::clone(&self.shared_states),
275            Arc::clone(&self.pubsub_sender),
276            Arc::clone(&self.isolated_runtime),
277            diagnostics.clone(),
278        )
279        .await?;
280        Ok(machine)
281    }
282
283    /// Provides capabilities for the durable TVC identified by `shard_id` at
284    /// its current since and upper frontiers.
285    ///
286    /// This method is a best-effort attempt to regain control of the frontiers
287    /// of a shard. Its most common uses are to recover capabilities that have
288    /// expired (leases) or to attempt to read a TVC that one did not create (or
289    /// otherwise receive capabilities for). If the frontiers have been fully
290    /// released by all other parties, this call may result in capabilities with
291    /// empty frontiers (which are useless).
292    ///
293    /// If `shard_id` has never been used before, initializes a new shard and
294    /// returns handles with `since` and `upper` frontiers set to initial values
295    /// of `Antichain::from_elem(T::minimum())`.
296    ///
297    /// The `schema` parameter is currently unused, but should be an object
298    /// that represents the schema of the data in the shard. This will be required
299    /// in the future.
300    #[instrument(level = "debug", fields(shard = %shard_id))]
301    pub async fn open<K, V, T, D>(
302        &self,
303        shard_id: ShardId,
304        key_schema: Arc<K::Schema>,
305        val_schema: Arc<V::Schema>,
306        diagnostics: Diagnostics,
307        use_critical_since: bool,
308    ) -> Result<(WriteHandle<K, V, T, D>, ReadHandle<K, V, T, D>), InvalidUsage<T>>
309    where
310        K: Debug + Codec,
311        V: Debug + Codec,
312        T: Timestamp + TotalOrder + Lattice + Codec64 + Sync,
313        D: Monoid + Ord + Codec64 + Send + Sync,
314    {
315        Ok((
316            self.open_writer(
317                shard_id,
318                Arc::clone(&key_schema),
319                Arc::clone(&val_schema),
320                diagnostics.clone(),
321            )
322            .await?,
323            self.open_leased_reader(
324                shard_id,
325                key_schema,
326                val_schema,
327                diagnostics,
328                use_critical_since,
329            )
330            .await?,
331        ))
332    }
333
334    /// [Self::open], but returning only a [ReadHandle].
335    ///
336    /// Use this to save latency and a bit of persist traffic if you're just
337    /// going to immediately drop or expire the [WriteHandle].
338    ///
339    /// The `_schema` parameter is currently unused, but should be an object
340    /// that represents the schema of the data in the shard. This will be required
341    /// in the future.
342    #[instrument(level = "debug", fields(shard = %shard_id))]
343    pub async fn open_leased_reader<K, V, T, D>(
344        &self,
345        shard_id: ShardId,
346        key_schema: Arc<K::Schema>,
347        val_schema: Arc<V::Schema>,
348        diagnostics: Diagnostics,
349        use_critical_since: bool,
350    ) -> Result<ReadHandle<K, V, T, D>, InvalidUsage<T>>
351    where
352        K: Debug + Codec,
353        V: Debug + Codec,
354        T: Timestamp + TotalOrder + Lattice + Codec64 + Sync,
355        D: Monoid + Codec64 + Send + Sync,
356    {
357        let machine = self.make_machine(shard_id, diagnostics.clone()).await?;
358        let gc = GarbageCollector::new(machine.clone(), Arc::clone(&self.isolated_runtime));
359
360        let reader_id = LeasedReaderId::new();
361        let (reader_state, maintenance) = machine
362            .register_leased_reader(
363                &reader_id,
364                &diagnostics.handle_purpose,
365                READER_LEASE_DURATION.get(&self.cfg),
366                use_critical_since,
367            )
368            .await;
369        maintenance.start_performing(&machine, &gc);
370        let schemas = Schemas {
371            id: None,
372            key: key_schema,
373            val: val_schema,
374        };
375        let reader = ReadHandle::new(
376            self.cfg.clone(),
377            Arc::clone(&self.metrics),
378            machine,
379            gc,
380            Arc::clone(&self.blob),
381            reader_id,
382            schemas,
383            reader_state,
384        )
385        .await;
386
387        Ok(reader)
388    }
389
390    /// Creates and returns a [BatchFetcher] for the given shard id.
391    #[instrument(level = "debug", fields(shard = %shard_id))]
392    pub async fn create_batch_fetcher<K, V, T, D>(
393        &self,
394        shard_id: ShardId,
395        key_schema: Arc<K::Schema>,
396        val_schema: Arc<V::Schema>,
397        is_transient: bool,
398        diagnostics: Diagnostics,
399    ) -> Result<BatchFetcher<K, V, T, D>, InvalidUsage<T>>
400    where
401        K: Debug + Codec,
402        V: Debug + Codec,
403        T: Timestamp + Lattice + Codec64 + Sync,
404        D: Monoid + Codec64 + Send + Sync,
405    {
406        let machine = self.make_machine(shard_id, diagnostics.clone()).await?;
407        let read_schemas = Schemas {
408            id: None,
409            key: key_schema,
410            val: val_schema,
411        };
412        let schema_cache = machine.applier.schema_cache();
413        let fetcher = BatchFetcher {
414            cfg: BatchFetcherConfig::new(&self.cfg),
415            blob: Arc::clone(&self.blob),
416            metrics: Arc::clone(&self.metrics),
417            shard_metrics: Arc::clone(&machine.applier.shard_metrics),
418            shard_id,
419            read_schemas,
420            schema_cache,
421            is_transient,
422            _phantom: PhantomData,
423        };
424
425        Ok(fetcher)
426    }
427
428    /// A convenience [CriticalReaderId] for Materialize controllers.
429    ///
430    /// For most (soon to be all?) shards in Materialize, a centralized
431    /// "controller" is the authority for when a user no longer needs to read at
432    /// a given frontier. (Other uses are temporary holds where correctness of
433    /// the overall system can be maintained through a lease timeout.) To make
434    /// [SinceHandle] easier to work with, we offer this convenience id for
435    /// Materialize controllers, so they don't have to durably record it.
436    ///
437    /// TODO: We're still shaking out whether the controller should be the only
438    /// critical since hold or if there are other places we want them. If the
439    /// former, we should remove [CriticalReaderId] and bake in the singular
440    /// nature of the controller critical handle.
441    ///
442    /// ```rust
443    /// // This prints as something that is not 0 but is visually recognizable.
444    /// assert_eq!(
445    ///     mz_persist_client::PersistClient::CONTROLLER_CRITICAL_SINCE.to_string(),
446    ///     "c00000000-1111-2222-3333-444444444444",
447    /// )
448    /// ```
449    pub const CONTROLLER_CRITICAL_SINCE: CriticalReaderId =
450        CriticalReaderId([0, 0, 0, 0, 17, 17, 34, 34, 51, 51, 68, 68, 68, 68, 68, 68]);
451
452    /// Provides a capability for the durable TVC identified by `shard_id` at
453    /// its current since frontier.
454    ///
455    /// In contrast to the time-leased [ReadHandle] returned by [Self::open] and
456    /// [Self::open_leased_reader], this handle and its associated capability
457    /// are not leased. A [SinceHandle] does not release its since capability;
458    /// downgrade to the empty antichain to hold back the since.
459    /// Also unlike `ReadHandle`, the handle is not expired on drop.
460    /// This is less ergonomic, but useful for "critical" since
461    /// holds which must survive even lease timeouts.
462    ///
463    /// **IMPORTANT**: The above means that if a SinceHandle is registered and
464    /// then lost, the shard's since will be permanently "stuck", forever
465    /// preventing logical compaction. Users are advised to durably record
466    /// (preferably in code) the intended [CriticalReaderId] _before_ registering
467    /// a SinceHandle (in case the process crashes at the wrong time).
468    ///
469    /// If `shard_id` has never been used before, initializes a new shard and
470    /// return a handle with its `since` frontier set to the initial value of
471    /// `Antichain::from_elem(T::minimum())`.
472    #[instrument(level = "debug", fields(shard = %shard_id))]
473    pub async fn open_critical_since<K, V, T, D>(
474        &self,
475        shard_id: ShardId,
476        reader_id: CriticalReaderId,
477        default_opaque: Opaque,
478        diagnostics: Diagnostics,
479    ) -> Result<SinceHandle<K, V, T, D>, InvalidUsage<T>>
480    where
481        K: Debug + Codec,
482        V: Debug + Codec,
483        T: Timestamp + Lattice + Codec64 + Sync,
484        D: Monoid + Codec64 + Send + Sync,
485    {
486        let machine = self.make_machine(shard_id, diagnostics.clone()).await?;
487        let gc = GarbageCollector::new(machine.clone(), Arc::clone(&self.isolated_runtime));
488
489        let (state, maintenance) = machine
490            .register_critical_reader(&reader_id, default_opaque, &diagnostics.handle_purpose)
491            .await;
492        maintenance.start_performing(&machine, &gc);
493        let handle = SinceHandle::new(machine, gc, reader_id, state.since, state.opaque);
494
495        Ok(handle)
496    }
497
498    /// [Self::open], but returning only a [WriteHandle].
499    ///
500    /// Use this to save latency and a bit of persist traffic if you're just
501    /// going to immediately drop or expire the [ReadHandle].
502    #[instrument(level = "debug", fields(shard = %shard_id))]
503    pub async fn open_writer<K, V, T, D>(
504        &self,
505        shard_id: ShardId,
506        key_schema: Arc<K::Schema>,
507        val_schema: Arc<V::Schema>,
508        diagnostics: Diagnostics,
509    ) -> Result<WriteHandle<K, V, T, D>, InvalidUsage<T>>
510    where
511        K: Debug + Codec,
512        V: Debug + Codec,
513        T: Timestamp + TotalOrder + Lattice + Codec64 + Sync,
514        D: Monoid + Ord + Codec64 + Send + Sync,
515    {
516        let machine = self.make_machine(shard_id, diagnostics.clone()).await?;
517        let gc = GarbageCollector::new(machine.clone(), Arc::clone(&self.isolated_runtime));
518
519        // We defer registering the schema until write time, to allow opening
520        // write handles in a "read-only" mode where they don't implicitly
521        // modify persist state. But it might already be registered, in which
522        // case we can fetch its ID.
523        let schema_id = machine.find_schema(&*key_schema, &*val_schema);
524
525        let writer_id = WriterId::new();
526        let schemas = Schemas {
527            id: schema_id,
528            key: key_schema,
529            val: val_schema,
530        };
531        let writer = WriteHandle::new(
532            self.cfg.clone(),
533            Arc::clone(&self.metrics),
534            machine,
535            gc,
536            Arc::clone(&self.blob),
537            writer_id,
538            &diagnostics.handle_purpose,
539            schemas,
540        );
541        Ok(writer)
542    }
543
544    /// Returns a [BatchBuilder] that can be used to write a batch of updates to
545    /// blob storage which can then be appended to the given shard using
546    /// [WriteHandle::compare_and_append_batch] or [WriteHandle::append_batch],
547    /// or which can be read using [PersistClient::read_batches_consolidated].
548    ///
549    /// The builder uses a bounded amount of memory, even when the number of
550    /// updates is very large. Individual records, however, should be small
551    /// enough that we can reasonably chunk them up: O(KB) is definitely fine,
552    /// O(MB) come talk to us.
553    ///
554    /// `max_runs` overrides `persist_batch_max_runs` when `Some`. With `None`, the builder honors
555    /// the configured value, as [WriteHandle::builder] does.
556    #[instrument(level = "debug", fields(shard = %shard_id))]
557    pub fn batch_builder<K, V, T, D>(
558        &self,
559        shard_id: ShardId,
560        shard_name: &str,
561        write_schemas: Schemas<K, V>,
562        lower: Antichain<T>,
563        max_runs: Option<usize>,
564    ) -> BatchBuilder<K, V, T, D>
565    where
566        K: Debug + Codec,
567        V: Debug + Codec,
568        T: Timestamp + Lattice + Codec64 + TotalOrder + Sync,
569        D: Monoid + Ord + Codec64 + Send + Sync,
570    {
571        let mut compact_cfg = CompactConfig::new(&self.cfg, shard_id);
572        if let Some(max_runs) = max_runs {
573            compact_cfg.batch.max_runs = Some(max_runs);
574        }
575        WriteHandle::builder_inner(
576            &self.cfg,
577            compact_cfg,
578            Arc::clone(&self.metrics),
579            self.metrics.shards.shard(&shard_id, shard_name),
580            &self.metrics.user,
581            Arc::clone(&self.isolated_runtime),
582            Arc::clone(&self.blob),
583            shard_id,
584            write_schemas,
585            lower,
586        )
587    }
588
589    /// Turns the given [`ProtoBatch`] back into a [`Batch`] which can be used
590    /// to append it to the given shard or to read it via
591    /// [PersistClient::read_batches_consolidated]
592    ///
593    /// CAUTION: This API allows turning a [ProtoBatch] into a [Batch] multiple
594    /// times, but if a batch is deleted the backing data goes away, so at that
595    /// point all in-memory copies of a batch become invalid and cannot be read
596    /// anymore.
597    pub fn batch_from_transmittable_batch<K, V, T, D>(
598        &self,
599        shard_id: &ShardId,
600        batch: ProtoBatch,
601    ) -> Batch<K, V, T, D>
602    where
603        K: Debug + Codec,
604        V: Debug + Codec,
605        T: Timestamp + Lattice + Codec64 + Sync,
606        D: Monoid + Ord + Codec64 + Send + Sync,
607    {
608        let batch_shard_id: ShardId = batch
609            .shard_id
610            .into_rust()
611            .expect("valid transmittable batch");
612        assert_eq!(&batch_shard_id, shard_id);
613
614        let shard_metrics = self.metrics.shards.shard(shard_id, "peek_stash");
615
616        let ret = Batch {
617            batch_delete_enabled: BATCH_DELETE_ENABLED.get(&self.cfg),
618            metrics: Arc::clone(&self.metrics),
619            shard_metrics,
620            version: Version::parse(&batch.version).expect("valid transmittable batch"),
621            schemas: (batch.key_schema, batch.val_schema),
622            batch: batch
623                .batch
624                .into_rust_if_some("ProtoBatch::batch")
625                .expect("valid transmittable batch"),
626            blob: Arc::clone(&self.blob),
627            _phantom: std::marker::PhantomData,
628        };
629
630        assert_eq!(&ret.shard_id(), shard_id);
631        ret
632    }
633
634    /// Returns a [Cursor] for reading the given batches. Yielded updates are
635    /// consolidated if the given batches contain sorted runs, which is true
636    /// when they have been written using a [BatchBuilder].
637    ///
638    /// To keep memory usage down when reading a snapshot that consolidates
639    /// well, this consolidates as it goes. However, note that only the
640    /// serialized data is consolidated: the deserialized data will only be
641    /// consolidated if your K/V codecs are one-to-one.
642    ///
643    /// CAUTION: The caller needs to make sure that the given batches are
644    /// readable and they have to remain readable for the lifetime of the
645    /// returned [Cursor]. The caller is also responsible for the lifecycle of
646    /// the batches: once the cursor and the batches are no longer needed you
647    /// must call [Cursor::into_lease] to get back the batches and delete them.
648    #[allow(clippy::unused_async)]
649    pub async fn read_batches_consolidated<K, V, T, D>(
650        &mut self,
651        shard_id: ShardId,
652        as_of: Antichain<T>,
653        read_schemas: Schemas<K, V>,
654        batches: Vec<Batch<K, V, T, D>>,
655        should_fetch_part: impl for<'a> Fn(Option<&'a LazyPartStats>) -> bool,
656        memory_budget_bytes: usize,
657    ) -> Result<Cursor<K, V, T, D, Vec<Batch<K, V, T, D>>>, Since<T>>
658    where
659        K: Debug + Codec + Ord,
660        V: Debug + Codec + Ord,
661        T: Timestamp + Lattice + Codec64 + TotalOrder + Sync,
662        D: Monoid + Ord + Codec64 + Send + Sync,
663    {
664        let shard_metrics = self.metrics.shards.shard(&shard_id, "peek_stash");
665
666        let hollow_batches = batches.iter().map(|b| b.batch.clone()).collect_vec();
667
668        ReadHandle::read_batches_consolidated(
669            &self.cfg,
670            Arc::clone(&self.metrics),
671            shard_metrics,
672            self.metrics.read.snapshot.clone(),
673            Arc::clone(&self.blob),
674            shard_id,
675            as_of,
676            read_schemas,
677            &hollow_batches,
678            batches,
679            should_fetch_part,
680            memory_budget_bytes,
681        )
682    }
683
684    /// Returns the requested schema, if known at the current state.
685    pub async fn get_schema<K, V, T, D>(
686        &self,
687        shard_id: ShardId,
688        schema_id: SchemaId,
689        diagnostics: Diagnostics,
690    ) -> Result<Option<(K::Schema, V::Schema)>, InvalidUsage<T>>
691    where
692        K: Debug + Codec,
693        V: Debug + Codec,
694        T: Timestamp + Lattice + Codec64 + Sync,
695        D: Monoid + Codec64 + Send + Sync,
696    {
697        let machine = self
698            .make_machine::<K, V, T, D>(shard_id, diagnostics)
699            .await?;
700        Ok(machine.get_schema(schema_id))
701    }
702
703    /// Returns the latest schema registered at the current state.
704    pub async fn latest_schema<K, V, T, D>(
705        &self,
706        shard_id: ShardId,
707        diagnostics: Diagnostics,
708    ) -> Result<Option<(SchemaId, K::Schema, V::Schema)>, InvalidUsage<T>>
709    where
710        K: Debug + Codec,
711        V: Debug + Codec,
712        T: Timestamp + Lattice + Codec64 + Sync,
713        D: Monoid + Codec64 + Send + Sync,
714    {
715        let machine = self
716            .make_machine::<K, V, T, D>(shard_id, diagnostics)
717            .await?;
718        Ok(machine.latest_schema())
719    }
720
721    /// Fetches and returns a recent shard-global `upper`, without requiring a
722    /// [`WriteHandle`].
723    ///
724    /// Importantly, this operation is linearized with write operations, giving
725    /// the same guarantee as [`WriteHandle::fetch_recent_upper`]. It requires
726    /// fetching the latest state from consensus and is therefore a potentially
727    /// expensive operation.
728    ///
729    /// If `shard_id` has never been used before, initializes the shard and
730    /// returns an upper of `Antichain::from_elem(T::minimum())`.
731    pub async fn recent_upper<K, V, T, D>(
732        &self,
733        shard_id: ShardId,
734        diagnostics: Diagnostics,
735    ) -> Result<Antichain<T>, InvalidUsage<T>>
736    where
737        K: Debug + Codec,
738        V: Debug + Codec,
739        T: Timestamp + Lattice + Codec64 + Sync,
740        D: Monoid + Codec64 + Send + Sync,
741    {
742        let machine = self
743            .make_machine::<K, V, T, D>(shard_id, diagnostics)
744            .await?;
745        Ok(machine.applier.fetch_upper(|upper| upper.clone()).await)
746    }
747
748    /// Registers a schema for the given shard.
749    ///
750    /// Returns the new schema ID if the registration succeeds, and `None`
751    /// otherwise. Schema registration succeeds in two cases:
752    ///  a) No schema was currently registered for the shard.
753    ///  b) The given schema is already registered for the shard.
754    ///
755    /// To evolve an existing schema instead, use
756    /// [PersistClient::compare_and_evolve_schema].
757    //
758    // TODO: unify with `compare_and_evolve_schema`
759    pub async fn register_schema<K, V, T, D>(
760        &self,
761        shard_id: ShardId,
762        key_schema: &K::Schema,
763        val_schema: &V::Schema,
764        diagnostics: Diagnostics,
765    ) -> Result<Option<SchemaId>, InvalidUsage<T>>
766    where
767        K: Debug + Codec,
768        V: Debug + Codec,
769        T: Timestamp + Lattice + Codec64 + Sync,
770        D: Monoid + Codec64 + Send + Sync,
771    {
772        let machine = self
773            .make_machine::<K, V, T, D>(shard_id, diagnostics)
774            .await?;
775        let gc = GarbageCollector::new(machine.clone(), Arc::clone(&self.isolated_runtime));
776
777        let (schema_id, maintenance) = machine.register_schema(key_schema, val_schema).await;
778        maintenance.start_performing(&machine, &gc);
779
780        Ok(schema_id)
781    }
782
783    /// Registers a new latest schema for the given shard.
784    ///
785    /// This new schema must be [backward_compatible] with all previous schemas
786    /// for this shard. If it's not, [CaESchema::Incompatible] is returned.
787    ///
788    /// [backward_compatible]: mz_persist_types::schema::backward_compatible
789    ///
790    /// To prevent races, the caller must declare what it believes to be the
791    /// latest schema id. A request that already matches the current schema
792    /// returns [CaESchema::Ok] even if `expected` is stale, so retried or
793    /// duplicated evolutions are idempotent. [CaESchema::ExpectedMismatch]
794    /// means a genuine divergence: the current schema matches neither the
795    /// request nor `expected`.
796    pub async fn compare_and_evolve_schema<K, V, T, D>(
797        &self,
798        shard_id: ShardId,
799        expected: SchemaId,
800        key_schema: &K::Schema,
801        val_schema: &V::Schema,
802        diagnostics: Diagnostics,
803    ) -> Result<CaESchema<K, V>, InvalidUsage<T>>
804    where
805        K: Debug + Codec,
806        V: Debug + Codec,
807        T: Timestamp + Lattice + Codec64 + Sync,
808        D: Monoid + Codec64 + Send + Sync,
809    {
810        let machine = self
811            .make_machine::<K, V, T, D>(shard_id, diagnostics)
812            .await?;
813        let gc = GarbageCollector::new(machine.clone(), Arc::clone(&self.isolated_runtime));
814        let (res, maintenance) = machine
815            .compare_and_evolve_schema(expected, key_schema, val_schema)
816            .await;
817        maintenance.start_performing(&machine, &gc);
818        Ok(res)
819    }
820
821    /// Check if the given shard is in a finalized state; ie. it can no longer be
822    /// read, any data that was written to it is no longer accessible, and we've
823    /// discarded references to that data from state.
824    pub async fn is_finalized<K, V, T, D>(
825        &self,
826        shard_id: ShardId,
827        diagnostics: Diagnostics,
828    ) -> Result<bool, InvalidUsage<T>>
829    where
830        K: Debug + Codec,
831        V: Debug + Codec,
832        T: Timestamp + Lattice + Codec64 + Sync,
833        D: Monoid + Codec64 + Send + Sync,
834    {
835        let machine = self
836            .make_machine::<K, V, T, D>(shard_id, diagnostics)
837            .await?;
838        Ok(machine.is_finalized())
839    }
840
841    /// If a shard is guaranteed to never be used again, finalize it to delete
842    /// the associated data and release any associated resources. (Except for a
843    /// little state in consensus we use to represent the tombstone.)
844    ///
845    /// The caller should ensure that both the `since` and `upper` of the shard
846    /// have been advanced to `[]`: ie. the shard is no longer writable or readable.
847    /// Otherwise an error is returned.
848    ///
849    /// Once `finalize_shard` has been called, the result of future operations on
850    /// the shard are not defined. They may return errors or succeed as a noop.
851    #[instrument(level = "debug", fields(shard = %shard_id))]
852    pub async fn finalize_shard<K, V, T, D>(
853        &self,
854        shard_id: ShardId,
855        diagnostics: Diagnostics,
856    ) -> Result<(), InvalidUsage<T>>
857    where
858        K: Debug + Codec,
859        V: Debug + Codec,
860        T: Timestamp + Lattice + Codec64 + Sync,
861        D: Monoid + Codec64 + Send + Sync,
862    {
863        let machine = self
864            .make_machine::<K, V, T, D>(shard_id, diagnostics)
865            .await?;
866
867        let maintenance = machine.become_tombstone().await?;
868        let gc = GarbageCollector::new(machine.clone(), Arc::clone(&self.isolated_runtime));
869
870        let () = maintenance.perform(&machine, &gc).await;
871
872        Ok(())
873    }
874
875    /// Upgrade the state to the latest version. This should only be called once we will no longer
876    /// need to interoperate with older versions, like after a successful upgrade.
877    pub async fn upgrade_version<K, V, T, D>(
878        &self,
879        shard_id: ShardId,
880        diagnostics: Diagnostics,
881    ) -> Result<(), InvalidUsage<T>>
882    where
883        K: Debug + Codec,
884        V: Debug + Codec,
885        T: Timestamp + Lattice + Codec64 + Sync,
886        D: Monoid + Codec64 + Send + Sync,
887    {
888        let machine = self
889            .make_machine::<K, V, T, D>(shard_id, diagnostics)
890            .await?;
891
892        match machine.upgrade_version().await {
893            Ok(maintenance) => {
894                let gc = GarbageCollector::new(machine.clone(), Arc::clone(&self.isolated_runtime));
895                let () = maintenance.perform(&machine, &gc).await;
896                Ok(())
897            }
898            Err(version) => Err(InvalidUsage::IncompatibleVersion { version }),
899        }
900    }
901
902    /// Returns the internal state of the shard for debugging and QA.
903    ///
904    /// We'll be thoughtful about making unnecessary changes, but the **output
905    /// of this method needs to be gated from users**, so that it's not subject
906    /// to our backward compatibility guarantees.
907    pub async fn inspect_shard<T: Timestamp + Lattice + Codec64>(
908        &self,
909        shard_id: &ShardId,
910    ) -> Result<impl serde::Serialize, anyhow::Error> {
911        let state_versions = StateVersions::new(
912            self.cfg.clone(),
913            Arc::clone(&self.consensus),
914            Arc::clone(&self.blob),
915            Arc::clone(&self.metrics),
916        );
917        // TODO: Don't fetch all live diffs. Feels like we should pull out a new
918        // method in StateVersions for fetching the latest version of State of a
919        // shard that might or might not exist.
920        let versions = state_versions.fetch_all_live_diffs(shard_id).await;
921        if versions.is_empty() {
922            return Err(anyhow::anyhow!("{} does not exist", shard_id));
923        }
924        let state = state_versions
925            .fetch_current_state::<T>(shard_id, versions)
926            .await;
927        let state = state.check_ts_codec(shard_id)?;
928        Ok(state)
929    }
930
931    /// Test helper for a [Self::open] call that is expected to succeed.
932    #[cfg(test)]
933    #[track_caller]
934    pub async fn expect_open<K, V, T, D>(
935        &self,
936        shard_id: ShardId,
937    ) -> (WriteHandle<K, V, T, D>, ReadHandle<K, V, T, D>)
938    where
939        K: Debug + Codec,
940        V: Debug + Codec,
941        T: Timestamp + TotalOrder + Lattice + Codec64 + Sync,
942        D: Monoid + Ord + Codec64 + Send + Sync,
943        K::Schema: Default,
944        V::Schema: Default,
945    {
946        self.open(
947            shard_id,
948            Arc::new(K::Schema::default()),
949            Arc::new(V::Schema::default()),
950            Diagnostics::for_tests(),
951            true,
952        )
953        .await
954        .expect("codec mismatch")
955    }
956
957    /// Return the metrics being used by this client.
958    ///
959    /// Only exposed for tests, persistcli, and benchmarks.
960    pub fn metrics(&self) -> &Arc<Metrics> {
961        &self.metrics
962    }
963}
964
965#[cfg(test)]
966mod tests {
967    use std::future::Future;
968    use std::pin::Pin;
969    use std::task::Context;
970    use std::time::Duration;
971
972    use differential_dataflow::consolidation::consolidate_updates;
973    use differential_dataflow::lattice::Lattice;
974    use futures_task::noop_waker;
975    use mz_dyncfg::ConfigUpdates;
976    use mz_ore::assert_ok;
977    use mz_persist::indexed::encoding::BlobTraceBatchPart;
978    use mz_persist::workload::DataGenerator;
979    use mz_persist_types::codec_impls::{StringSchema, VecU8Schema};
980    use mz_proto::protobuf_roundtrip;
981    use proptest::prelude::*;
982    use timely::order::PartialOrder;
983    use timely::progress::Antichain;
984
985    use crate::batch::BLOB_TARGET_SIZE;
986    use crate::cache::PersistClientCache;
987    use crate::cfg::{BATCH_BUILDER_MAX_OUTSTANDING_PARTS, GC_REMOVE_FINALIZED_ROLLUPS};
988    use crate::critical::Opaque;
989    use crate::error::{CodecConcreteType, CodecMismatch, UpperMismatch};
990    use crate::internal::paths::BlobKey;
991    use crate::read::ListenEvent;
992
993    use super::*;
994
995    pub fn new_test_client_cache(dyncfgs: &ConfigUpdates) -> PersistClientCache {
996        // Configure an aggressively small blob_target_size so we get some
997        // amount of coverage of that in tests. Similarly, for max_outstanding.
998        let mut cache = PersistClientCache::new_no_metrics();
999        cache.cfg.set_config(&BLOB_TARGET_SIZE, 10);
1000        cache
1001            .cfg
1002            .set_config(&BATCH_BUILDER_MAX_OUTSTANDING_PARTS, 1);
1003        // Off by default only for upgrade compatibility.
1004        cache.cfg.set_config(&GC_REMOVE_FINALIZED_ROLLUPS, true);
1005        dyncfgs.apply(cache.cfg());
1006
1007        // Enable compaction in tests to ensure we get coverage.
1008        cache.cfg.compaction_enabled = true;
1009        cache
1010    }
1011
1012    pub async fn new_test_client(dyncfgs: &ConfigUpdates) -> PersistClient {
1013        let cache = new_test_client_cache(dyncfgs);
1014        cache
1015            .open(PersistLocation::new_in_mem())
1016            .await
1017            .expect("client construction failed")
1018    }
1019
1020    pub fn all_ok<'a, K, V, T, D, I>(iter: I, as_of: T) -> Vec<((K, V), T, D)>
1021    where
1022        K: Ord + Clone + 'a,
1023        V: Ord + Clone + 'a,
1024        T: Timestamp + Lattice + Clone + 'a,
1025        D: Monoid + Clone + 'a,
1026        I: IntoIterator<Item = &'a ((K, V), T, D)>,
1027    {
1028        let as_of = Antichain::from_elem(as_of);
1029        let mut ret = iter
1030            .into_iter()
1031            .map(|((k, v), t, d)| {
1032                let mut t = t.clone();
1033                t.advance_by(as_of.borrow());
1034                ((k.clone(), v.clone()), t, d.clone())
1035            })
1036            .collect();
1037        consolidate_updates(&mut ret);
1038        ret
1039    }
1040
1041    pub async fn expect_fetch_part<K, V, T, D>(
1042        blob: &dyn Blob,
1043        key: &BlobKey,
1044        metrics: &Metrics,
1045        read_schemas: &Schemas<K, V>,
1046    ) -> (BlobTraceBatchPart<T>, Vec<((K, V), T, D)>)
1047    where
1048        K: Codec + Clone,
1049        V: Codec + Clone,
1050        T: Timestamp + Codec64,
1051        D: Codec64,
1052    {
1053        let value = blob
1054            .get(key)
1055            .await
1056            .expect("failed to fetch part")
1057            .expect("missing part");
1058        let mut part =
1059            BlobTraceBatchPart::decode(&value, &metrics.columnar).expect("failed to decode part");
1060        let structured = part
1061            .updates
1062            .into_part::<K, V>(&*read_schemas.key, &*read_schemas.val);
1063        let updates = structured
1064            .decode_iter::<K, V, T, D>(&*read_schemas.key, &*read_schemas.val)
1065            .expect("structured data")
1066            .collect();
1067        (part, updates)
1068    }
1069
1070    #[mz_persist_proc::test(tokio::test)]
1071    #[cfg_attr(miri, ignore)] // unsupported operation: returning ready events from epoll_wait is not yet implemented
1072    async fn sanity_check(dyncfgs: ConfigUpdates) {
1073        let data = [
1074            (("1".to_owned(), "one".to_owned()), 1, 1),
1075            (("2".to_owned(), "two".to_owned()), 2, 1),
1076            (("3".to_owned(), "three".to_owned()), 3, 1),
1077        ];
1078
1079        let (mut write, mut read) = new_test_client(&dyncfgs)
1080            .await
1081            .expect_open::<String, String, u64, i64>(ShardId::new())
1082            .await;
1083        assert_eq!(write.upper(), &Antichain::from_elem(u64::minimum()));
1084        assert_eq!(read.since(), &Antichain::from_elem(u64::minimum()));
1085
1086        // Write a [0,3) batch.
1087        write
1088            .expect_append(&data[..2], write.upper().clone(), vec![3])
1089            .await;
1090        assert_eq!(write.upper(), &Antichain::from_elem(3));
1091
1092        // Grab a snapshot and listener as_of 1. Snapshot should only have part of what we wrote.
1093        assert_eq!(
1094            read.expect_snapshot_and_fetch(1).await,
1095            all_ok(&data[..1], 1)
1096        );
1097
1098        let mut listen = read.clone("").await.expect_listen(1).await;
1099
1100        // Write a [3,4) batch.
1101        write
1102            .expect_append(&data[2..], write.upper().clone(), vec![4])
1103            .await;
1104        assert_eq!(write.upper(), &Antichain::from_elem(4));
1105
1106        // Listen should have part of the initial write plus the new one.
1107        assert_eq!(
1108            listen.read_until(&4).await,
1109            (all_ok(&data[1..], 1), Antichain::from_elem(4))
1110        );
1111
1112        // Downgrading the since is tracked locally (but otherwise is a no-op).
1113        read.downgrade_since(&Antichain::from_elem(2)).await;
1114        assert_eq!(read.since(), &Antichain::from_elem(2));
1115    }
1116
1117    // Sanity check that the open_reader and open_writer calls work.
1118    #[mz_persist_proc::test(tokio::test)]
1119    #[cfg_attr(miri, ignore)] // unsupported operation: returning ready events from epoll_wait is not yet implemented
1120    async fn open_reader_writer(dyncfgs: ConfigUpdates) {
1121        let data = vec![
1122            (("1".to_owned(), "one".to_owned()), 1, 1),
1123            (("2".to_owned(), "two".to_owned()), 2, 1),
1124            (("3".to_owned(), "three".to_owned()), 3, 1),
1125        ];
1126
1127        let shard_id = ShardId::new();
1128        let client = new_test_client(&dyncfgs).await;
1129        let mut write1 = client
1130            .open_writer::<String, String, u64, i64>(
1131                shard_id,
1132                Arc::new(StringSchema),
1133                Arc::new(StringSchema),
1134                Diagnostics::for_tests(),
1135            )
1136            .await
1137            .expect("codec mismatch");
1138        let mut read1 = client
1139            .open_leased_reader::<String, String, u64, i64>(
1140                shard_id,
1141                Arc::new(StringSchema),
1142                Arc::new(StringSchema),
1143                Diagnostics::for_tests(),
1144                true,
1145            )
1146            .await
1147            .expect("codec mismatch");
1148        let mut read2 = client
1149            .open_leased_reader::<String, String, u64, i64>(
1150                shard_id,
1151                Arc::new(StringSchema),
1152                Arc::new(StringSchema),
1153                Diagnostics::for_tests(),
1154                true,
1155            )
1156            .await
1157            .expect("codec mismatch");
1158        let mut write2 = client
1159            .open_writer::<String, String, u64, i64>(
1160                shard_id,
1161                Arc::new(StringSchema),
1162                Arc::new(StringSchema),
1163                Diagnostics::for_tests(),
1164            )
1165            .await
1166            .expect("codec mismatch");
1167
1168        write2.expect_compare_and_append(&data[..1], 0, 2).await;
1169        assert_eq!(
1170            read2.expect_snapshot_and_fetch(1).await,
1171            all_ok(&data[..1], 1)
1172        );
1173        write1.expect_compare_and_append(&data[1..], 2, 4).await;
1174        assert_eq!(read1.expect_snapshot_and_fetch(3).await, all_ok(&data, 3));
1175    }
1176
1177    #[mz_persist_proc::test(tokio::test)]
1178    #[cfg_attr(miri, ignore)] // too slow
1179    async fn invalid_usage(dyncfgs: ConfigUpdates) {
1180        let data = vec![
1181            (("1".to_owned(), "one".to_owned()), 1, 1),
1182            (("2".to_owned(), "two".to_owned()), 2, 1),
1183            (("3".to_owned(), "three".to_owned()), 3, 1),
1184        ];
1185
1186        let shard_id0 = "s00000000-0000-0000-0000-000000000000"
1187            .parse::<ShardId>()
1188            .expect("invalid shard id");
1189        let mut client = new_test_client(&dyncfgs).await;
1190
1191        let (mut write0, mut read0) = client
1192            .expect_open::<String, String, u64, i64>(shard_id0)
1193            .await;
1194
1195        write0.expect_compare_and_append(&data, 0, 4).await;
1196
1197        // InvalidUsage from PersistClient methods.
1198        {
1199            fn codecs(
1200                k: &str,
1201                v: &str,
1202                t: &str,
1203                d: &str,
1204            ) -> (String, String, String, String, Option<CodecConcreteType>) {
1205                (k.to_owned(), v.to_owned(), t.to_owned(), d.to_owned(), None)
1206            }
1207
1208            client.shared_states = Arc::new(StateCache::new_no_metrics());
1209            assert_eq!(
1210                client
1211                    .open::<Vec<u8>, String, u64, i64>(
1212                        shard_id0,
1213                        Arc::new(VecU8Schema),
1214                        Arc::new(StringSchema),
1215                        Diagnostics::for_tests(),
1216                        true,
1217                    )
1218                    .await
1219                    .unwrap_err(),
1220                InvalidUsage::CodecMismatch(Box::new(CodecMismatch {
1221                    requested: codecs("Vec<u8>", "String", "u64", "i64"),
1222                    actual: codecs("String", "String", "u64", "i64"),
1223                }))
1224            );
1225            assert_eq!(
1226                client
1227                    .open::<String, Vec<u8>, u64, i64>(
1228                        shard_id0,
1229                        Arc::new(StringSchema),
1230                        Arc::new(VecU8Schema),
1231                        Diagnostics::for_tests(),
1232                        true,
1233                    )
1234                    .await
1235                    .unwrap_err(),
1236                InvalidUsage::CodecMismatch(Box::new(CodecMismatch {
1237                    requested: codecs("String", "Vec<u8>", "u64", "i64"),
1238                    actual: codecs("String", "String", "u64", "i64"),
1239                }))
1240            );
1241            assert_eq!(
1242                client
1243                    .open::<String, String, i64, i64>(
1244                        shard_id0,
1245                        Arc::new(StringSchema),
1246                        Arc::new(StringSchema),
1247                        Diagnostics::for_tests(),
1248                        true,
1249                    )
1250                    .await
1251                    .unwrap_err(),
1252                InvalidUsage::CodecMismatch(Box::new(CodecMismatch {
1253                    requested: codecs("String", "String", "i64", "i64"),
1254                    actual: codecs("String", "String", "u64", "i64"),
1255                }))
1256            );
1257            assert_eq!(
1258                client
1259                    .open::<String, String, u64, u64>(
1260                        shard_id0,
1261                        Arc::new(StringSchema),
1262                        Arc::new(StringSchema),
1263                        Diagnostics::for_tests(),
1264                        true,
1265                    )
1266                    .await
1267                    .unwrap_err(),
1268                InvalidUsage::CodecMismatch(Box::new(CodecMismatch {
1269                    requested: codecs("String", "String", "u64", "u64"),
1270                    actual: codecs("String", "String", "u64", "i64"),
1271                }))
1272            );
1273
1274            // open_reader and open_writer end up using the same checks, so just
1275            // verify one type each to verify the plumbing instead of the full
1276            // set.
1277            assert_eq!(
1278                client
1279                    .open_leased_reader::<Vec<u8>, String, u64, i64>(
1280                        shard_id0,
1281                        Arc::new(VecU8Schema),
1282                        Arc::new(StringSchema),
1283                        Diagnostics::for_tests(),
1284                        true,
1285                    )
1286                    .await
1287                    .unwrap_err(),
1288                InvalidUsage::CodecMismatch(Box::new(CodecMismatch {
1289                    requested: codecs("Vec<u8>", "String", "u64", "i64"),
1290                    actual: codecs("String", "String", "u64", "i64"),
1291                }))
1292            );
1293            assert_eq!(
1294                client
1295                    .open_writer::<Vec<u8>, String, u64, i64>(
1296                        shard_id0,
1297                        Arc::new(VecU8Schema),
1298                        Arc::new(StringSchema),
1299                        Diagnostics::for_tests(),
1300                    )
1301                    .await
1302                    .unwrap_err(),
1303                InvalidUsage::CodecMismatch(Box::new(CodecMismatch {
1304                    requested: codecs("Vec<u8>", "String", "u64", "i64"),
1305                    actual: codecs("String", "String", "u64", "i64"),
1306                }))
1307            );
1308        }
1309
1310        // InvalidUsage from ReadHandle methods.
1311        {
1312            let snap = read0
1313                .snapshot(Antichain::from_elem(3))
1314                .await
1315                .expect("cannot serve requested as_of");
1316
1317            let shard_id1 = "s11111111-1111-1111-1111-111111111111"
1318                .parse::<ShardId>()
1319                .expect("invalid shard id");
1320            let mut fetcher1 = client
1321                .create_batch_fetcher::<String, String, u64, i64>(
1322                    shard_id1,
1323                    Default::default(),
1324                    Default::default(),
1325                    false,
1326                    Diagnostics::for_tests(),
1327                )
1328                .await
1329                .unwrap();
1330            for part in snap {
1331                let (part, _lease) = part.into_exchangeable_part();
1332                let res = fetcher1.fetch_leased_part(part).await;
1333                assert_eq!(
1334                    res.unwrap_err(),
1335                    InvalidUsage::BatchNotFromThisShard {
1336                        batch_shard: shard_id0,
1337                        handle_shard: shard_id1,
1338                    }
1339                );
1340            }
1341        }
1342
1343        // InvalidUsage from WriteHandle methods.
1344        {
1345            let ts3 = &data[2];
1346            assert_eq!(ts3.1, 3);
1347            let ts3 = vec![ts3.clone()];
1348
1349            // WriteHandle::append also covers append_batch,
1350            // compare_and_append_batch, compare_and_append.
1351            assert_eq!(
1352                write0
1353                    .append(&ts3, Antichain::from_elem(4), Antichain::from_elem(5))
1354                    .await
1355                    .unwrap_err(),
1356                InvalidUsage::UpdateNotBeyondLower {
1357                    ts: 3,
1358                    lower: Antichain::from_elem(4),
1359                },
1360            );
1361            assert_eq!(
1362                write0
1363                    .append(&ts3, Antichain::from_elem(2), Antichain::from_elem(3))
1364                    .await
1365                    .unwrap_err(),
1366                InvalidUsage::UpdateBeyondUpper {
1367                    ts: 3,
1368                    expected_upper: Antichain::from_elem(3),
1369                },
1370            );
1371            // NB unlike the previous tests, this one has empty updates.
1372            assert_eq!(
1373                write0
1374                    .append(&data[..0], Antichain::from_elem(3), Antichain::from_elem(2))
1375                    .await
1376                    .unwrap_err(),
1377                InvalidUsage::InvalidBounds {
1378                    lower: Antichain::from_elem(3),
1379                    upper: Antichain::from_elem(2),
1380                },
1381            );
1382
1383            // Tests for the BatchBuilder.
1384            assert_eq!(
1385                write0
1386                    .builder(Antichain::from_elem(3))
1387                    .finish(Antichain::from_elem(2))
1388                    .await
1389                    .unwrap_err(),
1390                InvalidUsage::InvalidBounds {
1391                    lower: Antichain::from_elem(3),
1392                    upper: Antichain::from_elem(2)
1393                },
1394            );
1395            let batch = write0
1396                .batch(&ts3, Antichain::from_elem(3), Antichain::from_elem(4))
1397                .await
1398                .expect("invalid usage");
1399            assert_eq!(
1400                write0
1401                    .append_batch(batch, Antichain::from_elem(4), Antichain::from_elem(5))
1402                    .await
1403                    .unwrap_err(),
1404                InvalidUsage::InvalidBatchBounds {
1405                    batch_lower: Antichain::from_elem(3),
1406                    batch_upper: Antichain::from_elem(4),
1407                    append_lower: Antichain::from_elem(4),
1408                    append_upper: Antichain::from_elem(5),
1409                },
1410            );
1411            let batch = write0
1412                .batch(&ts3, Antichain::from_elem(3), Antichain::from_elem(4))
1413                .await
1414                .expect("invalid usage");
1415            assert_eq!(
1416                write0
1417                    .append_batch(batch, Antichain::from_elem(2), Antichain::from_elem(3))
1418                    .await
1419                    .unwrap_err(),
1420                InvalidUsage::InvalidBatchBounds {
1421                    batch_lower: Antichain::from_elem(3),
1422                    batch_upper: Antichain::from_elem(4),
1423                    append_lower: Antichain::from_elem(2),
1424                    append_upper: Antichain::from_elem(3),
1425                },
1426            );
1427            let batch = write0
1428                .batch(&ts3, Antichain::from_elem(3), Antichain::from_elem(4))
1429                .await
1430                .expect("invalid usage");
1431            // NB unlike the others, this one uses matches! because it's
1432            // non-deterministic (the key)
1433            assert!(matches!(
1434                write0
1435                    .append_batch(batch, Antichain::from_elem(3), Antichain::from_elem(3))
1436                    .await
1437                    .unwrap_err(),
1438                InvalidUsage::InvalidEmptyTimeInterval { .. }
1439            ));
1440        }
1441    }
1442
1443    #[mz_persist_proc::test(tokio::test)]
1444    #[cfg_attr(miri, ignore)] // unsupported operation: returning ready events from epoll_wait is not yet implemented
1445    async fn multiple_shards(dyncfgs: ConfigUpdates) {
1446        let data1 = [
1447            (("1".to_owned(), "one".to_owned()), 1, 1),
1448            (("2".to_owned(), "two".to_owned()), 2, 1),
1449        ];
1450
1451        let data2 = [(("1".to_owned(), ()), 1, 1), (("2".to_owned(), ()), 2, 1)];
1452
1453        let client = new_test_client(&dyncfgs).await;
1454
1455        let (mut write1, mut read1) = client
1456            .expect_open::<String, String, u64, i64>(ShardId::new())
1457            .await;
1458
1459        // Different types, so that checks would fail in case we were not separating these
1460        // collections internally.
1461        let (mut write2, mut read2) = client
1462            .expect_open::<String, (), u64, i64>(ShardId::new())
1463            .await;
1464
1465        write1
1466            .expect_compare_and_append(&data1[..], u64::minimum(), 3)
1467            .await;
1468
1469        write2
1470            .expect_compare_and_append(&data2[..], u64::minimum(), 3)
1471            .await;
1472
1473        assert_eq!(
1474            read1.expect_snapshot_and_fetch(2).await,
1475            all_ok(&data1[..], 2)
1476        );
1477
1478        assert_eq!(
1479            read2.expect_snapshot_and_fetch(2).await,
1480            all_ok(&data2[..], 2)
1481        );
1482    }
1483
1484    #[mz_persist_proc::test(tokio::test)]
1485    #[cfg_attr(miri, ignore)] // unsupported operation: returning ready events from epoll_wait is not yet implemented
1486    async fn fetch_upper(dyncfgs: ConfigUpdates) {
1487        let data = [
1488            (("1".to_owned(), "one".to_owned()), 1, 1),
1489            (("2".to_owned(), "two".to_owned()), 2, 1),
1490        ];
1491
1492        let client = new_test_client(&dyncfgs).await;
1493
1494        let shard_id = ShardId::new();
1495
1496        let (mut write1, _read1) = client
1497            .expect_open::<String, String, u64, i64>(shard_id)
1498            .await;
1499
1500        let (mut write2, _read2) = client
1501            .expect_open::<String, String, u64, i64>(shard_id)
1502            .await;
1503
1504        write1
1505            .expect_append(&data[..], write1.upper().clone(), vec![3])
1506            .await;
1507
1508        // The shard-global upper does advance, even if this writer didn't advance its local upper.
1509        assert_eq!(write2.fetch_recent_upper().await, &Antichain::from_elem(3));
1510
1511        // The writer-local upper should advance, even if it was another writer
1512        // that advanced the frontier.
1513        assert_eq!(write2.upper(), &Antichain::from_elem(3));
1514    }
1515
1516    #[mz_persist_proc::test(tokio::test)]
1517    #[cfg_attr(miri, ignore)] // unsupported operation: returning ready events from epoll_wait is not yet implemented
1518    async fn append_with_invalid_upper(dyncfgs: ConfigUpdates) {
1519        let data = [
1520            (("1".to_owned(), "one".to_owned()), 1, 1),
1521            (("2".to_owned(), "two".to_owned()), 2, 1),
1522        ];
1523
1524        let client = new_test_client(&dyncfgs).await;
1525
1526        let shard_id = ShardId::new();
1527
1528        let (mut write, _read) = client
1529            .expect_open::<String, String, u64, i64>(shard_id)
1530            .await;
1531
1532        write
1533            .expect_append(&data[..], write.upper().clone(), vec![3])
1534            .await;
1535
1536        let data = [
1537            (("5".to_owned(), "fünf".to_owned()), 5, 1),
1538            (("6".to_owned(), "sechs".to_owned()), 6, 1),
1539        ];
1540        let res = write
1541            .append(
1542                data.iter(),
1543                Antichain::from_elem(5),
1544                Antichain::from_elem(7),
1545            )
1546            .await;
1547        assert_eq!(
1548            res,
1549            Ok(Err(UpperMismatch {
1550                expected: Antichain::from_elem(5),
1551                current: Antichain::from_elem(3)
1552            }))
1553        );
1554
1555        // Writing with an outdated upper updates the write handle's upper to the correct upper.
1556        assert_eq!(write.upper(), &Antichain::from_elem(3));
1557    }
1558
1559    // Make sure that the API structs are Sync + Send, so that they can be used in async tasks.
1560    // NOTE: This is a compile-time only test. If it compiles, we're good.
1561    #[allow(unused)]
1562    async fn sync_send(dyncfgs: ConfigUpdates) {
1563        mz_ore::test::init_logging();
1564
1565        fn is_send_sync<T: Send + Sync>(_x: T) -> bool {
1566            true
1567        }
1568
1569        let client = new_test_client(&dyncfgs).await;
1570
1571        let (write, read) = client
1572            .expect_open::<String, String, u64, i64>(ShardId::new())
1573            .await;
1574
1575        assert!(is_send_sync(client));
1576        assert!(is_send_sync(write));
1577        assert!(is_send_sync(read));
1578    }
1579
1580    #[mz_persist_proc::test(tokio::test)]
1581    #[cfg_attr(miri, ignore)] // unsupported operation: returning ready events from epoll_wait is not yet implemented
1582    async fn compare_and_append(dyncfgs: ConfigUpdates) {
1583        let data = vec![
1584            (("1".to_owned(), "one".to_owned()), 1, 1),
1585            (("2".to_owned(), "two".to_owned()), 2, 1),
1586            (("3".to_owned(), "three".to_owned()), 3, 1),
1587        ];
1588
1589        let id = ShardId::new();
1590        let client = new_test_client(&dyncfgs).await;
1591        let (mut write1, mut read) = client.expect_open::<String, String, u64, i64>(id).await;
1592
1593        let (mut write2, _read) = client.expect_open::<String, String, u64, i64>(id).await;
1594
1595        assert_eq!(write1.upper(), &Antichain::from_elem(u64::minimum()));
1596        assert_eq!(write2.upper(), &Antichain::from_elem(u64::minimum()));
1597        assert_eq!(read.since(), &Antichain::from_elem(u64::minimum()));
1598
1599        // Write a [0,3) batch.
1600        write1
1601            .expect_compare_and_append(&data[..2], u64::minimum(), 3)
1602            .await;
1603        assert_eq!(write1.upper(), &Antichain::from_elem(3));
1604
1605        assert_eq!(
1606            read.expect_snapshot_and_fetch(2).await,
1607            all_ok(&data[..2], 2)
1608        );
1609
1610        // Try and write with a wrong expected upper.
1611        let res = write2
1612            .compare_and_append(
1613                &data[..2],
1614                Antichain::from_elem(u64::minimum()),
1615                Antichain::from_elem(3),
1616            )
1617            .await;
1618        assert_eq!(
1619            res,
1620            Ok(Err(UpperMismatch {
1621                expected: Antichain::from_elem(u64::minimum()),
1622                current: Antichain::from_elem(3)
1623            }))
1624        );
1625
1626        // A failed write updates our local cache of the shard upper.
1627        assert_eq!(write2.upper(), &Antichain::from_elem(3));
1628
1629        // Try again with a good expected upper.
1630        write2.expect_compare_and_append(&data[2..], 3, 4).await;
1631
1632        assert_eq!(write2.upper(), &Antichain::from_elem(4));
1633
1634        assert_eq!(read.expect_snapshot_and_fetch(3).await, all_ok(&data, 3));
1635    }
1636
1637    #[mz_persist_proc::test(tokio::test)]
1638    #[cfg_attr(miri, ignore)] // unsupported operation: returning ready events from epoll_wait is not yet implemented
1639    async fn overlapping_append(dyncfgs: ConfigUpdates) {
1640        mz_ore::test::init_logging_default("info");
1641
1642        let data = vec![
1643            (("1".to_owned(), "one".to_owned()), 1, 1),
1644            (("2".to_owned(), "two".to_owned()), 2, 1),
1645            (("3".to_owned(), "three".to_owned()), 3, 1),
1646            (("4".to_owned(), "vier".to_owned()), 4, 1),
1647            (("5".to_owned(), "cinque".to_owned()), 5, 1),
1648        ];
1649
1650        let id = ShardId::new();
1651        let client = new_test_client(&dyncfgs).await;
1652
1653        let (mut write1, mut read) = client.expect_open::<String, String, u64, i64>(id).await;
1654
1655        let (mut write2, _read) = client.expect_open::<String, String, u64, i64>(id).await;
1656
1657        // Grab a listener before we do any writing
1658        let mut listen = read.clone("").await.expect_listen(0).await;
1659
1660        // Write a [0,3) batch.
1661        write1
1662            .expect_append(&data[..2], write1.upper().clone(), vec![3])
1663            .await;
1664        assert_eq!(write1.upper(), &Antichain::from_elem(3));
1665
1666        // Write a [0,5) batch with the second writer.
1667        write2
1668            .expect_append(&data[..4], write2.upper().clone(), vec![5])
1669            .await;
1670        assert_eq!(write2.upper(), &Antichain::from_elem(5));
1671
1672        // Write a [3,6) batch with the first writer.
1673        write1
1674            .expect_append(&data[2..5], write1.upper().clone(), vec![6])
1675            .await;
1676        assert_eq!(write1.upper(), &Antichain::from_elem(6));
1677
1678        assert_eq!(read.expect_snapshot_and_fetch(5).await, all_ok(&data, 5));
1679
1680        assert_eq!(
1681            listen.read_until(&6).await,
1682            (all_ok(&data[..], 1), Antichain::from_elem(6))
1683        );
1684    }
1685
1686    // Appends need to be contiguous for a shard, meaning the lower of an appended batch must not
1687    // be in advance of the current shard upper.
1688    #[mz_persist_proc::test(tokio::test)]
1689    #[cfg_attr(miri, ignore)] // unsupported operation: returning ready events from epoll_wait is not yet implemented
1690    async fn contiguous_append(dyncfgs: ConfigUpdates) {
1691        let data = vec![
1692            (("1".to_owned(), "one".to_owned()), 1, 1),
1693            (("2".to_owned(), "two".to_owned()), 2, 1),
1694            (("3".to_owned(), "three".to_owned()), 3, 1),
1695            (("4".to_owned(), "vier".to_owned()), 4, 1),
1696            (("5".to_owned(), "cinque".to_owned()), 5, 1),
1697        ];
1698
1699        let id = ShardId::new();
1700        let client = new_test_client(&dyncfgs).await;
1701
1702        let (mut write, mut read) = client.expect_open::<String, String, u64, i64>(id).await;
1703
1704        // Write a [0,3) batch.
1705        write
1706            .expect_append(&data[..2], write.upper().clone(), vec![3])
1707            .await;
1708        assert_eq!(write.upper(), &Antichain::from_elem(3));
1709
1710        // Appending a non-contiguous batch should fail.
1711        // Write a [5,6) batch with the second writer.
1712        let result = write
1713            .append(
1714                &data[4..5],
1715                Antichain::from_elem(5),
1716                Antichain::from_elem(6),
1717            )
1718            .await;
1719        assert_eq!(
1720            result,
1721            Ok(Err(UpperMismatch {
1722                expected: Antichain::from_elem(5),
1723                current: Antichain::from_elem(3)
1724            }))
1725        );
1726
1727        // Fixing the lower to make the write contiguous should make the append succeed.
1728        write.expect_append(&data[2..5], vec![3], vec![6]).await;
1729        assert_eq!(write.upper(), &Antichain::from_elem(6));
1730
1731        assert_eq!(read.expect_snapshot_and_fetch(5).await, all_ok(&data, 5));
1732    }
1733
1734    // Per-writer appends can be non-contiguous, as long as appends to the shard from all writers
1735    // combined are contiguous.
1736    #[mz_persist_proc::test(tokio::test)]
1737    #[cfg_attr(miri, ignore)] // unsupported operation: returning ready events from epoll_wait is not yet implemented
1738    async fn noncontiguous_append_per_writer(dyncfgs: ConfigUpdates) {
1739        let data = vec![
1740            (("1".to_owned(), "one".to_owned()), 1, 1),
1741            (("2".to_owned(), "two".to_owned()), 2, 1),
1742            (("3".to_owned(), "three".to_owned()), 3, 1),
1743            (("4".to_owned(), "vier".to_owned()), 4, 1),
1744            (("5".to_owned(), "cinque".to_owned()), 5, 1),
1745        ];
1746
1747        let id = ShardId::new();
1748        let client = new_test_client(&dyncfgs).await;
1749
1750        let (mut write1, mut read) = client.expect_open::<String, String, u64, i64>(id).await;
1751
1752        let (mut write2, _read) = client.expect_open::<String, String, u64, i64>(id).await;
1753
1754        // Write a [0,3) batch with writer 1.
1755        write1
1756            .expect_append(&data[..2], write1.upper().clone(), vec![3])
1757            .await;
1758        assert_eq!(write1.upper(), &Antichain::from_elem(3));
1759
1760        // Write a [3,5) batch with writer 2.
1761        write2.upper = Antichain::from_elem(3);
1762        write2
1763            .expect_append(&data[2..4], write2.upper().clone(), vec![5])
1764            .await;
1765        assert_eq!(write2.upper(), &Antichain::from_elem(5));
1766
1767        // Write a [5,6) batch with writer 1.
1768        write1.upper = Antichain::from_elem(5);
1769        write1
1770            .expect_append(&data[4..5], write1.upper().clone(), vec![6])
1771            .await;
1772        assert_eq!(write1.upper(), &Antichain::from_elem(6));
1773
1774        assert_eq!(read.expect_snapshot_and_fetch(5).await, all_ok(&data, 5));
1775    }
1776
1777    // Compare_and_appends need to be contiguous for a shard, meaning the lower of an appended
1778    // batch needs to match the current shard upper.
1779    #[mz_persist_proc::test(tokio::test)]
1780    #[cfg_attr(miri, ignore)] // unsupported operation: returning ready events from epoll_wait is not yet implemented
1781    async fn contiguous_compare_and_append(dyncfgs: ConfigUpdates) {
1782        let data = vec![
1783            (("1".to_owned(), "one".to_owned()), 1, 1),
1784            (("2".to_owned(), "two".to_owned()), 2, 1),
1785            (("3".to_owned(), "three".to_owned()), 3, 1),
1786            (("4".to_owned(), "vier".to_owned()), 4, 1),
1787            (("5".to_owned(), "cinque".to_owned()), 5, 1),
1788        ];
1789
1790        let id = ShardId::new();
1791        let client = new_test_client(&dyncfgs).await;
1792
1793        let (mut write, mut read) = client.expect_open::<String, String, u64, i64>(id).await;
1794
1795        // Write a [0,3) batch.
1796        write.expect_compare_and_append(&data[..2], 0, 3).await;
1797        assert_eq!(write.upper(), &Antichain::from_elem(3));
1798
1799        // Appending a non-contiguous batch should fail.
1800        // Write a [5,6) batch with the second writer.
1801        let result = write
1802            .compare_and_append(
1803                &data[4..5],
1804                Antichain::from_elem(5),
1805                Antichain::from_elem(6),
1806            )
1807            .await;
1808        assert_eq!(
1809            result,
1810            Ok(Err(UpperMismatch {
1811                expected: Antichain::from_elem(5),
1812                current: Antichain::from_elem(3)
1813            }))
1814        );
1815
1816        // Writing with the correct expected upper to make the write contiguous should make the
1817        // append succeed.
1818        write.expect_compare_and_append(&data[2..5], 3, 6).await;
1819        assert_eq!(write.upper(), &Antichain::from_elem(6));
1820
1821        assert_eq!(read.expect_snapshot_and_fetch(5).await, all_ok(&data, 5));
1822    }
1823
1824    // Per-writer compare_and_appends can be non-contiguous, as long as appends to the shard from
1825    // all writers combined are contiguous.
1826    #[mz_persist_proc::test(tokio::test)]
1827    #[cfg_attr(miri, ignore)] // unsupported operation: returning ready events from epoll_wait is not yet implemented
1828    async fn noncontiguous_compare_and_append_per_writer(dyncfgs: ConfigUpdates) {
1829        let data = vec![
1830            (("1".to_owned(), "one".to_owned()), 1, 1),
1831            (("2".to_owned(), "two".to_owned()), 2, 1),
1832            (("3".to_owned(), "three".to_owned()), 3, 1),
1833            (("4".to_owned(), "vier".to_owned()), 4, 1),
1834            (("5".to_owned(), "cinque".to_owned()), 5, 1),
1835        ];
1836
1837        let id = ShardId::new();
1838        let client = new_test_client(&dyncfgs).await;
1839
1840        let (mut write1, mut read) = client.expect_open::<String, String, u64, i64>(id).await;
1841
1842        let (mut write2, _read) = client.expect_open::<String, String, u64, i64>(id).await;
1843
1844        // Write a [0,3) batch with writer 1.
1845        write1.expect_compare_and_append(&data[..2], 0, 3).await;
1846        assert_eq!(write1.upper(), &Antichain::from_elem(3));
1847
1848        // Write a [3,5) batch with writer 2.
1849        write2.expect_compare_and_append(&data[2..4], 3, 5).await;
1850        assert_eq!(write2.upper(), &Antichain::from_elem(5));
1851
1852        // Write a [5,6) batch with writer 1.
1853        write1.expect_compare_and_append(&data[4..5], 5, 6).await;
1854        assert_eq!(write1.upper(), &Antichain::from_elem(6));
1855
1856        assert_eq!(read.expect_snapshot_and_fetch(5).await, all_ok(&data, 5));
1857    }
1858
1859    #[mz_ore::test]
1860    fn fmt_ids() {
1861        assert_eq!(
1862            format!("{}", LeasedReaderId([0u8; 16])),
1863            "r00000000-0000-0000-0000-000000000000"
1864        );
1865        assert_eq!(
1866            format!("{:?}", LeasedReaderId([0u8; 16])),
1867            "LeasedReaderId(00000000-0000-0000-0000-000000000000)"
1868        );
1869    }
1870
1871    #[mz_persist_proc::test(tokio::test(flavor = "multi_thread"))]
1872    #[cfg_attr(miri, ignore)] // error: unsupported operation: integer-to-pointer casts and `ptr::from_exposed_addr` are not supported with `-Zmiri-strict-provenance`
1873    async fn concurrency(dyncfgs: ConfigUpdates) {
1874        let data = DataGenerator::small();
1875
1876        const NUM_WRITERS: usize = 2;
1877        let id = ShardId::new();
1878        let client = new_test_client(&dyncfgs).await;
1879        let mut handles = Vec::<mz_ore::task::JoinHandle<()>>::new();
1880        for idx in 0..NUM_WRITERS {
1881            let (data, client) = (data.clone(), client.clone());
1882
1883            let (batch_tx, mut batch_rx) = tokio::sync::mpsc::channel(1);
1884
1885            let client1 = client.clone();
1886            let handle = mz_ore::task::spawn(|| format!("writer-{}", idx), async move {
1887                let (write, _) = client1.expect_open::<Vec<u8>, Vec<u8>, u64, i64>(id).await;
1888                let mut current_upper = 0;
1889                for batch in data.batches() {
1890                    let new_upper = match batch.get(batch.len() - 1) {
1891                        Some((_, max_ts, _)) => u64::decode(max_ts) + 1,
1892                        None => continue,
1893                    };
1894                    // Because we (intentionally) call open inside the task,
1895                    // some other writer may have raced ahead and already
1896                    // appended some data before this one was registered. As a
1897                    // result, this writer may not be starting with an upper of
1898                    // the initial empty antichain. This is nice because it
1899                    // mimics how a real HA source would work, but it means we
1900                    // have to skip any batches that have already been committed
1901                    // (otherwise our new_upper would be before our upper).
1902                    //
1903                    // Note however, that unlike a real source, our
1904                    // DataGenerator-derived batches are guaranteed to be
1905                    // chunked along the same boundaries. This means we don't
1906                    // have to consider partial batches when generating the
1907                    // updates below.
1908                    if PartialOrder::less_equal(&Antichain::from_elem(new_upper), write.upper()) {
1909                        continue;
1910                    }
1911
1912                    let current_upper_chain = Antichain::from_elem(current_upper);
1913                    current_upper = new_upper;
1914                    let new_upper_chain = Antichain::from_elem(new_upper);
1915                    let mut builder = write.builder(current_upper_chain);
1916
1917                    for ((k, v), t, d) in batch.iter() {
1918                        builder
1919                            .add(&k.to_vec(), &v.to_vec(), &u64::decode(t), &i64::decode(d))
1920                            .await
1921                            .expect("invalid usage");
1922                    }
1923
1924                    let batch = builder
1925                        .finish(new_upper_chain)
1926                        .await
1927                        .expect("invalid usage");
1928
1929                    match batch_tx.send(batch).await {
1930                        Ok(_) => (),
1931                        Err(e) => panic!("send error: {}", e),
1932                    }
1933                }
1934            });
1935            handles.push(handle);
1936
1937            let handle = mz_ore::task::spawn(|| format!("appender-{}", idx), async move {
1938                let (mut write, _) = client.expect_open::<Vec<u8>, Vec<u8>, u64, i64>(id).await;
1939
1940                while let Some(batch) = batch_rx.recv().await {
1941                    let lower = batch.lower().clone();
1942                    let upper = batch.upper().clone();
1943                    write
1944                        .append_batch(batch, lower, upper)
1945                        .await
1946                        .expect("invalid usage")
1947                        .expect("unexpected upper");
1948                }
1949            });
1950            handles.push(handle);
1951        }
1952
1953        for handle in handles {
1954            let () = handle.await;
1955        }
1956
1957        let expected = data.records().collect::<Vec<_>>();
1958        let max_ts = expected.last().map(|(_, t, _)| *t).unwrap_or_default();
1959        let (_, mut read) = client.expect_open::<Vec<u8>, Vec<u8>, u64, i64>(id).await;
1960        assert_eq!(
1961            read.expect_snapshot_and_fetch(max_ts).await,
1962            all_ok(expected.iter(), max_ts)
1963        );
1964    }
1965
1966    // Regression test for database-issues#3523. Snapshot with as_of >= upper would
1967    // immediately return the data currently available instead of waiting for
1968    // upper to advance past as_of.
1969    #[mz_persist_proc::test(tokio::test)]
1970    #[cfg_attr(miri, ignore)] // unsupported operation: returning ready events from epoll_wait is not yet implemented
1971    async fn regression_blocking_reads(dyncfgs: ConfigUpdates) {
1972        let waker = noop_waker();
1973        let mut cx = Context::from_waker(&waker);
1974
1975        let data = [
1976            (("1".to_owned(), "one".to_owned()), 1, 1),
1977            (("2".to_owned(), "two".to_owned()), 2, 1),
1978            (("3".to_owned(), "three".to_owned()), 3, 1),
1979        ];
1980
1981        let id = ShardId::new();
1982        let client = new_test_client(&dyncfgs).await;
1983        let (mut write, mut read) = client.expect_open::<String, String, u64, i64>(id).await;
1984
1985        // Grab a listener as_of (aka gt) 1, which is not yet closed out.
1986        let mut listen = read.clone("").await.expect_listen(1).await;
1987        let mut listen_next = Box::pin(listen.fetch_next());
1988        // Intentionally don't await the listen_next, but instead manually poke
1989        // it for a while and assert that it doesn't resolve yet. See below for
1990        // discussion of some alternative ways of writing this unit test.
1991        for _ in 0..100 {
1992            assert!(
1993                Pin::new(&mut listen_next).poll(&mut cx).is_pending(),
1994                "listen::next unexpectedly ready"
1995            );
1996        }
1997
1998        // Write a [0,3) batch.
1999        write
2000            .expect_compare_and_append(&data[..2], u64::minimum(), 3)
2001            .await;
2002
2003        // The initial listen_next call should now be able to return data at 2.
2004        // It doesn't get 1 because the as_of was 1 and listen is strictly gt.
2005        assert_eq!(
2006            listen_next.await,
2007            vec![
2008                ListenEvent::Updates(vec![(("2".to_owned(), "two".to_owned()), 2, 1)]),
2009                ListenEvent::Progress(Antichain::from_elem(3)),
2010            ]
2011        );
2012
2013        // Grab a snapshot as_of 3, which is not yet closed out. Intentionally
2014        // don't await the snap, but instead manually poke it for a while and
2015        // assert that it doesn't resolve yet.
2016        //
2017        // An alternative to this would be to run it in a task and poll the task
2018        // with some timeout, but this would introduce a fixed test execution
2019        // latency of the timeout in the happy case. Plus, it would be
2020        // non-deterministic.
2021        //
2022        // Another alternative (that's potentially quite interesting!) would be
2023        // to separate creating a snapshot immediately (which would fail if
2024        // as_of was >= upper) from a bit of logic that retries until that case
2025        // is ready.
2026        let mut snap = Box::pin(read.expect_snapshot_and_fetch(3));
2027        for _ in 0..100 {
2028            assert!(
2029                Pin::new(&mut snap).poll(&mut cx).is_pending(),
2030                "snapshot unexpectedly ready"
2031            );
2032        }
2033
2034        // Now add the data at 3 and also unblock the snapshot.
2035        write.expect_compare_and_append(&data[2..], 3, 4).await;
2036
2037        // Read the snapshot and check that it got all the appropriate data.
2038        assert_eq!(snap.await, all_ok(&data[..], 3));
2039    }
2040
2041    #[mz_persist_proc::test(tokio::test)]
2042    #[cfg_attr(miri, ignore)] // unsupported operation: returning ready events from epoll_wait is not yet implemented
2043    async fn heartbeat_task_shutdown(dyncfgs: ConfigUpdates) {
2044        // Verify that the ReadHandle and WriteHandle background heartbeat tasks
2045        // shut down cleanly after the handle is expired.
2046        let mut cache = new_test_client_cache(&dyncfgs);
2047        cache
2048            .cfg
2049            .set_config(&READER_LEASE_DURATION, Duration::from_millis(1));
2050        cache.cfg.writer_lease_duration = Duration::from_millis(1);
2051        let (_write, mut read) = cache
2052            .open(PersistLocation::new_in_mem())
2053            .await
2054            .expect("client construction failed")
2055            .expect_open::<(), (), u64, i64>(ShardId::new())
2056            .await;
2057        let read_unexpired_state = read
2058            .unexpired_state
2059            .take()
2060            .expect("handle should have unexpired state");
2061        read.expire().await;
2062        read_unexpired_state.heartbeat_task.await
2063    }
2064
2065    /// Verify that shard finalization works with empty shards, shards that have
2066    /// an empty write up to the empty upper Antichain.
2067    #[mz_persist_proc::test(tokio::test)]
2068    #[cfg_attr(miri, ignore)] // unsupported operation: returning ready events from epoll_wait is not yet implemented
2069    async fn finalize_empty_shard(dyncfgs: ConfigUpdates) {
2070        let persist_client = new_test_client(&dyncfgs).await;
2071
2072        let shard_id = ShardId::new();
2073        pub const CRITICAL_SINCE: CriticalReaderId =
2074            CriticalReaderId([0, 0, 0, 0, 17, 17, 34, 34, 51, 51, 68, 68, 68, 68, 68, 68]);
2075
2076        let (mut write, mut read) = persist_client
2077            .expect_open::<(), (), u64, i64>(shard_id)
2078            .await;
2079
2080        // Advance since and upper to empty, which is a pre-requisite for
2081        // finalization/tombstoning.
2082        let () = read.downgrade_since(&Antichain::new()).await;
2083        let () = write.advance_upper(&Antichain::new()).await;
2084
2085        let mut since_handle: SinceHandle<(), (), u64, i64> = persist_client
2086            .open_critical_since(
2087                shard_id,
2088                CRITICAL_SINCE,
2089                Opaque::encode(&0u64),
2090                Diagnostics::for_tests(),
2091            )
2092            .await
2093            .expect("invalid persist usage");
2094
2095        let epoch = since_handle.opaque().clone();
2096        let new_since = Antichain::new();
2097        let downgrade = since_handle
2098            .compare_and_downgrade_since(&epoch, (&epoch, &new_since))
2099            .await;
2100
2101        assert!(
2102            downgrade.is_ok(),
2103            "downgrade of critical handle must succeed"
2104        );
2105
2106        let finalize = persist_client
2107            .finalize_shard::<(), (), u64, i64>(shard_id, Diagnostics::for_tests())
2108            .await;
2109
2110        assert_ok!(finalize, "finalization must succeed");
2111
2112        let is_finalized = persist_client
2113            .is_finalized::<(), (), u64, i64>(shard_id, Diagnostics::for_tests())
2114            .await
2115            .expect("invalid persist usage");
2116        assert!(is_finalized, "shard must still be finalized");
2117    }
2118
2119    /// Verify that shard finalization works with shards that had some data
2120    /// written to them, plus then an empty batch to bring their upper to the
2121    /// empty Antichain.
2122    #[mz_persist_proc::test(tokio::test)]
2123    #[cfg_attr(miri, ignore)] // unsupported operation: returning ready events from epoll_wait is not yet implemented
2124    async fn finalize_shard(dyncfgs: ConfigUpdates) {
2125        const DATA: &[(((), ()), u64, i64)] = &[(((), ()), 0, 1)];
2126        let persist_client = new_test_client(&dyncfgs).await;
2127
2128        let shard_id = ShardId::new();
2129        pub const CRITICAL_SINCE: CriticalReaderId =
2130            CriticalReaderId([0, 0, 0, 0, 17, 17, 34, 34, 51, 51, 68, 68, 68, 68, 68, 68]);
2131
2132        let (mut write, mut read) = persist_client
2133            .expect_open::<(), (), u64, i64>(shard_id)
2134            .await;
2135
2136        // Write some data.
2137        let () = write
2138            .compare_and_append(DATA, Antichain::from_elem(0), Antichain::from_elem(1))
2139            .await
2140            .expect("usage should be valid")
2141            .expect("upper should match");
2142
2143        // Advance since and upper to empty, which is a pre-requisite for
2144        // finalization/tombstoning.
2145        let () = read.downgrade_since(&Antichain::new()).await;
2146        let () = write.advance_upper(&Antichain::new()).await;
2147
2148        let mut since_handle: SinceHandle<(), (), u64, i64> = persist_client
2149            .open_critical_since(
2150                shard_id,
2151                CRITICAL_SINCE,
2152                Opaque::encode(&0u64),
2153                Diagnostics::for_tests(),
2154            )
2155            .await
2156            .expect("invalid persist usage");
2157
2158        let epoch = since_handle.opaque().clone();
2159        let new_since = Antichain::new();
2160        let downgrade = since_handle
2161            .compare_and_downgrade_since(&epoch, (&epoch, &new_since))
2162            .await;
2163
2164        assert!(
2165            downgrade.is_ok(),
2166            "downgrade of critical handle must succeed"
2167        );
2168
2169        let finalize = persist_client
2170            .finalize_shard::<(), (), u64, i64>(shard_id, Diagnostics::for_tests())
2171            .await;
2172
2173        assert_ok!(finalize, "finalization must succeed");
2174
2175        let is_finalized = persist_client
2176            .is_finalized::<(), (), u64, i64>(shard_id, Diagnostics::for_tests())
2177            .await
2178            .expect("invalid persist usage");
2179        assert!(is_finalized, "shard must still be finalized");
2180    }
2181
2182    proptest! {
2183        #![proptest_config(ProptestConfig::with_cases(4096))]
2184
2185        #[mz_ore::test]
2186        #[cfg_attr(miri, ignore)] // too slow
2187        fn shard_id_protobuf_roundtrip(expect in any::<ShardId>() ) {
2188            let actual = protobuf_roundtrip::<_, String>(&expect);
2189            assert_ok!(actual);
2190            assert_eq!(actual.unwrap(), expect);
2191        }
2192    }
2193}