Skip to main content

mz_persist_client/
schema.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//! Persist shard schema information.
11
12use std::collections::BTreeMap;
13use std::fmt::Debug;
14use std::sync::{Arc, RwLock};
15use std::time::Instant;
16
17use differential_dataflow::difference::Monoid;
18use differential_dataflow::lattice::Lattice;
19use mz_ore::cast::CastFrom;
20use mz_persist_types::columnar::data_type;
21use mz_persist_types::schema::{Migration, SchemaId, backward_compatible};
22use mz_persist_types::{Codec, Codec64};
23use timely::progress::Timestamp;
24
25use crate::internal::apply::Applier;
26use crate::internal::encoding::Schemas;
27use crate::internal::metrics::{SchemaCacheMetrics, SchemaMetrics};
28use crate::internal::state::{BatchPart, EncodedSchemas};
29
30/// The result returned by [crate::PersistClient::compare_and_evolve_schema].
31#[derive(Debug)]
32#[cfg_attr(test, derive(PartialEq))]
33pub enum CaESchema<K: Codec, V: Codec> {
34    /// The schema was successfully evolved and registered with the included id.
35    Ok(SchemaId),
36    /// The schema was not compatible with previously registered schemas.
37    Incompatible,
38    /// The `expected` SchemaId did not match reality. The current one is
39    /// included for easy of retry.
40    ExpectedMismatch {
41        /// The current schema id.
42        schema_id: SchemaId,
43        /// The key schema at this id.
44        key: K::Schema,
45        /// The val schema at this id.
46        val: V::Schema,
47    },
48}
49
50/// A cache of decoded schemas and schema migrations.
51///
52/// The decoded schemas are a cache of the registry in state, and so are shared
53/// process-wide.
54///
55/// On the other hand, the migrations have an N^2 problem and so are per-handle.
56/// This also seems reasonable because for any given write handle, the write
57/// schema will be the same for all migration entries, and ditto for read handle
58/// and read schema.
59#[derive(Debug)]
60pub(crate) struct SchemaCache<K: Codec, V: Codec, T, D> {
61    maps: Arc<SchemaCacheMaps<K, V>>,
62    applier: Applier<K, V, T, D>,
63    key_migration_by_ids: MigrationCacheMap,
64    val_migration_by_ids: MigrationCacheMap,
65}
66
67impl<K: Codec, V: Codec, T: Clone, D> Clone for SchemaCache<K, V, T, D> {
68    fn clone(&self) -> Self {
69        Self {
70            maps: Arc::clone(&self.maps),
71            applier: self.applier.clone(),
72            key_migration_by_ids: self.key_migration_by_ids.clone(),
73            val_migration_by_ids: self.val_migration_by_ids.clone(),
74        }
75    }
76}
77
78impl<K: Codec, V: Codec, T, D> Drop for SchemaCache<K, V, T, D> {
79    fn drop(&mut self) {
80        let dropped = u64::cast_from(
81            self.key_migration_by_ids.by_ids.len() + self.val_migration_by_ids.by_ids.len(),
82        );
83        self.applier
84            .metrics
85            .schema
86            .cache_migration
87            .dropped_count
88            .inc_by(dropped);
89    }
90}
91
92impl<K, V, T, D> SchemaCache<K, V, T, D>
93where
94    K: Debug + Codec,
95    V: Debug + Codec,
96    T: Timestamp + Lattice + Codec64 + Sync,
97    D: Monoid + Codec64,
98{
99    /// Returns the [Applier] backing this cache.
100    pub(crate) fn applier(&self) -> &Applier<K, V, T, D> {
101        &self.applier
102    }
103
104    pub fn new(maps: Arc<SchemaCacheMaps<K, V>>, applier: Applier<K, V, T, D>) -> Self {
105        let key_migration_by_ids = MigrationCacheMap {
106            metrics: applier.metrics.schema.cache_migration.clone(),
107            by_ids: BTreeMap::new(),
108        };
109        let val_migration_by_ids = MigrationCacheMap {
110            metrics: applier.metrics.schema.cache_migration.clone(),
111            by_ids: BTreeMap::new(),
112        };
113        SchemaCache {
114            maps,
115            applier,
116            key_migration_by_ids,
117            val_migration_by_ids,
118        }
119    }
120
121    async fn schemas(&self, id: &SchemaId) -> Option<Schemas<K, V>> {
122        let key = self
123            .get_or_try_init(&self.maps.key_by_id, id, |schemas| {
124                self.maps.key_by_id.metrics.computed_count.inc();
125                schemas.get(id).map(|x| K::decode_schema(&x.key))
126            })
127            .await?;
128        let val = self
129            .get_or_try_init(&self.maps.val_by_id, id, |schemas| {
130                self.maps.val_by_id.metrics.computed_count.inc();
131                schemas.get(id).map(|x| V::decode_schema(&x.val))
132            })
133            .await?;
134        Some(Schemas {
135            id: Some(*id),
136            key,
137            val,
138        })
139    }
140
141    fn key_migration(
142        &mut self,
143        write: &Schemas<K, V>,
144        read: &Schemas<K, V>,
145    ) -> Option<Arc<Migration>> {
146        let migration_fn = || Self::migration::<K>(&write.key, &read.key);
147        let (Some(write_id), Some(read_id)) = (write.id, read.id) else {
148            // TODO: Annoying to cache this because we're missing an id. This
149            // will probably require some sort of refactor to fix so punting for
150            // now.
151            self.key_migration_by_ids.metrics.computed_count.inc();
152            return migration_fn().map(Arc::new);
153        };
154        self.key_migration_by_ids
155            .get_or_try_insert(write_id, read_id, migration_fn)
156    }
157
158    fn val_migration(
159        &mut self,
160        write: &Schemas<K, V>,
161        read: &Schemas<K, V>,
162    ) -> Option<Arc<Migration>> {
163        let migration_fn = || Self::migration::<V>(&write.val, &read.val);
164        let (Some(write_id), Some(read_id)) = (write.id, read.id) else {
165            // TODO: Annoying to cache this because we're missing an id. This
166            // will probably require some sort of refactor to fix so punting for
167            // now.
168            self.val_migration_by_ids.metrics.computed_count.inc();
169            return migration_fn().map(Arc::new);
170        };
171        self.val_migration_by_ids
172            .get_or_try_insert(write_id, read_id, migration_fn)
173    }
174
175    fn migration<C: Codec>(write: &C::Schema, read: &C::Schema) -> Option<Migration> {
176        let write_dt = data_type::<C>(write).expect("valid schema");
177        let read_dt = data_type::<C>(read).expect("valid schema");
178        backward_compatible(&write_dt, &read_dt)
179    }
180
181    async fn get_or_try_init<MK: Clone + Ord, MV: PartialEq + Debug>(
182        &self,
183        map: &SchemaCacheMap<MK, MV>,
184        key: &MK,
185        f: impl Fn(&BTreeMap<SchemaId, EncodedSchemas>) -> Option<MV>,
186    ) -> Option<Arc<MV>> {
187        let ret = map.get_or_try_init(key, || {
188            self.applier
189                .schemas(|seqno, schemas| f(schemas).ok_or(seqno))
190        });
191        let seqno = match ret {
192            Ok(ret) => return Some(ret),
193            Err(seqno) => seqno,
194        };
195        self.applier.metrics.schema.cache_fetch_state_count.inc();
196        self.applier.fetch_and_update_state(Some(seqno)).await;
197        map.get_or_try_init(key, || {
198            self.applier
199                .schemas(|seqno, schemas| f(schemas).ok_or(seqno))
200        })
201        .ok()
202    }
203}
204
205#[derive(Debug)]
206pub(crate) struct SchemaCacheMaps<K: Codec, V: Codec> {
207    key_by_id: SchemaCacheMap<SchemaId, K::Schema>,
208    val_by_id: SchemaCacheMap<SchemaId, V::Schema>,
209}
210
211impl<K: Codec, V: Codec> SchemaCacheMaps<K, V> {
212    pub(crate) fn new(metrics: &SchemaMetrics) -> Self {
213        Self {
214            key_by_id: SchemaCacheMap {
215                metrics: metrics.cache_schema.clone(),
216                map: RwLock::new(BTreeMap::new()),
217            },
218            val_by_id: SchemaCacheMap {
219                metrics: metrics.cache_schema.clone(),
220                map: RwLock::new(BTreeMap::new()),
221            },
222        }
223    }
224}
225
226#[derive(Debug)]
227struct SchemaCacheMap<I, S> {
228    metrics: SchemaCacheMetrics,
229    map: RwLock<BTreeMap<I, Arc<S>>>,
230}
231
232impl<I: Clone + Ord, S: PartialEq + Debug> SchemaCacheMap<I, S> {
233    fn get_or_try_init<E>(
234        &self,
235        id: &I,
236        state_fn: impl FnOnce() -> Result<S, E>,
237    ) -> Result<Arc<S>, E> {
238        // First see if we have the value cached.
239        {
240            let map = self.map.read().expect("lock");
241            if let Some(ret) = map.get(id).map(Arc::clone) {
242                self.metrics.cached_count.inc();
243                return Ok(ret);
244            }
245        }
246        // If not, see if we can get the value from current state.
247        let ret = state_fn().map(Arc::new);
248        if let Ok(val) = ret.as_ref() {
249            let mut map = self.map.write().expect("lock");
250            // If any answers got written in the meantime, they should be the
251            // same, so just overwrite
252            let prev = map.insert(id.clone(), Arc::clone(val));
253            match prev {
254                Some(prev) => mz_ore::soft_assert_eq_no_log!(*val, prev),
255                None => self.metrics.added_count.inc(),
256            }
257        } else {
258            self.metrics.unavailable_count.inc();
259        }
260        ret
261    }
262}
263
264impl<I, K> Drop for SchemaCacheMap<I, K> {
265    fn drop(&mut self) {
266        let map = self.map.read().expect("lock");
267        self.metrics.dropped_count.inc_by(u64::cast_from(map.len()));
268    }
269}
270
271#[derive(Debug, Clone)]
272struct MigrationCacheMap {
273    metrics: SchemaCacheMetrics,
274    by_ids: BTreeMap<(SchemaId, SchemaId), Arc<Migration>>,
275}
276
277impl MigrationCacheMap {
278    fn get_or_try_insert(
279        &mut self,
280        write_id: SchemaId,
281        read_id: SchemaId,
282        migration_fn: impl FnOnce() -> Option<Migration>,
283    ) -> Option<Arc<Migration>> {
284        if let Some(migration) = self.by_ids.get(&(write_id, read_id)) {
285            self.metrics.cached_count.inc();
286            return Some(Arc::clone(migration));
287        };
288        self.metrics.computed_count.inc();
289        let migration = migration_fn().map(Arc::new);
290        if let Some(migration) = migration.as_ref() {
291            self.metrics.added_count.inc();
292            // We just looked this up above and we've got mutable access, so no
293            // race issues.
294            self.by_ids
295                .insert((write_id, read_id), Arc::clone(migration));
296        } else {
297            self.metrics.unavailable_count.inc();
298        }
299        migration
300    }
301}
302
303#[derive(Debug)]
304pub(crate) enum PartMigration<K: Codec, V: Codec> {
305    /// No-op!
306    SameSchema { both: Schemas<K, V> },
307    /// We don't have a schema id for write schema.
308    Schemaless { read: Schemas<K, V> },
309    /// We have both write and read schemas, and they don't match.
310    Either {
311        write: Schemas<K, V>,
312        read: Schemas<K, V>,
313        key_migration: Arc<Migration>,
314        val_migration: Arc<Migration>,
315    },
316}
317
318impl<K: Codec, V: Codec> Clone for PartMigration<K, V> {
319    fn clone(&self) -> Self {
320        match self {
321            Self::SameSchema { both } => Self::SameSchema { both: both.clone() },
322            Self::Schemaless { read } => Self::Schemaless { read: read.clone() },
323            Self::Either {
324                write,
325                read,
326                key_migration,
327                val_migration,
328            } => Self::Either {
329                write: write.clone(),
330                read: read.clone(),
331                key_migration: Arc::clone(key_migration),
332                val_migration: Arc::clone(val_migration),
333            },
334        }
335    }
336}
337
338impl<K, V> PartMigration<K, V>
339where
340    K: Debug + Codec,
341    V: Debug + Codec,
342{
343    pub(crate) async fn new<T, D>(
344        part: &BatchPart<T>,
345        read: Schemas<K, V>,
346        schema_cache: &mut SchemaCache<K, V, T, D>,
347    ) -> Result<Self, Schemas<K, V>>
348    where
349        T: Timestamp + Lattice + Codec64 + Sync,
350        D: Monoid + Codec64,
351    {
352        // At one point in time during our structured data migration, we deprecated the
353        // already written schema IDs because we made all columns at the Arrow/Parquet
354        // level nullable, thus changing the schema parts were written with.
355        //
356        // _After_ this deprecation, we've observed at least one instance where a
357        // structured only Part was written with the schema ID in the _old_ deprecated
358        // field. While unexpected, given the ordering of our releases it is safe to
359        // use the deprecated schema ID if we have a structured only part.
360        let write = match (part.schema_id(), part.deprecated_schema_id()) {
361            (Some(write_id), _) => Some(write_id),
362            (None, Some(deprecated_id))
363                if part.is_structured_only(&schema_cache.applier.metrics.columnar) =>
364            {
365                tracing::warn!(?deprecated_id, "falling back to deprecated schema ID");
366                Some(deprecated_id)
367            }
368            (None, _) => None,
369        };
370
371        match (write, read.id) {
372            (None, _) => Ok(PartMigration::Schemaless { read }),
373            (Some(w), Some(r)) if w == r => Ok(PartMigration::SameSchema { both: read }),
374            (Some(w), _) => {
375                let write = schema_cache
376                    .schemas(&w)
377                    .await
378                    .expect("appended part should reference registered schema");
379                // Even if we missing a schema id, if the schemas are equal, use
380                // `SameSchema`. This isn't a correctness issue, we'd just
381                // generate NoOp migrations, but it'll make the metrics more
382                // intuitive.
383                if write.key == read.key && write.val == read.val {
384                    return Ok(PartMigration::SameSchema { both: read });
385                }
386
387                let start = Instant::now();
388                let key_migration = schema_cache
389                    .key_migration(&write, &read)
390                    .ok_or_else(|| read.clone())?;
391                let val_migration = schema_cache
392                    .val_migration(&write, &read)
393                    .ok_or_else(|| read.clone())?;
394                schema_cache
395                    .applier
396                    .metrics
397                    .schema
398                    .migration_new_count
399                    .inc();
400                schema_cache
401                    .applier
402                    .metrics
403                    .schema
404                    .migration_new_seconds
405                    .inc_by(start.elapsed().as_secs_f64());
406
407                Ok(PartMigration::Either {
408                    write,
409                    read,
410                    key_migration,
411                    val_migration,
412                })
413            }
414        }
415    }
416}
417
418impl<K: Codec, V: Codec> PartMigration<K, V> {
419    pub(crate) fn codec_read(&self) -> &Schemas<K, V> {
420        match self {
421            PartMigration::SameSchema { both } => both,
422            PartMigration::Schemaless { read } => read,
423            PartMigration::Either { read, .. } => read,
424        }
425    }
426}
427
428#[cfg(test)]
429mod tests {
430    use arrow::array::{
431        Array, ArrayBuilder, StringArray, StringBuilder, StructArray, as_string_array,
432    };
433    use arrow::datatypes::{DataType, Field};
434    use bytes::BufMut;
435    use futures::StreamExt;
436    use mz_dyncfg::ConfigUpdates;
437    use mz_persist_types::ShardId;
438    use mz_persist_types::arrow::ArrayOrd;
439    use mz_persist_types::codec_impls::UnitSchema;
440    use mz_persist_types::columnar::{ColumnDecoder, ColumnEncoder, Schema};
441    use mz_persist_types::stats::{NoneStats, StructStats};
442    use timely::progress::Antichain;
443
444    use async_trait::async_trait;
445    use mz_ore::metrics::MetricsRegistry;
446    use mz_persist::location::{
447        CaSResult, Consensus, ExternalError, ResultStream, SeqNo, VersionedData,
448    };
449    use mz_persist::mem::{MemBlob, MemBlobConfig, MemConsensus};
450    use std::sync::atomic::{AtomicBool, Ordering};
451
452    use crate::async_runtime::IsolatedRuntime;
453    use crate::cache::StateCache;
454    use crate::cli::admin::info_log_non_zero_metrics;
455    use crate::internal::metrics::Metrics;
456    use crate::read::ReadHandle;
457    use crate::rpc::NoopPubSubSender;
458    use crate::tests::new_test_client;
459    use crate::{Diagnostics, PersistClient, PersistConfig};
460
461    use super::*;
462
463    #[mz_ore::test]
464    fn schema_id() {
465        assert_eq!(SchemaId(1).to_string(), "h1");
466        assert_eq!(SchemaId::try_from("h1".to_owned()), Ok(SchemaId(1)));
467        assert!(SchemaId::try_from("nope".to_owned()).is_err());
468    }
469
470    #[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
471    struct Strings(Vec<String>);
472
473    impl Codec for Strings {
474        type Schema = StringsSchema;
475        type Storage = ();
476
477        fn codec_name() -> String {
478            "Strings".into()
479        }
480
481        fn encode<B: BufMut>(&self, buf: &mut B) {
482            buf.put_slice(self.0.join(",").as_bytes());
483        }
484        fn decode<'a>(buf: &'a [u8], schema: &Self::Schema) -> Result<Self, String> {
485            let buf = std::str::from_utf8(buf).map_err(|err| err.to_string())?;
486            let mut ret = buf.split(",").map(|x| x.to_owned()).collect::<Vec<_>>();
487            // Fill in nulls or drop columns to match the requested schema.
488            while schema.0.len() > ret.len() {
489                ret.push("".into());
490            }
491            while schema.0.len() < ret.len() {
492                ret.pop();
493            }
494            Ok(Strings(ret))
495        }
496
497        fn encode_schema(schema: &Self::Schema) -> bytes::Bytes {
498            schema
499                .0
500                .iter()
501                .map(|x| x.then_some('n').unwrap_or(' '))
502                .collect::<String>()
503                .into_bytes()
504                .into()
505        }
506        fn decode_schema(buf: &bytes::Bytes) -> Self::Schema {
507            let buf = std::str::from_utf8(buf).expect("valid schema");
508            StringsSchema(
509                buf.chars()
510                    .map(|x| match x {
511                        'n' => true,
512                        ' ' => false,
513                        _ => unreachable!(),
514                    })
515                    .collect(),
516            )
517        }
518    }
519
520    #[derive(Debug, Clone, Default, PartialEq)]
521    struct StringsSchema(Vec<bool>);
522
523    impl Schema<Strings> for StringsSchema {
524        type ArrowColumn = StructArray;
525        type Statistics = NoneStats;
526        type Decoder = StringsDecoder;
527        type Encoder = StringsEncoder;
528
529        fn decoder(&self, col: Self::ArrowColumn) -> Result<Self::Decoder, anyhow::Error> {
530            let mut cols = Vec::new();
531            for (idx, _) in self.0.iter().enumerate() {
532                cols.push(as_string_array(col.column_by_name(&idx.to_string()).unwrap()).clone());
533            }
534            Ok(StringsDecoder(cols))
535        }
536        fn encoder(&self) -> Result<Self::Encoder, anyhow::Error> {
537            let mut fields = Vec::new();
538            let mut arrays = Vec::new();
539            for (idx, nullable) in self.0.iter().enumerate() {
540                fields.push(Field::new(idx.to_string(), DataType::Utf8, *nullable));
541                arrays.push(StringBuilder::new());
542            }
543            Ok(StringsEncoder { fields, arrays })
544        }
545    }
546
547    #[derive(Debug)]
548    struct StringsDecoder(Vec<StringArray>);
549    impl ColumnDecoder<Strings> for StringsDecoder {
550        fn decode(&self, idx: usize, val: &mut Strings) {
551            val.0.clear();
552            for col in self.0.iter() {
553                if col.is_valid(idx) {
554                    val.0.push(col.value(idx).into());
555                } else {
556                    val.0.push("".into());
557                }
558            }
559        }
560        fn is_null(&self, _: usize) -> bool {
561            false
562        }
563        fn goodbytes(&self) -> usize {
564            self.0
565                .iter()
566                .map(|val| ArrayOrd::String(val.clone()).goodbytes())
567                .sum()
568        }
569        fn stats(&self) -> StructStats {
570            StructStats {
571                len: self.0[0].len(),
572                cols: Default::default(),
573            }
574        }
575    }
576
577    #[derive(Debug)]
578    struct StringsEncoder {
579        fields: Vec<Field>,
580        arrays: Vec<StringBuilder>,
581    }
582    impl ColumnEncoder<Strings> for StringsEncoder {
583        type FinishedColumn = StructArray;
584
585        fn goodbytes(&self) -> usize {
586            self.arrays.iter().map(|a| a.values_slice().len()).sum()
587        }
588
589        fn append(&mut self, val: &Strings) {
590            for (idx, val) in val.0.iter().enumerate() {
591                if val.is_empty() {
592                    self.arrays[idx].append_null();
593                } else {
594                    self.arrays[idx].append_value(val);
595                }
596            }
597        }
598        fn append_null(&mut self) {
599            unreachable!()
600        }
601        fn finish(self) -> Self::FinishedColumn {
602            assert_eq!(self.fields.len(), self.arrays.len(), "invalid schema");
603            if self.fields.is_empty() {
604                StructArray::new_empty_fields(0, None)
605            } else {
606                let arrays = self
607                    .arrays
608                    .into_iter()
609                    .map(|mut x| ArrayBuilder::finish(&mut x))
610                    .collect();
611                StructArray::new(self.fields.into(), arrays, None)
612            }
613        }
614    }
615
616    #[mz_persist_proc::test(tokio::test)]
617    #[cfg_attr(miri, ignore)]
618    async fn compare_and_evolve_schema(dyncfgs: ConfigUpdates) {
619        let client = new_test_client(&dyncfgs).await;
620        let d = Diagnostics::for_tests();
621        let shard_id = ShardId::new();
622        let schema0 = StringsSchema(vec![false]);
623        let schema1 = StringsSchema(vec![false, true]);
624
625        let mut write0 = client
626            .open_writer::<Strings, (), u64, i64>(
627                shard_id,
628                Arc::new(schema0.clone()),
629                Arc::new(UnitSchema),
630                d.clone(),
631            )
632            .await
633            .unwrap();
634
635        write0.try_register_schema().await;
636        assert_eq!(write0.write_schemas.id.unwrap(), SchemaId(0));
637
638        // Not backward compatible (yet... we don't support dropping a column at
639        // the moment).
640        let res = client
641            .compare_and_evolve_schema::<Strings, (), u64, i64>(
642                shard_id,
643                SchemaId(0),
644                &StringsSchema(vec![]),
645                &UnitSchema,
646                d.clone(),
647            )
648            .await
649            .unwrap();
650        assert_eq!(res, CaESchema::Incompatible);
651
652        // Incorrect expectation
653        let res = client
654            .compare_and_evolve_schema::<Strings, (), u64, i64>(
655                shard_id,
656                SchemaId(1),
657                &schema1,
658                &UnitSchema,
659                d.clone(),
660            )
661            .await
662            .unwrap();
663        assert_eq!(
664            res,
665            CaESchema::ExpectedMismatch {
666                schema_id: SchemaId(0),
667                key: schema0,
668                val: UnitSchema
669            }
670        );
671
672        // Successful evolution
673        let res = client
674            .compare_and_evolve_schema::<Strings, (), u64, i64>(
675                shard_id,
676                SchemaId(0),
677                &schema1,
678                &UnitSchema,
679                d.clone(),
680            )
681            .await
682            .unwrap();
683        assert_eq!(res, CaESchema::Ok(SchemaId(1)));
684
685        // Create a write handle with the new schema and validate that it picks
686        // up the correct schema id.
687        let write1 = client
688            .open_writer::<Strings, (), u64, i64>(
689                shard_id,
690                Arc::new(schema1),
691                Arc::new(UnitSchema),
692                d.clone(),
693            )
694            .await
695            .unwrap();
696        assert_eq!(write1.write_schemas.id.unwrap(), SchemaId(1));
697    }
698
699    /// A [Consensus] that, once armed, runs one `compare_and_set` and then
700    /// reports it as timed out. Models the metadata store committing a write
701    /// but losing the response, which persist surfaces as an indeterminate
702    /// error.
703    ///
704    /// UnreliableConsensus can also run-then-timeout, but only probabilistically
705    /// and per-op-kind. This test needs one deterministic timeout on one
706    /// `compare_and_set`.
707    #[derive(Debug)]
708    struct LoseOneCasResponse {
709        inner: Arc<dyn Consensus>,
710        armed: Arc<AtomicBool>,
711    }
712
713    #[async_trait]
714    impl Consensus for LoseOneCasResponse {
715        fn list_keys(&self) -> ResultStream<'_, String> {
716            self.inner.list_keys()
717        }
718
719        async fn head(&self, key: &str) -> Result<Option<VersionedData>, ExternalError> {
720            self.inner.head(key).await
721        }
722
723        async fn compare_and_set(
724            &self,
725            key: &str,
726            new: VersionedData,
727        ) -> Result<CaSResult, ExternalError> {
728            // Delegate first so the write commits, then drop the response. A
729            // committed-but-lost CaS is the case under test.
730            let res = self.inner.compare_and_set(key, new).await;
731            if self.armed.swap(false, Ordering::SeqCst) {
732                return Err(ExternalError::new_timeout(Instant::now()));
733            }
734            res
735        }
736
737        async fn scan(
738            &self,
739            key: &str,
740            from: SeqNo,
741            limit: usize,
742        ) -> Result<Vec<VersionedData>, ExternalError> {
743            self.inner.scan(key, from, limit).await
744        }
745
746        async fn truncate(&self, key: &str, seqno: SeqNo) -> Result<Option<usize>, ExternalError> {
747            self.inner.truncate(key, seqno).await
748        }
749    }
750
751    /// Regression test for SQL-616. `compare_and_evolve_schema` runs through
752    /// `apply_unbatched_idempotent_cmd`, which retries indeterminate errors. A
753    /// retry re-runs the command against state that already carries our own
754    /// evolution, so it must report success rather than an expectation
755    /// mismatch.
756    #[mz_ore::test(tokio::test)]
757    #[cfg_attr(miri, ignore)]
758    async fn compare_and_evolve_schema_indeterminate_retry() {
759        let cfg = PersistConfig::new_for_tests();
760        let armed = Arc::new(AtomicBool::new(false));
761        let consensus = Arc::new(LoseOneCasResponse {
762            inner: Arc::new(MemConsensus::default()),
763            armed: Arc::clone(&armed),
764        });
765        let metrics = Arc::new(Metrics::new(&cfg, &MetricsRegistry::new()));
766        let client = PersistClient::new(
767            cfg,
768            Arc::new(MemBlob::open(MemBlobConfig::default())),
769            consensus,
770            Arc::clone(&metrics),
771            Arc::new(IsolatedRuntime::new_for_tests()),
772            Arc::new(StateCache::new_no_metrics()),
773            Arc::new(NoopPubSubSender),
774        )
775        .expect("client construction failed");
776
777        let d = Diagnostics::for_tests();
778        let shard_id = ShardId::new();
779        let schema0 = StringsSchema(vec![false]);
780        let schema1 = StringsSchema(vec![false, true]);
781
782        let mut write0 = client
783            .open_writer::<Strings, (), u64, i64>(
784                shard_id,
785                Arc::new(schema0),
786                Arc::new(UnitSchema),
787                d.clone(),
788            )
789            .await
790            .unwrap();
791        write0.try_register_schema().await;
792        assert_eq!(write0.write_schemas.id.unwrap(), SchemaId(0));
793
794        // Check that the evolve actually retried, not just that some CaS
795        // consumed the arm. Single writer, no compaction, so only the evolve's
796        // CaS can.
797        let retries_before = metrics.retries.idempotent_cmd.retries.get();
798        armed.store(true, Ordering::SeqCst);
799        let res = client
800            .compare_and_evolve_schema::<Strings, (), u64, i64>(
801                shard_id,
802                SchemaId(0),
803                &schema1,
804                &UnitSchema,
805                d.clone(),
806            )
807            .await
808            .unwrap();
809        assert!(
810            !armed.load(Ordering::SeqCst),
811            "indeterminate error was never injected"
812        );
813        assert!(
814            metrics.retries.idempotent_cmd.retries.get() > retries_before,
815            "evolve did not retry an indeterminate error"
816        );
817        // Old check order returns ExpectedMismatch here; the fix returns Ok.
818        assert_eq!(res, CaESchema::Ok(SchemaId(1)));
819    }
820
821    fn strings(xs: &[((Strings, ()), u64, i64)]) -> Vec<Vec<&str>> {
822        xs.iter()
823            .map(|((k, _), _, _)| k.0.iter().map(|x| x.as_str()).collect())
824            .collect()
825    }
826
827    #[mz_persist_proc::test(tokio::test)]
828    #[cfg_attr(miri, ignore)]
829    async fn schema_evolution(dyncfgs: ConfigUpdates) {
830        async fn snap_streaming(
831            as_of: u64,
832            read: &mut ReadHandle<Strings, (), u64, i64>,
833        ) -> Vec<((Strings, ()), u64, i64)> {
834            // NB: We test with both snapshot_and_fetch and snapshot_and_stream
835            // because one uses the consolidating iter and one doesn't.
836            let mut ret = read
837                .snapshot_and_stream(Antichain::from_elem(as_of))
838                .await
839                .unwrap()
840                .collect::<Vec<_>>()
841                .await;
842            ret.sort();
843            ret
844        }
845
846        let client = new_test_client(&dyncfgs).await;
847        let d = Diagnostics::for_tests();
848        let shard_id = ShardId::new();
849        let schema0 = StringsSchema(vec![false]);
850        let schema1 = StringsSchema(vec![false, true]);
851
852        // Write some data at the original schema.
853        let (mut write0, mut read0) = client
854            .open::<Strings, (), u64, i64>(
855                shard_id,
856                Arc::new(schema0.clone()),
857                Arc::new(UnitSchema),
858                d.clone(),
859                true,
860            )
861            .await
862            .unwrap();
863        write0
864            .expect_compare_and_append(&[((Strings(vec!["0 before".into()]), ()), 0, 1)], 0, 1)
865            .await;
866        let expected = vec![vec!["0 before"]];
867        assert_eq!(strings(&snap_streaming(0, &mut read0).await), expected);
868        assert_eq!(strings(&read0.expect_snapshot_and_fetch(0).await), expected);
869
870        // Register and write some data at the new schema.
871        let res = client
872            .compare_and_evolve_schema::<Strings, (), u64, i64>(
873                shard_id,
874                SchemaId(0),
875                &schema1,
876                &UnitSchema,
877                d.clone(),
878            )
879            .await
880            .unwrap();
881        assert_eq!(res, CaESchema::Ok(SchemaId(1)));
882        let (mut write1, mut read1) = client
883            .open::<Strings, (), u64, i64>(
884                shard_id,
885                Arc::new(schema1.clone()),
886                Arc::new(UnitSchema),
887                d.clone(),
888                true,
889            )
890            .await
891            .unwrap();
892        write1
893            .expect_compare_and_append(
894                &[
895                    ((Strings(vec!["1 null".into(), "".into()]), ()), 1, 1),
896                    ((Strings(vec!["1 not".into(), "x".into()]), ()), 1, 1),
897                ],
898                1,
899                2,
900            )
901            .await;
902
903        // Continue to write data with the original schema.
904        write0
905            .expect_compare_and_append(&[((Strings(vec!["0 after".into()]), ()), 2, 1)], 2, 3)
906            .await;
907
908        // Original schema drops the new column in data written by new schema.
909        let expected = vec![
910            vec!["0 after"],
911            vec!["0 before"],
912            vec!["1 not"],
913            vec!["1 null"],
914        ];
915        assert_eq!(strings(&snap_streaming(2, &mut read0).await), expected);
916        assert_eq!(strings(&read0.expect_snapshot_and_fetch(2).await), expected);
917
918        // New schema adds nulls (represented by empty string in Strings) in
919        // data written by old schema.
920        let expected = vec![
921            vec!["0 after", ""],
922            vec!["0 before", ""],
923            vec!["1 not", "x"],
924            vec!["1 null", ""],
925        ];
926        assert_eq!(strings(&snap_streaming(2, &mut read1).await), expected);
927        assert_eq!(strings(&read1.expect_snapshot_and_fetch(2).await), expected);
928
929        // Probably too spammy to leave in the logs, but it was useful to have
930        // hooked up while iterating.
931        if false {
932            info_log_non_zero_metrics(&client.metrics.registry.gather());
933        }
934    }
935}