Skip to main content

mz_persist_client/
fetch.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//! Fetching batches of data from persist's backing store
11
12use std::fmt::{self, Debug};
13use std::marker::PhantomData;
14use std::sync::Arc;
15use std::time::{Duration, Instant};
16
17use anyhow::anyhow;
18use arrow::array::{Array, ArrayRef, AsArray, BooleanArray, Int64Array};
19use arrow::compute::FilterBuilder;
20use differential_dataflow::difference::Monoid;
21use differential_dataflow::lattice::Lattice;
22use differential_dataflow::trace::Description;
23use itertools::EitherOrBoth;
24use mz_dyncfg::{Config, ConfigSet, ConfigValHandle, ParameterScope};
25use mz_ore::bytes::SegmentedBytes;
26use mz_ore::cast::CastFrom;
27use mz_ore::{soft_assert_or_log, soft_panic_no_log, soft_panic_or_log};
28use mz_persist::indexed::columnar::arrow::{realloc_any, realloc_array};
29use mz_persist::indexed::columnar::parquet::BlobTraceBatchPartReader;
30use mz_persist::indexed::columnar::{ColumnarRecords, ColumnarRecordsStructuredExt};
31use mz_persist::indexed::encoding::{BlobTraceBatchPart, BlobTraceUpdates};
32use mz_persist::location::{Blob, SeqNo};
33use mz_persist::metrics::ColumnarMetrics;
34use mz_persist_types::arrow::ArrayOrd;
35use mz_persist_types::columnar::{ColumnDecoder, Schema, data_type};
36use mz_persist_types::part::Codec64Mut;
37use mz_persist_types::schema::backward_compatible;
38use mz_persist_types::stats::PartStats;
39use mz_persist_types::{Codec, Codec64};
40use mz_proto::RustType;
41use serde::{Deserialize, Serialize};
42use timely::PartialOrder;
43use timely::progress::frontier::AntichainRef;
44use timely::progress::{Antichain, Timestamp};
45use tracing::{Instrument, debug, debug_span, trace_span};
46
47use crate::ShardId;
48use crate::cfg::PersistConfig;
49use crate::error::InvalidUsage;
50use crate::internal::apply::Applier;
51use crate::internal::encoding::{LazyInlineBatchPart, LazyPartStats, LazyProto, Schemas};
52use crate::internal::machine::retry_external;
53use crate::internal::metrics::{Metrics, MetricsPermits, ReadMetrics, ShardMetrics};
54use crate::internal::paths::BlobKey;
55use crate::internal::state::{
56    BatchPart, HollowBatchPart, ProtoHollowBatchPart, ProtoInlineBatchPart,
57};
58use crate::read::LeasedReaderId;
59use crate::schema::{PartMigration, SchemaCache};
60
61pub(crate) const FETCH_SEMAPHORE_COST_ADJUSTMENT: Config<f64> = Config::new(
62    "persist_fetch_semaphore_cost_adjustment",
63    // We use `encoded_size_bytes` as the number of permits, but the parsed size
64    // is larger than the encoded one, so adjust it. This default value is from
65    // eyeballing graphs in experiments that were run on tpch loadgen data.
66    1.2,
67    "\
68    An adjustment multiplied by encoded_size_bytes to approximate an upper \
69    bound on the size in lgalloc, which includes the decoded version.",
70    ParameterScope::Environment,
71);
72
73pub(crate) const FETCH_SEMAPHORE_PERMIT_ADJUSTMENT: Config<f64> = Config::new(
74    "persist_fetch_semaphore_permit_adjustment",
75    1.0,
76    "\
77    A limit on the number of outstanding persist bytes being fetched and \
78    parsed, expressed as a multiplier of the process's memory limit. This data \
79    all spills to lgalloc, so values > 1.0 are safe. Only applied to cc \
80    replicas.",
81    ParameterScope::Environment,
82);
83
84pub(crate) const PART_DECODE_FORMAT: Config<&'static str> = Config::new(
85    "persist_part_decode_format",
86    PartDecodeFormat::default().as_str(),
87    "\
88    Format we'll use to decode a Persist Part, either 'row', \
89    'row_with_validate', or 'arrow' (Materialize).",
90    ParameterScope::Environment,
91);
92
93pub(crate) const PART_DECODE_BATCH_ROWS: Config<Option<usize>> = Config::new(
94    "persist_part_decode_batch_rows",
95    None,
96    "\
97    Maximum number of rows of a fetched blob part that a reader decodes at \
98    once. Unset decodes the whole part at once.",
99    ParameterScope::Environment,
100);
101
102pub(crate) const OPTIMIZE_IGNORED_DATA_FETCH: Config<bool> = Config::new(
103    "persist_optimize_ignored_data_fetch",
104    true,
105    "CYA to allow opt-out of a performance optimization to skip fetching ignored data",
106    ParameterScope::Environment,
107);
108
109pub(crate) const VALIDATE_PART_BOUNDS_ON_READ: Config<bool> = Config::new(
110    "persist_validate_part_bounds_on_read",
111    false,
112    "Validate the part lower <= the batch lower and the part upper <= batch upper,\
113    for the batch containing that part",
114    ParameterScope::Environment,
115);
116
117#[derive(Debug, Clone)]
118pub(crate) struct FetchConfig {
119    pub(crate) validate_bounds_on_read: bool,
120    /// See [`PART_DECODE_BATCH_ROWS`]. Only consulted by readers that decode
121    /// through [`FetchedPart`].
122    pub(crate) part_decode_batch_rows: Option<usize>,
123}
124
125impl FetchConfig {
126    pub fn from_persist_config(cfg: &PersistConfig) -> Self {
127        Self {
128            validate_bounds_on_read: VALIDATE_PART_BOUNDS_ON_READ.get(cfg),
129            part_decode_batch_rows: PART_DECODE_BATCH_ROWS.get(cfg),
130        }
131    }
132}
133
134#[derive(Debug, Clone)]
135pub(crate) struct BatchFetcherConfig {
136    pub(crate) part_decode_format: ConfigValHandle<String>,
137    pub(crate) validate_bounds_on_read: ConfigValHandle<bool>,
138    pub(crate) part_decode_batch_rows: ConfigValHandle<Option<usize>>,
139}
140
141impl BatchFetcherConfig {
142    pub fn new(value: &PersistConfig) -> Self {
143        Self {
144            part_decode_format: PART_DECODE_FORMAT.handle(value),
145            validate_bounds_on_read: VALIDATE_PART_BOUNDS_ON_READ.handle(value),
146            part_decode_batch_rows: PART_DECODE_BATCH_ROWS.handle(value),
147        }
148    }
149
150    pub fn part_decode_format(&self) -> PartDecodeFormat {
151        PartDecodeFormat::from_str(self.part_decode_format.get().as_str())
152    }
153
154    /// The [`FetchConfig`] with values read at call time.
155    pub fn fetch_config(&self) -> FetchConfig {
156        FetchConfig {
157            validate_bounds_on_read: self.validate_bounds_on_read.get(),
158            part_decode_batch_rows: self.part_decode_batch_rows.get(),
159        }
160    }
161}
162
163/// Capable of fetching [`LeasedBatchPart`] while not holding any capabilities.
164#[derive(Debug)]
165pub struct BatchFetcher<K, V, T, D>
166where
167    T: Timestamp + Lattice + Codec64,
168    // These are only here so we can use them in the auto-expiring `Drop` impl.
169    K: Debug + Codec,
170    V: Debug + Codec,
171    D: Monoid + Codec64 + Send + Sync,
172{
173    pub(crate) cfg: BatchFetcherConfig,
174    pub(crate) blob: Arc<dyn Blob>,
175    pub(crate) metrics: Arc<Metrics>,
176    pub(crate) shard_metrics: Arc<ShardMetrics>,
177    pub(crate) shard_id: ShardId,
178    pub(crate) read_schemas: Schemas<K, V>,
179    pub(crate) schema_cache: SchemaCache<K, V, T, D>,
180    pub(crate) is_transient: bool,
181
182    // Ensures that `BatchFetcher` is of the same type as the `ReadHandle` it's
183    // derived from.
184    pub(crate) _phantom: PhantomData<fn() -> (K, V, T, D)>,
185}
186
187// Hand-written (rather than derived) so cloning does not require `K: Clone`
188// etc.: every field is an `Arc` or independently `Clone`. The `schema_cache`
189// clone shares the schema-lookup maps and applier, so clones reuse cached
190// schema fetches and only duplicate a small per-clone migration memo. Used to
191// run several `fetch_leased_part` calls concurrently, each on its own clone.
192impl<K, V, T, D> Clone for BatchFetcher<K, V, T, D>
193where
194    T: Timestamp + Lattice + Codec64,
195    K: Debug + Codec,
196    V: Debug + Codec,
197    D: Monoid + Codec64 + Send + Sync,
198{
199    fn clone(&self) -> Self {
200        Self {
201            cfg: self.cfg.clone(),
202            blob: Arc::clone(&self.blob),
203            metrics: Arc::clone(&self.metrics),
204            shard_metrics: Arc::clone(&self.shard_metrics),
205            shard_id: self.shard_id.clone(),
206            read_schemas: self.read_schemas.clone(),
207            schema_cache: self.schema_cache.clone(),
208            is_transient: self.is_transient,
209            _phantom: PhantomData,
210        }
211    }
212}
213
214impl<K, V, T, D> BatchFetcher<K, V, T, D>
215where
216    K: Debug + Codec,
217    V: Debug + Codec,
218    T: Timestamp + Lattice + Codec64 + Sync,
219    D: Monoid + Codec64 + Send + Sync,
220{
221    /// Trade in an exchange-able [LeasedBatchPart] for the data it represents.
222    ///
223    /// Note to check the `LeasedBatchPart` documentation for how to handle the
224    /// returned value.
225    pub async fn fetch_leased_part(
226        &mut self,
227        part: ExchangeableBatchPart<T>,
228    ) -> Result<Result<FetchedBlob<K, V, T, D>, BlobKey>, InvalidUsage<T>> {
229        let ExchangeableBatchPart {
230            shard_id,
231            encoded_size_bytes: _,
232            desc,
233            filter,
234            filter_pushdown_audit,
235            part,
236            reader_id: _,
237        } = part;
238        let part: BatchPart<T> = part.decode_to().expect("valid part");
239        if shard_id != self.shard_id {
240            return Err(InvalidUsage::BatchNotFromThisShard {
241                batch_shard: shard_id,
242                handle_shard: self.shard_id.clone(),
243            });
244        }
245
246        let migration =
247            PartMigration::new(&part, self.read_schemas.clone(), &mut self.schema_cache)
248                .await
249                .unwrap_or_else(|read_schemas| {
250                    panic!(
251                        "could not decode part {:?} with schema: {:?}",
252                        part.schema_id(),
253                        read_schemas
254                    )
255                });
256
257        let (buf, fetch_permit) = match &part {
258            BatchPart::Hollow(x) => {
259                let fetch_permit = self
260                    .metrics
261                    .semaphore
262                    .acquire_fetch_permits(x.encoded_size_bytes)
263                    .await;
264                let read_metrics = if self.is_transient {
265                    &self.metrics.read.unindexed
266                } else {
267                    &self.metrics.read.batch_fetcher
268                };
269                let buf = fetch_batch_part_blob(
270                    &shard_id,
271                    self.blob.as_ref(),
272                    &self.metrics,
273                    &self.shard_metrics,
274                    read_metrics,
275                    x,
276                )
277                .await;
278                let buf = match buf {
279                    Ok(buf) => buf,
280                    Err(key) => return Ok(Err(key)),
281                };
282                let buf = FetchedBlobBuf::Hollow {
283                    buf,
284                    part: x.clone(),
285                };
286                (buf, Some(Arc::new(fetch_permit)))
287            }
288            BatchPart::Inline {
289                updates,
290                ts_rewrite,
291                ..
292            } => {
293                let buf = FetchedBlobBuf::Inline {
294                    desc: desc.clone(),
295                    updates: updates.clone(),
296                    ts_rewrite: ts_rewrite.clone(),
297                };
298                (buf, None)
299            }
300        };
301        let fetched_blob = FetchedBlob {
302            metrics: Arc::clone(&self.metrics),
303            read_metrics: self.metrics.read.batch_fetcher.clone(),
304            buf,
305            registered_desc: desc.clone(),
306            migration,
307            filter: filter.clone(),
308            filter_pushdown_audit,
309            structured_part_audit: self.cfg.part_decode_format(),
310            fetch_permit,
311            _phantom: PhantomData,
312            fetch_config: self.cfg.fetch_config(),
313        };
314        Ok(Ok(fetched_blob))
315    }
316
317    /// Diagnoses a missing-blob fetch failure for a part leased by the given
318    /// reader. See the free function `missing_blob_diagnostics`.
319    pub async fn missing_blob_diagnostics(
320        &self,
321        reader_id: &LeasedReaderId,
322    ) -> MissingBlobDiagnosis {
323        missing_blob_diagnostics(self.schema_cache.applier(), reader_id.clone()).await
324    }
325}
326
327/// How long [missing_blob_diagnostics] waits to refresh state.
328const MISSING_BLOB_DIAGNOSIS_TIMEOUT: Duration = Duration::from_secs(30);
329
330/// The result of [BatchFetcher::missing_blob_diagnostics].
331#[derive(Debug)]
332pub enum MissingBlobDiagnosis {
333    /// The reader has been expired out of state, so its lease was lost.
334    LeaseExpired {
335        /// The reader that leased the part.
336        reader_id: LeasedReaderId,
337    },
338    /// The shard was tombstoned, which removes every reader. Not a lost lease.
339    ShardTombstoned {
340        /// The reader that leased the part.
341        reader_id: LeasedReaderId,
342    },
343    /// The reader is still in state, so its hold did not protect the blob.
344    LeasePresent {
345        /// The reader that leased the part.
346        reader_id: LeasedReaderId,
347        /// The reader's lease state, for the error message.
348        lease_state: String,
349    },
350    /// State could not be refreshed in time to tell.
351    Unknown {
352        /// The reader that leased the part.
353        reader_id: LeasedReaderId,
354    },
355}
356
357impl fmt::Display for MissingBlobDiagnosis {
358    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
359        match self {
360            MissingBlobDiagnosis::LeaseExpired { reader_id } => write!(
361                f,
362                "reader {reader_id} has been expired out of state; \
363                 the process likely failed to heartbeat it within the lease duration \
364                 (machine sleep, CPU/memory starvation, or a partition from consensus?)"
365            ),
366            MissingBlobDiagnosis::ShardTombstoned { reader_id } => write!(
367                f,
368                "reader {reader_id}: the shard has been tombstoned, which removes every reader"
369            ),
370            MissingBlobDiagnosis::LeasePresent {
371                reader_id,
372                lease_state,
373            } => write!(
374                f,
375                "reader {reader_id} is still present in state ({lease_state}); \
376                 a missing blob despite a live lease indicates a GC bug"
377            ),
378            MissingBlobDiagnosis::Unknown { reader_id } => write!(
379                f,
380                "reader {reader_id}: could not refresh state within {}s to diagnose the lease; \
381                 partitioned from consensus?",
382                MISSING_BLOB_DIAGNOSIS_TIMEOUT.as_secs()
383            ),
384        }
385    }
386}
387
388/// Diagnoses a missing-blob fetch failure: refreshes the shard state and
389/// reports whether the reader that leased the part is still present in it.
390///
391/// A missing blob means garbage collection deleted a blob that the part's
392/// lease (a seqno hold in shard state) should have protected. See
393/// [MissingBlobDiagnosis] for what each outcome means.
394pub(crate) async fn missing_blob_diagnostics<K, V, T, D>(
395    applier: &Applier<K, V, T, D>,
396    reader_id: LeasedReaderId,
397) -> MissingBlobDiagnosis
398where
399    K: Debug + Codec,
400    V: Debug + Codec,
401    T: Timestamp + Lattice + Codec64 + Sync,
402    D: Monoid + Codec64,
403{
404    // Refreshing state talks to consensus; this runs on an already-fatal path
405    // and a partition from consensus may be the very reason the lease was
406    // lost, so don't let the diagnosis block the restart indefinitely.
407    let refresh = applier.fetch_and_update_state(None);
408    if tokio::time::timeout(MISSING_BLOB_DIAGNOSIS_TIMEOUT, refresh)
409        .await
410        .is_err()
411    {
412        return MissingBlobDiagnosis::Unknown { reader_id };
413    }
414    if applier.is_tombstone() {
415        return MissingBlobDiagnosis::ShardTombstoned { reader_id };
416    }
417    match applier.reader_lease(reader_id.clone()) {
418        Some(lease_state) => MissingBlobDiagnosis::LeasePresent {
419            reader_id,
420            lease_state: format!("{lease_state:?}"),
421        },
422        None => MissingBlobDiagnosis::LeaseExpired { reader_id },
423    }
424}
425
426#[derive(Debug, Clone, Serialize, Deserialize)]
427pub(crate) enum FetchBatchFilter<T> {
428    Snapshot {
429        as_of: Antichain<T>,
430    },
431    Listen {
432        as_of: Antichain<T>,
433        lower: Antichain<T>,
434    },
435    Compaction {
436        since: Antichain<T>,
437    },
438}
439
440impl<T: Timestamp + Lattice> FetchBatchFilter<T> {
441    pub(crate) fn filter_ts(&self, t: &mut T) -> bool {
442        match self {
443            FetchBatchFilter::Snapshot { as_of } => {
444                // This time is covered by a listen
445                if as_of.less_than(t) {
446                    return false;
447                }
448                t.advance_by(as_of.borrow());
449                true
450            }
451            FetchBatchFilter::Listen { as_of, lower } => {
452                // This time is covered by a snapshot
453                if !as_of.less_than(t) {
454                    return false;
455                }
456
457                // Because of compaction, the next batch we get might also
458                // contain updates we've already emitted. For example, we
459                // emitted `[1, 2)` and then compaction combined that batch with
460                // a `[2, 3)` batch into a new `[1, 3)` batch. If this happens,
461                // we just need to filter out anything < the frontier. This
462                // frontier was the upper of the last batch (and thus exclusive)
463                // so for the == case, we still emit.
464                if !lower.less_equal(t) {
465                    return false;
466                }
467                true
468            }
469            FetchBatchFilter::Compaction { since } => {
470                t.advance_by(since.borrow());
471                true
472            }
473        }
474    }
475}
476
477/// Trade in an exchange-able [LeasedBatchPart] for the data it represents.
478///
479/// Note to check the `LeasedBatchPart` documentation for how to handle the
480/// returned value.
481pub(crate) async fn fetch_leased_part<K, V, T, D>(
482    cfg: &PersistConfig,
483    part: &LeasedBatchPart<T>,
484    blob: &dyn Blob,
485    metrics: Arc<Metrics>,
486    read_metrics: &ReadMetrics,
487    shard_metrics: &ShardMetrics,
488    reader_id: &LeasedReaderId,
489    read_schemas: Schemas<K, V>,
490    schema_cache: &mut SchemaCache<K, V, T, D>,
491) -> FetchedPart<K, V, T, D>
492where
493    K: Debug + Codec,
494    V: Debug + Codec,
495    T: Timestamp + Lattice + Codec64 + Sync,
496    D: Monoid + Codec64 + Send + Sync,
497{
498    let fetch_config = FetchConfig::from_persist_config(cfg);
499    let fetched = match &part.part {
500        BatchPart::Hollow(x) => fetch_batch_part_blob(
501            &part.shard_id,
502            blob,
503            &metrics,
504            shard_metrics,
505            read_metrics,
506            x,
507        )
508        .await
509        .map(|buf| {
510            PartSource::from_hollow_blob(
511                &fetch_config,
512                &metrics,
513                read_metrics,
514                part.desc.clone(),
515                x,
516                &buf,
517            )
518        }),
519        BatchPart::Inline {
520            updates,
521            ts_rewrite,
522            ..
523        } => Ok(PartSource::Whole(EncodedPart::from_inline(
524            &fetch_config,
525            &metrics,
526            read_metrics.clone(),
527            part.desc.clone(),
528            updates,
529            ts_rewrite.as_ref(),
530        ))),
531    };
532    let source = match fetched {
533        Ok(x) => x,
534        Err(blob_key) => {
535            // Ideally, readers should never encounter a missing blob. They place a seqno
536            // hold as they consume their snapshot/listen, preventing any blobs they need
537            // from being deleted by garbage collection, and all blob implementations are
538            // linearizable so there should be no possibility of stale reads.
539            //
540            // If we do have a bug and a reader does encounter a missing blob, the state
541            // cannot be recovered, and our best option is to panic and retry the whole
542            // process.
543            let diagnostics =
544                missing_blob_diagnostics(schema_cache.applier(), reader_id.clone()).await;
545            panic!("could not fetch batch part {}: {}", blob_key, diagnostics)
546        }
547    };
548    let part_cfg = BatchFetcherConfig::new(cfg);
549    let migration = PartMigration::new(&part.part, read_schemas, schema_cache)
550        .await
551        .unwrap_or_else(|read_schemas| {
552            panic!(
553                "could not decode part {:?} with schema: {:?}",
554                part.part.schema_id(),
555                read_schemas
556            )
557        });
558    FetchedPart::new(
559        metrics,
560        source,
561        migration,
562        part.filter.clone(),
563        part.filter_pushdown_audit,
564        part_cfg.part_decode_format(),
565        part.part.stats(),
566    )
567}
568
569pub(crate) async fn fetch_batch_part_blob<T>(
570    shard_id: &ShardId,
571    blob: &dyn Blob,
572    metrics: &Metrics,
573    shard_metrics: &ShardMetrics,
574    read_metrics: &ReadMetrics,
575    part: &HollowBatchPart<T>,
576) -> Result<SegmentedBytes, BlobKey> {
577    let now = Instant::now();
578    let get_span = debug_span!("fetch_batch::get");
579    let blob_key = part.key.complete(shard_id);
580    let value = retry_external(&metrics.retries.external.fetch_batch_get, || async {
581        shard_metrics.blob_gets.inc();
582        // Name the blob in the error. A GET stuck retrying forever surfaces only in the retry log
583        // (see `retry_external`), which prints the error, so without this the log cannot say which
584        // blob, and thus which shard, is wedged.
585        blob.get(&blob_key)
586            .await
587            .map_err(|err| err.context(format!("blob {blob_key}")))
588    })
589    .instrument(get_span.clone())
590    .await
591    .ok_or(blob_key)?;
592
593    drop(get_span);
594
595    read_metrics.part_count.inc();
596    read_metrics.part_bytes.inc_by(u64::cast_from(value.len()));
597    read_metrics.seconds.inc_by(now.elapsed().as_secs_f64());
598
599    Ok(value)
600}
601
602pub(crate) fn decode_batch_part_blob<T>(
603    cfg: &FetchConfig,
604    metrics: &Metrics,
605    read_metrics: &ReadMetrics,
606    registered_desc: Description<T>,
607    part: &HollowBatchPart<T>,
608    buf: &SegmentedBytes,
609) -> EncodedPart<T>
610where
611    T: Timestamp + Lattice + Codec64,
612{
613    trace_span!("fetch_batch::decode").in_scope(|| {
614        let parsed = metrics
615            .codecs
616            .batch
617            .decode(|| BlobTraceBatchPart::decode(buf, &metrics.columnar))
618            .map_err(|err| anyhow!("couldn't decode batch at key {}: {}", part.key, err))
619            // We received a State that we couldn't decode. This could happen if
620            // persist messes up backward/forward compatibility, if the durable
621            // data was corrupted, or if operations messes up deployment. In any
622            // case, fail loudly.
623            .expect("internal error: invalid encoded state");
624        read_metrics
625            .part_goodbytes
626            .inc_by(u64::cast_from(parsed.updates.goodbytes()));
627        EncodedPart::from_hollow(cfg, read_metrics.clone(), registered_desc, part, parsed)
628    })
629}
630
631pub(crate) async fn fetch_batch_part<T>(
632    cfg: &FetchConfig,
633    shard_id: &ShardId,
634    blob: &dyn Blob,
635    metrics: &Metrics,
636    shard_metrics: &ShardMetrics,
637    read_metrics: &ReadMetrics,
638    registered_desc: &Description<T>,
639    part: &HollowBatchPart<T>,
640) -> Result<EncodedPart<T>, BlobKey>
641where
642    T: Timestamp + Lattice + Codec64,
643{
644    let buf =
645        fetch_batch_part_blob(shard_id, blob, metrics, shard_metrics, read_metrics, part).await?;
646    let part = decode_batch_part_blob(
647        cfg,
648        metrics,
649        read_metrics,
650        registered_desc.clone(),
651        part,
652        &buf,
653    );
654    Ok(part)
655}
656
657/// This represents the lease of a seqno. It's generally paired with some external state,
658/// like a hollow part: holding this lease indicates that we may still want to fetch that part,
659/// and should hold back GC to keep it around.
660///
661/// Generally the state and lease are bundled together, as in [LeasedBatchPart]... but sometimes
662/// it's necessary to handle them separately, so this struct is exposed as well. Handle with care.
663#[derive(Clone, Debug)]
664pub struct Lease(Arc<SeqNo>);
665
666impl Lease {
667    /// Creates a new [Lease] that holds the given [SeqNo].
668    pub fn new(seqno: SeqNo) -> Self {
669        Self(Arc::new(seqno))
670    }
671
672    /// Returns the inner [SeqNo] of this [Lease].
673    pub fn seqno(&self) -> SeqNo {
674        *self.0
675    }
676
677    /// Returns the number of live copies of this lease, including this one.
678    pub fn count(&self) -> usize {
679        Arc::strong_count(&self.0)
680    }
681}
682
683/// A token representing one fetch-able batch part.
684///
685/// It is tradeable via `crate::fetch::fetch_batch` for the resulting data
686/// stored in the part.
687///
688/// # Exchange
689///
690/// You can exchange `LeasedBatchPart`:
691/// - If `leased_seqno.is_none()`
692/// - By converting it to [`ExchangeableBatchPart`] through
693///   `Self::into_exchangeable_part`. [`ExchangeableBatchPart`] is exchangeable,
694///   including over the network.
695///
696/// n.b. `Self::into_exchangeable_part` is known to be equivalent to
697/// `SerdeLeasedBatchPart::from(self)`, but we want the additional warning message to
698/// be visible and sufficiently scary.
699///
700/// # Panics
701/// `LeasedBatchPart` panics when dropped unless a very strict set of invariants are
702/// held:
703///
704/// `LeasedBatchPart` may only be dropped if it:
705/// - Does not have a leased `SeqNo (i.e. `self.leased_seqno.is_none()`)
706///
707/// In any other circumstance, dropping `LeasedBatchPart` panics.
708#[derive(Debug)]
709pub struct LeasedBatchPart<T> {
710    pub(crate) metrics: Arc<Metrics>,
711    pub(crate) shard_id: ShardId,
712    pub(crate) filter: FetchBatchFilter<T>,
713    pub(crate) desc: Description<T>,
714    pub(crate) part: BatchPart<T>,
715    /// The lease that prevents this part from being GCed. Code should ensure that this lease
716    /// lives as long as the part is needed.
717    pub(crate) lease: Lease,
718    /// The id of the reader that leased this part, for diagnostics: if the
719    /// blob backing the part goes missing, knowing whether this reader is
720    /// still present in state distinguishes a lost lease from a GC bug.
721    pub(crate) reader_id: LeasedReaderId,
722    pub(crate) filter_pushdown_audit: bool,
723    /// Whether the containing batch has a run that may hold updates outside
724    /// the registered desc (see `RunMeta::bounds_truncated`). A fetch filters
725    /// those updates out, but write-time part statistics count them, so
726    /// optimizations that substitute statistics for a fetch must not fire.
727    pub(crate) bounds_truncated: bool,
728}
729
730impl<T> LeasedBatchPart<T>
731where
732    T: Timestamp + Codec64,
733{
734    /// Takes `self` into a [`ExchangeableBatchPart`], which allows `self` to be
735    /// exchanged (potentially across the network).
736    ///
737    /// !!!WARNING!!!
738    ///
739    /// This method also returns the [Lease] associated with the given part, since
740    /// that can't travel across process boundaries. The caller is responsible for
741    /// ensuring that the lease is held for as long as the batch part may be in use:
742    /// dropping it too early may cause a fetch to fail.
743    pub(crate) fn into_exchangeable_part(self) -> (ExchangeableBatchPart<T>, Lease) {
744        // If `x` has a lease, we've effectively transferred it to `r`.
745        let lease = self.lease.clone();
746        let part = ExchangeableBatchPart {
747            shard_id: self.shard_id,
748            encoded_size_bytes: self.part.encoded_size_bytes(),
749            desc: self.desc.clone(),
750            filter: self.filter.clone(),
751            part: LazyProto::from(&self.part.into_proto()),
752            reader_id: self.reader_id.clone(),
753            filter_pushdown_audit: self.filter_pushdown_audit,
754        };
755        (part, lease)
756    }
757
758    /// The encoded size of this part in bytes
759    pub fn encoded_size_bytes(&self) -> usize {
760        self.part.encoded_size_bytes()
761    }
762
763    /// The filter has indicated we don't need this part, we can verify the
764    /// ongoing end-to-end correctness of corner cases via "audit". This means
765    /// we fetch the part like normal and if the MFP keeps anything from it,
766    /// then something has gone horribly wrong.
767    pub fn request_filter_pushdown_audit(&mut self) {
768        self.filter_pushdown_audit = true;
769    }
770
771    /// Returns the pushdown stats for this part.
772    ///
773    /// Stats written by a newer version may not decode; those return `None`,
774    /// the same as a part that carries no stats.
775    pub fn stats(&self) -> Option<PartStats> {
776        self.part.stats().and_then(|x| x.try_decode().ok())
777    }
778
779    /// Apply any relevant projection pushdown optimizations, assuming that the data in the part
780    /// is equivalent to the provided key and value.
781    pub fn maybe_optimize(&mut self, cfg: &ConfigSet, key: ArrayRef, val: ArrayRef) {
782        assert_eq!(key.len(), 1, "expect a single-row key array");
783        assert_eq!(val.len(), 1, "expect a single-row val array");
784        let as_of = match &self.filter {
785            FetchBatchFilter::Snapshot { as_of } => as_of,
786            FetchBatchFilter::Listen { .. } | FetchBatchFilter::Compaction { .. } => return,
787        };
788        if !OPTIMIZE_IGNORED_DATA_FETCH.get(cfg) {
789            return;
790        }
791        // A truncated batch's parts may physically hold updates outside the
792        // registered desc. A fetch filters those out, but the write-time
793        // diffs_sum counts them, so substituting it would fabricate data.
794        if self.bounds_truncated {
795            return;
796        }
797        let (diffs_sum, _stats) = match &self.part {
798            BatchPart::Hollow(x) => (x.diffs_sum, x.stats.as_ref()),
799            BatchPart::Inline { .. } => return,
800        };
801        debug!(
802            "try_optimize_ignored_data_fetch diffs_sum={:?} as_of={:?} lower={:?} upper={:?}",
803            // This is only used for debugging, so hack to assume that D is i64.
804            diffs_sum.map(i64::decode),
805            as_of.elements(),
806            self.desc.lower().elements(),
807            self.desc.upper().elements()
808        );
809        let as_of = match &as_of.elements() {
810            &[as_of] => as_of,
811            _ => return,
812        };
813        // NOTE: `diffs_sum` sums every row physically in the blob, while
814        // reads truncate rows outside the registered desc. Substituting it is
815        // sound only while no writer registers a batch with tighter bounds
816        // than the blob holds (none does today, and rewritten batches prove
817        // it), which nothing here can re-check without fetching the blob.
818        let eligible = self.desc.upper().less_equal(as_of) && self.desc.since().less_equal(as_of);
819        if !eligible {
820            return;
821        }
822        let Some(diffs_sum) = diffs_sum else {
823            return;
824        };
825
826        debug!(
827            "try_optimize_ignored_data_fetch faked {:?} diffs at ts {:?} skipping fetch of {} bytes",
828            // This is only used for debugging, so hack to assume that D is i64.
829            i64::decode(diffs_sum),
830            as_of,
831            self.part.encoded_size_bytes(),
832        );
833        self.metrics.pushdown.parts_faked_count.inc();
834        self.metrics
835            .pushdown
836            .parts_faked_bytes
837            .inc_by(u64::cast_from(self.part.encoded_size_bytes()));
838        let timestamps = {
839            let mut col = Codec64Mut::with_capacity(1);
840            col.push(as_of);
841            col.finish()
842        };
843        let diffs = {
844            let mut col = Codec64Mut::with_capacity(1);
845            col.push_raw(diffs_sum);
846            col.finish()
847        };
848        let updates = BlobTraceUpdates::Structured {
849            key_values: ColumnarRecordsStructuredExt { key, val },
850            timestamps,
851            diffs,
852        };
853        let faked_data = LazyInlineBatchPart::from(&ProtoInlineBatchPart {
854            desc: Some(self.desc.into_proto()),
855            index: 0,
856            updates: Some(updates.into_proto()),
857        });
858        self.part = BatchPart::Inline {
859            updates: faked_data,
860            ts_rewrite: None,
861            schema_id: None,
862            deprecated_schema_id: None,
863        };
864    }
865}
866
867impl<T> Drop for LeasedBatchPart<T> {
868    /// For details, see [`LeasedBatchPart`].
869    fn drop(&mut self) {
870        self.metrics.lease.dropped_part.inc()
871    }
872}
873
874/// A [Blob] object that has been fetched, but not at all decoded.
875///
876/// In contrast to [FetchedPart], this representation hasn't yet done parquet
877/// decoding.
878#[derive(Debug)]
879pub struct FetchedBlob<K: Codec, V: Codec, T, D> {
880    metrics: Arc<Metrics>,
881    read_metrics: ReadMetrics,
882    buf: FetchedBlobBuf<T>,
883    registered_desc: Description<T>,
884    migration: PartMigration<K, V>,
885    filter: FetchBatchFilter<T>,
886    filter_pushdown_audit: bool,
887    structured_part_audit: PartDecodeFormat,
888    fetch_permit: Option<Arc<MetricsPermits>>,
889    fetch_config: FetchConfig,
890    _phantom: PhantomData<fn() -> D>,
891}
892
893#[derive(Debug, Clone)]
894enum FetchedBlobBuf<T> {
895    Hollow {
896        buf: SegmentedBytes,
897        part: HollowBatchPart<T>,
898    },
899    Inline {
900        desc: Description<T>,
901        updates: LazyInlineBatchPart,
902        ts_rewrite: Option<Antichain<T>>,
903    },
904}
905
906impl<K: Codec, V: Codec, T: Clone, D> Clone for FetchedBlob<K, V, T, D> {
907    fn clone(&self) -> Self {
908        Self {
909            metrics: Arc::clone(&self.metrics),
910            read_metrics: self.read_metrics.clone(),
911            buf: self.buf.clone(),
912            registered_desc: self.registered_desc.clone(),
913            migration: self.migration.clone(),
914            filter: self.filter.clone(),
915            filter_pushdown_audit: self.filter_pushdown_audit.clone(),
916            fetch_permit: self.fetch_permit.clone(),
917            structured_part_audit: self.structured_part_audit.clone(),
918            fetch_config: self.fetch_config.clone(),
919            _phantom: self._phantom.clone(),
920        }
921    }
922}
923
924/// [FetchedPart] but with an accompanying permit from the fetch mem/disk
925/// semaphore.
926pub struct ShardSourcePart<K: Codec, V: Codec, T, D> {
927    /// The underlying [FetchedPart].
928    pub part: FetchedPart<K, V, T, D>,
929    fetch_permit: Option<Arc<MetricsPermits>>,
930}
931
932impl<K, V, T: Debug, D: Debug> Debug for ShardSourcePart<K, V, T, D>
933where
934    K: Codec + Debug,
935    <K as Codec>::Storage: Debug,
936    V: Codec + Debug,
937    <V as Codec>::Storage: Debug,
938{
939    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
940        let ShardSourcePart { part, fetch_permit } = self;
941        f.debug_struct("ShardSourcePart")
942            .field("part", part)
943            .field("fetch_permit", fetch_permit)
944            .finish()
945    }
946}
947
948impl<K: Codec, V: Codec, T: Timestamp + Lattice + Codec64, D> FetchedBlob<K, V, T, D> {
949    /// Partially decodes this blob into a [FetchedPart].
950    pub fn parse(&self) -> ShardSourcePart<K, V, T, D> {
951        self.parse_internal(&self.fetch_config)
952    }
953
954    /// Partially decodes this blob into a [FetchedPart].
955    pub(crate) fn parse_internal(&self, cfg: &FetchConfig) -> ShardSourcePart<K, V, T, D> {
956        let (part, stats) = match &self.buf {
957            FetchedBlobBuf::Hollow { buf, part } => {
958                let source = PartSource::from_hollow_blob(
959                    cfg,
960                    &self.metrics,
961                    &self.read_metrics,
962                    self.registered_desc.clone(),
963                    part,
964                    buf,
965                );
966                (source, part.stats.as_ref())
967            }
968            FetchedBlobBuf::Inline {
969                desc,
970                updates,
971                ts_rewrite,
972            } => {
973                let parsed = EncodedPart::from_inline(
974                    cfg,
975                    &self.metrics,
976                    self.read_metrics.clone(),
977                    desc.clone(),
978                    updates,
979                    ts_rewrite.as_ref(),
980                );
981                (PartSource::Whole(parsed), None)
982            }
983        };
984        let part = FetchedPart::new(
985            Arc::clone(&self.metrics),
986            part,
987            self.migration.clone(),
988            self.filter.clone(),
989            self.filter_pushdown_audit,
990            self.structured_part_audit,
991            stats,
992        );
993        ShardSourcePart {
994            part,
995            fetch_permit: self.fetch_permit.clone(),
996        }
997    }
998
999    /// Decodes and returns the pushdown stats for this part, if known.
1000    ///
1001    /// Stats written by a newer version may not decode; those return `None`,
1002    /// the same as a part that carries no stats.
1003    pub fn stats(&self) -> Option<PartStats> {
1004        match &self.buf {
1005            FetchedBlobBuf::Hollow { part, .. } => {
1006                part.stats.as_ref().and_then(|x| x.try_decode().ok())
1007            }
1008            FetchedBlobBuf::Inline { .. } => None,
1009        }
1010    }
1011}
1012
1013/// A [Blob] object that has been fetched, but not yet fully decoded.
1014///
1015/// In contrast to [FetchedBlob], this representation has already done parquet
1016/// decoding.
1017#[derive(Debug)]
1018pub struct FetchedPart<K: Codec, V: Codec, T, D> {
1019    metrics: Arc<Metrics>,
1020    ts_filter: FetchBatchFilter<T>,
1021    // The decoded rows of the whole part, or of the current batch of
1022    // `batches`. `None` before the first batch and after the last.
1023    rows: Option<DecodedRows<K, V>>,
1024    // Batches not yet decoded into `rows`, if the part is decoded in batches.
1025    batches: Option<EncodedPartBatches<T>>,
1026    part_decode_format: PartDecodeFormat,
1027    // Row count of the whole part before normalization.
1028    part_rows: usize,
1029    migration: PartMigration<K, V>,
1030    filter_pushdown_audit: Option<LazyPartStats>,
1031    peek_stash: Option<((K, V), T, D)>,
1032    key_storage: Option<K::Storage>,
1033    val_storage: Option<V::Storage>,
1034
1035    _phantom: PhantomData<fn() -> D>,
1036}
1037
1038/// Decoded rows of a [`FetchedPart`] and the cursor into them.
1039#[derive(Debug)]
1040struct DecodedRows<K: Codec, V: Codec> {
1041    // If migration is Either, then the columnar one will have already been
1042    // applied here on the structured data only.
1043    part: EitherOrBoth<
1044        ColumnarRecords,
1045        (
1046            <K::Schema as Schema<K>>::Decoder,
1047            <V::Schema as Schema<V>>::Decoder,
1048        ),
1049    >,
1050    timestamps: Int64Array,
1051    diffs: Int64Array,
1052    cursor: usize,
1053}
1054
1055impl<K: Codec, V: Codec, T: Timestamp + Lattice + Codec64, D> FetchedPart<K, V, T, D> {
1056    pub(crate) fn new(
1057        metrics: Arc<Metrics>,
1058        source: PartSource<T>,
1059        migration: PartMigration<K, V>,
1060        ts_filter: FetchBatchFilter<T>,
1061        filter_pushdown_audit: bool,
1062        part_decode_format: PartDecodeFormat,
1063        stats: Option<&LazyPartStats>,
1064    ) -> Self {
1065        let (updates, batches, part_rows) = match source {
1066            PartSource::Whole(part) => {
1067                let part_rows = part.updates().len();
1068                (Some(part.normalize(&metrics.columnar)), None, part_rows)
1069            }
1070            PartSource::Batched(batches) => {
1071                let part_rows = batches.reader.num_rows();
1072                (None, Some(batches), part_rows)
1073            }
1074        };
1075        let part_len = u64::cast_from(part_rows);
1076        match &migration {
1077            PartMigration::SameSchema { .. } => metrics.schema.migration_count_same.inc(),
1078            PartMigration::Schemaless { .. } => {
1079                metrics.schema.migration_count_codec.inc();
1080                metrics.schema.migration_len_legacy_codec.inc_by(part_len);
1081            }
1082            PartMigration::Either { .. } => {
1083                metrics.schema.migration_count_either.inc();
1084                match part_decode_format {
1085                    PartDecodeFormat::Row {
1086                        validate_structured: false,
1087                    } => metrics.schema.migration_len_either_codec.inc_by(part_len),
1088                    PartDecodeFormat::Row {
1089                        validate_structured: true,
1090                    } => {
1091                        metrics.schema.migration_len_either_codec.inc_by(part_len);
1092                        metrics.schema.migration_len_either_arrow.inc_by(part_len);
1093                    }
1094                    PartDecodeFormat::Arrow => {
1095                        metrics.schema.migration_len_either_arrow.inc_by(part_len)
1096                    }
1097                }
1098            }
1099        }
1100
1101        let filter_pushdown_audit = if filter_pushdown_audit {
1102            stats.cloned()
1103        } else {
1104            None
1105        };
1106
1107        let mut ret = FetchedPart {
1108            metrics,
1109            ts_filter,
1110            rows: None,
1111            peek_stash: None,
1112            batches,
1113            part_decode_format,
1114            part_rows,
1115            migration,
1116            filter_pushdown_audit,
1117            key_storage: None,
1118            val_storage: None,
1119            _phantom: PhantomData,
1120        };
1121        ret.rows = updates.map(|updates| ret.decode_rows(updates));
1122        ret
1123    }
1124
1125    /// Replaces the current decoded rows with the next batch, if any.
1126    ///
1127    /// Returns false once all batches have been decoded, or if the part was
1128    /// decoded whole.
1129    fn next_batch(&mut self) -> bool {
1130        let Some(batches) = self.batches.as_mut() else {
1131            return false;
1132        };
1133        // Release the current batch before decoding the next, so at most one
1134        // decoded batch is alive.
1135        self.rows = None;
1136        match batches.next_updates(&self.metrics) {
1137            Some(updates) => {
1138                self.rows = Some(self.decode_rows(updates));
1139                true
1140            }
1141            None => {
1142                self.batches = None;
1143                false
1144            }
1145        }
1146    }
1147
1148    /// Builds decoders over normalized `updates`.
1149    fn decode_rows(&self, updates: BlobTraceUpdates) -> DecodedRows<K, V> {
1150        let metrics = &*self.metrics;
1151        let migration = &self.migration;
1152        let downcast_structured = |structured: ColumnarRecordsStructuredExt,
1153                                   structured_only: bool| {
1154            let key_size_before = ArrayOrd::new(&structured.key).goodbytes();
1155
1156            let structured = match migration {
1157                PartMigration::SameSchema { .. } => structured,
1158                PartMigration::Schemaless { read } if structured_only => {
1159                    // We don't know the source schema, but we do know the source datatype; migrate it directly.
1160                    let start = Instant::now();
1161                    let read_key = data_type::<K>(&*read.key).ok()?;
1162                    let read_val = data_type::<V>(&*read.val).ok()?;
1163                    let key_migration = backward_compatible(structured.key.data_type(), &read_key)?;
1164                    let val_migration = backward_compatible(structured.val.data_type(), &read_val)?;
1165                    let key = key_migration.migrate(structured.key);
1166                    let val = val_migration.migrate(structured.val);
1167                    metrics
1168                        .schema
1169                        .migration_migrate_seconds
1170                        .inc_by(start.elapsed().as_secs_f64());
1171                    ColumnarRecordsStructuredExt { key, val }
1172                }
1173                PartMigration::Schemaless { .. } => return None,
1174                PartMigration::Either {
1175                    write: _,
1176                    read: _,
1177                    key_migration,
1178                    val_migration,
1179                } => {
1180                    let start = Instant::now();
1181                    let key = key_migration.migrate(structured.key);
1182                    let val = val_migration.migrate(structured.val);
1183                    metrics
1184                        .schema
1185                        .migration_migrate_seconds
1186                        .inc_by(start.elapsed().as_secs_f64());
1187                    ColumnarRecordsStructuredExt { key, val }
1188                }
1189            };
1190
1191            let read_schema = migration.codec_read();
1192            let key = K::Schema::decoder_any(&*read_schema.key, &*structured.key);
1193            let val = V::Schema::decoder_any(&*read_schema.val, &*structured.val);
1194
1195            match &key {
1196                Ok(key_decoder) => {
1197                    let key_size_after = key_decoder.goodbytes();
1198                    let key_diff = key_size_before.saturating_sub(key_size_after);
1199                    metrics
1200                        .pushdown
1201                        .parts_projection_trimmed_bytes
1202                        .inc_by(u64::cast_from(key_diff));
1203                }
1204                Err(e) => {
1205                    soft_panic_or_log!("failed to create decoder: {e:#?}");
1206                }
1207            }
1208
1209            Some((key.ok()?, val.ok()?))
1210        };
1211
1212        let timestamps = updates.timestamps().clone();
1213        let diffs = updates.diffs().clone();
1214        let part = match updates {
1215            // If only one encoding is available, decode via that encoding.
1216            BlobTraceUpdates::Row(records) => EitherOrBoth::Left(records),
1217            BlobTraceUpdates::Structured { key_values, .. } => EitherOrBoth::Right(
1218                // The structured-only data format was added after schema ids were recorded everywhere,
1219                // so we expect this data to be present.
1220                downcast_structured(key_values, true).expect("valid schemas for structured data"),
1221            ),
1222            // If both are available, respect the specified part decode format.
1223            BlobTraceUpdates::Both(records, ext) => match self.part_decode_format {
1224                PartDecodeFormat::Row {
1225                    validate_structured: false,
1226                } => EitherOrBoth::Left(records),
1227                PartDecodeFormat::Row {
1228                    validate_structured: true,
1229                } => match downcast_structured(ext, false) {
1230                    Some(decoders) => EitherOrBoth::Both(records, decoders),
1231                    None => EitherOrBoth::Left(records),
1232                },
1233                PartDecodeFormat::Arrow => match downcast_structured(ext, false) {
1234                    Some(decoders) => EitherOrBoth::Right(decoders),
1235                    None => EitherOrBoth::Left(records),
1236                },
1237            },
1238        };
1239
1240        DecodedRows {
1241            part,
1242            timestamps,
1243            diffs,
1244            cursor: 0,
1245        }
1246    }
1247
1248    /// Returns Some if this part was only fetched as part of a filter pushdown
1249    /// audit. See [LeasedBatchPart::request_filter_pushdown_audit].
1250    ///
1251    /// If set, the value in the Option is for debugging and should be included
1252    /// in any error messages.
1253    pub fn is_filter_pushdown_audit(&self) -> Option<impl std::fmt::Debug + use<K, V, T, D>> {
1254        self.filter_pushdown_audit.clone()
1255    }
1256}
1257
1258/// A [Blob] object that has been fetched, but has no associated decoding
1259/// logic.
1260#[derive(Debug)]
1261pub(crate) struct EncodedPart<T> {
1262    part: BlobTraceBatchPart<T>,
1263    normalization: PartNormalization<T>,
1264}
1265
1266/// The updates [`FetchedPart`] reads: a part decoded whole, or one decoded
1267/// in bounded batches on demand.
1268///
1269/// Readers decode hollow parts in batches when
1270/// [`FetchConfig::part_decode_batch_rows`] is set. Inline parts, and
1271/// all parts read through [`EncodedPart`] directly (compaction, consolidating
1272/// iteration, inspect), are decoded whole.
1273#[derive(Debug)]
1274pub(crate) enum PartSource<T> {
1275    Whole(EncodedPart<T>),
1276    Batched(EncodedPartBatches<T>),
1277}
1278
1279impl<T: Timestamp + Lattice + Codec64> PartSource<T> {
1280    /// Decodes the fetched blob `buf` of the hollow `part`, in batches if
1281    /// [`FetchConfig::part_decode_batch_rows`] is set.
1282    pub(crate) fn from_hollow_blob(
1283        cfg: &FetchConfig,
1284        metrics: &Metrics,
1285        read_metrics: &ReadMetrics,
1286        registered_desc: Description<T>,
1287        part: &HollowBatchPart<T>,
1288        buf: &SegmentedBytes,
1289    ) -> Self {
1290        match cfg.part_decode_batch_rows {
1291            None => PartSource::Whole(decode_batch_part_blob(
1292                cfg,
1293                metrics,
1294                read_metrics,
1295                registered_desc,
1296                part,
1297                buf,
1298            )),
1299            Some(batch_rows) => PartSource::Batched(EncodedPartBatches::new(
1300                cfg,
1301                batch_rows,
1302                metrics,
1303                read_metrics.clone(),
1304                registered_desc,
1305                part,
1306                buf.clone(),
1307            )),
1308        }
1309    }
1310}
1311
1312/// The normalization of a fetched part's updates to its registered
1313/// description: truncation to the registered bounds and `ts_rewrite`.
1314#[derive(Debug)]
1315struct PartNormalization<T> {
1316    metrics: ReadMetrics,
1317    registered_desc: Description<T>,
1318    needs_truncation: bool,
1319    ts_rewrite: Option<Antichain<T>>,
1320}
1321
1322/// A hollow part decoded into normalized updates in batches of at most
1323/// `batch_rows` rows.
1324///
1325/// Holds the encoded bytes until the last batch is decoded.
1326#[derive(Debug)]
1327pub(crate) struct EncodedPartBatches<T> {
1328    reader: BlobTraceBatchPartReader<T>,
1329    normalization: PartNormalization<T>,
1330}
1331
1332impl<T: Timestamp + Lattice + Codec64> EncodedPartBatches<T> {
1333    pub(crate) fn new(
1334        cfg: &FetchConfig,
1335        batch_rows: usize,
1336        metrics: &Metrics,
1337        read_metrics: ReadMetrics,
1338        registered_desc: Description<T>,
1339        part: &HollowBatchPart<T>,
1340        buf: SegmentedBytes,
1341    ) -> Self {
1342        let reader = metrics
1343            .codecs
1344            .batch
1345            .decode(|| BlobTraceBatchPartReader::new(buf, batch_rows))
1346            .map_err(|err| anyhow!("couldn't decode batch at key {}: {}", part.key, err))
1347            // Same as `decode_batch_part_blob`: undecodable durable data is
1348            // not recoverable.
1349            .expect("internal error: invalid encoded state");
1350        let normalization = PartNormalization::new(
1351            cfg,
1352            read_metrics,
1353            registered_desc,
1354            &part.key.0,
1355            part.ts_rewrite.as_ref(),
1356            reader.desc(),
1357        );
1358        EncodedPartBatches {
1359            reader,
1360            normalization,
1361        }
1362    }
1363
1364    /// Decodes and normalizes the next batch, or returns `None` once all rows
1365    /// have been returned.
1366    fn next_updates(&mut self, metrics: &Metrics) -> Option<BlobTraceUpdates> {
1367        let start = Instant::now();
1368        let updates = self
1369            .reader
1370            .next_updates(&metrics.columnar)?
1371            .expect("internal error: invalid encoded state");
1372        // Counts parts, not batches, so only the open in `new` increments
1373        // `decode_count`.
1374        metrics
1375            .codecs
1376            .batch
1377            .decode_seconds
1378            .inc_by(start.elapsed().as_secs_f64());
1379        self.normalization
1380            .metrics
1381            .part_goodbytes
1382            .inc_by(u64::cast_from(updates.goodbytes()));
1383        Some(self.normalization.normalize(&updates, &metrics.columnar))
1384    }
1385}
1386
1387impl<K, V, T, D> FetchedPart<K, V, T, D>
1388where
1389    K: Debug + Codec,
1390    V: Debug + Codec,
1391    T: Timestamp + Lattice + Codec64,
1392    D: Monoid + Codec64 + Send + Sync,
1393{
1394    /// [Self::next] but optionally providing a `K` and `V` for alloc reuse.
1395    ///
1396    /// When `result_override` is specified, return it instead of decoding data.
1397    /// This is used when we know the decoded result will be ignored.
1398    pub fn next_with_storage(
1399        &mut self,
1400        key: &mut Option<K>,
1401        val: &mut Option<V>,
1402    ) -> Option<((K, V), T, D)> {
1403        let mut consolidated = self.peek_stash.take();
1404        loop {
1405            // Fetch and decode the next tuple in the sequence. (Or break if there is none.)
1406            let row = match &mut self.rows {
1407                Some(rows) if rows.cursor < rows.timestamps.len() => {
1408                    let idx = rows.cursor;
1409                    rows.cursor += 1;
1410                    Some((idx, rows.timestamps.values()[idx], rows.diffs.values()[idx]))
1411                }
1412                _ => None,
1413            };
1414            let next = if let Some((next_idx, t, d)) = row {
1415                // These `to_le_bytes` calls were previously encapsulated by `ColumnarRecords`.
1416                // TODO(structured): re-encapsulate these once we've finished the structured migration.
1417                let mut t = T::decode(t.to_le_bytes());
1418                if !self.ts_filter.filter_ts(&mut t) {
1419                    continue;
1420                }
1421                let d = D::decode(d.to_le_bytes());
1422                if d.is_zero() {
1423                    continue;
1424                }
1425                let kv = self.decode_kv(next_idx, key, val);
1426                (kv, t, d)
1427            } else if self.next_batch() {
1428                // `consolidated` and `peek_stash` are owned, so consolidation
1429                // continues across the batch boundary.
1430                continue;
1431            } else {
1432                break;
1433            };
1434
1435            // Attempt to consolidate in the next tuple, stashing it if that's not possible.
1436            if let Some((kv, t, d)) = &mut consolidated {
1437                let (kv_next, t_next, d_next) = &next;
1438                if kv == kv_next && t == t_next {
1439                    d.plus_equals(d_next);
1440                    if d.is_zero() {
1441                        consolidated = None;
1442                    }
1443                } else {
1444                    self.peek_stash = Some(next);
1445                    break;
1446                }
1447            } else {
1448                consolidated = Some(next);
1449            }
1450        }
1451
1452        let (kv, t, d) = consolidated?;
1453
1454        Some((kv, t, d))
1455    }
1456
1457    fn decode_kv(&mut self, index: usize, key: &mut Option<K>, val: &mut Option<V>) -> (K, V) {
1458        let decoded = self
1459            .rows
1460            .as_ref()
1461            .expect("decoded rows present while the cursor is in bounds")
1462            .part
1463            .as_ref()
1464            .map_left(|codec| {
1465                let ((ck, cv), _, _) = codec.get(index).expect("valid index");
1466                let (k, v) = Self::decode_codec(
1467                    &*self.metrics,
1468                    self.migration.codec_read(),
1469                    ck,
1470                    cv,
1471                    key,
1472                    val,
1473                    &mut self.key_storage,
1474                    &mut self.val_storage,
1475                );
1476                (k.expect("valid legacy key"), v.expect("valid legacy value"))
1477            })
1478            .map_right(|(structured_key, structured_val)| {
1479                self.decode_structured(index, structured_key, structured_val, key, val)
1480            });
1481
1482        match decoded {
1483            EitherOrBoth::Both((k, v), (k_s, v_s)) => {
1484                // Purposefully do not trace to prevent blowing up Sentry.
1485                let is_valid = self
1486                    .metrics
1487                    .columnar
1488                    .arrow()
1489                    .key()
1490                    .report_valid(|| k_s == k);
1491                if !is_valid {
1492                    soft_panic_no_log!("structured key did not match, {k_s:?} != {k:?}");
1493                }
1494                // Purposefully do not trace to prevent blowing up Sentry.
1495                let is_valid = self
1496                    .metrics
1497                    .columnar
1498                    .arrow()
1499                    .val()
1500                    .report_valid(|| v_s == v);
1501                if !is_valid {
1502                    soft_panic_no_log!("structured val did not match, {v_s:?} != {v:?}");
1503                }
1504
1505                (k, v)
1506            }
1507            EitherOrBoth::Left(kv) => kv,
1508            EitherOrBoth::Right(kv) => kv,
1509        }
1510    }
1511
1512    fn decode_codec(
1513        metrics: &Metrics,
1514        read_schemas: &Schemas<K, V>,
1515        key_buf: &[u8],
1516        val_buf: &[u8],
1517        key: &mut Option<K>,
1518        val: &mut Option<V>,
1519        key_storage: &mut Option<K::Storage>,
1520        val_storage: &mut Option<V::Storage>,
1521    ) -> (Result<K, String>, Result<V, String>) {
1522        let k = metrics.codecs.key.decode(|| match key.take() {
1523            Some(mut key) => {
1524                match K::decode_from(&mut key, key_buf, key_storage, &read_schemas.key) {
1525                    Ok(()) => Ok(key),
1526                    Err(err) => Err(err),
1527                }
1528            }
1529            None => K::decode(key_buf, &read_schemas.key),
1530        });
1531        let v = metrics.codecs.val.decode(|| match val.take() {
1532            Some(mut val) => {
1533                match V::decode_from(&mut val, val_buf, val_storage, &read_schemas.val) {
1534                    Ok(()) => Ok(val),
1535                    Err(err) => Err(err),
1536                }
1537            }
1538            None => V::decode(val_buf, &read_schemas.val),
1539        });
1540        (k, v)
1541    }
1542
1543    fn decode_structured(
1544        &self,
1545        idx: usize,
1546        keys: &<K::Schema as Schema<K>>::Decoder,
1547        vals: &<V::Schema as Schema<V>>::Decoder,
1548        key: &mut Option<K>,
1549        val: &mut Option<V>,
1550    ) -> (K, V) {
1551        let mut key = key.take().unwrap_or_default();
1552        keys.decode(idx, &mut key);
1553
1554        let mut val = val.take().unwrap_or_default();
1555        vals.decode(idx, &mut val);
1556
1557        (key, val)
1558    }
1559}
1560
1561impl<K, V, T, D> Iterator for FetchedPart<K, V, T, D>
1562where
1563    K: Debug + Codec,
1564    V: Debug + Codec,
1565    T: Timestamp + Lattice + Codec64,
1566    D: Monoid + Codec64 + Send + Sync,
1567{
1568    type Item = ((K, V), T, D);
1569
1570    fn next(&mut self) -> Option<Self::Item> {
1571        self.next_with_storage(&mut None, &mut None)
1572    }
1573
1574    fn size_hint(&self) -> (usize, Option<usize>) {
1575        // We don't know in advance how restrictive the filter will be.
1576        let max_len = self.part_rows;
1577        (0, Some(max_len))
1578    }
1579}
1580
1581impl<T> EncodedPart<T>
1582where
1583    T: Timestamp + Lattice + Codec64,
1584{
1585    pub async fn fetch(
1586        cfg: &FetchConfig,
1587        shard_id: &ShardId,
1588        blob: &dyn Blob,
1589        metrics: &Metrics,
1590        shard_metrics: &ShardMetrics,
1591        read_metrics: &ReadMetrics,
1592        registered_desc: &Description<T>,
1593        part: &BatchPart<T>,
1594    ) -> Result<Self, BlobKey> {
1595        match part {
1596            BatchPart::Hollow(x) => {
1597                fetch_batch_part(
1598                    cfg,
1599                    shard_id,
1600                    blob,
1601                    metrics,
1602                    shard_metrics,
1603                    read_metrics,
1604                    registered_desc,
1605                    x,
1606                )
1607                .await
1608            }
1609            BatchPart::Inline {
1610                updates,
1611                ts_rewrite,
1612                ..
1613            } => Ok(EncodedPart::from_inline(
1614                cfg,
1615                metrics,
1616                read_metrics.clone(),
1617                registered_desc.clone(),
1618                updates,
1619                ts_rewrite.as_ref(),
1620            )),
1621        }
1622    }
1623
1624    pub(crate) fn from_inline(
1625        cfg: &FetchConfig,
1626        metrics: &Metrics,
1627        read_metrics: ReadMetrics,
1628        desc: Description<T>,
1629        x: &LazyInlineBatchPart,
1630        ts_rewrite: Option<&Antichain<T>>,
1631    ) -> Self {
1632        let parsed = x.decode(&metrics.columnar).expect("valid inline part");
1633        Self::new(cfg, read_metrics, desc, "inline", ts_rewrite, parsed)
1634    }
1635
1636    pub(crate) fn from_hollow(
1637        cfg: &FetchConfig,
1638        metrics: ReadMetrics,
1639        registered_desc: Description<T>,
1640        part: &HollowBatchPart<T>,
1641        parsed: BlobTraceBatchPart<T>,
1642    ) -> Self {
1643        Self::new(
1644            cfg,
1645            metrics,
1646            registered_desc,
1647            &part.key.0,
1648            part.ts_rewrite.as_ref(),
1649            parsed,
1650        )
1651    }
1652
1653    pub(crate) fn new(
1654        cfg: &FetchConfig,
1655        metrics: ReadMetrics,
1656        registered_desc: Description<T>,
1657        printable_name: &str,
1658        ts_rewrite: Option<&Antichain<T>>,
1659        parsed: BlobTraceBatchPart<T>,
1660    ) -> Self {
1661        let normalization = PartNormalization::new(
1662            cfg,
1663            metrics,
1664            registered_desc,
1665            printable_name,
1666            ts_rewrite,
1667            &parsed.desc,
1668        );
1669        EncodedPart {
1670            part: parsed,
1671            normalization,
1672        }
1673    }
1674
1675    pub(crate) fn maybe_unconsolidated(&self) -> bool {
1676        // At time of writing, only user parts may be unconsolidated, and they are always
1677        // written with a since of [T::minimum()].
1678        self.part.desc.since().borrow() == AntichainRef::new(&[T::minimum()])
1679    }
1680
1681    pub(crate) fn updates(&self) -> &BlobTraceUpdates {
1682        &self.part.updates
1683    }
1684
1685    /// Returns the updates with all truncation / timestamp rewriting applied.
1686    pub(crate) fn normalize(&self, metrics: &ColumnarMetrics) -> BlobTraceUpdates {
1687        self.normalization.normalize(&self.part.updates, metrics)
1688    }
1689}
1690
1691impl<T: Timestamp + Lattice + Codec64> PartNormalization<T> {
1692    fn new(
1693        cfg: &FetchConfig,
1694        metrics: ReadMetrics,
1695        registered_desc: Description<T>,
1696        printable_name: &str,
1697        ts_rewrite: Option<&Antichain<T>>,
1698        inline_desc: &Description<T>,
1699    ) -> Self {
1700        // There are two types of batches in persist:
1701        // - Batches written by a persist user (either directly or indirectly
1702        //   via BatchBuilder). These always have a since of the minimum
1703        //   timestamp and may be registered in persist state with a tighter set
1704        //   of bounds than are inline in the batch (truncation). To read one of
1705        //   these batches, all data physically in the batch but outside of the
1706        //   truncated bounds must be ignored. Not every user batch is
1707        //   truncated.
1708        // - Batches written by compaction. These always have an inline desc
1709        //   lower and upper that matches the registered desc lower and upper,
1710        //   and a since that is less than or equal to the registered desc.
1711        //   The inline since may be less than the registered desc since,
1712        //   this is because of incremental compaction, where we might rewrite
1713        //   certain runs in a batch but not others.
1714        let needs_truncation = inline_desc.lower() != registered_desc.lower()
1715            || inline_desc.upper() != registered_desc.upper();
1716        if needs_truncation {
1717            if cfg.validate_bounds_on_read {
1718                soft_assert_or_log!(
1719                    PartialOrder::less_equal(inline_desc.lower(), registered_desc.lower()),
1720                    "key={} inline={:?} registered={:?}",
1721                    printable_name,
1722                    inline_desc,
1723                    registered_desc
1724                );
1725
1726                if ts_rewrite.is_none() {
1727                    // The ts rewrite feature allows us to advance the registered
1728                    // upper of a batch that's already been staged (the inline
1729                    // upper), so if it's been used, then there's no useful
1730                    // invariant that we can assert here.
1731                    soft_assert_or_log!(
1732                        PartialOrder::less_equal(registered_desc.upper(), inline_desc.upper()),
1733                        "key={} inline={:?} registered={:?}",
1734                        printable_name,
1735                        inline_desc,
1736                        registered_desc
1737                    );
1738                }
1739            }
1740            // As mentioned above, batches that needs truncation will always have a
1741            // since of the minimum timestamp. Technically we could truncate any
1742            // batch where the since is less_than the output_desc's lower, but we're
1743            // strict here so we don't get any surprises.
1744            assert_eq!(
1745                inline_desc.since(),
1746                &Antichain::from_elem(T::minimum()),
1747                "key={} inline={:?} registered={:?}",
1748                printable_name,
1749                inline_desc,
1750                registered_desc
1751            );
1752        } else {
1753            soft_assert_or_log!(
1754                PartialOrder::less_equal(inline_desc.since(), registered_desc.since()),
1755                "key={} inline={:?} registered={:?}",
1756                printable_name,
1757                inline_desc,
1758                registered_desc
1759            );
1760            assert_eq!(
1761                inline_desc.lower(),
1762                registered_desc.lower(),
1763                "key={} inline={:?} registered={:?}",
1764                printable_name,
1765                inline_desc,
1766                registered_desc
1767            );
1768            assert_eq!(
1769                inline_desc.upper(),
1770                registered_desc.upper(),
1771                "key={} inline={:?} registered={:?}",
1772                printable_name,
1773                inline_desc,
1774                registered_desc
1775            );
1776        }
1777
1778        PartNormalization {
1779            metrics,
1780            registered_desc,
1781            needs_truncation,
1782            ts_rewrite: ts_rewrite.cloned(),
1783        }
1784    }
1785
1786    /// Returns `updates` with all truncation / timestamp rewriting applied.
1787    fn normalize(&self, updates: &BlobTraceUpdates, metrics: &ColumnarMetrics) -> BlobTraceUpdates {
1788        let updates = updates.clone();
1789        if !self.needs_truncation && self.ts_rewrite.is_none() {
1790            return updates;
1791        }
1792
1793        let mut codec = updates
1794            .records()
1795            .map(|r| (r.keys().clone(), r.vals().clone()));
1796        let mut structured = updates.structured().cloned();
1797        let mut timestamps = updates.timestamps().clone();
1798        let mut diffs = updates.diffs().clone();
1799
1800        if let Some(rewrite) = self.ts_rewrite.as_ref() {
1801            timestamps = arrow::compute::unary(&timestamps, |i: i64| {
1802                let mut t = T::decode(i.to_le_bytes());
1803                t.advance_by(rewrite.borrow());
1804                i64::from_le_bytes(T::encode(&t))
1805            });
1806        }
1807
1808        let reallocated = if self.needs_truncation {
1809            let filter = BooleanArray::from_unary(&timestamps, |i| {
1810                let t = T::decode(i.to_le_bytes());
1811                let truncate_t = {
1812                    !self.registered_desc.lower().less_equal(&t)
1813                        || self.registered_desc.upper().less_equal(&t)
1814                };
1815                !truncate_t
1816            });
1817            if filter.false_count() == 0 {
1818                // If we're not filtering anything in practice, skip filtering and reallocating.
1819                false
1820            } else {
1821                let filter = FilterBuilder::new(&filter).optimize().build();
1822                let do_filter = |array: &dyn Array| filter.filter(array).expect("valid filter len");
1823                if let Some((keys, vals)) = codec {
1824                    codec = Some((
1825                        realloc_array(do_filter(&keys).as_binary(), metrics),
1826                        realloc_array(do_filter(&vals).as_binary(), metrics),
1827                    ));
1828                }
1829                if let Some(ext) = structured {
1830                    structured = Some(ColumnarRecordsStructuredExt {
1831                        key: realloc_any(do_filter(&*ext.key), metrics),
1832                        val: realloc_any(do_filter(&*ext.val), metrics),
1833                    });
1834                }
1835                timestamps = realloc_array(do_filter(&timestamps).as_primitive(), metrics);
1836                diffs = realloc_array(do_filter(&diffs).as_primitive(), metrics);
1837                true
1838            }
1839        } else {
1840            false
1841        };
1842
1843        if self.ts_rewrite.is_some() && !reallocated {
1844            timestamps = realloc_array(&timestamps, metrics);
1845        }
1846
1847        if self.ts_rewrite.is_some() {
1848            self.metrics
1849                .ts_rewrite
1850                .inc_by(u64::cast_from(timestamps.len()));
1851        }
1852
1853        match (codec, structured) {
1854            (Some((key, value)), None) => {
1855                BlobTraceUpdates::Row(ColumnarRecords::new(key, value, timestamps, diffs))
1856            }
1857            (Some((key, value)), Some(ext)) => {
1858                BlobTraceUpdates::Both(ColumnarRecords::new(key, value, timestamps, diffs), ext)
1859            }
1860            (None, Some(ext)) => BlobTraceUpdates::Structured {
1861                key_values: ext,
1862                timestamps,
1863                diffs,
1864            },
1865            (None, None) => unreachable!(),
1866        }
1867    }
1868}
1869
1870/// This represents the serde encoding for [`LeasedBatchPart`]. We expose the struct
1871/// itself (unlike other encodable structs) to attempt to provide stricter drop
1872/// semantics on `LeasedBatchPart`, i.e. `SerdeLeasedBatchPart` is exchangeable
1873/// (including over the network), where `LeasedBatchPart` is not.
1874///
1875/// For more details see documentation and comments on:
1876/// - [`LeasedBatchPart`]
1877/// - `From<SerdeLeasedBatchPart>` for `LeasedBatchPart<T>`
1878#[derive(Debug, Serialize, Deserialize, Clone)]
1879pub struct ExchangeableBatchPart<T> {
1880    shard_id: ShardId,
1881    // Duplicated with the one serialized in the proto for use in backpressure.
1882    encoded_size_bytes: usize,
1883    desc: Description<T>,
1884    filter: FetchBatchFilter<T>,
1885    part: LazyProto<ProtoHollowBatchPart>,
1886    /// The id of the reader that leased this part. See the corresponding field
1887    /// on [LeasedBatchPart].
1888    reader_id: LeasedReaderId,
1889    filter_pushdown_audit: bool,
1890}
1891
1892impl<T> ExchangeableBatchPart<T> {
1893    /// Returns the encoded size of the given part.
1894    pub fn encoded_size_bytes(&self) -> usize {
1895        self.encoded_size_bytes
1896    }
1897
1898    /// Returns the id of the reader that leased this part.
1899    pub fn reader_id(&self) -> &LeasedReaderId {
1900        &self.reader_id
1901    }
1902}
1903
1904/// Format we'll use when decoding a [`Part`].
1905///
1906/// [`Part`]: mz_persist_types::part::Part
1907#[derive(Debug, Copy, Clone)]
1908pub enum PartDecodeFormat {
1909    /// Decode from opaque `Codec` data.
1910    Row {
1911        /// Will also decode the structured data, and validate it matches.
1912        validate_structured: bool,
1913    },
1914    /// Decode from arrow data
1915    Arrow,
1916}
1917
1918impl PartDecodeFormat {
1919    /// Returns a default value for [`PartDecodeFormat`].
1920    pub const fn default() -> Self {
1921        PartDecodeFormat::Arrow
1922    }
1923
1924    /// Parses a [`PartDecodeFormat`] from the provided string, falling back to the default if the
1925    /// provided value is unrecognized.
1926    pub fn from_str(s: &str) -> Self {
1927        match s {
1928            "row" => PartDecodeFormat::Row {
1929                validate_structured: false,
1930            },
1931            "row_with_validate" => PartDecodeFormat::Row {
1932                validate_structured: true,
1933            },
1934            "arrow" => PartDecodeFormat::Arrow,
1935            x => {
1936                let default = PartDecodeFormat::default();
1937                soft_panic_or_log!("Invalid part decode format: '{x}', falling back to {default}");
1938                default
1939            }
1940        }
1941    }
1942
1943    /// Returns a string representation of [`PartDecodeFormat`].
1944    pub const fn as_str(&self) -> &'static str {
1945        match self {
1946            PartDecodeFormat::Row {
1947                validate_structured: false,
1948            } => "row",
1949            PartDecodeFormat::Row {
1950                validate_structured: true,
1951            } => "row_with_validate",
1952            PartDecodeFormat::Arrow => "arrow",
1953        }
1954    }
1955}
1956
1957impl fmt::Display for PartDecodeFormat {
1958    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1959        f.write_str(self.as_str())
1960    }
1961}
1962
1963#[mz_ore::test]
1964fn client_exchange_data() {
1965    // The whole point of SerdeLeasedBatchPart is that it can be exchanged
1966    // between timely workers, including over the network. Enforce then that it
1967    // implements ExchangeData.
1968    fn is_exchange_data<T: timely::ExchangeData>() {}
1969    is_exchange_data::<ExchangeableBatchPart<u64>>();
1970    is_exchange_data::<ExchangeableBatchPart<u64>>();
1971}
1972
1973#[cfg(test)]
1974mod tests;