Skip to main content

mz_persist_client/
batch.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//! A handle to a batch of updates
11
12use std::borrow::Cow;
13use std::collections::{BTreeMap, BTreeSet};
14use std::fmt::Debug;
15use std::marker::PhantomData;
16use std::mem;
17use std::sync::Arc;
18use std::time::Instant;
19
20use arrow::array::{Array, Int64Array};
21use bytes::Bytes;
22use differential_dataflow::difference::Monoid;
23use differential_dataflow::lattice::Lattice;
24use differential_dataflow::trace::Description;
25use futures_util::stream::StreamExt;
26use futures_util::{FutureExt, stream};
27use mz_dyncfg::{Config, ParameterScope};
28use mz_ore::cast::CastFrom;
29use mz_ore::instrument;
30use mz_persist::indexed::encoding::{BatchColumnarFormat, BlobTraceBatchPart, BlobTraceUpdates};
31use mz_persist::location::Blob;
32use mz_persist_types::arrow::{ArrayBound, ArrayOrd};
33use mz_persist_types::columnar::{ColumnDecoder, Schema};
34use mz_persist_types::parquet::{CompressionFormat, EncodingConfig};
35use mz_persist_types::part::{Part, PartBuilder};
36use mz_persist_types::schema::SchemaId;
37use mz_persist_types::stats::{
38    PartStats, TRUNCATE_LEN, TruncateBound, trim_to_budget, truncate_bytes,
39};
40use mz_persist_types::{Codec, Codec64};
41use mz_proto::RustType;
42use mz_timely_util::order::Reverse;
43use proptest_derive::Arbitrary;
44use semver::Version;
45use timely::PartialOrder;
46use timely::order::TotalOrder;
47use timely::progress::{Antichain, Timestamp};
48use tracing::{Instrument, debug_span, trace_span, warn};
49
50use crate::async_runtime::IsolatedRuntime;
51use crate::cfg::{BATCH_BUILDER_MAX_OUTSTANDING_PARTS, MiB};
52use crate::error::InvalidUsage;
53use crate::internal::compact::{CompactConfig, Compactor};
54use crate::internal::encoding::{
55    LazyInlineBatchPart, LazyPartStats, LazyProto, MetadataMap, Schemas,
56};
57use crate::internal::machine::retry_external;
58use crate::internal::merge::{MergeTree, Pending};
59use crate::internal::metrics::{BatchWriteMetrics, Metrics, RetryMetrics, ShardMetrics};
60use crate::internal::paths::{PartId, PartialBatchKey, WriterKey};
61use crate::internal::state::{
62    BatchPart, ENABLE_INCREMENTAL_COMPACTION, HollowBatch, HollowBatchPart, HollowRun,
63    HollowRunRef, ProtoInlineBatchPart, RunId, RunMeta, RunOrder, RunPart,
64};
65use crate::stats::{STATS_BUDGET_BYTES, STATS_COLLECTION_ENABLED, untrimmable_columns};
66use crate::{PersistConfig, ShardId};
67
68include!(concat!(env!("OUT_DIR"), "/mz_persist_client.batch.rs"));
69
70/// A handle to a batch of updates that has been written to blob storage but
71/// which has not yet been appended to a shard.
72///
73/// A [Batch] needs to be marked as consumed or it needs to be deleted via [Self::delete].
74/// Otherwise, a dangling batch will leak and backing blobs will remain in blob storage.
75#[derive(Debug)]
76pub struct Batch<K, V, T, D> {
77    pub(crate) batch_delete_enabled: bool,
78    pub(crate) metrics: Arc<Metrics>,
79    pub(crate) shard_metrics: Arc<ShardMetrics>,
80
81    /// The version of Materialize which wrote this batch.
82    pub(crate) version: Version,
83
84    /// The encoded schemas of the data in the batch.
85    pub(crate) schemas: (Bytes, Bytes),
86
87    /// A handle to the data represented by this batch.
88    pub(crate) batch: HollowBatch<T>,
89
90    /// Handle to the [Blob] that the blobs of this batch were uploaded to.
91    pub(crate) blob: Arc<dyn Blob>,
92
93    // These provide a bit more safety against appending a batch with the wrong
94    // type to a shard.
95    pub(crate) _phantom: PhantomData<fn() -> (K, V, T, D)>,
96}
97
98impl<K, V, T, D> Drop for Batch<K, V, T, D> {
99    fn drop(&mut self) {
100        if self.batch.part_count() > 0 {
101            warn!(
102                "un-consumed Batch, with {} parts and dangling blob keys: {:?}",
103                self.batch.part_count(),
104                self.batch
105                    .parts
106                    .iter()
107                    .map(|x| x.printable_name())
108                    .collect::<Vec<_>>(),
109            );
110        }
111    }
112}
113
114impl<K, V, T, D> Batch<K, V, T, D>
115where
116    K: Debug + Codec,
117    V: Debug + Codec,
118    T: Timestamp + Lattice + Codec64,
119    D: Monoid + Codec64,
120{
121    pub(crate) fn new(
122        batch_delete_enabled: bool,
123        metrics: Arc<Metrics>,
124        blob: Arc<dyn Blob>,
125        shard_metrics: Arc<ShardMetrics>,
126        version: Version,
127        schemas: (Bytes, Bytes),
128        batch: HollowBatch<T>,
129    ) -> Self {
130        Self {
131            batch_delete_enabled,
132            metrics,
133            shard_metrics,
134            version,
135            schemas,
136            batch,
137            blob,
138            _phantom: PhantomData,
139        }
140    }
141
142    /// The `shard_id` of this [Batch].
143    pub fn shard_id(&self) -> ShardId {
144        self.shard_metrics.shard_id
145    }
146
147    /// The `upper` of this [Batch].
148    pub fn upper(&self) -> &Antichain<T> {
149        self.batch.desc.upper()
150    }
151
152    /// The `lower` of this [Batch].
153    pub fn lower(&self) -> &Antichain<T> {
154        self.batch.desc.lower()
155    }
156
157    /// Marks the blobs that this batch handle points to as consumed, likely
158    /// because they were appended to a shard.
159    ///
160    /// Consumers of a blob need to make this explicit, so that we can log
161    /// warnings in case a batch is not used.
162    pub(crate) fn mark_consumed(&mut self) {
163        self.batch.parts.clear();
164    }
165
166    /// Deletes the blobs that make up this batch from the given blob store and
167    /// marks them as deleted.
168    #[instrument(level = "debug", fields(shard = %self.shard_id()))]
169    pub async fn delete(mut self) {
170        if !self.batch_delete_enabled {
171            self.mark_consumed();
172            return;
173        }
174        let mut deletes = PartDeletes::default();
175        for part in self.batch.parts.drain(..) {
176            deletes.add(&part);
177        }
178        let () = deletes
179            .delete(
180                &*self.blob,
181                self.shard_id(),
182                usize::MAX,
183                &*self.metrics,
184                &*self.metrics.retries.external.batch_delete,
185            )
186            .await;
187    }
188
189    /// Returns the schemas of parts in this batch.
190    pub fn schemas(&self) -> impl Iterator<Item = SchemaId> + '_ {
191        self.batch.parts.iter().flat_map(|b| b.schema_id())
192    }
193
194    /// Turns this [`Batch`] into a `HollowBatch`.
195    ///
196    /// **NOTE**: If this batch is not eventually appended to a shard or
197    /// dropped, the data that it represents will have leaked.
198    pub fn into_hollow_batch(mut self) -> HollowBatch<T> {
199        let ret = self.batch.clone();
200        self.mark_consumed();
201        ret
202    }
203
204    /// Turns this [`Batch`] into a [`ProtoBatch`], which can be used to
205    /// transfer this batch across process boundaries, for example when
206    /// exchanging data between timely workers.
207    ///
208    /// **NOTE**: If this batch is not eventually appended to a shard or
209    /// dropped, the data that it represents will have leaked. The caller is
210    /// responsible for turning this back into a [`Batch`] using
211    /// [`WriteHandle::batch_from_transmittable_batch`](crate::write::WriteHandle::batch_from_transmittable_batch).
212    pub fn into_transmittable_batch(mut self) -> ProtoBatch {
213        let ret = ProtoBatch {
214            shard_id: self.shard_metrics.shard_id.into_proto(),
215            version: self.version.to_string(),
216            batch: Some(self.batch.into_proto()),
217            key_schema: self.schemas.0.clone(),
218            val_schema: self.schemas.1.clone(),
219        };
220        self.mark_consumed();
221        ret
222    }
223
224    pub(crate) async fn flush_to_blob(
225        &mut self,
226        cfg: &BatchBuilderConfig,
227        batch_metrics: &BatchWriteMetrics,
228        isolated_runtime: &Arc<IsolatedRuntime>,
229        write_schemas: &Schemas<K, V>,
230    ) {
231        // It's necessary for correctness to keep the parts in the same order.
232        // We could introduce concurrency here with FuturesOrdered, but it would
233        // be pretty unexpected to have inline writes in more than one part, so
234        // don't bother.
235        let mut parts = Vec::new();
236        for (run_meta, run_parts) in self.batch.runs() {
237            for part in run_parts {
238                let (updates, ts_rewrite, schema_id) = match part {
239                    RunPart::Single(BatchPart::Inline {
240                        updates,
241                        ts_rewrite,
242                        schema_id,
243                        deprecated_schema_id: _,
244                    }) => (updates, ts_rewrite, schema_id),
245                    other @ RunPart::Many(_) | other @ RunPart::Single(BatchPart::Hollow(_)) => {
246                        parts.push(other.clone());
247                        continue;
248                    }
249                };
250                let updates = updates
251                    .decode::<T>(&self.metrics.columnar)
252                    .expect("valid inline part");
253                let diffs_sum = diffs_sum::<D>(updates.updates.diffs());
254                let mut write_schemas = write_schemas.clone();
255                write_schemas.id = *schema_id;
256
257                let write_span =
258                    debug_span!("batch::flush_to_blob", shard = %self.shard_metrics.shard_id)
259                        .or_current();
260                let handle = mz_ore::task::spawn(
261                    || "batch::flush_to_blob",
262                    BatchParts::write_hollow_part(
263                        cfg.clone(),
264                        Arc::clone(&self.blob),
265                        Arc::clone(&self.metrics),
266                        Arc::clone(&self.shard_metrics),
267                        batch_metrics.clone(),
268                        Arc::clone(isolated_runtime),
269                        updates,
270                        run_meta.order.unwrap_or(RunOrder::Unordered),
271                        ts_rewrite.clone(),
272                        D::encode(&diffs_sum),
273                        write_schemas,
274                    )
275                    .instrument(write_span),
276                );
277                let part = handle.await;
278                parts.push(RunPart::Single(part));
279            }
280        }
281        self.batch.parts = parts;
282    }
283
284    /// The sum of the encoded sizes of all parts in the batch.
285    pub fn encoded_size_bytes(&self) -> usize {
286        self.batch.encoded_size_bytes()
287    }
288}
289
290impl<K, V, T, D> Batch<K, V, T, D>
291where
292    K: Debug + Codec,
293    V: Debug + Codec,
294    T: Timestamp + Lattice + Codec64 + TotalOrder,
295    D: Monoid + Codec64,
296{
297    /// Efficiently rewrites the timestamps in this not-yet-committed batch.
298    ///
299    /// This [Batch] represents potentially large amounts of data, which may
300    /// have partly or entirely been spilled to s3. This call bulk edits the
301    /// timestamps of all data in this batch in a metadata-only operation (i.e.
302    /// without network calls).
303    ///
304    /// Specifically, every timestamp in the batch is logically advanced_by the
305    /// provided `frontier`.
306    ///
307    /// This method may be called multiple times, with later calls overriding
308    /// previous ones, but the rewrite frontier may not regress across calls.
309    ///
310    /// When this batch was created, it was given an `upper`, which bounds the
311    /// staged data it represents. To allow rewrite past this original `upper`,
312    /// this call accepts a new `upper` which replaces the previous one. Like
313    /// the rewrite frontier, the upper may not regress across calls.
314    ///
315    /// Multiple batches with various rewrite frontiers may be used in a single
316    /// [crate::write::WriteHandle::compare_and_append_batch] call. This is an
317    /// expected usage.
318    ///
319    /// This feature requires that the timestamp impls `TotalOrder`. This is
320    /// because we need to be able to verify that the contained data, after the
321    /// rewrite forward operation, still respects the new upper. It turns out
322    /// that, given the metadata persist currently collects during batch
323    /// collection, this is possible for totally ordered times, but it's known
324    /// to be _not possible_ for partially ordered times. It is believed that we
325    /// could fix this by collecting different metadata in batch creation (e.g.
326    /// the join of or an antichain of the original contained timestamps), but
327    /// the experience of database-issues#7825 has shaken our confidence in our own abilities
328    /// to reason about partially ordered times and anyway all the initial uses
329    /// have totally ordered times.
330    pub fn rewrite_ts(
331        &mut self,
332        frontier: &Antichain<T>,
333        new_upper: Antichain<T>,
334    ) -> Result<(), InvalidUsage<T>> {
335        self.batch
336            .rewrite_ts(frontier, new_upper)
337            .map_err(InvalidUsage::InvalidRewrite)
338    }
339}
340
341/// Indicates what work was done in a call to [BatchBuilder::add]
342#[derive(Debug)]
343pub enum Added {
344    /// A record was inserted into a pending batch part
345    Record,
346    /// A record was inserted into a pending batch part
347    /// and the part was sent to blob storage
348    RecordAndParts,
349}
350
351/// A snapshot of dynamic configs to make it easier to reason about an individual
352/// run of BatchBuilder.
353#[derive(Debug, Clone)]
354pub struct BatchBuilderConfig {
355    writer_key: WriterKey,
356    pub(crate) blob_target_size: usize,
357    pub(crate) batch_delete_enabled: bool,
358    pub(crate) batch_builder_max_outstanding_parts: usize,
359    pub(crate) inline_writes_single_max_bytes: usize,
360    pub(crate) stats_collection_enabled: bool,
361    pub(crate) stats_budget: usize,
362    pub(crate) stats_untrimmable_columns: Arc<UntrimmableColumns>,
363    pub(crate) encoding_config: EncodingConfig,
364    pub(crate) preferred_order: RunOrder,
365    pub(crate) structured_key_lower_len: usize,
366    pub(crate) run_length_limit: usize,
367    pub(crate) enable_incremental_compaction: bool,
368    /// The number of runs to cap the built batch at, or None if we should
369    /// continue to generate one run per part for unordered batches.
370    /// See the config definition for details.
371    pub(crate) max_runs: Option<usize>,
372}
373
374// TODO: Remove this once we're comfortable that there aren't any bugs.
375pub(crate) const BATCH_DELETE_ENABLED: Config<bool> = Config::new(
376    "persist_batch_delete_enabled",
377    true,
378    "Whether to actually delete blobs when batch delete is called (Materialize).",
379    ParameterScope::Environment,
380);
381
382pub(crate) const ENCODING_ENABLE_DICTIONARY: Config<bool> = Config::new(
383    "persist_encoding_enable_dictionary",
384    true,
385    "A feature flag to enable dictionary encoding for Parquet data (Materialize).",
386    ParameterScope::Environment,
387);
388
389pub(crate) const ENCODING_COMPRESSION_FORMAT: Config<&'static str> = Config::new(
390    "persist_encoding_compression_format",
391    "none",
392    "A feature flag to enable compression of Parquet data (Materialize).",
393    ParameterScope::Environment,
394);
395
396pub(crate) const STRUCTURED_KEY_LOWER_LEN: Config<usize> = Config::new(
397    "persist_batch_structured_key_lower_len",
398    256,
399    "The maximum size in proto bytes of any structured key-lower metadata to preserve. \
400    (If we're unable to fit the lower in budget, or the budget is zero, no metadata is kept.)",
401    ParameterScope::Environment,
402);
403
404pub(crate) const MAX_RUN_LEN: Config<usize> = Config::new(
405    "persist_batch_max_run_len",
406    usize::MAX,
407    "The maximum length a run can have before it will be spilled as a hollow run \
408    into the blob store.",
409    ParameterScope::Environment,
410);
411
412pub(crate) const MAX_RUNS: Config<usize> = Config::new(
413    "persist_batch_max_runs",
414    1,
415    "The maximum number of runs a batch builder should generate for user batches. \
416    (Compaction outputs always generate a single run.) \
417    The minimum value is 2; below this, compaction is disabled.",
418    ParameterScope::Environment,
419);
420
421/// A target maximum size of blob payloads in bytes. If a logical "batch" is
422/// bigger than this, it will be broken up into smaller, independent pieces.
423/// This is best-effort, not a guarantee (though as of 2022-06-09, we happen to
424/// always respect it). This target size doesn't apply for an individual update
425/// that exceeds it in size, but that scenario is almost certainly a mis-use of
426/// the system.
427pub(crate) const BLOB_TARGET_SIZE: Config<usize> = Config::new(
428    "persist_blob_target_size",
429    128 * MiB,
430    "A target maximum size of persist blob payloads in bytes (Materialize).",
431    ParameterScope::Environment,
432);
433
434pub(crate) const INLINE_WRITES_SINGLE_MAX_BYTES: Config<usize> = Config::new(
435    "persist_inline_writes_single_max_bytes",
436    4096,
437    "The (exclusive) maximum size of a write that persist will inline in metadata.",
438    ParameterScope::Environment,
439);
440
441pub(crate) const INLINE_WRITES_TOTAL_MAX_BYTES: Config<usize> = Config::new(
442    "persist_inline_writes_total_max_bytes",
443    1 * MiB,
444    "\
445    The (exclusive) maximum total size of inline writes in metadata before \
446    persist will backpressure them by flushing out to s3.",
447    ParameterScope::Environment,
448);
449
450impl BatchBuilderConfig {
451    /// Initialize a batch builder config based on a snapshot of the Persist config.
452    pub fn new(value: &PersistConfig, _shard_id: ShardId) -> Self {
453        let writer_key = WriterKey::for_version(&value.build_version);
454
455        let preferred_order = RunOrder::Structured;
456
457        BatchBuilderConfig {
458            writer_key,
459            blob_target_size: BLOB_TARGET_SIZE.get(value).clamp(1, usize::MAX),
460            batch_delete_enabled: BATCH_DELETE_ENABLED.get(value),
461            batch_builder_max_outstanding_parts: BATCH_BUILDER_MAX_OUTSTANDING_PARTS.get(value),
462            inline_writes_single_max_bytes: INLINE_WRITES_SINGLE_MAX_BYTES.get(value),
463            stats_collection_enabled: STATS_COLLECTION_ENABLED.get(value),
464            stats_budget: STATS_BUDGET_BYTES.get(value),
465            stats_untrimmable_columns: Arc::new(untrimmable_columns(value)),
466            encoding_config: EncodingConfig {
467                use_dictionary: ENCODING_ENABLE_DICTIONARY.get(value),
468                compression: CompressionFormat::from_str(&ENCODING_COMPRESSION_FORMAT.get(value)),
469            },
470            preferred_order,
471            structured_key_lower_len: STRUCTURED_KEY_LOWER_LEN.get(value),
472            run_length_limit: MAX_RUN_LEN.get(value).clamp(2, usize::MAX),
473            max_runs: match MAX_RUNS.get(value) {
474                limit @ 2.. => Some(limit),
475                _ => None,
476            },
477            enable_incremental_compaction: ENABLE_INCREMENTAL_COMPACTION.get(value),
478        }
479    }
480}
481
482/// A list of (lowercase) column names that persist will always retain
483/// stats for, even if it means going over the stats budget.
484#[derive(
485    Debug,
486    Clone,
487    PartialEq,
488    Eq,
489    serde::Serialize,
490    serde::Deserialize,
491    Arbitrary
492)]
493pub(crate) struct UntrimmableColumns {
494    /// Always retain columns whose lowercased names exactly equal any of these strings.
495    pub equals: Vec<Cow<'static, str>>,
496    /// Always retain columns whose lowercased names start with any of these strings.
497    pub prefixes: Vec<Cow<'static, str>>,
498    /// Always retain columns whose lowercased names end with any of these strings.
499    pub suffixes: Vec<Cow<'static, str>>,
500}
501
502impl UntrimmableColumns {
503    pub(crate) fn should_retain(&self, name: &str) -> bool {
504        // TODO: see if there's a better way to match different formats than lowercasing
505        // https://github.com/MaterializeInc/database-issues/issues/6421#issue-1863623805
506        let name_lower = name.to_lowercase();
507        for s in &self.equals {
508            if *s == name_lower {
509                return true;
510            }
511        }
512        for s in &self.prefixes {
513            if name_lower.starts_with(s.as_ref()) {
514                return true;
515            }
516        }
517        for s in &self.suffixes {
518            if name_lower.ends_with(s.as_ref()) {
519                return true;
520            }
521        }
522        false
523    }
524}
525
526/// A builder for [Batches](Batch) that allows adding updates piece by piece and
527/// then finishing it.
528#[derive(Debug)]
529pub struct BatchBuilder<K, V, T, D>
530where
531    K: Codec,
532    V: Codec,
533    T: Timestamp + Lattice + Codec64,
534{
535    inline_desc: Description<T>,
536    inclusive_upper: Antichain<Reverse<T>>,
537
538    records_builder: PartBuilder<K, K::Schema, V, V::Schema>,
539    pub(crate) builder: BatchBuilderInternal<K, V, T, D>,
540}
541
542impl<K, V, T, D> BatchBuilder<K, V, T, D>
543where
544    K: Debug + Codec,
545    V: Debug + Codec,
546    T: Timestamp + Lattice + Codec64,
547    D: Monoid + Codec64,
548{
549    pub(crate) fn new(
550        builder: BatchBuilderInternal<K, V, T, D>,
551        inline_desc: Description<T>,
552    ) -> Self {
553        let records_builder = PartBuilder::new(
554            builder.write_schemas.key.as_ref(),
555            builder.write_schemas.val.as_ref(),
556        );
557        Self {
558            inline_desc,
559            inclusive_upper: Antichain::new(),
560            records_builder,
561            builder,
562        }
563    }
564
565    /// Finish writing this batch and return a handle to the written batch.
566    ///
567    /// This fails if any of the updates in this batch are beyond the given
568    /// `upper`.
569    pub async fn finish(
570        mut self,
571        registered_upper: Antichain<T>,
572    ) -> Result<Batch<K, V, T, D>, InvalidUsage<T>> {
573        if PartialOrder::less_than(&registered_upper, self.inline_desc.lower()) {
574            return Err(InvalidUsage::InvalidBounds {
575                lower: self.inline_desc.lower().clone(),
576                upper: registered_upper,
577            });
578        }
579
580        // When since is less than or equal to lower, the upper is a strict bound
581        // on the updates' timestamp because no advancement has been performed. Because user batches
582        // are always unadvanced, this ensures that new updates are recorded with valid timestamps.
583        // Otherwise, we can make no assumptions about the timestamps
584        if PartialOrder::less_equal(self.inline_desc.since(), self.inline_desc.lower()) {
585            for ts in self.inclusive_upper.iter() {
586                if registered_upper.less_equal(&ts.0) {
587                    return Err(InvalidUsage::UpdateBeyondUpper {
588                        ts: ts.0.clone(),
589                        expected_upper: registered_upper.clone(),
590                    });
591                }
592            }
593        }
594
595        let updates = self.records_builder.finish();
596        self.builder
597            .flush_part(self.inline_desc.clone(), updates)
598            .await;
599
600        self.builder
601            .finish(Description::new(
602                self.inline_desc.lower().clone(),
603                registered_upper,
604                self.inline_desc.since().clone(),
605            ))
606            .await
607    }
608
609    /// Adds the given update to the batch.
610    ///
611    /// The update timestamp must be greater or equal to `lower` that was given
612    /// when creating this [BatchBuilder].
613    pub async fn add(
614        &mut self,
615        key: &K,
616        val: &V,
617        ts: &T,
618        diff: &D,
619    ) -> Result<Added, InvalidUsage<T>> {
620        if !self.inline_desc.lower().less_equal(ts) {
621            return Err(InvalidUsage::UpdateNotBeyondLower {
622                ts: ts.clone(),
623                lower: self.inline_desc.lower().clone(),
624            });
625        }
626        self.inclusive_upper.insert(Reverse(ts.clone()));
627
628        let added = {
629            self.records_builder
630                .push(key, val, ts.clone(), diff.clone());
631            if self.records_builder.goodbytes() >= self.builder.parts.cfg.blob_target_size {
632                let part = self.records_builder.finish_and_replace(
633                    self.builder.write_schemas.key.as_ref(),
634                    self.builder.write_schemas.val.as_ref(),
635                );
636                Some(part)
637            } else {
638                None
639            }
640        };
641
642        let added = if let Some(full_batch) = added {
643            self.builder
644                .flush_part(self.inline_desc.clone(), full_batch)
645                .await;
646            Added::RecordAndParts
647        } else {
648            Added::Record
649        };
650        Ok(added)
651    }
652}
653
654#[derive(Debug)]
655pub(crate) struct BatchBuilderInternal<K, V, T, D>
656where
657    K: Codec,
658    V: Codec,
659    T: Timestamp + Lattice + Codec64,
660{
661    shard_id: ShardId,
662    version: Version,
663    blob: Arc<dyn Blob>,
664    metrics: Arc<Metrics>,
665
666    write_schemas: Schemas<K, V>,
667    parts: BatchParts<T>,
668
669    // These provide a bit more safety against appending a batch with the wrong
670    // type to a shard.
671    _phantom: PhantomData<fn(K, V, T, D)>,
672}
673
674impl<K, V, T, D> BatchBuilderInternal<K, V, T, D>
675where
676    K: Debug + Codec,
677    V: Debug + Codec,
678    T: Timestamp + Lattice + Codec64,
679    D: Monoid + Codec64,
680{
681    pub(crate) fn new(
682        _cfg: BatchBuilderConfig,
683        parts: BatchParts<T>,
684        metrics: Arc<Metrics>,
685        write_schemas: Schemas<K, V>,
686        blob: Arc<dyn Blob>,
687        shard_id: ShardId,
688        version: Version,
689    ) -> Self {
690        Self {
691            blob,
692            metrics,
693            write_schemas,
694            parts,
695            shard_id,
696            version,
697            _phantom: PhantomData,
698        }
699    }
700
701    /// Finish writing this batch and return a handle to the written batch.
702    ///
703    /// This fails if any of the updates in this batch are beyond the given
704    /// `upper`.
705    #[instrument(level = "debug", name = "batch::finish", fields(shard = %self.shard_id))]
706    pub async fn finish(
707        self,
708        registered_desc: Description<T>,
709    ) -> Result<Batch<K, V, T, D>, InvalidUsage<T>> {
710        let write_run_ids = self.parts.cfg.enable_incremental_compaction;
711        let batch_delete_enabled = self.parts.cfg.batch_delete_enabled;
712        let shard_metrics = Arc::clone(&self.parts.shard_metrics);
713        let runs = self.parts.finish().await;
714
715        let mut run_parts = vec![];
716        let mut run_splits = vec![];
717        let mut run_meta = vec![];
718        let total_updates = runs
719            .iter()
720            .map(|(_, _, num_updates)| num_updates)
721            .sum::<usize>();
722        for (order, parts, num_updates) in runs {
723            if parts.is_empty() {
724                continue;
725            }
726            if run_parts.len() != 0 {
727                run_splits.push(run_parts.len());
728            }
729            run_meta.push(RunMeta {
730                order: Some(order),
731                schema: self.write_schemas.id,
732                // Field has been deprecated but kept around to roundtrip state.
733                deprecated_schema: None,
734                id: if write_run_ids {
735                    Some(RunId::new())
736                } else {
737                    None
738                },
739                len: if write_run_ids {
740                    Some(num_updates)
741                } else {
742                    None
743                },
744                meta: MetadataMap::default(),
745            });
746            run_parts.extend(parts);
747        }
748        let desc = registered_desc;
749
750        let batch = Batch::new(
751            batch_delete_enabled,
752            Arc::clone(&self.metrics),
753            self.blob,
754            shard_metrics,
755            self.version,
756            (
757                K::encode_schema(&*self.write_schemas.key),
758                V::encode_schema(&*self.write_schemas.val),
759            ),
760            HollowBatch::new(desc, run_parts, total_updates, run_meta, run_splits),
761        );
762
763        Ok(batch)
764    }
765
766    /// Flushes the current part to Blob storage, first consolidating and then
767    /// columnar encoding the updates. It is the caller's responsibility to
768    /// chunk `current_part` to be no greater than
769    /// [BatchBuilderConfig::blob_target_size], and must absolutely be less than
770    /// [mz_persist::indexed::columnar::KEY_VAL_DATA_MAX_LEN]
771    pub async fn flush_part(&mut self, part_desc: Description<T>, columnar: Part) {
772        let num_updates = columnar.len();
773        if num_updates == 0 {
774            return;
775        }
776        let diffs_sum = diffs_sum::<D>(&columnar.diff);
777
778        let start = Instant::now();
779        self.parts
780            .write(&self.write_schemas, part_desc, columnar, diffs_sum)
781            .await;
782        self.metrics
783            .compaction
784            .batch
785            .step_part_writing
786            .inc_by(start.elapsed().as_secs_f64());
787    }
788}
789
790#[derive(Debug, Clone)]
791pub(crate) struct RunWithMeta<T> {
792    pub parts: Vec<RunPart<T>>,
793    pub num_updates: usize,
794}
795
796impl<T> RunWithMeta<T> {
797    pub fn new(parts: Vec<RunPart<T>>, num_updates: usize) -> Self {
798        Self { parts, num_updates }
799    }
800
801    pub fn single(part: RunPart<T>, num_updates: usize) -> Self {
802        Self {
803            parts: vec![part],
804            num_updates,
805        }
806    }
807}
808
809#[derive(Debug)]
810enum WritingRuns<T> {
811    /// Building a single run with the specified ordering. Parts are expected to be internally
812    /// sorted and added in order. Merging a vec of parts will shift them out to a hollow run
813    /// in blob, bounding the total length of a run in memory.
814    Ordered(RunOrder, MergeTree<Pending<RunWithMeta<T>>>),
815    /// Building multiple runs which may have different orders. Merging a vec of runs will cause
816    /// them to be compacted together, bounding the total number of runs we generate.
817    Compacting(MergeTree<(RunOrder, Pending<RunWithMeta<T>>)>),
818}
819
820// TODO: If this is dropped, cancel (and delete?) any writing parts and delete
821// any finished ones.
822#[derive(Debug)]
823pub(crate) struct BatchParts<T> {
824    cfg: BatchBuilderConfig,
825    metrics: Arc<Metrics>,
826    shard_metrics: Arc<ShardMetrics>,
827    shard_id: ShardId,
828    blob: Arc<dyn Blob>,
829    isolated_runtime: Arc<IsolatedRuntime>,
830    next_index: u64,
831    writing_runs: WritingRuns<T>,
832    batch_metrics: BatchWriteMetrics,
833}
834
835impl<T: Timestamp + Codec64> BatchParts<T> {
836    pub(crate) fn new_compacting<K, V, D>(
837        cfg: CompactConfig,
838        desc: Description<T>,
839        runs_per_compaction: usize,
840        metrics: Arc<Metrics>,
841        shard_metrics: Arc<ShardMetrics>,
842        shard_id: ShardId,
843        blob: Arc<dyn Blob>,
844        isolated_runtime: Arc<IsolatedRuntime>,
845        batch_metrics: &BatchWriteMetrics,
846        schemas: Schemas<K, V>,
847    ) -> Self
848    where
849        K: Codec + Debug,
850        V: Codec + Debug,
851        T: Lattice + Send + Sync,
852        D: Monoid + Ord + Codec64 + Send + Sync,
853    {
854        let writing_runs = {
855            let cfg = cfg.clone();
856            let blob = Arc::clone(&blob);
857            let metrics = Arc::clone(&metrics);
858            let shard_metrics = Arc::clone(&shard_metrics);
859            let isolated_runtime = Arc::clone(&isolated_runtime);
860            // Clamping to prevent extreme values given weird configs.
861            let runs_per_compaction = runs_per_compaction.clamp(2, 1024);
862
863            let merge_fn = move |parts: Vec<(RunOrder, Pending<RunWithMeta<T>>)>| {
864                let blob = Arc::clone(&blob);
865                let metrics = Arc::clone(&metrics);
866                let shard_metrics = Arc::clone(&shard_metrics);
867                let cfg = cfg.clone();
868                let isolated_runtime = Arc::clone(&isolated_runtime);
869                let write_schemas = schemas.clone();
870                let compact_desc = desc.clone();
871                let handle = mz_ore::task::spawn(
872                    || "batch::compact_runs",
873                    async move {
874                        let runs: Vec<_> = stream::iter(parts)
875                            .then(|(order, parts)| async move {
876                                let completed_run = parts.into_result().await;
877                                (
878                                    RunMeta {
879                                        order: Some(order),
880                                        schema: schemas.id,
881                                        // Field has been deprecated but kept around to
882                                        // roundtrip state.
883                                        deprecated_schema: None,
884                                        id: if cfg.batch.enable_incremental_compaction {
885                                            Some(RunId::new())
886                                        } else {
887                                            None
888                                        },
889                                        len: if cfg.batch.enable_incremental_compaction {
890                                            Some(completed_run.num_updates)
891                                        } else {
892                                            None
893                                        },
894                                        meta: MetadataMap::default(),
895                                    },
896                                    completed_run.parts,
897                                )
898                            })
899                            .collect()
900                            .await;
901
902                        let run_refs: Vec<_> = runs
903                            .iter()
904                            .map(|(meta, run)| (&compact_desc, meta, run.as_slice()))
905                            .collect();
906
907                        let output_batch = Compactor::<K, V, T, D>::compact_runs(
908                            &cfg,
909                            &shard_id,
910                            &compact_desc,
911                            run_refs,
912                            blob,
913                            metrics,
914                            shard_metrics,
915                            isolated_runtime,
916                            write_schemas,
917                        )
918                        .await
919                        .expect("successful compaction");
920
921                        assert_eq!(
922                            output_batch.run_meta.len(),
923                            1,
924                            "compaction is guaranteed to emit a single run"
925                        );
926                        let total_compacted_updates: usize = output_batch.len;
927
928                        RunWithMeta::new(output_batch.parts, total_compacted_updates)
929                    }
930                    .instrument(debug_span!("batch::compact_runs")),
931                );
932                (RunOrder::Structured, Pending::new(handle))
933            };
934            WritingRuns::Compacting(MergeTree::new(runs_per_compaction, merge_fn))
935        };
936        BatchParts {
937            cfg: cfg.batch,
938            metrics,
939            shard_metrics,
940            shard_id,
941            blob,
942            isolated_runtime,
943            next_index: 0,
944            writing_runs,
945            batch_metrics: batch_metrics.clone(),
946        }
947    }
948
949    pub(crate) fn new_ordered<D: Monoid + Codec64>(
950        cfg: BatchBuilderConfig,
951        order: RunOrder,
952        metrics: Arc<Metrics>,
953        shard_metrics: Arc<ShardMetrics>,
954        shard_id: ShardId,
955        blob: Arc<dyn Blob>,
956        isolated_runtime: Arc<IsolatedRuntime>,
957        batch_metrics: &BatchWriteMetrics,
958    ) -> Self {
959        let writing_runs = {
960            let cfg = cfg.clone();
961            let blob = Arc::clone(&blob);
962            let metrics = Arc::clone(&metrics);
963            let writer_key = cfg.writer_key.clone();
964            // Don't spill "unordered" runs to S3, since we'll split them up into many single-element
965            // runs below.
966            let run_length_limit = (order == RunOrder::Unordered)
967                .then_some(usize::MAX)
968                .unwrap_or(cfg.run_length_limit);
969            let merge_fn = move |parts: Vec<Pending<RunWithMeta<T>>>| {
970                let blob = Arc::clone(&blob);
971                let writer_key = writer_key.clone();
972                let metrics = Arc::clone(&metrics);
973                let handle = mz_ore::task::spawn(
974                    || "batch::spill_run",
975                    async move {
976                        let completed_runs: Vec<RunWithMeta<T>> = stream::iter(parts)
977                            .then(|p| p.into_result())
978                            .collect()
979                            .await;
980
981                        let mut all_run_parts = Vec::new();
982                        let mut total_updates = 0;
983
984                        for completed_run in completed_runs {
985                            all_run_parts.extend(completed_run.parts);
986                            total_updates += completed_run.num_updates;
987                        }
988
989                        let run_ref = HollowRunRef::set::<D>(
990                            shard_id,
991                            blob.as_ref(),
992                            &writer_key,
993                            HollowRun {
994                                parts: all_run_parts,
995                            },
996                            &*metrics,
997                        )
998                        .await;
999
1000                        RunWithMeta::single(RunPart::Many(run_ref), total_updates)
1001                    }
1002                    .instrument(debug_span!("batch::spill_run")),
1003                );
1004                Pending::new(handle)
1005            };
1006            WritingRuns::Ordered(order, MergeTree::new(run_length_limit, merge_fn))
1007        };
1008        BatchParts {
1009            cfg,
1010            metrics,
1011            shard_metrics,
1012            shard_id,
1013            blob,
1014            isolated_runtime,
1015            next_index: 0,
1016            writing_runs,
1017            batch_metrics: batch_metrics.clone(),
1018        }
1019    }
1020
1021    pub(crate) fn expected_order(&self) -> RunOrder {
1022        match self.writing_runs {
1023            WritingRuns::Ordered(order, _) => order,
1024            WritingRuns::Compacting(_) => RunOrder::Unordered,
1025        }
1026    }
1027
1028    pub(crate) async fn write<K: Codec, V: Codec, D: Codec64>(
1029        &mut self,
1030        write_schemas: &Schemas<K, V>,
1031        desc: Description<T>,
1032        updates: Part,
1033        diffs_sum: D,
1034    ) {
1035        let batch_metrics = self.batch_metrics.clone();
1036        let index = self.next_index;
1037        self.next_index += 1;
1038        let num_updates = updates.len();
1039        let ts_rewrite = None;
1040        let schema_id = write_schemas.id;
1041
1042        // If we're going to encode structured data then halve our limit since we're storing
1043        // it twice, once as binary encoded and once as structured.
1044        let inline_threshold = self.cfg.inline_writes_single_max_bytes;
1045
1046        let updates = BlobTraceUpdates::from_part(updates);
1047        let (name, write_future) = if updates.goodbytes() < inline_threshold {
1048            let span = debug_span!("batch::inline_part", shard = %self.shard_id).or_current();
1049            (
1050                "batch::inline_part",
1051                async move {
1052                    let start = Instant::now();
1053                    let updates = LazyInlineBatchPart::from(&ProtoInlineBatchPart {
1054                        desc: Some(desc.into_proto()),
1055                        index: index.into_proto(),
1056                        updates: Some(updates.into_proto()),
1057                    });
1058                    batch_metrics
1059                        .step_inline
1060                        .inc_by(start.elapsed().as_secs_f64());
1061
1062                    RunWithMeta::single(
1063                        RunPart::Single(BatchPart::Inline {
1064                            updates,
1065                            ts_rewrite,
1066                            schema_id,
1067                            // Field has been deprecated but kept around to roundtrip state.
1068                            deprecated_schema_id: None,
1069                        }),
1070                        num_updates,
1071                    )
1072                }
1073                .instrument(span)
1074                .boxed(),
1075            )
1076        } else {
1077            let part = BlobTraceBatchPart {
1078                desc,
1079                updates,
1080                index,
1081            };
1082            let cfg = self.cfg.clone();
1083            let blob = Arc::clone(&self.blob);
1084            let metrics = Arc::clone(&self.metrics);
1085            let shard_metrics = Arc::clone(&self.shard_metrics);
1086            let isolated_runtime = Arc::clone(&self.isolated_runtime);
1087            let expected_order = self.expected_order();
1088            let encoded_diffs_sum = D::encode(&diffs_sum);
1089            let write_schemas_clone = write_schemas.clone();
1090            let write_span =
1091                debug_span!("batch::write_part", shard = %self.shard_metrics.shard_id).or_current();
1092            (
1093                "batch::write_part",
1094                async move {
1095                    let part = BatchParts::write_hollow_part(
1096                        cfg,
1097                        blob,
1098                        metrics,
1099                        shard_metrics,
1100                        batch_metrics,
1101                        isolated_runtime,
1102                        part,
1103                        expected_order,
1104                        ts_rewrite,
1105                        encoded_diffs_sum,
1106                        write_schemas_clone,
1107                    )
1108                    .await;
1109                    RunWithMeta::single(RunPart::Single(part), num_updates)
1110                }
1111                .instrument(write_span)
1112                .boxed(),
1113            )
1114        };
1115
1116        match &mut self.writing_runs {
1117            WritingRuns::Ordered(_order, run) => {
1118                let part = Pending::new(mz_ore::task::spawn(|| name, write_future));
1119                run.push(part);
1120
1121                // If there are more than the max outstanding parts, block on all but the
1122                //  most recent.
1123                for part in run
1124                    .iter_mut()
1125                    .rev()
1126                    .skip(self.cfg.batch_builder_max_outstanding_parts)
1127                    .take_while(|p| !p.is_finished())
1128                {
1129                    self.batch_metrics.write_stalls.inc();
1130                    part.block_until_ready().await;
1131                }
1132            }
1133            WritingRuns::Compacting(batches) => {
1134                let run = Pending::Writing(mz_ore::task::spawn(|| name, write_future));
1135                batches.push((RunOrder::Unordered, run));
1136
1137                // Allow up to `max_outstanding_parts` (or one compaction) to be pending, and block
1138                // on the rest.
1139                let mut part_budget = self.cfg.batch_builder_max_outstanding_parts;
1140                let mut compaction_budget = 1;
1141                for (_, part) in batches
1142                    .iter_mut()
1143                    .rev()
1144                    .skip_while(|(order, _)| match order {
1145                        RunOrder::Unordered if part_budget > 0 => {
1146                            part_budget -= 1;
1147                            true
1148                        }
1149                        RunOrder::Structured | RunOrder::Codec if compaction_budget > 0 => {
1150                            compaction_budget -= 1;
1151                            true
1152                        }
1153                        _ => false,
1154                    })
1155                    .take_while(|(_, p)| !p.is_finished())
1156                {
1157                    self.batch_metrics.write_stalls.inc();
1158                    part.block_until_ready().await;
1159                }
1160            }
1161        }
1162    }
1163
1164    async fn write_hollow_part<K: Codec, V: Codec>(
1165        cfg: BatchBuilderConfig,
1166        blob: Arc<dyn Blob>,
1167        metrics: Arc<Metrics>,
1168        shard_metrics: Arc<ShardMetrics>,
1169        batch_metrics: BatchWriteMetrics,
1170        isolated_runtime: Arc<IsolatedRuntime>,
1171        mut updates: BlobTraceBatchPart<T>,
1172        run_order: RunOrder,
1173        ts_rewrite: Option<Antichain<T>>,
1174        diffs_sum: [u8; 8],
1175        write_schemas: Schemas<K, V>,
1176    ) -> BatchPart<T> {
1177        let partial_key = PartialBatchKey::new(&cfg.writer_key, &PartId::new());
1178        let key = partial_key.complete(&shard_metrics.shard_id);
1179        let goodbytes = updates.updates.goodbytes();
1180        let metrics_ = Arc::clone(&metrics);
1181        let schema_id = write_schemas.id;
1182
1183        let (stats, key_lower, structured_key_lower, (buf, encode_time)) = isolated_runtime
1184            .spawn_named(|| "batch::encode_part", async move {
1185                // Measure the expensive steps of the part build - re-encoding and stats collection.
1186                let stats = metrics_.columnar.arrow().measure_part_build(|| {
1187                    let stats = if cfg.stats_collection_enabled {
1188                        let ext = updates.updates.get_or_make_structured::<K, V>(
1189                            write_schemas.key.as_ref(),
1190                            write_schemas.val.as_ref(),
1191                        );
1192
1193                        let key_stats = write_schemas
1194                            .key
1195                            .decoder_any(ext.key.as_ref())
1196                            .expect("decoding just-encoded data")
1197                            .stats();
1198
1199                        let part_stats = PartStats { key: key_stats };
1200
1201                        // Collect stats about the updates, if stats collection is enabled.
1202                        let trimmed_start = Instant::now();
1203                        let mut trimmed_bytes = 0;
1204                        let trimmed_stats = LazyPartStats::encode(&part_stats, |s| {
1205                            trimmed_bytes = trim_to_budget(s, cfg.stats_budget, |s| {
1206                                cfg.stats_untrimmable_columns.should_retain(s)
1207                            })
1208                        });
1209                        let trimmed_duration = trimmed_start.elapsed();
1210                        Some((trimmed_stats, trimmed_duration, trimmed_bytes))
1211                    } else {
1212                        None
1213                    };
1214
1215                    // Ensure the updates are in the specified columnar format before encoding.
1216                    updates.updates = updates.updates.as_structured::<K, V>(
1217                        write_schemas.key.as_ref(),
1218                        write_schemas.val.as_ref(),
1219                    );
1220
1221                    stats
1222                });
1223
1224                let key_lower = if let Some(records) = updates.updates.records() {
1225                    let key_bytes = records.keys();
1226                    if key_bytes.is_empty() {
1227                        &[]
1228                    } else if run_order == RunOrder::Codec {
1229                        key_bytes.value(0)
1230                    } else {
1231                        ::arrow::compute::min_binary(key_bytes).expect("min of nonempty array")
1232                    }
1233                } else {
1234                    &[]
1235                };
1236                let key_lower = truncate_bytes(key_lower, TRUNCATE_LEN, TruncateBound::Lower)
1237                    .expect("lower bound always exists");
1238
1239                let structured_key_lower = if cfg.structured_key_lower_len > 0 {
1240                    updates.updates.structured().and_then(|ext| {
1241                        let min_key = if run_order == RunOrder::Structured {
1242                            0
1243                        } else {
1244                            let ord = ArrayOrd::new(ext.key.as_ref());
1245                            (0..ext.key.len())
1246                                .min_by_key(|i| ord.at(*i))
1247                                .expect("non-empty batch")
1248                        };
1249                        let lower = ArrayBound::new(Arc::clone(&ext.key), min_key)
1250                            .to_proto_lower(cfg.structured_key_lower_len);
1251                        if lower.is_none() {
1252                            batch_metrics.key_lower_too_big.inc()
1253                        }
1254                        lower.map(|proto| LazyProto::from(&proto))
1255                    })
1256                } else {
1257                    None
1258                };
1259
1260                let encode_start = Instant::now();
1261                let mut buf = Vec::new();
1262                updates.encode(&mut buf, &metrics_.columnar, &cfg.encoding_config);
1263
1264                // Drop batch as soon as we can to reclaim its memory.
1265                drop(updates);
1266                (
1267                    stats,
1268                    key_lower,
1269                    structured_key_lower,
1270                    (Bytes::from(buf), encode_start.elapsed()),
1271                )
1272            })
1273            .instrument(debug_span!("batch::encode_part"))
1274            .await;
1275        // Can't use the `CodecMetrics::encode` helper because of async.
1276        metrics.codecs.batch.encode_count.inc();
1277        metrics
1278            .codecs
1279            .batch
1280            .encode_seconds
1281            .inc_by(encode_time.as_secs_f64());
1282
1283        let start = Instant::now();
1284        let payload_len = buf.len();
1285        let () = retry_external(&metrics.retries.external.batch_set, || async {
1286            shard_metrics.blob_sets.inc();
1287            blob.set(&key, Bytes::clone(&buf)).await
1288        })
1289        .instrument(trace_span!("batch::set", payload_len))
1290        .await;
1291        batch_metrics.seconds.inc_by(start.elapsed().as_secs_f64());
1292        batch_metrics.bytes.inc_by(u64::cast_from(payload_len));
1293        batch_metrics.goodbytes.inc_by(u64::cast_from(goodbytes));
1294        match run_order {
1295            RunOrder::Unordered => batch_metrics.unordered.inc(),
1296            RunOrder::Codec => batch_metrics.codec_order.inc(),
1297            RunOrder::Structured => batch_metrics.structured_order.inc(),
1298        }
1299        let stats = stats.map(|(stats, stats_step_timing, trimmed_bytes)| {
1300            batch_metrics
1301                .step_stats
1302                .inc_by(stats_step_timing.as_secs_f64());
1303            if trimmed_bytes > 0 {
1304                metrics.pushdown.parts_stats_trimmed_count.inc();
1305                metrics
1306                    .pushdown
1307                    .parts_stats_trimmed_bytes
1308                    .inc_by(u64::cast_from(trimmed_bytes));
1309            }
1310            stats
1311        });
1312
1313        let meta = MetadataMap::default();
1314        BatchPart::Hollow(HollowBatchPart {
1315            key: partial_key,
1316            meta,
1317            encoded_size_bytes: payload_len,
1318            key_lower,
1319            structured_key_lower,
1320            stats,
1321            ts_rewrite,
1322            diffs_sum: Some(diffs_sum),
1323            format: Some(BatchColumnarFormat::Structured),
1324            schema_id,
1325            // Field has been deprecated but kept around to roundtrip state.
1326            deprecated_schema_id: None,
1327        })
1328    }
1329
1330    #[instrument(level = "debug", name = "batch::finish_upload", fields(shard = %self.shard_id))]
1331    pub(crate) async fn finish(self) -> Vec<(RunOrder, Vec<RunPart<T>>, usize)> {
1332        match self.writing_runs {
1333            WritingRuns::Ordered(RunOrder::Unordered, run) => {
1334                let completed_runs = run.finish();
1335                let mut output = Vec::with_capacity(completed_runs.len());
1336                for completed_run in completed_runs {
1337                    let completed_run = completed_run.into_result().await;
1338                    // Each part becomes its own run for unordered case
1339                    for part in completed_run.parts {
1340                        output.push((RunOrder::Unordered, vec![part], completed_run.num_updates));
1341                    }
1342                }
1343                output
1344            }
1345            WritingRuns::Ordered(order, run) => {
1346                let completed_runs = run.finish();
1347                let mut all_parts = Vec::new();
1348                let mut all_update_counts = 0;
1349                for completed_run in completed_runs {
1350                    let completed_run = completed_run.into_result().await;
1351                    all_parts.extend(completed_run.parts);
1352                    all_update_counts += completed_run.num_updates;
1353                }
1354                vec![(order, all_parts, all_update_counts)]
1355            }
1356            WritingRuns::Compacting(batches) => {
1357                let runs = batches.finish();
1358                let mut output = Vec::new();
1359                for (order, run) in runs {
1360                    let completed_run = run.into_result().await;
1361                    output.push((order, completed_run.parts, completed_run.num_updates));
1362                }
1363                output
1364            }
1365        }
1366    }
1367}
1368
1369pub(crate) fn validate_truncate_batch<T: Timestamp>(
1370    batch: &HollowBatch<T>,
1371    truncate: &Description<T>,
1372    any_batch_rewrite: bool,
1373    validate_part_bounds_on_write: bool,
1374) -> Result<(), InvalidUsage<T>> {
1375    // If rewrite_ts is used, we don't allow truncation, to keep things simpler
1376    // to reason about.
1377    if any_batch_rewrite {
1378        // We allow a new upper to be specified at rewrite time, so that's easy:
1379        // it must match exactly. This is both consistent with the upper
1380        // requirement below and proves that there is no data to truncate past
1381        // the upper.
1382        if truncate.upper() != batch.desc.upper() {
1383            return Err(InvalidUsage::InvalidRewrite(format!(
1384                "rewritten batch might have data past {:?} up to {:?}",
1385                truncate.upper().elements(),
1386                batch.desc.upper().elements(),
1387            )));
1388        }
1389        // To prove that there is no data to truncate below the lower, require
1390        // that the lower is <= the rewrite ts.
1391        for part in batch.parts.iter() {
1392            let part_lower_bound = part.ts_rewrite().unwrap_or_else(|| batch.desc.lower());
1393            if !PartialOrder::less_equal(truncate.lower(), part_lower_bound) {
1394                return Err(InvalidUsage::InvalidRewrite(format!(
1395                    "rewritten batch might have data below {:?} at {:?}",
1396                    truncate.lower().elements(),
1397                    part_lower_bound.elements(),
1398                )));
1399            }
1400        }
1401    }
1402
1403    if !validate_part_bounds_on_write {
1404        return Ok(());
1405    }
1406
1407    let batch = &batch.desc;
1408    if !PartialOrder::less_equal(batch.lower(), truncate.lower())
1409        || PartialOrder::less_than(batch.upper(), truncate.upper())
1410    {
1411        return Err(InvalidUsage::InvalidBatchBounds {
1412            batch_lower: batch.lower().clone(),
1413            batch_upper: batch.upper().clone(),
1414            append_lower: truncate.lower().clone(),
1415            append_upper: truncate.upper().clone(),
1416        });
1417    }
1418
1419    Ok(())
1420}
1421
1422#[derive(Debug)]
1423pub(crate) struct PartDeletes<T> {
1424    /// Keys to hollow parts or runs that we're ready to delete.
1425    blob_keys: BTreeSet<PartialBatchKey>,
1426    /// Keys to hollow runs that may not have had all their parts deleted (or added to blob_keys) yet.
1427    hollow_runs: BTreeMap<PartialBatchKey, HollowRunRef<T>>,
1428}
1429
1430impl<T> Default for PartDeletes<T> {
1431    fn default() -> Self {
1432        Self {
1433            blob_keys: Default::default(),
1434            hollow_runs: Default::default(),
1435        }
1436    }
1437}
1438
1439impl<T: Timestamp> PartDeletes<T> {
1440    // Adds the part to the set to be deleted and returns true if it was newly
1441    // inserted.
1442    pub fn add(&mut self, part: &RunPart<T>) -> bool {
1443        match part {
1444            RunPart::Many(r) => self.hollow_runs.insert(r.key.clone(), r.clone()).is_none(),
1445            RunPart::Single(BatchPart::Hollow(x)) => self.blob_keys.insert(x.key.clone()),
1446            RunPart::Single(BatchPart::Inline { .. }) => {
1447                // Nothing to delete.
1448                true
1449            }
1450        }
1451    }
1452
1453    pub fn contains(&self, part: &RunPart<T>) -> bool {
1454        match part {
1455            RunPart::Many(r) => self.hollow_runs.contains_key(&r.key),
1456            RunPart::Single(BatchPart::Hollow(x)) => self.blob_keys.contains(&x.key),
1457            RunPart::Single(BatchPart::Inline { .. }) => false,
1458        }
1459    }
1460
1461    pub fn is_empty(&self) -> bool {
1462        self.len() == 0
1463    }
1464
1465    pub fn len(&self) -> usize {
1466        match self {
1467            Self {
1468                blob_keys,
1469                hollow_runs,
1470            } => blob_keys.len() + hollow_runs.len(),
1471        }
1472    }
1473
1474    pub async fn delete(
1475        mut self,
1476        blob: &dyn Blob,
1477        shard_id: ShardId,
1478        concurrency: usize,
1479        metrics: &Metrics,
1480        delete_metrics: &RetryMetrics,
1481    ) where
1482        T: Codec64,
1483    {
1484        loop {
1485            let () = stream::iter(mem::take(&mut self.blob_keys))
1486                .map(|key| {
1487                    let key = key.complete(&shard_id);
1488                    async move {
1489                        retry_external(delete_metrics, || blob.delete(&key)).await;
1490                    }
1491                })
1492                .buffer_unordered(concurrency)
1493                .collect()
1494                .await;
1495
1496            let Some((run_key, run_ref)) = self.hollow_runs.pop_first() else {
1497                break;
1498            };
1499
1500            if let Some(run) = run_ref.get(shard_id, blob, metrics).await {
1501                // Queue up both all the individual parts and the run itself for deletion.
1502                for part in &run.parts {
1503                    self.add(part);
1504                }
1505                self.blob_keys.insert(run_key);
1506            };
1507        }
1508    }
1509}
1510
1511/// Returns the total sum of diffs or None if there were no updates.
1512fn diffs_sum<D: Monoid + Codec64>(updates: &Int64Array) -> D {
1513    let mut sum = D::zero();
1514    for d in updates.values().iter() {
1515        let d = D::decode(d.to_le_bytes());
1516        sum.plus_equals(&d);
1517    }
1518    sum
1519}
1520
1521#[cfg(test)]
1522mod tests {
1523    use mz_dyncfg::ConfigUpdates;
1524
1525    use super::*;
1526    use crate::PersistLocation;
1527    use crate::cache::PersistClientCache;
1528    use crate::cfg::BATCH_BUILDER_MAX_OUTSTANDING_PARTS;
1529    use crate::internal::paths::{BlobKey, PartialBlobKey};
1530    use crate::tests::{all_ok, new_test_client};
1531
1532    #[mz_ore::test(tokio::test)]
1533    #[cfg_attr(miri, ignore)] // unsupported operation: returning ready events from epoll_wait is not yet implemented
1534    async fn batch_builder_flushing() {
1535        let data = vec![
1536            (("1".to_owned(), "one".to_owned()), 1, 1),
1537            (("2".to_owned(), "two".to_owned()), 2, 1),
1538            (("3".to_owned(), "three".to_owned()), 3, 1),
1539            (("4".to_owned(), "four".to_owned()), 4, 1),
1540        ];
1541
1542        let cache = PersistClientCache::new_no_metrics();
1543
1544        // Set blob_target_size to 0 so that each row gets forced into its own
1545        // batch. Set max_outstanding to a small value that's >1 to test various
1546        // edge cases below.
1547        cache.cfg.set_config(&BLOB_TARGET_SIZE, 0);
1548        cache.cfg.set_config(&MAX_RUNS, 3);
1549        cache
1550            .cfg
1551            .set_config(&BATCH_BUILDER_MAX_OUTSTANDING_PARTS, 2);
1552
1553        let client = cache
1554            .open(PersistLocation::new_in_mem())
1555            .await
1556            .expect("client construction failed");
1557        let (mut write, mut read) = client
1558            .expect_open::<String, String, u64, i64>(ShardId::new())
1559            .await;
1560
1561        // A new builder has no writing or finished parts.
1562        let mut builder = write.builder(Antichain::from_elem(0));
1563
1564        fn assert_writing(
1565            builder: &BatchBuilder<String, String, u64, i64>,
1566            expected_finished: &[bool],
1567        ) {
1568            let WritingRuns::Compacting(run) = &builder.builder.parts.writing_runs else {
1569                unreachable!("ordered run!")
1570            };
1571
1572            let actual: Vec<_> = run.iter().map(|(_, p)| p.is_finished()).collect();
1573            assert_eq!(*expected_finished, actual);
1574        }
1575
1576        assert_writing(&builder, &[]);
1577
1578        // We set blob_target_size to 0, so the first update gets forced out
1579        // into a run.
1580        let ((k, v), t, d) = &data[0];
1581        builder.add(k, v, t, d).await.expect("invalid usage");
1582        assert_writing(&builder, &[false]);
1583
1584        // We set batch_builder_max_outstanding_parts to 2, so we are allowed to
1585        // pipeline a second part.
1586        let ((k, v), t, d) = &data[1];
1587        builder.add(k, v, t, d).await.expect("invalid usage");
1588        assert_writing(&builder, &[false, false]);
1589
1590        // But now that we have 3 parts, the add call back-pressures until the
1591        // first one finishes.
1592        let ((k, v), t, d) = &data[2];
1593        builder.add(k, v, t, d).await.expect("invalid usage");
1594        assert_writing(&builder, &[true, false, false]);
1595
1596        // Finally, pushing a fourth part will cause the first three to spill out into
1597        // a new compacted run.
1598        let ((k, v), t, d) = &data[3];
1599        builder.add(k, v, t, d).await.expect("invalid usage");
1600        assert_writing(&builder, &[false, false]);
1601
1602        // Finish off the batch and verify that the keys and such get plumbed
1603        // correctly by reading the data back.
1604        let batch = builder
1605            .finish(Antichain::from_elem(5))
1606            .await
1607            .expect("invalid usage");
1608        assert_eq!(batch.batch.runs().count(), 2);
1609        assert_eq!(batch.batch.part_count(), 4);
1610        write
1611            .append_batch(batch, Antichain::from_elem(0), Antichain::from_elem(5))
1612            .await
1613            .expect("invalid usage")
1614            .expect("unexpected upper");
1615        assert_eq!(read.expect_snapshot_and_fetch(4).await, all_ok(&data, 4));
1616    }
1617
1618    #[mz_ore::test(tokio::test)]
1619    #[cfg_attr(miri, ignore)] // unsupported operation: returning ready events from epoll_wait is not yet implemented
1620    async fn batch_builder_keys() {
1621        let cache = PersistClientCache::new_no_metrics();
1622        // Set blob_target_size to 0 so that each row gets forced into its own batch part
1623        cache.cfg.set_config(&BLOB_TARGET_SIZE, 0);
1624        // Otherwise fails: expected hollow part!
1625        cache.cfg.set_config(&STRUCTURED_KEY_LOWER_LEN, 0);
1626        cache.cfg.set_config(&INLINE_WRITES_SINGLE_MAX_BYTES, 0);
1627        cache.cfg.set_config(&INLINE_WRITES_TOTAL_MAX_BYTES, 0);
1628        let client = cache
1629            .open(PersistLocation::new_in_mem())
1630            .await
1631            .expect("client construction failed");
1632        let shard_id = ShardId::new();
1633        let (mut write, _) = client
1634            .expect_open::<String, String, u64, i64>(shard_id)
1635            .await;
1636
1637        let batch = write
1638            .expect_batch(
1639                &[
1640                    (("1".into(), "one".into()), 1, 1),
1641                    (("2".into(), "two".into()), 2, 1),
1642                    (("3".into(), "three".into()), 3, 1),
1643                ],
1644                0,
1645                4,
1646            )
1647            .await;
1648
1649        assert_eq!(batch.batch.part_count(), 3);
1650        for part in &batch.batch.parts {
1651            let part = part.expect_hollow_part();
1652            match BlobKey::parse_ids(&part.key.complete(&shard_id)) {
1653                Ok((shard, PartialBlobKey::Batch(writer, _))) => {
1654                    assert_eq!(shard.to_string(), shard_id.to_string());
1655                    assert_eq!(writer, WriterKey::for_version(&cache.cfg.build_version));
1656                }
1657                _ => panic!("unparseable blob key"),
1658            }
1659        }
1660    }
1661
1662    #[mz_ore::test(tokio::test)]
1663    #[cfg_attr(miri, ignore)] // unsupported operation: returning ready events from epoll_wait is not yet implemented
1664    async fn batch_delete() {
1665        let cache = PersistClientCache::new_no_metrics();
1666        cache.cfg.set_config(&INLINE_WRITES_SINGLE_MAX_BYTES, 0);
1667        cache.cfg.set_config(&INLINE_WRITES_TOTAL_MAX_BYTES, 0);
1668        cache.cfg.set_config(&BATCH_DELETE_ENABLED, true);
1669        let client = cache
1670            .open(PersistLocation::new_in_mem())
1671            .await
1672            .expect("client construction failed");
1673        let shard_id = ShardId::new();
1674        let (mut write, _) = client
1675            .expect_open::<String, String, u64, i64>(shard_id)
1676            .await;
1677
1678        let batch = write
1679            .expect_batch(
1680                &[
1681                    (("1".into(), "one".into()), 1, 1),
1682                    (("2".into(), "two".into()), 2, 1),
1683                    (("3".into(), "three".into()), 3, 1),
1684                ],
1685                0,
1686                4,
1687            )
1688            .await;
1689
1690        assert_eq!(batch.batch.part_count(), 1);
1691        let part_key = batch.batch.parts[0]
1692            .expect_hollow_part()
1693            .key
1694            .complete(&shard_id);
1695
1696        let part_bytes = client.blob.get(&part_key).await.expect("invalid usage");
1697        assert!(part_bytes.is_some());
1698
1699        batch.delete().await;
1700
1701        let part_bytes = client.blob.get(&part_key).await.expect("invalid usage");
1702        assert!(part_bytes.is_none());
1703    }
1704
1705    #[mz_ore::test]
1706    fn untrimmable_columns() {
1707        let untrimmable = UntrimmableColumns {
1708            equals: vec!["abc".into(), "def".into()],
1709            prefixes: vec!["123".into(), "234".into()],
1710            suffixes: vec!["xyz".into()],
1711        };
1712
1713        // equals
1714        assert!(untrimmable.should_retain("abc"));
1715        assert!(untrimmable.should_retain("ABC"));
1716        assert!(untrimmable.should_retain("aBc"));
1717        assert!(!untrimmable.should_retain("abcd"));
1718        assert!(untrimmable.should_retain("deF"));
1719        assert!(!untrimmable.should_retain("defg"));
1720
1721        // prefix
1722        assert!(untrimmable.should_retain("123"));
1723        assert!(untrimmable.should_retain("123-4"));
1724        assert!(untrimmable.should_retain("1234"));
1725        assert!(untrimmable.should_retain("234"));
1726        assert!(!untrimmable.should_retain("345"));
1727
1728        // suffix
1729        assert!(untrimmable.should_retain("ijk_xyZ"));
1730        assert!(untrimmable.should_retain("ww-XYZ"));
1731        assert!(!untrimmable.should_retain("xya"));
1732    }
1733
1734    // NB: Most edge cases are exercised in datadriven tests.
1735    #[mz_persist_proc::test(tokio::test)]
1736    #[cfg_attr(miri, ignore)] // too slow
1737    async fn rewrite_ts_example(dyncfgs: ConfigUpdates) {
1738        let client = new_test_client(&dyncfgs).await;
1739        let (mut write, read) = client
1740            .expect_open::<String, (), u64, i64>(ShardId::new())
1741            .await;
1742
1743        let mut batch = write.builder(Antichain::from_elem(0));
1744        batch.add(&"foo".to_owned(), &(), &0, &1).await.unwrap();
1745        let batch = batch.finish(Antichain::from_elem(1)).await.unwrap();
1746
1747        // Roundtrip through a transmittable batch.
1748        let batch = batch.into_transmittable_batch();
1749        let mut batch = write.batch_from_transmittable_batch(batch);
1750        batch
1751            .rewrite_ts(&Antichain::from_elem(2), Antichain::from_elem(3))
1752            .unwrap();
1753        write
1754            .expect_compare_and_append_batch(&mut [&mut batch], 0, 3)
1755            .await;
1756
1757        let (actual, _) = read.expect_listen(0).await.read_until(&3).await;
1758        let expected = vec![((("foo".to_owned()), ()), 2, 1)];
1759        assert_eq!(actual, expected);
1760    }
1761
1762    #[mz_ore::test(tokio::test)]
1763    #[cfg_attr(miri, ignore)] // unsupported operation: returning ready events from epoll_wait is not yet implemented
1764    async fn structured_lowers() {
1765        let cache = PersistClientCache::new_no_metrics();
1766        // Ensure structured data is calculated, and that we give some budget for a key lower.
1767        cache.cfg().set_config(&STRUCTURED_KEY_LOWER_LEN, 1024);
1768        // Otherwise fails: expected hollow part!
1769        cache.cfg().set_config(&INLINE_WRITES_SINGLE_MAX_BYTES, 0);
1770        cache.cfg().set_config(&INLINE_WRITES_TOTAL_MAX_BYTES, 0);
1771        let client = cache
1772            .open(PersistLocation::new_in_mem())
1773            .await
1774            .expect("client construction failed");
1775        let shard_id = ShardId::new();
1776        let (mut write, _) = client
1777            .expect_open::<String, String, u64, i64>(shard_id)
1778            .await;
1779
1780        let batch = write
1781            .expect_batch(
1782                &[
1783                    (("1".into(), "one".into()), 1, 1),
1784                    (("2".into(), "two".into()), 2, 1),
1785                    (("3".into(), "three".into()), 3, 1),
1786                ],
1787                0,
1788                4,
1789            )
1790            .await;
1791
1792        assert_eq!(batch.batch.part_count(), 1);
1793        let [part] = batch.batch.parts.as_slice() else {
1794            panic!("expected single part")
1795        };
1796        // Verifies that the structured key lower is stored and decoded.
1797        assert!(part.structured_key_lower().is_some());
1798    }
1799}