1use 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 arrow::datatypes::DataType;
22use bytes::Bytes;
23use differential_dataflow::difference::Monoid;
24use differential_dataflow::lattice::Lattice;
25use differential_dataflow::trace::Description;
26use futures_util::stream::StreamExt;
27use futures_util::{FutureExt, stream};
28use mz_dyncfg::{Config, ParameterScope};
29use mz_ore::cast::CastFrom;
30use mz_ore::instrument;
31use mz_persist::indexed::encoding::{BatchColumnarFormat, BlobTraceBatchPart, BlobTraceUpdates};
32use mz_persist::location::Blob;
33use mz_persist_types::arrow::{ArrayBound, ArrayOrd};
34use mz_persist_types::columnar::{ColumnDecoder, Schema, data_type};
35use mz_persist_types::parquet::{CompressionFormat, EncodingConfig};
36use mz_persist_types::part::{Part, PartBuilder};
37use mz_persist_types::schema::SchemaId;
38use mz_persist_types::stats::{
39 PartStats, TRUNCATE_LEN, TruncateBound, trim_to_budget, truncate_bytes,
40};
41use mz_persist_types::{Codec, Codec64};
42use mz_proto::RustType;
43use mz_timely_util::order::Reverse;
44use proptest_derive::Arbitrary;
45use semver::Version;
46use timely::PartialOrder;
47use timely::order::TotalOrder;
48use timely::progress::{Antichain, Timestamp};
49use tracing::{Instrument, debug_span, trace_span, warn};
50
51use crate::async_runtime::IsolatedRuntime;
52use crate::cfg::{BATCH_BUILDER_MAX_OUTSTANDING_PARTS, MiB};
53use crate::error::InvalidUsage;
54use crate::internal::compact::{CompactConfig, Compactor};
55use crate::internal::encoding::{
56 LazyInlineBatchPart, LazyPartStats, LazyProto, MetadataMap, Schemas,
57};
58use crate::internal::machine::retry_external;
59use crate::internal::merge::{MergeTree, Pending};
60use crate::internal::metrics::{BatchWriteMetrics, Metrics, RetryMetrics, ShardMetrics};
61use crate::internal::paths::{PartId, PartialBatchKey, WriterKey};
62use crate::internal::state::{
63 BatchPart, ENABLE_INCREMENTAL_COMPACTION, HollowBatch, HollowBatchPart, HollowRun,
64 HollowRunRef, ProtoInlineBatchPart, RunId, RunMeta, RunOrder, RunPart,
65};
66use crate::stats::{STATS_BUDGET_BYTES, STATS_COLLECTION_ENABLED, untrimmable_columns};
67use crate::{PersistConfig, ShardId};
68
69include!(concat!(env!("OUT_DIR"), "/mz_persist_client.batch.rs"));
70
71#[derive(Debug)]
77pub struct Batch<K, V, T, D> {
78 pub(crate) batch_delete_enabled: bool,
79 pub(crate) metrics: Arc<Metrics>,
80 pub(crate) shard_metrics: Arc<ShardMetrics>,
81
82 pub(crate) version: Version,
84
85 pub(crate) schemas: (Bytes, Bytes),
87
88 pub(crate) batch: HollowBatch<T>,
90
91 pub(crate) blob: Arc<dyn Blob>,
93
94 pub(crate) _phantom: PhantomData<fn() -> (K, V, T, D)>,
97}
98
99impl<K, V, T, D> Drop for Batch<K, V, T, D> {
100 fn drop(&mut self) {
101 if self.batch.part_count() > 0 {
102 warn!(
103 "un-consumed Batch, with {} parts and dangling blob keys: {:?}",
104 self.batch.part_count(),
105 self.batch
106 .parts
107 .iter()
108 .map(|x| x.printable_name())
109 .collect::<Vec<_>>(),
110 );
111 }
112 }
113}
114
115impl<K, V, T, D> Batch<K, V, T, D>
116where
117 K: Debug + Codec,
118 V: Debug + Codec,
119 T: Timestamp + Lattice + Codec64,
120 D: Monoid + Codec64,
121{
122 pub(crate) fn new(
123 batch_delete_enabled: bool,
124 metrics: Arc<Metrics>,
125 blob: Arc<dyn Blob>,
126 shard_metrics: Arc<ShardMetrics>,
127 version: Version,
128 schemas: (Bytes, Bytes),
129 batch: HollowBatch<T>,
130 ) -> Self {
131 Self {
132 batch_delete_enabled,
133 metrics,
134 shard_metrics,
135 version,
136 schemas,
137 batch,
138 blob,
139 _phantom: PhantomData,
140 }
141 }
142
143 pub fn shard_id(&self) -> ShardId {
145 self.shard_metrics.shard_id
146 }
147
148 pub fn upper(&self) -> &Antichain<T> {
150 self.batch.desc.upper()
151 }
152
153 pub fn lower(&self) -> &Antichain<T> {
155 self.batch.desc.lower()
156 }
157
158 pub(crate) fn mark_consumed(&mut self) {
164 self.batch.parts.clear();
165 }
166
167 #[instrument(level = "debug", fields(shard = %self.shard_id()))]
170 pub async fn delete(mut self) {
171 if !self.batch_delete_enabled {
172 self.mark_consumed();
173 return;
174 }
175 let mut deletes = PartDeletes::default();
176 for part in self.batch.parts.drain(..) {
177 deletes.add(&part);
178 }
179 let () = deletes
180 .delete(
181 &*self.blob,
182 self.shard_id(),
183 usize::MAX,
184 &*self.metrics,
185 &*self.metrics.retries.external.batch_delete,
186 )
187 .await;
188 }
189
190 pub fn schemas(&self) -> impl Iterator<Item = SchemaId> + '_ {
192 self.batch.parts.iter().flat_map(|b| b.schema_id())
193 }
194
195 pub fn into_hollow_batch(mut self) -> HollowBatch<T> {
200 let ret = self.batch.clone();
201 self.mark_consumed();
202 ret
203 }
204
205 pub fn into_transmittable_batch(mut self) -> ProtoBatch {
214 let ret = ProtoBatch {
215 shard_id: self.shard_metrics.shard_id.into_proto(),
216 version: self.version.to_string(),
217 batch: Some(self.batch.into_proto()),
218 key_schema: self.schemas.0.clone(),
219 val_schema: self.schemas.1.clone(),
220 };
221 self.mark_consumed();
222 ret
223 }
224
225 pub(crate) async fn flush_to_blob(
226 &mut self,
227 cfg: &BatchBuilderConfig,
228 batch_metrics: &BatchWriteMetrics,
229 isolated_runtime: &Arc<IsolatedRuntime>,
230 write_schemas: &Schemas<K, V>,
231 ) {
232 let mut parts = Vec::new();
237 for (run_meta, run_parts) in self.batch.runs() {
238 for part in run_parts {
239 let (updates, ts_rewrite, schema_id) = match part {
240 RunPart::Single(BatchPart::Inline {
241 updates,
242 ts_rewrite,
243 schema_id,
244 deprecated_schema_id: _,
245 }) => (updates, ts_rewrite, schema_id),
246 other @ RunPart::Many(_) | other @ RunPart::Single(BatchPart::Hollow(_)) => {
247 parts.push(other.clone());
248 continue;
249 }
250 };
251 let updates = updates
252 .decode::<T>(&self.metrics.columnar)
253 .expect("valid inline part");
254 let diffs_sum = diffs_sum::<D>(updates.updates.diffs());
255 let mut write_schemas = write_schemas.clone();
256 write_schemas.id = *schema_id;
257
258 let write_span =
259 debug_span!("batch::flush_to_blob", shard = %self.shard_metrics.shard_id)
260 .or_current();
261 let handle = mz_ore::task::spawn(
262 || "batch::flush_to_blob",
263 BatchParts::write_hollow_part(
264 cfg.clone(),
265 Arc::clone(&self.blob),
266 Arc::clone(&self.metrics),
267 Arc::clone(&self.shard_metrics),
268 batch_metrics.clone(),
269 Arc::clone(isolated_runtime),
270 updates,
271 run_meta.order.unwrap_or(RunOrder::Unordered),
272 ts_rewrite.clone(),
273 D::encode(&diffs_sum),
274 write_schemas,
275 )
276 .instrument(write_span),
277 );
278 let part = handle.await;
279 parts.push(RunPart::Single(part));
280 }
281 }
282 self.batch.parts = parts;
283 }
284
285 pub fn encoded_size_bytes(&self) -> usize {
287 self.batch.encoded_size_bytes()
288 }
289}
290
291impl<K, V, T, D> Batch<K, V, T, D>
292where
293 K: Debug + Codec,
294 V: Debug + Codec,
295 T: Timestamp + Lattice + Codec64 + TotalOrder,
296 D: Monoid + Codec64,
297{
298 pub fn rewrite_ts(
332 &mut self,
333 frontier: &Antichain<T>,
334 new_upper: Antichain<T>,
335 ) -> Result<(), InvalidUsage<T>> {
336 self.batch
337 .rewrite_ts(frontier, new_upper)
338 .map_err(InvalidUsage::InvalidRewrite)
339 }
340}
341
342#[derive(Debug)]
344pub enum Added {
345 Record,
347 RecordAndParts,
350}
351
352#[derive(Debug, Clone)]
355pub struct BatchBuilderConfig {
356 writer_key: WriterKey,
357 pub(crate) blob_target_size: usize,
358 pub(crate) batch_delete_enabled: bool,
359 pub(crate) batch_builder_max_outstanding_parts: usize,
360 pub(crate) inline_writes_single_max_bytes: usize,
361 pub(crate) stats_collection_enabled: bool,
362 pub(crate) stats_budget: usize,
363 pub(crate) stats_untrimmable_columns: Arc<UntrimmableColumns>,
364 pub(crate) encoding_config: EncodingConfig,
365 pub(crate) preferred_order: RunOrder,
366 pub(crate) structured_key_lower_len: usize,
367 pub(crate) run_length_limit: usize,
368 pub(crate) enable_incremental_compaction: bool,
369 pub(crate) max_runs: Option<usize>,
373}
374
375pub(crate) const BATCH_DELETE_ENABLED: Config<bool> = Config::new(
377 "persist_batch_delete_enabled",
378 true,
379 "Whether to actually delete blobs when batch delete is called (Materialize).",
380 ParameterScope::Environment,
381);
382
383pub(crate) const ENCODING_ENABLE_DICTIONARY: Config<bool> = Config::new(
384 "persist_encoding_enable_dictionary",
385 true,
386 "A feature flag to enable dictionary encoding for Parquet data (Materialize).",
387 ParameterScope::Environment,
388);
389
390pub(crate) const ENCODING_COMPRESSION_FORMAT: Config<&'static str> = Config::new(
391 "persist_encoding_compression_format",
392 "none",
393 "A feature flag to enable compression of Parquet data (Materialize).",
394 ParameterScope::Environment,
395);
396
397pub(crate) const STRUCTURED_KEY_LOWER_LEN: Config<usize> = Config::new(
398 "persist_batch_structured_key_lower_len",
399 256,
400 "The maximum size in proto bytes of any structured key-lower metadata to preserve. \
401 (If we're unable to fit the lower in budget, or the budget is zero, no metadata is kept.)",
402 ParameterScope::Environment,
403);
404
405pub(crate) const MAX_RUN_LEN: Config<usize> = Config::new(
406 "persist_batch_max_run_len",
407 usize::MAX,
408 "The maximum length a run can have before it will be spilled as a hollow run \
409 into the blob store.",
410 ParameterScope::Environment,
411);
412
413pub(crate) const MAX_RUNS: Config<usize> = Config::new(
414 "persist_batch_max_runs",
415 1,
416 "The maximum number of runs a batch builder should generate for user batches. \
417 (Compaction outputs always generate a single run.) \
418 The minimum value is 2; below this, compaction is disabled.",
419 ParameterScope::Environment,
420);
421
422pub(crate) const BLOB_TARGET_SIZE: Config<usize> = Config::new(
429 "persist_blob_target_size",
430 128 * MiB,
431 "A target maximum size of persist blob payloads in bytes (Materialize).",
432 ParameterScope::Environment,
433);
434
435pub(crate) const INLINE_WRITES_SINGLE_MAX_BYTES: Config<usize> = Config::new(
436 "persist_inline_writes_single_max_bytes",
437 4096,
438 "The (exclusive) maximum size of a write that persist will inline in metadata.",
439 ParameterScope::Environment,
440);
441
442pub(crate) const INLINE_WRITES_TOTAL_MAX_BYTES: Config<usize> = Config::new(
443 "persist_inline_writes_total_max_bytes",
444 1 * MiB,
445 "\
446 The (exclusive) maximum total size of inline writes in metadata before \
447 persist will backpressure them by flushing out to s3.",
448 ParameterScope::Environment,
449);
450
451impl BatchBuilderConfig {
452 pub fn new(value: &PersistConfig, _shard_id: ShardId) -> Self {
454 let writer_key = WriterKey::for_version(&value.build_version);
455
456 let preferred_order = RunOrder::Structured;
457
458 BatchBuilderConfig {
459 writer_key,
460 blob_target_size: BLOB_TARGET_SIZE.get(value).clamp(1, usize::MAX),
461 batch_delete_enabled: BATCH_DELETE_ENABLED.get(value),
462 batch_builder_max_outstanding_parts: BATCH_BUILDER_MAX_OUTSTANDING_PARTS.get(value),
463 inline_writes_single_max_bytes: INLINE_WRITES_SINGLE_MAX_BYTES.get(value),
464 stats_collection_enabled: STATS_COLLECTION_ENABLED.get(value),
465 stats_budget: STATS_BUDGET_BYTES.get(value),
466 stats_untrimmable_columns: Arc::new(untrimmable_columns(value)),
467 encoding_config: EncodingConfig {
468 use_dictionary: ENCODING_ENABLE_DICTIONARY.get(value),
469 compression: CompressionFormat::from_str(&ENCODING_COMPRESSION_FORMAT.get(value)),
470 },
471 preferred_order,
472 structured_key_lower_len: STRUCTURED_KEY_LOWER_LEN.get(value),
473 run_length_limit: MAX_RUN_LEN.get(value).clamp(2, usize::MAX),
474 max_runs: match MAX_RUNS.get(value) {
475 limit @ 2.. => Some(limit),
476 _ => None,
477 },
478 enable_incremental_compaction: ENABLE_INCREMENTAL_COMPACTION.get(value),
479 }
480 }
481}
482
483#[derive(
486 Debug,
487 Clone,
488 PartialEq,
489 Eq,
490 serde::Serialize,
491 serde::Deserialize,
492 Arbitrary
493)]
494pub(crate) struct UntrimmableColumns {
495 pub equals: Vec<Cow<'static, str>>,
497 pub prefixes: Vec<Cow<'static, str>>,
499 pub suffixes: Vec<Cow<'static, str>>,
501}
502
503impl UntrimmableColumns {
504 pub(crate) fn should_retain(&self, name: &str) -> bool {
505 let name_lower = name.to_lowercase();
508 for s in &self.equals {
509 if *s == name_lower {
510 return true;
511 }
512 }
513 for s in &self.prefixes {
514 if name_lower.starts_with(s.as_ref()) {
515 return true;
516 }
517 }
518 for s in &self.suffixes {
519 if name_lower.ends_with(s.as_ref()) {
520 return true;
521 }
522 }
523 false
524 }
525}
526
527#[derive(Debug)]
530pub struct BatchBuilder<K, V, T, D>
531where
532 K: Codec,
533 V: Codec,
534 T: Timestamp + Lattice + Codec64,
535{
536 inline_desc: Description<T>,
537 inclusive_upper: Antichain<Reverse<T>>,
538
539 part_builder: PartBuilder<K, K::Schema, V, V::Schema>,
540 finished_parts: Vec<Part>,
541 finished_parts_goodbytes: usize,
548 part_data_types: Option<(DataType, DataType)>,
550 pub(crate) builder: BatchBuilderInternal<K, V, T, D>,
551}
552
553impl<K, V, T, D> BatchBuilder<K, V, T, D>
554where
555 K: Debug + Codec,
556 V: Debug + Codec,
557 T: Timestamp + Lattice + Codec64,
558 D: Monoid + Codec64,
559{
560 pub(crate) fn new(
561 builder: BatchBuilderInternal<K, V, T, D>,
562 inline_desc: Description<T>,
563 ) -> Self {
564 let records_builder = PartBuilder::new(
565 builder.write_schemas.key.as_ref(),
566 builder.write_schemas.val.as_ref(),
567 );
568 Self {
569 inline_desc,
570 inclusive_upper: Antichain::new(),
571 part_builder: records_builder,
572 finished_parts: vec![],
573 finished_parts_goodbytes: 0,
574 part_data_types: None,
575 builder,
576 }
577 }
578
579 fn in_progress_goodbytes(&self) -> usize {
580 self.part_builder.goodbytes() + self.finished_parts_goodbytes
581 }
582
583 fn push_finished_part(&mut self, part: Part) {
584 self.finished_parts_goodbytes += part.goodbytes();
585 self.finished_parts.push(part);
586 }
587
588 fn finish_part_builder(&mut self) {
589 if self.part_builder.len() > 0 {
590 let part = self.part_builder.finish_and_replace(
591 &*self.builder.write_schemas.key,
592 &*self.builder.write_schemas.val,
593 );
594 self.push_finished_part(part);
595 }
596 }
597
598 async fn flush_parts(&mut self) -> bool {
599 self.finish_part_builder();
600 if let Some(part) = Part::concat(&self.finished_parts).expect("type aligned") {
601 self.builder
602 .flush_part(self.inline_desc.clone(), part)
603 .await;
604 self.finished_parts.clear();
605 self.finished_parts_goodbytes = 0;
606 true
607 } else {
608 false
609 }
610 }
611
612 pub async fn finish(
617 mut self,
618 registered_upper: Antichain<T>,
619 ) -> Result<Batch<K, V, T, D>, InvalidUsage<T>> {
620 if PartialOrder::less_than(®istered_upper, self.inline_desc.lower()) {
621 return Err(InvalidUsage::InvalidBounds {
622 lower: self.inline_desc.lower().clone(),
623 upper: registered_upper,
624 });
625 }
626
627 if PartialOrder::less_equal(self.inline_desc.since(), self.inline_desc.lower()) {
632 for ts in self.inclusive_upper.iter() {
633 if registered_upper.less_equal(&ts.0) {
634 return Err(InvalidUsage::UpdateBeyondUpper {
635 ts: ts.0.clone(),
636 expected_upper: registered_upper.clone(),
637 });
638 }
639 }
640 }
641
642 self.flush_parts().await;
643
644 self.builder
645 .finish(Description::new(
646 self.inline_desc.lower().clone(),
647 registered_upper,
648 self.inline_desc.since().clone(),
649 ))
650 .await
651 }
652
653 pub async fn add_part(&mut self, part: Part) -> Result<Added, InvalidUsage<T>> {
664 for time in part.time.values() {
665 let ts = T::decode(time.to_le_bytes());
666 if !self.inline_desc.lower().less_equal(&ts) {
667 return Err(InvalidUsage::UpdateNotBeyondLower {
668 ts,
669 lower: self.inline_desc.lower().clone(),
670 });
671 }
672 self.inclusive_upper.insert(Reverse(ts));
673 }
674
675 let schemas = &self.builder.write_schemas;
676 let (key_type, val_type) = self.part_data_types.get_or_insert_with(|| {
677 (
678 data_type::<K>(&schemas.key).expect("valid type"),
679 data_type::<V>(&schemas.val).expect("valid type"),
680 )
681 });
682 assert_eq!(*key_type, *part.key.data_type());
683 assert_eq!(*val_type, *part.val.data_type());
684
685 self.finish_part_builder();
686
687 let mut flushed = false;
688 if self.in_progress_goodbytes() + part.goodbytes() > self.builder.parts.cfg.blob_target_size
689 {
690 flushed |= self.flush_parts().await;
691 }
692 self.push_finished_part(part);
693 if self.in_progress_goodbytes() > self.builder.parts.cfg.blob_target_size {
694 flushed |= self.flush_parts().await;
695 }
696
697 let added = if flushed {
698 Added::RecordAndParts
699 } else {
700 Added::Record
701 };
702 Ok(added)
703 }
704
705 pub async fn add(
710 &mut self,
711 key: &K,
712 val: &V,
713 ts: &T,
714 diff: &D,
715 ) -> Result<Added, InvalidUsage<T>> {
716 if !self.inline_desc.lower().less_equal(ts) {
717 return Err(InvalidUsage::UpdateNotBeyondLower {
718 ts: ts.clone(),
719 lower: self.inline_desc.lower().clone(),
720 });
721 }
722 self.inclusive_upper.insert(Reverse(ts.clone()));
723
724 let mut flushed = false;
725 self.part_builder.push(key, val, ts.clone(), diff.clone());
726 if self.in_progress_goodbytes() >= self.builder.parts.cfg.blob_target_size {
727 flushed |= self.flush_parts().await;
728 }
729
730 let added = if flushed {
731 Added::RecordAndParts
732 } else {
733 Added::Record
734 };
735 Ok(added)
736 }
737}
738
739#[derive(Debug)]
740pub(crate) struct BatchBuilderInternal<K, V, T, D>
741where
742 K: Codec,
743 V: Codec,
744 T: Timestamp + Lattice + Codec64,
745{
746 shard_id: ShardId,
747 version: Version,
748 blob: Arc<dyn Blob>,
749 metrics: Arc<Metrics>,
750
751 write_schemas: Schemas<K, V>,
752 parts: BatchParts<T>,
753
754 _phantom: PhantomData<fn(K, V, T, D)>,
757}
758
759impl<K, V, T, D> BatchBuilderInternal<K, V, T, D>
760where
761 K: Debug + Codec,
762 V: Debug + Codec,
763 T: Timestamp + Lattice + Codec64,
764 D: Monoid + Codec64,
765{
766 pub(crate) fn new(
767 _cfg: BatchBuilderConfig,
768 parts: BatchParts<T>,
769 metrics: Arc<Metrics>,
770 write_schemas: Schemas<K, V>,
771 blob: Arc<dyn Blob>,
772 shard_id: ShardId,
773 version: Version,
774 ) -> Self {
775 Self {
776 blob,
777 metrics,
778 write_schemas,
779 parts,
780 shard_id,
781 version,
782 _phantom: PhantomData,
783 }
784 }
785
786 #[instrument(level = "debug", name = "batch::finish", fields(shard = %self.shard_id))]
791 pub async fn finish(
792 self,
793 registered_desc: Description<T>,
794 ) -> Result<Batch<K, V, T, D>, InvalidUsage<T>> {
795 let write_run_ids = self.parts.cfg.enable_incremental_compaction;
796 let batch_delete_enabled = self.parts.cfg.batch_delete_enabled;
797 let shard_metrics = Arc::clone(&self.parts.shard_metrics);
798 let runs = self.parts.finish().await;
799
800 let mut run_parts = vec![];
801 let mut run_splits = vec![];
802 let mut run_meta = vec![];
803 let total_updates = runs
804 .iter()
805 .map(|(_, _, num_updates)| num_updates)
806 .sum::<usize>();
807 for (order, parts, num_updates) in runs {
808 if parts.is_empty() {
809 continue;
810 }
811 if run_parts.len() != 0 {
812 run_splits.push(run_parts.len());
813 }
814 run_meta.push(RunMeta {
815 order: Some(order),
816 schema: self.write_schemas.id,
817 deprecated_schema: None,
819 id: if write_run_ids {
820 Some(RunId::new())
821 } else {
822 None
823 },
824 len: if write_run_ids {
825 Some(num_updates)
826 } else {
827 None
828 },
829 meta: MetadataMap::default(),
830 });
831 run_parts.extend(parts);
832 }
833 let desc = registered_desc;
834
835 let batch = Batch::new(
836 batch_delete_enabled,
837 Arc::clone(&self.metrics),
838 self.blob,
839 shard_metrics,
840 self.version,
841 (
842 K::encode_schema(&*self.write_schemas.key),
843 V::encode_schema(&*self.write_schemas.val),
844 ),
845 HollowBatch::new(desc, run_parts, total_updates, run_meta, run_splits),
846 );
847
848 Ok(batch)
849 }
850
851 pub async fn flush_part(&mut self, part_desc: Description<T>, columnar: Part) {
856 let num_updates = columnar.len();
857 if num_updates == 0 {
858 return;
859 }
860 let diffs_sum = diffs_sum::<D>(&columnar.diff);
861
862 let start = Instant::now();
863 self.parts
864 .write(&self.write_schemas, part_desc, columnar, diffs_sum)
865 .await;
866 self.metrics
867 .compaction
868 .batch
869 .step_part_writing
870 .inc_by(start.elapsed().as_secs_f64());
871 }
872}
873
874#[derive(Debug, Clone)]
875pub(crate) struct RunWithMeta<T> {
876 pub parts: Vec<RunPart<T>>,
877 pub num_updates: usize,
878}
879
880impl<T> RunWithMeta<T> {
881 pub fn new(parts: Vec<RunPart<T>>, num_updates: usize) -> Self {
882 Self { parts, num_updates }
883 }
884
885 pub fn single(part: RunPart<T>, num_updates: usize) -> Self {
886 Self {
887 parts: vec![part],
888 num_updates,
889 }
890 }
891}
892
893#[derive(Debug)]
894enum WritingRuns<T> {
895 Ordered(RunOrder, MergeTree<Pending<RunWithMeta<T>>>),
899 Compacting(MergeTree<(RunOrder, Pending<RunWithMeta<T>>)>),
902}
903
904#[derive(Debug)]
907pub(crate) struct BatchParts<T> {
908 cfg: BatchBuilderConfig,
909 metrics: Arc<Metrics>,
910 shard_metrics: Arc<ShardMetrics>,
911 shard_id: ShardId,
912 blob: Arc<dyn Blob>,
913 isolated_runtime: Arc<IsolatedRuntime>,
914 next_index: u64,
915 writing_runs: WritingRuns<T>,
916 batch_metrics: BatchWriteMetrics,
917}
918
919impl<T: Timestamp + Codec64> BatchParts<T> {
920 pub(crate) fn new_compacting<K, V, D>(
921 cfg: CompactConfig,
922 desc: Description<T>,
923 runs_per_compaction: usize,
924 metrics: Arc<Metrics>,
925 shard_metrics: Arc<ShardMetrics>,
926 shard_id: ShardId,
927 blob: Arc<dyn Blob>,
928 isolated_runtime: Arc<IsolatedRuntime>,
929 batch_metrics: &BatchWriteMetrics,
930 schemas: Schemas<K, V>,
931 ) -> Self
932 where
933 K: Codec + Debug,
934 V: Codec + Debug,
935 T: Lattice + Send + Sync,
936 D: Monoid + Ord + Codec64 + Send + Sync,
937 {
938 let writing_runs = {
939 let cfg = cfg.clone();
940 let blob = Arc::clone(&blob);
941 let metrics = Arc::clone(&metrics);
942 let shard_metrics = Arc::clone(&shard_metrics);
943 let isolated_runtime = Arc::clone(&isolated_runtime);
944 let runs_per_compaction = runs_per_compaction.clamp(2, 1024);
946
947 let merge_fn = move |parts: Vec<(RunOrder, Pending<RunWithMeta<T>>)>| {
948 let blob = Arc::clone(&blob);
949 let metrics = Arc::clone(&metrics);
950 let shard_metrics = Arc::clone(&shard_metrics);
951 let cfg = cfg.clone();
952 let isolated_runtime = Arc::clone(&isolated_runtime);
953 let write_schemas = schemas.clone();
954 let compact_desc = desc.clone();
955 let handle = mz_ore::task::spawn(
956 || "batch::compact_runs",
957 async move {
958 let runs: Vec<_> = stream::iter(parts)
959 .then(|(order, parts)| async move {
960 let completed_run = parts.into_result().await;
961 (
962 RunMeta {
963 order: Some(order),
964 schema: schemas.id,
965 deprecated_schema: None,
968 id: if cfg.batch.enable_incremental_compaction {
969 Some(RunId::new())
970 } else {
971 None
972 },
973 len: if cfg.batch.enable_incremental_compaction {
974 Some(completed_run.num_updates)
975 } else {
976 None
977 },
978 meta: MetadataMap::default(),
979 },
980 completed_run.parts,
981 )
982 })
983 .collect()
984 .await;
985
986 let run_refs: Vec<_> = runs
987 .iter()
988 .map(|(meta, run)| (&compact_desc, meta, run.as_slice()))
989 .collect();
990
991 let output_batch = Compactor::<K, V, T, D>::compact_runs(
992 &cfg,
993 &shard_id,
994 &compact_desc,
995 run_refs,
996 blob,
997 metrics,
998 shard_metrics,
999 isolated_runtime,
1000 write_schemas,
1001 )
1002 .await
1003 .expect("successful compaction");
1004
1005 assert_eq!(
1006 output_batch.run_meta.len(),
1007 1,
1008 "compaction is guaranteed to emit a single run"
1009 );
1010 let total_compacted_updates: usize = output_batch.len;
1011
1012 RunWithMeta::new(output_batch.parts, total_compacted_updates)
1013 }
1014 .instrument(debug_span!("batch::compact_runs")),
1015 );
1016 (RunOrder::Structured, Pending::new(handle))
1017 };
1018 WritingRuns::Compacting(MergeTree::new(runs_per_compaction, merge_fn))
1019 };
1020 BatchParts {
1021 cfg: cfg.batch,
1022 metrics,
1023 shard_metrics,
1024 shard_id,
1025 blob,
1026 isolated_runtime,
1027 next_index: 0,
1028 writing_runs,
1029 batch_metrics: batch_metrics.clone(),
1030 }
1031 }
1032
1033 pub(crate) fn new_ordered<D: Monoid + Codec64>(
1034 cfg: BatchBuilderConfig,
1035 order: RunOrder,
1036 metrics: Arc<Metrics>,
1037 shard_metrics: Arc<ShardMetrics>,
1038 shard_id: ShardId,
1039 blob: Arc<dyn Blob>,
1040 isolated_runtime: Arc<IsolatedRuntime>,
1041 batch_metrics: &BatchWriteMetrics,
1042 ) -> Self {
1043 let writing_runs = {
1044 let cfg = cfg.clone();
1045 let blob = Arc::clone(&blob);
1046 let metrics = Arc::clone(&metrics);
1047 let writer_key = cfg.writer_key.clone();
1048 let run_length_limit = (order == RunOrder::Unordered)
1051 .then_some(usize::MAX)
1052 .unwrap_or(cfg.run_length_limit);
1053 let merge_fn = move |parts: Vec<Pending<RunWithMeta<T>>>| {
1054 let blob = Arc::clone(&blob);
1055 let writer_key = writer_key.clone();
1056 let metrics = Arc::clone(&metrics);
1057 let handle = mz_ore::task::spawn(
1058 || "batch::spill_run",
1059 async move {
1060 let completed_runs: Vec<RunWithMeta<T>> = stream::iter(parts)
1061 .then(|p| p.into_result())
1062 .collect()
1063 .await;
1064
1065 let mut all_run_parts = Vec::new();
1066 let mut total_updates = 0;
1067
1068 for completed_run in completed_runs {
1069 all_run_parts.extend(completed_run.parts);
1070 total_updates += completed_run.num_updates;
1071 }
1072
1073 let run_ref = HollowRunRef::set::<D>(
1074 shard_id,
1075 blob.as_ref(),
1076 &writer_key,
1077 HollowRun {
1078 parts: all_run_parts,
1079 },
1080 &*metrics,
1081 )
1082 .await;
1083
1084 RunWithMeta::single(RunPart::Many(run_ref), total_updates)
1085 }
1086 .instrument(debug_span!("batch::spill_run")),
1087 );
1088 Pending::new(handle)
1089 };
1090 WritingRuns::Ordered(order, MergeTree::new(run_length_limit, merge_fn))
1091 };
1092 BatchParts {
1093 cfg,
1094 metrics,
1095 shard_metrics,
1096 shard_id,
1097 blob,
1098 isolated_runtime,
1099 next_index: 0,
1100 writing_runs,
1101 batch_metrics: batch_metrics.clone(),
1102 }
1103 }
1104
1105 pub(crate) fn expected_order(&self) -> RunOrder {
1106 match self.writing_runs {
1107 WritingRuns::Ordered(order, _) => order,
1108 WritingRuns::Compacting(_) => RunOrder::Unordered,
1109 }
1110 }
1111
1112 pub(crate) async fn write<K: Codec, V: Codec, D: Codec64>(
1113 &mut self,
1114 write_schemas: &Schemas<K, V>,
1115 desc: Description<T>,
1116 updates: Part,
1117 diffs_sum: D,
1118 ) {
1119 let batch_metrics = self.batch_metrics.clone();
1120 let index = self.next_index;
1121 self.next_index += 1;
1122 let num_updates = updates.len();
1123 let ts_rewrite = None;
1124 let schema_id = write_schemas.id;
1125
1126 let inline_threshold = self.cfg.inline_writes_single_max_bytes;
1129
1130 let updates = BlobTraceUpdates::from_part(updates);
1131 let (name, write_future) = if updates.goodbytes() < inline_threshold {
1132 let span = debug_span!("batch::inline_part", shard = %self.shard_id).or_current();
1133 (
1134 "batch::inline_part",
1135 async move {
1136 let start = Instant::now();
1137 let updates = LazyInlineBatchPart::from(&ProtoInlineBatchPart {
1138 desc: Some(desc.into_proto()),
1139 index: index.into_proto(),
1140 updates: Some(updates.into_proto()),
1141 });
1142 batch_metrics
1143 .step_inline
1144 .inc_by(start.elapsed().as_secs_f64());
1145
1146 RunWithMeta::single(
1147 RunPart::Single(BatchPart::Inline {
1148 updates,
1149 ts_rewrite,
1150 schema_id,
1151 deprecated_schema_id: None,
1153 }),
1154 num_updates,
1155 )
1156 }
1157 .instrument(span)
1158 .boxed(),
1159 )
1160 } else {
1161 let part = BlobTraceBatchPart {
1162 desc,
1163 updates,
1164 index,
1165 };
1166 let cfg = self.cfg.clone();
1167 let blob = Arc::clone(&self.blob);
1168 let metrics = Arc::clone(&self.metrics);
1169 let shard_metrics = Arc::clone(&self.shard_metrics);
1170 let isolated_runtime = Arc::clone(&self.isolated_runtime);
1171 let expected_order = self.expected_order();
1172 let encoded_diffs_sum = D::encode(&diffs_sum);
1173 let write_schemas_clone = write_schemas.clone();
1174 let write_span =
1175 debug_span!("batch::write_part", shard = %self.shard_metrics.shard_id).or_current();
1176 (
1177 "batch::write_part",
1178 async move {
1179 let part = BatchParts::write_hollow_part(
1180 cfg,
1181 blob,
1182 metrics,
1183 shard_metrics,
1184 batch_metrics,
1185 isolated_runtime,
1186 part,
1187 expected_order,
1188 ts_rewrite,
1189 encoded_diffs_sum,
1190 write_schemas_clone,
1191 )
1192 .await;
1193 RunWithMeta::single(RunPart::Single(part), num_updates)
1194 }
1195 .instrument(write_span)
1196 .boxed(),
1197 )
1198 };
1199
1200 match &mut self.writing_runs {
1201 WritingRuns::Ordered(_order, run) => {
1202 let part = Pending::new(mz_ore::task::spawn(|| name, write_future));
1203 run.push(part);
1204
1205 for part in run
1208 .iter_mut()
1209 .rev()
1210 .skip(self.cfg.batch_builder_max_outstanding_parts)
1211 .take_while(|p| !p.is_finished())
1212 {
1213 self.batch_metrics.write_stalls.inc();
1214 part.block_until_ready().await;
1215 }
1216 }
1217 WritingRuns::Compacting(batches) => {
1218 let run = Pending::Writing(mz_ore::task::spawn(|| name, write_future));
1219 batches.push((RunOrder::Unordered, run));
1220
1221 let mut part_budget = self.cfg.batch_builder_max_outstanding_parts;
1224 let mut compaction_budget = 1;
1225 for (_, part) in batches
1226 .iter_mut()
1227 .rev()
1228 .skip_while(|(order, _)| match order {
1229 RunOrder::Unordered if part_budget > 0 => {
1230 part_budget -= 1;
1231 true
1232 }
1233 RunOrder::Structured | RunOrder::Codec if compaction_budget > 0 => {
1234 compaction_budget -= 1;
1235 true
1236 }
1237 _ => false,
1238 })
1239 .take_while(|(_, p)| !p.is_finished())
1240 {
1241 self.batch_metrics.write_stalls.inc();
1242 part.block_until_ready().await;
1243 }
1244 }
1245 }
1246 }
1247
1248 async fn write_hollow_part<K: Codec, V: Codec>(
1249 cfg: BatchBuilderConfig,
1250 blob: Arc<dyn Blob>,
1251 metrics: Arc<Metrics>,
1252 shard_metrics: Arc<ShardMetrics>,
1253 batch_metrics: BatchWriteMetrics,
1254 isolated_runtime: Arc<IsolatedRuntime>,
1255 mut updates: BlobTraceBatchPart<T>,
1256 run_order: RunOrder,
1257 ts_rewrite: Option<Antichain<T>>,
1258 diffs_sum: [u8; 8],
1259 write_schemas: Schemas<K, V>,
1260 ) -> BatchPart<T> {
1261 let partial_key = PartialBatchKey::new(&cfg.writer_key, &PartId::new());
1262 let key = partial_key.complete(&shard_metrics.shard_id);
1263 let goodbytes = updates.updates.goodbytes();
1264 let metrics_ = Arc::clone(&metrics);
1265 let schema_id = write_schemas.id;
1266
1267 let (stats, key_lower, structured_key_lower, (buf, encode_time)) = isolated_runtime
1268 .spawn_named(|| "batch::encode_part", async move {
1269 let stats = metrics_.columnar.arrow().measure_part_build(|| {
1271 let stats = if cfg.stats_collection_enabled {
1272 let ext = updates.updates.get_or_make_structured::<K, V>(
1273 write_schemas.key.as_ref(),
1274 write_schemas.val.as_ref(),
1275 );
1276
1277 let key_stats = write_schemas
1278 .key
1279 .decoder_any(ext.key.as_ref())
1280 .expect("decoding just-encoded data")
1281 .stats();
1282
1283 let part_stats = PartStats { key: key_stats };
1284
1285 let trimmed_start = Instant::now();
1287 let mut trimmed_bytes = 0;
1288 let trimmed_stats = LazyPartStats::encode(&part_stats, |s| {
1289 trimmed_bytes = trim_to_budget(s, cfg.stats_budget, |s| {
1290 cfg.stats_untrimmable_columns.should_retain(s)
1291 })
1292 });
1293 let trimmed_duration = trimmed_start.elapsed();
1294 Some((trimmed_stats, trimmed_duration, trimmed_bytes))
1295 } else {
1296 None
1297 };
1298
1299 updates.updates = updates.updates.as_structured::<K, V>(
1301 write_schemas.key.as_ref(),
1302 write_schemas.val.as_ref(),
1303 );
1304
1305 stats
1306 });
1307
1308 let key_lower = if let Some(records) = updates.updates.records() {
1309 let key_bytes = records.keys();
1310 if key_bytes.is_empty() {
1311 &[]
1312 } else if run_order == RunOrder::Codec {
1313 key_bytes.value(0)
1314 } else {
1315 ::arrow::compute::min_binary(key_bytes).expect("min of nonempty array")
1316 }
1317 } else {
1318 &[]
1319 };
1320 let key_lower = truncate_bytes(key_lower, TRUNCATE_LEN, TruncateBound::Lower)
1321 .expect("lower bound always exists");
1322
1323 let structured_key_lower = if cfg.structured_key_lower_len > 0 {
1324 updates.updates.structured().and_then(|ext| {
1325 let min_key = if run_order == RunOrder::Structured {
1326 0
1327 } else {
1328 let ord = ArrayOrd::new(ext.key.as_ref());
1329 (0..ext.key.len())
1330 .min_by_key(|i| ord.at(*i))
1331 .expect("non-empty batch")
1332 };
1333 let lower = ArrayBound::new(Arc::clone(&ext.key), min_key)
1334 .to_proto_lower(cfg.structured_key_lower_len);
1335 if lower.is_none() {
1336 batch_metrics.key_lower_too_big.inc()
1337 }
1338 lower.map(|proto| LazyProto::from(&proto))
1339 })
1340 } else {
1341 None
1342 };
1343
1344 let encode_start = Instant::now();
1345 let mut buf = Vec::new();
1346 updates.encode(&mut buf, &metrics_.columnar, &cfg.encoding_config);
1347
1348 drop(updates);
1350 (
1351 stats,
1352 key_lower,
1353 structured_key_lower,
1354 (Bytes::from(buf), encode_start.elapsed()),
1355 )
1356 })
1357 .instrument(debug_span!("batch::encode_part"))
1358 .await;
1359 metrics.codecs.batch.encode_count.inc();
1361 metrics
1362 .codecs
1363 .batch
1364 .encode_seconds
1365 .inc_by(encode_time.as_secs_f64());
1366
1367 let start = Instant::now();
1368 let payload_len = buf.len();
1369 let () = retry_external(&metrics.retries.external.batch_set, || async {
1370 shard_metrics.blob_sets.inc();
1371 blob.set(&key, Bytes::clone(&buf)).await
1372 })
1373 .instrument(trace_span!("batch::set", payload_len))
1374 .await;
1375 batch_metrics.seconds.inc_by(start.elapsed().as_secs_f64());
1376 batch_metrics.bytes.inc_by(u64::cast_from(payload_len));
1377 batch_metrics.goodbytes.inc_by(u64::cast_from(goodbytes));
1378 match run_order {
1379 RunOrder::Unordered => batch_metrics.unordered.inc(),
1380 RunOrder::Codec => batch_metrics.codec_order.inc(),
1381 RunOrder::Structured => batch_metrics.structured_order.inc(),
1382 }
1383 let stats = stats.map(|(stats, stats_step_timing, trimmed_bytes)| {
1384 batch_metrics
1385 .step_stats
1386 .inc_by(stats_step_timing.as_secs_f64());
1387 if trimmed_bytes > 0 {
1388 metrics.pushdown.parts_stats_trimmed_count.inc();
1389 metrics
1390 .pushdown
1391 .parts_stats_trimmed_bytes
1392 .inc_by(u64::cast_from(trimmed_bytes));
1393 }
1394 stats
1395 });
1396
1397 let meta = MetadataMap::default();
1398 BatchPart::Hollow(HollowBatchPart {
1399 key: partial_key,
1400 meta,
1401 encoded_size_bytes: payload_len,
1402 key_lower,
1403 structured_key_lower,
1404 stats,
1405 ts_rewrite,
1406 diffs_sum: Some(diffs_sum),
1407 format: Some(BatchColumnarFormat::Structured),
1408 schema_id,
1409 deprecated_schema_id: None,
1411 })
1412 }
1413
1414 #[instrument(level = "debug", name = "batch::finish_upload", fields(shard = %self.shard_id))]
1415 pub(crate) async fn finish(self) -> Vec<(RunOrder, Vec<RunPart<T>>, usize)> {
1416 match self.writing_runs {
1417 WritingRuns::Ordered(RunOrder::Unordered, run) => {
1418 let completed_runs = run.finish();
1419 let mut output = Vec::with_capacity(completed_runs.len());
1420 for completed_run in completed_runs {
1421 let completed_run = completed_run.into_result().await;
1422 for part in completed_run.parts {
1424 output.push((RunOrder::Unordered, vec![part], completed_run.num_updates));
1425 }
1426 }
1427 output
1428 }
1429 WritingRuns::Ordered(order, run) => {
1430 let completed_runs = run.finish();
1431 let mut all_parts = Vec::new();
1432 let mut all_update_counts = 0;
1433 for completed_run in completed_runs {
1434 let completed_run = completed_run.into_result().await;
1435 all_parts.extend(completed_run.parts);
1436 all_update_counts += completed_run.num_updates;
1437 }
1438 vec![(order, all_parts, all_update_counts)]
1439 }
1440 WritingRuns::Compacting(batches) => {
1441 let runs = batches.finish();
1442 let mut output = Vec::new();
1443 for (order, run) in runs {
1444 let completed_run = run.into_result().await;
1445 output.push((order, completed_run.parts, completed_run.num_updates));
1446 }
1447 output
1448 }
1449 }
1450 }
1451}
1452
1453pub(crate) fn validate_truncate_batch<T: Timestamp>(
1462 batch: &HollowBatch<T>,
1463 truncate: &Description<T>,
1464 any_batch_rewrite: bool,
1465 validate_part_bounds_on_write: bool,
1466) -> Result<bool, InvalidUsage<T>> {
1467 if any_batch_rewrite {
1470 if truncate.upper() != batch.desc.upper() {
1475 return Err(InvalidUsage::InvalidRewrite(format!(
1476 "rewritten batch might have data past {:?} up to {:?}",
1477 truncate.upper().elements(),
1478 batch.desc.upper().elements(),
1479 )));
1480 }
1481 for part in batch.parts.iter() {
1484 let part_lower_bound = part.ts_rewrite().unwrap_or_else(|| batch.desc.lower());
1485 if !PartialOrder::less_equal(truncate.lower(), part_lower_bound) {
1486 return Err(InvalidUsage::InvalidRewrite(format!(
1487 "rewritten batch might have data below {:?} at {:?}",
1488 truncate.lower().elements(),
1489 part_lower_bound.elements(),
1490 )));
1491 }
1492 }
1493 }
1494
1495 let bounds_truncated = !any_batch_rewrite
1501 && (!PartialOrder::less_equal(truncate.lower(), batch.desc.lower())
1502 || !PartialOrder::less_equal(batch.desc.upper(), truncate.upper()));
1503
1504 if !validate_part_bounds_on_write {
1505 return Ok(bounds_truncated);
1506 }
1507
1508 let batch = &batch.desc;
1509 if !PartialOrder::less_equal(batch.lower(), truncate.lower())
1510 || PartialOrder::less_than(batch.upper(), truncate.upper())
1511 {
1512 return Err(InvalidUsage::InvalidBatchBounds {
1513 batch_lower: batch.lower().clone(),
1514 batch_upper: batch.upper().clone(),
1515 append_lower: truncate.lower().clone(),
1516 append_upper: truncate.upper().clone(),
1517 });
1518 }
1519
1520 Ok(bounds_truncated)
1521}
1522
1523#[derive(Debug)]
1524pub(crate) struct PartDeletes<T> {
1525 blob_keys: BTreeSet<PartialBatchKey>,
1527 hollow_runs: BTreeMap<PartialBatchKey, HollowRunRef<T>>,
1529}
1530
1531impl<T> Default for PartDeletes<T> {
1532 fn default() -> Self {
1533 Self {
1534 blob_keys: Default::default(),
1535 hollow_runs: Default::default(),
1536 }
1537 }
1538}
1539
1540impl<T: Timestamp> PartDeletes<T> {
1541 pub fn add(&mut self, part: &RunPart<T>) -> bool {
1544 match part {
1545 RunPart::Many(r) => self.hollow_runs.insert(r.key.clone(), r.clone()).is_none(),
1546 RunPart::Single(BatchPart::Hollow(x)) => self.blob_keys.insert(x.key.clone()),
1547 RunPart::Single(BatchPart::Inline { .. }) => {
1548 true
1550 }
1551 }
1552 }
1553
1554 pub fn contains(&self, part: &RunPart<T>) -> bool {
1555 match part {
1556 RunPart::Many(r) => self.hollow_runs.contains_key(&r.key),
1557 RunPart::Single(BatchPart::Hollow(x)) => self.blob_keys.contains(&x.key),
1558 RunPart::Single(BatchPart::Inline { .. }) => false,
1559 }
1560 }
1561
1562 pub fn is_empty(&self) -> bool {
1563 self.len() == 0
1564 }
1565
1566 pub fn len(&self) -> usize {
1567 match self {
1568 Self {
1569 blob_keys,
1570 hollow_runs,
1571 } => blob_keys.len() + hollow_runs.len(),
1572 }
1573 }
1574
1575 pub async fn delete(
1576 mut self,
1577 blob: &dyn Blob,
1578 shard_id: ShardId,
1579 concurrency: usize,
1580 metrics: &Metrics,
1581 delete_metrics: &RetryMetrics,
1582 ) where
1583 T: Codec64,
1584 {
1585 loop {
1586 let () = stream::iter(mem::take(&mut self.blob_keys))
1587 .map(|key| {
1588 let key = key.complete(&shard_id);
1589 async move {
1590 retry_external(delete_metrics, || blob.delete(&key)).await;
1591 }
1592 })
1593 .buffer_unordered(concurrency)
1594 .collect()
1595 .await;
1596
1597 let Some((run_key, run_ref)) = self.hollow_runs.pop_first() else {
1598 break;
1599 };
1600
1601 if let Some(run) = run_ref.get(shard_id, blob, metrics).await {
1602 for part in &run.parts {
1604 self.add(part);
1605 }
1606 self.blob_keys.insert(run_key);
1607 };
1608 }
1609 }
1610}
1611
1612fn diffs_sum<D: Monoid + Codec64>(updates: &Int64Array) -> D {
1614 let mut sum = D::zero();
1615 for d in updates.values().iter() {
1616 let d = D::decode(d.to_le_bytes());
1617 sum.plus_equals(&d);
1618 }
1619 sum
1620}
1621
1622#[cfg(test)]
1623mod tests {
1624 use mz_dyncfg::ConfigUpdates;
1625
1626 use super::*;
1627 use crate::PersistLocation;
1628 use crate::cache::PersistClientCache;
1629 use crate::cfg::BATCH_BUILDER_MAX_OUTSTANDING_PARTS;
1630 use crate::internal::paths::{BlobKey, PartialBlobKey};
1631 use crate::tests::{all_ok, new_test_client};
1632 use mz_persist_types::codec_impls::StringSchema;
1633
1634 #[mz_ore::test(tokio::test)]
1635 #[cfg_attr(miri, ignore)] async fn batch_builder_flushing() {
1637 let data = vec![
1638 (("1".to_owned(), "one".to_owned()), 1, 1),
1639 (("2".to_owned(), "two".to_owned()), 2, 1),
1640 (("3".to_owned(), "three".to_owned()), 3, 1),
1641 (("4".to_owned(), "four".to_owned()), 4, 1),
1642 ];
1643
1644 let cache = PersistClientCache::new_no_metrics();
1645
1646 cache.cfg.set_config(&BLOB_TARGET_SIZE, 0);
1650 cache.cfg.set_config(&MAX_RUNS, 3);
1651 cache
1652 .cfg
1653 .set_config(&BATCH_BUILDER_MAX_OUTSTANDING_PARTS, 2);
1654
1655 let client = cache
1656 .open(PersistLocation::new_in_mem())
1657 .await
1658 .expect("client construction failed");
1659 let (mut write, mut read) = client
1660 .expect_open::<String, String, u64, i64>(ShardId::new())
1661 .await;
1662
1663 let mut builder = write.builder(Antichain::from_elem(0));
1665
1666 fn assert_writing(
1667 builder: &BatchBuilder<String, String, u64, i64>,
1668 expected_finished: &[bool],
1669 ) {
1670 let WritingRuns::Compacting(run) = &builder.builder.parts.writing_runs else {
1671 unreachable!("ordered run!")
1672 };
1673
1674 let actual: Vec<_> = run.iter().map(|(_, p)| p.is_finished()).collect();
1675 assert_eq!(*expected_finished, actual);
1676 }
1677
1678 assert_writing(&builder, &[]);
1679
1680 let ((k, v), t, d) = &data[0];
1683 builder.add(k, v, t, d).await.expect("invalid usage");
1684 assert_writing(&builder, &[false]);
1685
1686 let ((k, v), t, d) = &data[1];
1689 builder.add(k, v, t, d).await.expect("invalid usage");
1690 assert_writing(&builder, &[false, false]);
1691
1692 let ((k, v), t, d) = &data[2];
1695 builder.add(k, v, t, d).await.expect("invalid usage");
1696 assert_writing(&builder, &[true, false, false]);
1697
1698 let ((k, v), t, d) = &data[3];
1701 builder.add(k, v, t, d).await.expect("invalid usage");
1702 assert_writing(&builder, &[false, false]);
1703
1704 let batch = builder
1707 .finish(Antichain::from_elem(5))
1708 .await
1709 .expect("invalid usage");
1710 assert_eq!(batch.batch.runs().count(), 2);
1711 assert_eq!(batch.batch.part_count(), 4);
1712 write
1713 .append_batch(batch, Antichain::from_elem(0), Antichain::from_elem(5))
1714 .await
1715 .expect("invalid usage")
1716 .expect("unexpected upper");
1717 assert_eq!(read.expect_snapshot_and_fetch(4).await, all_ok(&data, 4));
1718 }
1719
1720 #[mz_ore::test(tokio::test)]
1721 #[cfg_attr(miri, ignore)] async fn batch_builder_keys() {
1723 let cache = PersistClientCache::new_no_metrics();
1724 cache.cfg.set_config(&BLOB_TARGET_SIZE, 0);
1726 cache.cfg.set_config(&STRUCTURED_KEY_LOWER_LEN, 0);
1728 cache.cfg.set_config(&INLINE_WRITES_SINGLE_MAX_BYTES, 0);
1729 cache.cfg.set_config(&INLINE_WRITES_TOTAL_MAX_BYTES, 0);
1730 let client = cache
1731 .open(PersistLocation::new_in_mem())
1732 .await
1733 .expect("client construction failed");
1734 let shard_id = ShardId::new();
1735 let (mut write, _) = client
1736 .expect_open::<String, String, u64, i64>(shard_id)
1737 .await;
1738
1739 let batch = write
1740 .expect_batch(
1741 &[
1742 (("1".into(), "one".into()), 1, 1),
1743 (("2".into(), "two".into()), 2, 1),
1744 (("3".into(), "three".into()), 3, 1),
1745 ],
1746 0,
1747 4,
1748 )
1749 .await;
1750
1751 assert_eq!(batch.batch.part_count(), 3);
1752 for part in &batch.batch.parts {
1753 let part = part.expect_hollow_part();
1754 match BlobKey::parse_ids(&part.key.complete(&shard_id)) {
1755 Ok((shard, PartialBlobKey::Batch(writer, _))) => {
1756 assert_eq!(shard.to_string(), shard_id.to_string());
1757 assert_eq!(writer, WriterKey::for_version(&cache.cfg.build_version));
1758 }
1759 _ => panic!("unparseable blob key"),
1760 }
1761 }
1762 }
1763
1764 fn string_part(updates: &[((&str, &str), u64, i64)]) -> Part {
1765 let mut builder = PartBuilder::new(&StringSchema, &StringSchema);
1766 for ((k, v), t, d) in updates {
1767 builder.push(&k.to_string(), &v.to_string(), *t, *d);
1768 }
1769 builder.finish()
1770 }
1771
1772 #[mz_ore::test(tokio::test)]
1773 #[cfg_attr(miri, ignore)] async fn batch_builder_add_part_before_lower() {
1775 let client = new_test_client(&ConfigUpdates::default()).await;
1776 let (write, _) = client
1777 .expect_open::<String, String, u64, i64>(ShardId::new())
1778 .await;
1779 let mut builder = write.builder(Antichain::from_elem(2));
1780 let part = string_part(&[(("1", "one"), 2, 1), (("2", "two"), 1, 1)]);
1781 match builder.add_part(part).await {
1782 Err(InvalidUsage::UpdateNotBeyondLower { ts, .. }) => assert_eq!(ts, 1),
1783 other => panic!("expected UpdateNotBeyondLower, got {other:?}"),
1784 }
1785 }
1786
1787 #[mz_ore::test(tokio::test)]
1788 #[cfg_attr(miri, ignore)] async fn batch_builder_add_part_flushing() {
1790 let small = || string_part(&[(("1", "one"), 1, 1)]);
1791 let big = string_part(&[
1792 (("2", "two"), 1, 1),
1793 (("3", "three"), 1, 1),
1794 (("4", "four"), 1, 1),
1795 (("5", "five"), 1, 1),
1796 ]);
1797 let small_bytes = small().goodbytes();
1798 assert!(
1799 big.goodbytes() > 2 * small_bytes,
1800 "big part must exceed the target"
1801 );
1802
1803 let cache = PersistClientCache::new_no_metrics();
1804 cache.cfg.set_config(&BLOB_TARGET_SIZE, 2 * small_bytes);
1807 cache.cfg.set_config(&INLINE_WRITES_SINGLE_MAX_BYTES, 0);
1808 cache.cfg.set_config(&INLINE_WRITES_TOTAL_MAX_BYTES, 0);
1809 let client = cache
1810 .open(PersistLocation::new_in_mem())
1811 .await
1812 .expect("client construction failed");
1813 let (mut write, mut read) = client
1814 .expect_open::<String, String, u64, i64>(ShardId::new())
1815 .await;
1816
1817 let mut builder = write.builder(Antichain::from_elem(0));
1818 let added = builder.add_part(small()).await.expect("valid usage");
1820 assert!(matches!(added, Added::Record), "first part accumulates");
1821 let added = builder.add_part(small()).await.expect("valid usage");
1822 assert!(
1823 matches!(added, Added::Record),
1824 "second part fits the target"
1825 );
1826 let added = builder.add_part(small()).await.expect("valid usage");
1828 assert!(matches!(added, Added::RecordAndParts), "third part flushes");
1829 let added = builder.add_part(big).await.expect("valid usage");
1831 assert!(
1832 matches!(added, Added::RecordAndParts),
1833 "oversized part flushes"
1834 );
1835 let added = builder.add_part(small()).await.expect("valid usage");
1836 assert!(
1837 matches!(added, Added::Record),
1838 "accounting resets after a flush"
1839 );
1840
1841 let batch = builder
1842 .finish(Antichain::from_elem(2))
1843 .await
1844 .expect("valid usage");
1845 assert_eq!(batch.batch.part_count(), 4);
1847 write
1848 .append_batch(batch, Antichain::from_elem(0), Antichain::from_elem(2))
1849 .await
1850 .expect("valid usage")
1851 .expect("unexpected upper");
1852 let mut expected = vec![
1853 (("1".to_owned(), "one".to_owned()), 1, 4),
1854 (("2".to_owned(), "two".to_owned()), 1, 1),
1855 (("3".to_owned(), "three".to_owned()), 1, 1),
1856 (("4".to_owned(), "four".to_owned()), 1, 1),
1857 (("5".to_owned(), "five".to_owned()), 1, 1),
1858 ];
1859 expected.sort();
1860 assert_eq!(
1861 read.expect_snapshot_and_fetch(1).await,
1862 all_ok(&expected, 1)
1863 );
1864 }
1865
1866 #[mz_ore::test(tokio::test)]
1867 #[cfg_attr(miri, ignore)] async fn batch_delete() {
1869 let cache = PersistClientCache::new_no_metrics();
1870 cache.cfg.set_config(&INLINE_WRITES_SINGLE_MAX_BYTES, 0);
1871 cache.cfg.set_config(&INLINE_WRITES_TOTAL_MAX_BYTES, 0);
1872 cache.cfg.set_config(&BATCH_DELETE_ENABLED, true);
1873 let client = cache
1874 .open(PersistLocation::new_in_mem())
1875 .await
1876 .expect("client construction failed");
1877 let shard_id = ShardId::new();
1878 let (mut write, _) = client
1879 .expect_open::<String, String, u64, i64>(shard_id)
1880 .await;
1881
1882 let batch = write
1883 .expect_batch(
1884 &[
1885 (("1".into(), "one".into()), 1, 1),
1886 (("2".into(), "two".into()), 2, 1),
1887 (("3".into(), "three".into()), 3, 1),
1888 ],
1889 0,
1890 4,
1891 )
1892 .await;
1893
1894 assert_eq!(batch.batch.part_count(), 1);
1895 let part_key = batch.batch.parts[0]
1896 .expect_hollow_part()
1897 .key
1898 .complete(&shard_id);
1899
1900 let part_bytes = client.blob.get(&part_key).await.expect("invalid usage");
1901 assert!(part_bytes.is_some());
1902
1903 batch.delete().await;
1904
1905 let part_bytes = client.blob.get(&part_key).await.expect("invalid usage");
1906 assert!(part_bytes.is_none());
1907 }
1908
1909 #[mz_ore::test]
1910 fn untrimmable_columns() {
1911 let untrimmable = UntrimmableColumns {
1912 equals: vec!["abc".into(), "def".into()],
1913 prefixes: vec!["123".into(), "234".into()],
1914 suffixes: vec!["xyz".into()],
1915 };
1916
1917 assert!(untrimmable.should_retain("abc"));
1919 assert!(untrimmable.should_retain("ABC"));
1920 assert!(untrimmable.should_retain("aBc"));
1921 assert!(!untrimmable.should_retain("abcd"));
1922 assert!(untrimmable.should_retain("deF"));
1923 assert!(!untrimmable.should_retain("defg"));
1924
1925 assert!(untrimmable.should_retain("123"));
1927 assert!(untrimmable.should_retain("123-4"));
1928 assert!(untrimmable.should_retain("1234"));
1929 assert!(untrimmable.should_retain("234"));
1930 assert!(!untrimmable.should_retain("345"));
1931
1932 assert!(untrimmable.should_retain("ijk_xyZ"));
1934 assert!(untrimmable.should_retain("ww-XYZ"));
1935 assert!(!untrimmable.should_retain("xya"));
1936 }
1937
1938 #[mz_persist_proc::test(tokio::test)]
1940 #[cfg_attr(miri, ignore)] async fn rewrite_ts_example(dyncfgs: ConfigUpdates) {
1942 let client = new_test_client(&dyncfgs).await;
1943 let (mut write, read) = client
1944 .expect_open::<String, (), u64, i64>(ShardId::new())
1945 .await;
1946
1947 let mut batch = write.builder(Antichain::from_elem(0));
1948 batch.add(&"foo".to_owned(), &(), &0, &1).await.unwrap();
1949 let batch = batch.finish(Antichain::from_elem(1)).await.unwrap();
1950
1951 let batch = batch.into_transmittable_batch();
1953 let mut batch = write.batch_from_transmittable_batch(batch);
1954 batch
1955 .rewrite_ts(&Antichain::from_elem(2), Antichain::from_elem(3))
1956 .unwrap();
1957 write
1958 .expect_compare_and_append_batch(&mut [&mut batch], 0, 3)
1959 .await;
1960
1961 let (actual, _) = read.expect_listen(0).await.read_until(&3).await;
1962 let expected = vec![((("foo".to_owned()), ()), 2, 1)];
1963 assert_eq!(actual, expected);
1964 }
1965
1966 #[mz_ore::test(tokio::test)]
1967 #[cfg_attr(miri, ignore)] async fn structured_lowers() {
1969 let cache = PersistClientCache::new_no_metrics();
1970 cache.cfg().set_config(&STRUCTURED_KEY_LOWER_LEN, 1024);
1972 cache.cfg().set_config(&INLINE_WRITES_SINGLE_MAX_BYTES, 0);
1974 cache.cfg().set_config(&INLINE_WRITES_TOTAL_MAX_BYTES, 0);
1975 let client = cache
1976 .open(PersistLocation::new_in_mem())
1977 .await
1978 .expect("client construction failed");
1979 let shard_id = ShardId::new();
1980 let (mut write, _) = client
1981 .expect_open::<String, String, u64, i64>(shard_id)
1982 .await;
1983
1984 let batch = write
1985 .expect_batch(
1986 &[
1987 (("1".into(), "one".into()), 1, 1),
1988 (("2".into(), "two".into()), 2, 1),
1989 (("3".into(), "three".into()), 3, 1),
1990 ],
1991 0,
1992 4,
1993 )
1994 .await;
1995
1996 assert_eq!(batch.batch.part_count(), 1);
1997 let [part] = batch.batch.parts.as_slice() else {
1998 panic!("expected single part")
1999 };
2000 assert!(part.structured_key_lower().is_some());
2002 }
2003
2004 #[mz_ore::test]
2005 fn validate_truncate_batch_bounds() {
2006 use crate::internal::state::tests::hollow;
2007
2008 let desc = |lower: u64, upper: u64| {
2009 Description::new(
2010 Antichain::from_elem(lower),
2011 Antichain::from_elem(upper),
2012 Antichain::from_elem(0),
2013 )
2014 };
2015
2016 let batch = hollow(0, 5, &["k"], 1);
2018 assert_eq!(
2019 validate_truncate_batch(&batch, &desc(0, 5), false, true).expect("valid"),
2020 false
2021 );
2022
2023 assert_eq!(
2026 validate_truncate_batch(&batch, &desc(1, 5), false, true).expect("valid"),
2027 true
2028 );
2029 assert_eq!(
2030 validate_truncate_batch(&batch, &desc(0, 4), false, true).expect("valid"),
2031 true
2032 );
2033
2034 let mut rewritten = hollow(0, 5, &["k"], 1);
2038 for part in rewritten.parts.iter_mut() {
2039 let RunPart::Single(BatchPart::Hollow(part)) = part else {
2040 panic!("expected hollow part");
2041 };
2042 part.ts_rewrite = Some(Antichain::from_elem(3));
2043 }
2044 assert_eq!(
2045 validate_truncate_batch(&rewritten, &desc(2, 5), true, true).expect("valid"),
2046 false
2047 );
2048 }
2049}