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