1use std::collections::BTreeSet;
11use std::fmt::Debug;
12use std::marker::PhantomData;
13use std::mem;
14use std::sync::Arc;
15use std::time::{Duration, Instant};
16
17use anyhow::anyhow;
18use differential_dataflow::difference::Monoid;
19use differential_dataflow::lattice::Lattice;
20use differential_dataflow::trace::Description;
21use futures::{Stream, pin_mut};
22use futures_util::StreamExt;
23use itertools::Either;
24use mz_dyncfg::{Config, ParameterScope};
25use mz_ore::cast::CastFrom;
26use mz_ore::error::ErrorExt;
27use mz_ore::now::NowFn;
28use mz_ore::soft_assert_or_log;
29use mz_persist::location::Blob;
30use mz_persist_types::part::Part;
31use mz_persist_types::{Codec, Codec64};
32use timely::PartialOrder;
33use timely::progress::{Antichain, Timestamp};
34use tokio::sync::mpsc::Sender;
35use tokio::sync::{TryAcquireError, mpsc, oneshot};
36use tracing::{Instrument, Span, debug, debug_span, error, trace, warn};
37
38use crate::async_runtime::IsolatedRuntime;
39use crate::batch::{BatchBuilderConfig, BatchBuilderInternal, BatchParts, PartDeletes};
40use crate::cfg::{
41 COMPACTION_HEURISTIC_MIN_INPUTS, COMPACTION_HEURISTIC_MIN_PARTS,
42 COMPACTION_HEURISTIC_MIN_UPDATES, COMPACTION_MEMORY_BOUND_BYTES,
43 GC_BLOB_DELETE_CONCURRENCY_LIMIT, MiB,
44};
45use crate::fetch::{FetchBatchFilter, FetchConfig};
46use crate::internal::encoding::Schemas;
47use crate::internal::gc::GarbageCollector;
48use crate::internal::machine::Machine;
49use crate::internal::maintenance::RoutineMaintenance;
50use crate::internal::metrics::ShardMetrics;
51use crate::internal::state::{HollowBatch, RunMeta, RunOrder, RunPart};
52use crate::internal::trace::{
53 ActiveCompaction, ApplyMergeResult, CompactionInput, FueledMergeRes, IdHollowBatch, SpineId,
54 id_range,
55};
56use crate::iter::{Consolidator, StructuredSort};
57use crate::{Metrics, PersistConfig, ShardId};
58
59#[derive(Debug, Clone)]
65pub struct CompactReq<T> {
66 pub shard_id: ShardId,
68 pub desc: Description<T>,
70 pub inputs: Vec<IdHollowBatch<T>>,
73}
74
75#[derive(Debug)]
77pub struct CompactRes<T> {
78 pub output: HollowBatch<T>,
80 pub input: CompactionInput,
82}
83
84#[derive(Debug, Clone)]
87pub struct CompactConfig {
88 pub(crate) compaction_memory_bound_bytes: usize,
89 pub(crate) compaction_yield_after_n_updates: usize,
90 pub(crate) version: semver::Version,
91 pub(crate) batch: BatchBuilderConfig,
92 pub(crate) fetch_config: FetchConfig,
93 pub(crate) now: NowFn,
94}
95
96impl CompactConfig {
97 pub fn new(value: &PersistConfig, shard_id: ShardId) -> Self {
99 CompactConfig {
100 compaction_memory_bound_bytes: COMPACTION_MEMORY_BOUND_BYTES.get(value),
101 compaction_yield_after_n_updates: value.compaction_yield_after_n_updates,
102 version: value.build_version.clone(),
103 batch: BatchBuilderConfig::new(value, shard_id),
104 fetch_config: FetchConfig::from_persist_config(value),
105 now: value.now.clone(),
106 }
107 }
108}
109
110#[derive(Debug)]
116pub struct Compactor<K, V, T, D> {
117 cfg: PersistConfig,
118 metrics: Arc<Metrics>,
119 sender: Sender<(
120 Instant,
121 CompactReq<T>,
122 Machine<K, V, T, D>,
123 oneshot::Sender<Result<(), anyhow::Error>>,
124 )>,
125 _phantom: PhantomData<fn() -> D>,
126}
127
128impl<K, V, T, D> Clone for Compactor<K, V, T, D> {
129 fn clone(&self) -> Self {
130 Compactor {
131 cfg: self.cfg.clone(),
132 metrics: Arc::clone(&self.metrics),
133 sender: self.sender.clone(),
134 _phantom: Default::default(),
135 }
136 }
137}
138
139pub(crate) const COMPACTION_MINIMUM_TIMEOUT: Config<Duration> = Config::new(
143 "persist_compaction_minimum_timeout",
144 Duration::from_secs(90),
145 "\
146 The minimum amount of time to allow a persist compaction request to run \
147 before timing it out (Materialize).",
148 ParameterScope::Environment,
149);
150
151pub(crate) const COMPACTION_CHECK_PROCESS_FLAG: Config<bool> = Config::new(
152 "persist_compaction_check_process_flag",
153 true,
154 "Whether Compactor will obey the process_requests flag in PersistConfig, \
155 which allows dynamically disabling compaction. If false, all compaction requests will be processed.",
156 ParameterScope::Environment,
157);
158
159fn input_id_range(ids: BTreeSet<SpineId>) -> CompactionInput {
161 let id = id_range(ids);
162
163 CompactionInput::IdRange(id)
164}
165
166impl<K, V, T, D> Compactor<K, V, T, D>
167where
168 K: Debug + Codec,
169 V: Debug + Codec,
170 T: Timestamp + Lattice + Codec64 + Sync,
171 D: Monoid + Ord + Codec64 + Send + Sync,
172{
173 pub fn new(
174 cfg: PersistConfig,
175 metrics: Arc<Metrics>,
176 gc: GarbageCollector<K, V, T, D>,
177 ) -> Self {
178 let (compact_req_sender, mut compact_req_receiver) = mpsc::channel::<(
179 Instant,
180 CompactReq<T>,
181 Machine<K, V, T, D>,
182 oneshot::Sender<Result<(), anyhow::Error>>,
183 )>(cfg.compaction_queue_size);
184 let concurrency_limit = Arc::new(tokio::sync::Semaphore::new(
185 cfg.compaction_concurrency_limit,
186 ));
187 let check_process_requests = COMPACTION_CHECK_PROCESS_FLAG.handle(&cfg.configs);
188 let process_requests = Arc::clone(&cfg.compaction_process_requests);
189
190 let _worker_handle = mz_ore::task::spawn(|| "PersistCompactionScheduler", async move {
193 while let Some((enqueued, req, machine, completer)) = compact_req_receiver.recv().await
194 {
195 assert_eq!(req.shard_id, machine.shard_id());
196 let metrics = Arc::clone(&machine.applier.metrics);
197
198 if check_process_requests.get()
200 && !process_requests.load(std::sync::atomic::Ordering::Relaxed)
201 {
202 let _ = completer.send(Err(anyhow::anyhow!("compaction disabled")));
205 metrics.compaction.disabled.inc();
206 tracing::warn!(shard_id = ?req.shard_id, "Dropping compaction request on the floor.");
207
208 continue;
209 }
210
211 let permit = {
212 let inner = Arc::clone(&concurrency_limit);
213 match inner.try_acquire_owned() {
216 Ok(permit) => permit,
217 Err(TryAcquireError::NoPermits) => {
218 metrics.compaction.concurrency_waits.inc();
219 Arc::clone(&concurrency_limit)
220 .acquire_owned()
221 .await
222 .expect("semaphore is never closed")
223 }
224 Err(TryAcquireError::Closed) => {
225 warn!("semaphore for shard {} is closed", machine.shard_id());
228 continue;
229 }
230 }
231 };
232 metrics
233 .compaction
234 .queued_seconds
235 .inc_by(enqueued.elapsed().as_secs_f64());
236
237 let compact_span =
238 debug_span!(parent: None, "compact::apply", shard_id=%machine.shard_id());
239 compact_span.follows_from(&Span::current());
240 let gc = gc.clone();
241 mz_ore::task::spawn(|| "PersistCompactionWorker", async move {
242 let res = Self::compact_and_apply(&machine, req)
243 .instrument(compact_span)
244 .await;
245
246 match res {
247 Ok(maintenance) => maintenance.start_performing(&machine, &gc),
248 Err(err) => {
249 debug!(shard_id =? machine.shard_id(), "compaction failed: {err:#}")
250 }
251 }
252
253 let _ = completer.send(Ok(()));
256
257 drop(permit);
259 });
260 }
261 });
262
263 Compactor {
264 cfg,
265 metrics,
266 sender: compact_req_sender,
267 _phantom: PhantomData,
268 }
269 }
270
271 pub fn compact_and_apply_background(
276 &self,
277 req: CompactReq<T>,
278 machine: &Machine<K, V, T, D>,
279 ) -> Option<oneshot::Receiver<Result<(), anyhow::Error>>> {
280 let should_compact = req.inputs.len() >= COMPACTION_HEURISTIC_MIN_INPUTS.get(&self.cfg)
286 || req
287 .inputs
288 .iter()
289 .map(|x| x.batch.part_count())
290 .sum::<usize>()
291 >= COMPACTION_HEURISTIC_MIN_PARTS.get(&self.cfg)
292 || req.inputs.iter().map(|x| x.batch.len).sum::<usize>()
293 >= COMPACTION_HEURISTIC_MIN_UPDATES.get(&self.cfg);
294 if !should_compact {
295 self.metrics.compaction.skipped.inc();
296 return None;
297 }
298
299 let (compaction_completed_sender, compaction_completed_receiver) = oneshot::channel();
300 let new_compaction_sender = self.sender.clone();
301
302 self.metrics.compaction.requested.inc();
303 let send = new_compaction_sender.try_send((
309 Instant::now(),
310 req,
311 machine.clone(),
312 compaction_completed_sender,
313 ));
314 if let Err(_) = send {
315 self.metrics.compaction.dropped.inc();
316 return None;
317 }
318
319 Some(compaction_completed_receiver)
320 }
321
322 pub(crate) async fn compact_and_apply(
323 machine: &Machine<K, V, T, D>,
324 req: CompactReq<T>,
325 ) -> Result<RoutineMaintenance, anyhow::Error> {
326 let metrics = Arc::clone(&machine.applier.metrics);
327 metrics.compaction.started.inc();
328 let start = Instant::now();
329
330 let total_input_bytes = req
333 .inputs
334 .iter()
335 .map(|batch| batch.batch.encoded_size_bytes())
336 .sum::<usize>();
337 let timeout = Duration::max(
338 COMPACTION_MINIMUM_TIMEOUT.get(&machine.applier.cfg),
340 Duration::from_secs(u64::cast_from(total_input_bytes / MiB)),
342 );
343 let Some(compaction_schema_id) = req
346 .inputs
347 .iter()
348 .flat_map(|batch| batch.batch.run_meta.iter())
349 .filter_map(|run_meta| run_meta.schema)
350 .max()
352 else {
353 metrics.compaction.schema_selection.no_schema.inc();
354 metrics.compaction.failed.inc();
355 return Err(anyhow!(
356 "compacting {shard_id} and spine ids {spine_ids}: could not determine schema id from inputs",
357 shard_id = req.shard_id,
358 spine_ids = mz_ore::str::separated(", ", req.inputs.iter().map(|i| i.id))
359 ));
360 };
361 let Some((key_schema, val_schema)) = machine.get_schema(compaction_schema_id) else {
362 metrics.compaction.schema_selection.no_schema.inc();
363 metrics.compaction.failed.inc();
364 return Err(anyhow!(
365 "compacting {shard_id} and spine ids {spine_ids}: schema id {compaction_schema_id} not present in machine state",
366 shard_id = req.shard_id,
367 spine_ids = mz_ore::str::separated(", ", req.inputs.iter().map(|i| i.id))
368 ));
369 };
370
371 metrics.compaction.schema_selection.recent_schema.inc();
372
373 let compaction_schema = Schemas {
374 id: Some(compaction_schema_id),
375 key: Arc::new(key_schema),
376 val: Arc::new(val_schema),
377 };
378
379 trace!(
380 "compaction request for {}MBs ({} bytes), with timeout of {}s, and schema {:?}.",
381 total_input_bytes / MiB,
382 total_input_bytes,
383 timeout.as_secs_f64(),
384 compaction_schema.id,
385 );
386
387 let isolated_runtime = Arc::clone(&machine.isolated_runtime);
388 let machine_clone = machine.clone();
389 let metrics_clone = Arc::clone(&machine.applier.metrics);
390 let compact_span = debug_span!("compact::consolidate");
391 let res = tokio::time::timeout(
392 timeout,
393 isolated_runtime.spawn_named(
395 || "persist::compact::consolidate",
396 async move {
397 let all_runs_have_uuids = req
401 .inputs
402 .iter()
403 .all(|x| x.batch.runs().all(|(meta, _)| meta.id.is_some()));
404 let all_runs_have_len = req
405 .inputs
406 .iter()
407 .all(|x| x.batch.runs().all(|(meta, _)| meta.len.is_some()));
408
409 let compact_cfg =
410 CompactConfig::new(&machine_clone.applier.cfg, machine_clone.shard_id());
411 let incremental_enabled = compact_cfg.batch.enable_incremental_compaction
412 && all_runs_have_uuids
413 && all_runs_have_len;
414 let stream = Self::compact_stream(
415 compact_cfg,
416 Arc::clone(&machine_clone.applier.state_versions.blob),
417 Arc::clone(&metrics_clone),
418 Arc::clone(&machine_clone.applier.shard_metrics),
419 Arc::clone(&machine_clone.isolated_runtime),
420 req.clone(),
421 compaction_schema,
422 incremental_enabled,
423 );
424
425 let maintenance = if incremental_enabled {
426 let mut maintenance = RoutineMaintenance::default();
427 pin_mut!(stream);
428 while let Some(res) = stream.next().await {
429 let res = res?;
430 let new_maintenance =
431 Self::apply(res, &metrics_clone, &machine_clone).await?;
432 maintenance.merge(new_maintenance);
433 }
434 maintenance
435 } else {
436 let res = Self::compact_all(stream, req.clone()).await?;
437 Self::apply(
438 FueledMergeRes {
439 output: res.output,
440 input: CompactionInput::Legacy,
441 new_active_compaction: None,
442 },
443 &metrics_clone,
444 &machine_clone,
445 )
446 .await?
447 };
448
449 Ok::<_, anyhow::Error>(maintenance)
450 }
451 .instrument(compact_span),
452 ),
453 )
454 .await;
455
456 metrics
457 .compaction
458 .seconds
459 .inc_by(start.elapsed().as_secs_f64());
460 let res = res.map_err(|e| {
461 metrics.compaction.timed_out.inc();
462 anyhow!(
463 "compaction timed out after {}s: {}",
464 timeout.as_secs_f64(),
465 e
466 )
467 })?;
468
469 match res {
470 Ok(maintenance) => Ok(maintenance),
471 Err(err) => {
472 metrics.compaction.failed.inc();
473 debug!(
474 "compaction for {} failed: {}",
475 machine.shard_id(),
476 err.display_with_causes()
477 );
478 Err(err)
479 }
480 }
481 }
482
483 pub async fn compact_all(
484 stream: impl Stream<Item = Result<FueledMergeRes<T>, anyhow::Error>>,
485 req: CompactReq<T>,
486 ) -> Result<CompactRes<T>, anyhow::Error> {
487 pin_mut!(stream);
488
489 let mut all_parts = vec![];
490 let mut all_run_splits = vec![];
491 let mut all_run_meta = vec![];
492 let mut len = 0;
493
494 while let Some(res) = stream.next().await {
495 let res = res?.output;
496 let (parts, updates, run_meta, run_splits) =
497 (res.parts, res.len, res.run_meta, res.run_splits);
498
499 if updates == 0 {
500 continue;
501 }
502
503 let run_offset = all_parts.len();
504 if !all_parts.is_empty() {
505 all_run_splits.push(run_offset);
506 }
507 all_run_splits.extend(run_splits.iter().map(|r| r + run_offset));
508 all_run_meta.extend(run_meta);
509 all_parts.extend(parts);
510 len += updates;
511 }
512
513 let batches = req.inputs.iter().map(|x| x.id).collect::<BTreeSet<_>>();
514 let input = input_id_range(batches);
515
516 Ok(CompactRes {
517 output: HollowBatch::new(
518 req.desc.clone(),
519 all_parts,
520 len,
521 all_run_meta,
522 all_run_splits,
523 ),
524 input,
525 })
526 }
527
528 pub async fn apply(
529 res: FueledMergeRes<T>,
530 metrics: &Metrics,
531 machine: &Machine<K, V, T, D>,
532 ) -> Result<RoutineMaintenance, anyhow::Error> {
533 let (apply_merge_result, maintenance) = machine.merge_res(&res).await;
534
535 match &apply_merge_result {
536 ApplyMergeResult::AppliedExact => {
537 metrics.compaction.applied.inc();
538 metrics.compaction.applied_exact_match.inc();
539 machine.applier.shard_metrics.compaction_applied.inc();
540 }
541 ApplyMergeResult::AppliedSubset => {
542 metrics.compaction.applied.inc();
543 metrics.compaction.applied_subset_match.inc();
544 machine.applier.shard_metrics.compaction_applied.inc();
545 }
546 ApplyMergeResult::NotAppliedNoMatch
547 | ApplyMergeResult::NotAppliedInvalidSince
548 | ApplyMergeResult::NotAppliedTooManyUpdates => {
549 if let ApplyMergeResult::NotAppliedTooManyUpdates = &apply_merge_result {
550 metrics.compaction.not_applied_too_many_updates.inc();
551 }
552 metrics.compaction.noop.inc();
553 let mut part_deletes = PartDeletes::default();
554 for part in &res.output.parts {
555 part_deletes.add(part);
556 }
557 part_deletes
558 .delete(
559 machine.applier.state_versions.blob.as_ref(),
560 machine.shard_id(),
561 GC_BLOB_DELETE_CONCURRENCY_LIMIT.get(&machine.applier.cfg),
562 &*metrics,
563 &metrics.retries.external.compaction_noop_delete,
564 )
565 .await;
566 }
567 };
568
569 Ok(maintenance)
570 }
571
572 pub fn compact_stream(
596 cfg: CompactConfig,
597 blob: Arc<dyn Blob>,
598 metrics: Arc<Metrics>,
599 shard_metrics: Arc<ShardMetrics>,
600 isolated_runtime: Arc<IsolatedRuntime>,
601 req: CompactReq<T>,
602 write_schemas: Schemas<K, V>,
603 incremental_enabled: bool,
604 ) -> impl Stream<Item = Result<FueledMergeRes<T>, anyhow::Error>> {
605 async_stream::stream! {
606 let () = Self::validate_req(&req)?;
607
608 let mut single_nonempty_batch = None;
613 for batch in &req.inputs {
614 if batch.batch.len > 0 {
615 match single_nonempty_batch {
616 None => single_nonempty_batch = Some(batch),
617 Some(_previous_nonempty_batch) => {
618 single_nonempty_batch = None;
619 break;
620 }
621 }
622 }
623 }
624 if let Some(single_nonempty_batch) = single_nonempty_batch {
625 if single_nonempty_batch.batch.run_splits.len() == 0
626 && single_nonempty_batch.batch.desc.since()
627 != &Antichain::from_elem(T::minimum())
628 {
629 metrics.compaction.fast_path_eligible.inc();
630 }
631 }
632
633 let in_progress_part_reserved_memory_bytes = 2 * cfg.batch.blob_target_size;
635 let run_reserved_memory_bytes = cfg
640 .compaction_memory_bound_bytes
641 .saturating_sub(in_progress_part_reserved_memory_bytes);
642
643 let chunked_runs = Self::chunk_runs(
644 &req,
645 &cfg,
646 &*metrics,
647 run_reserved_memory_bytes,
648 req.desc.since()
649 );
650 let total_chunked_runs = chunked_runs.len();
651
652 let parts_before = req.inputs.iter()
653 .map(|x| x.batch.parts.len()).sum::<usize>();
654 let parts_after = chunked_runs.iter()
655 .flat_map(|(_, _, runs, _)| {
656 runs.iter().map(|(_, _, parts)| parts.len())
657 })
658 .sum::<usize>();
659 assert_eq!(
660 parts_before, parts_after,
661 "chunking should not change the number of parts",
662 );
663
664 for (applied, (input, desc, runs, run_chunk_max_memory_usage)) in
665 chunked_runs.into_iter().enumerate()
666 {
667 metrics.compaction.chunks_compacted.inc();
668 metrics
669 .compaction
670 .runs_compacted
671 .inc_by(u64::cast_from(runs.len()));
672
673 let extra_outstanding_parts = (run_reserved_memory_bytes
677 .saturating_sub(run_chunk_max_memory_usage))
678 / cfg.batch.blob_target_size;
679 let mut run_cfg = cfg.clone();
680 run_cfg.batch.batch_builder_max_outstanding_parts = 1 + extra_outstanding_parts;
681
682 let desc = if incremental_enabled {
683 desc
684 } else {
685 req.desc.clone()
686 };
687
688 let runs = runs.iter()
689 .map(|(desc, meta, run)| (*desc, *meta, *run))
690 .collect::<Vec<_>>();
691
692 let batch = Self::compact_runs(
693 &run_cfg,
694 &req.shard_id,
695 &desc,
696 runs,
697 Arc::clone(&blob),
698 Arc::clone(&metrics),
699 Arc::clone(&shard_metrics),
700 Arc::clone(&isolated_runtime),
701 write_schemas.clone(),
702 )
703 .await?;
704
705 assert!(
706 (batch.len == 0 && batch.parts.len() == 0)
707 || (batch.len > 0 && batch.parts.len() > 0),
708 "updates={}, parts={}",
709 batch.len,
710 batch.parts.len(),
711 );
712
713 let clock = cfg.now.clone();
715 let active_compaction = if applied < total_chunked_runs - 1 {
716 Some(ActiveCompaction { start_ms: clock() })
717 } else {
718 None
719 };
720
721 let res = CompactRes {
722 output: batch,
723 input,
724 };
725
726 let res = FueledMergeRes {
727 output: res.output,
728 new_active_compaction: active_compaction,
729 input: res.input,
730 };
731
732 yield Ok(res);
733 }
734 }
735 }
736
737 pub async fn compact(
742 cfg: CompactConfig,
743 blob: Arc<dyn Blob>,
744 metrics: Arc<Metrics>,
745 shard_metrics: Arc<ShardMetrics>,
746 isolated_runtime: Arc<IsolatedRuntime>,
747 req: CompactReq<T>,
748 write_schemas: Schemas<K, V>,
749 ) -> Result<CompactRes<T>, anyhow::Error> {
750 let stream = Self::compact_stream(
751 cfg,
752 Arc::clone(&blob),
753 Arc::clone(&metrics),
754 Arc::clone(&shard_metrics),
755 Arc::clone(&isolated_runtime),
756 req.clone(),
757 write_schemas,
758 false,
759 );
760
761 Self::compact_all(stream, req).await
762 }
763
764 fn chunk_runs<'a>(
769 req: &'a CompactReq<T>,
770 cfg: &CompactConfig,
771 metrics: &Metrics,
772 run_reserved_memory_bytes: usize,
773 since: &Antichain<T>,
774 ) -> Vec<(
775 CompactionInput,
776 Description<T>,
777 Vec<(&'a Description<T>, &'a RunMeta, &'a [RunPart<T>])>,
778 usize,
779 )> {
780 let _ = input_id_range(req.inputs.iter().map(|x| x.id).collect());
782
783 let mut batches: Vec<_> = req.inputs.iter().map(|x| (x.id, &*x.batch)).collect();
785 batches.sort_by_key(|(id, _)| *id);
786
787 let mut chunks = vec![];
788 let mut current_chunk_ids = BTreeSet::new();
789 let mut current_chunk_descs = Vec::new();
790 let mut current_chunk_runs = vec![];
791 let mut current_chunk_max_memory_usage = 0;
792
793 fn max_part_bytes<T>(parts: &[RunPart<T>], cfg: &CompactConfig) -> usize {
794 parts
795 .iter()
796 .map(|p| p.max_part_bytes())
797 .max()
798 .unwrap_or(cfg.batch.blob_target_size)
799 }
800
801 fn desc_range<T: Timestamp>(
802 descs: impl IntoIterator<Item = Description<T>>,
803 since: Antichain<T>,
804 ) -> Description<T> {
805 let mut descs = descs.into_iter();
806 let first = descs.next().expect("non-empty set of descriptions");
807 let lower = first.lower().clone();
808 let mut upper = first.upper().clone();
809 for desc in descs {
810 assert_eq!(&upper, desc.lower());
811 upper = desc.upper().clone();
812 }
813 let upper = upper.clone();
814 Description::new(lower, upper, since)
815 }
816
817 for (spine_id, batch) in batches {
818 let batch_size = batch
819 .runs()
820 .map(|(_, parts)| max_part_bytes(parts, cfg))
821 .sum::<usize>();
822
823 let num_runs = batch.run_meta.len();
824
825 let runs = batch.runs().flat_map(|(meta, parts)| {
826 if meta.order.unwrap_or(RunOrder::Codec) == cfg.batch.preferred_order {
827 Either::Left(std::iter::once((&batch.desc, meta, parts)))
828 } else {
829 soft_assert_or_log!(
839 !parts.iter().any(|r| matches!(r, RunPart::Many(_))),
840 "unexpected out-of-order hollow run"
841 );
842 Either::Right(
843 parts
844 .iter()
845 .map(move |p| (&batch.desc, meta, std::slice::from_ref(p))),
846 )
847 }
848 });
849
850 if current_chunk_max_memory_usage + batch_size <= run_reserved_memory_bytes
854 || current_chunk_runs.len() + num_runs <= 2
855 {
856 if current_chunk_max_memory_usage + batch_size > run_reserved_memory_bytes {
857 metrics.compaction.memory_violations.inc();
860 }
861 current_chunk_ids.insert(spine_id);
862 current_chunk_descs.push(batch.desc.clone());
863 current_chunk_runs.extend(runs);
864 current_chunk_max_memory_usage += batch_size;
865 continue;
866 }
867
868 if !current_chunk_ids.is_empty() {
870 chunks.push((
871 input_id_range(std::mem::take(&mut current_chunk_ids)),
872 desc_range(mem::take(&mut current_chunk_descs), since.clone()),
873 std::mem::take(&mut current_chunk_runs),
874 current_chunk_max_memory_usage,
875 ));
876 current_chunk_max_memory_usage = 0;
877 }
878
879 if batch_size <= run_reserved_memory_bytes {
881 current_chunk_ids.insert(spine_id);
882 current_chunk_descs.push(batch.desc.clone());
883 current_chunk_runs.extend(runs);
884 current_chunk_max_memory_usage += batch_size;
885 continue;
886 }
887
888 let mut run_iter = runs.into_iter().peekable();
891 mz_ore::soft_assert_no_log!(current_chunk_ids.is_empty());
892 mz_ore::soft_assert_no_log!(current_chunk_descs.is_empty());
893 mz_ore::soft_assert_no_log!(current_chunk_runs.is_empty());
894 mz_ore::soft_assert_eq_no_log!(current_chunk_max_memory_usage, 0);
895 let mut current_chunk_run_ids = BTreeSet::new();
896
897 while let Some((desc, meta, parts)) = run_iter.next() {
898 let run_size = max_part_bytes(parts, cfg);
899 current_chunk_runs.push((desc, meta, parts));
900 current_chunk_max_memory_usage += run_size;
901 current_chunk_run_ids.extend(meta.id);
902
903 if let Some((_, _meta, next_parts)) = run_iter.peek() {
904 let next_size = max_part_bytes(next_parts, cfg);
905 if current_chunk_max_memory_usage + next_size > run_reserved_memory_bytes {
906 if current_chunk_runs.len() == 1 {
908 metrics.compaction.memory_violations.inc();
909 continue;
910 }
911 chunks.push((
913 CompactionInput::PartialBatch(
914 spine_id,
915 mem::take(&mut current_chunk_run_ids),
916 ),
917 desc_range([batch.desc.clone()], since.clone()),
918 std::mem::take(&mut current_chunk_runs),
919 current_chunk_max_memory_usage,
920 ));
921 current_chunk_max_memory_usage = 0;
922 }
923 }
924 }
925
926 if !current_chunk_runs.is_empty() {
927 chunks.push((
928 CompactionInput::PartialBatch(spine_id, mem::take(&mut current_chunk_run_ids)),
929 desc_range([batch.desc.clone()], since.clone()),
930 std::mem::take(&mut current_chunk_runs),
931 current_chunk_max_memory_usage,
932 ));
933 current_chunk_max_memory_usage = 0;
934 }
935 }
936
937 if !current_chunk_ids.is_empty() {
939 chunks.push((
940 input_id_range(current_chunk_ids),
941 desc_range(current_chunk_descs, since.clone()),
942 current_chunk_runs,
943 current_chunk_max_memory_usage,
944 ));
945 }
946
947 chunks
948 }
949
950 pub(crate) async fn compact_runs(
954 cfg: &CompactConfig,
955 shard_id: &ShardId,
956 desc: &Description<T>,
957 runs: Vec<(&Description<T>, &RunMeta, &[RunPart<T>])>,
958 blob: Arc<dyn Blob>,
959 metrics: Arc<Metrics>,
960 shard_metrics: Arc<ShardMetrics>,
961 isolated_runtime: Arc<IsolatedRuntime>,
962 write_schemas: Schemas<K, V>,
963 ) -> Result<HollowBatch<T>, anyhow::Error> {
964 let prefetch_budget_bytes = 2 * cfg.batch.blob_target_size;
972
973 let mut timings = Timings::default();
974
975 let mut batch_cfg = cfg.batch.clone();
976
977 batch_cfg.inline_writes_single_max_bytes = 0;
982
983 let parts = BatchParts::new_ordered::<D>(
984 batch_cfg,
985 cfg.batch.preferred_order,
986 Arc::clone(&metrics),
987 Arc::clone(&shard_metrics),
988 *shard_id,
989 Arc::clone(&blob),
990 Arc::clone(&isolated_runtime),
991 &metrics.compaction.batch,
992 );
993 let mut batch = BatchBuilderInternal::<K, V, T, D>::new(
994 cfg.batch.clone(),
995 parts,
996 Arc::clone(&metrics),
997 write_schemas.clone(),
998 Arc::clone(&blob),
999 shard_id.clone(),
1000 cfg.version.clone(),
1001 );
1002
1003 let mut consolidator = Consolidator::new(
1004 format!(
1005 "{}[lower={:?},upper={:?}]",
1006 shard_id,
1007 desc.lower().elements(),
1008 desc.upper().elements()
1009 ),
1010 cfg.fetch_config.clone(),
1011 *shard_id,
1012 StructuredSort::<K, V, T, D>::new(write_schemas.clone()),
1013 blob,
1014 Arc::clone(&metrics),
1015 shard_metrics,
1016 metrics.read.compaction.clone(),
1017 FetchBatchFilter::Compaction {
1018 since: desc.since().clone(),
1019 },
1020 None,
1021 prefetch_budget_bytes,
1022 );
1023
1024 for (desc, meta, parts) in runs {
1025 consolidator.enqueue_run(desc, meta, parts.iter().cloned());
1026 }
1027
1028 let remaining_budget = consolidator.start_prefetches();
1029 if remaining_budget.is_none() {
1030 metrics.compaction.not_all_prefetched.inc();
1031 }
1032
1033 loop {
1034 let mut chunks = vec![];
1035 let mut total_bytes = 0;
1036 while total_bytes < cfg.batch.blob_target_size {
1041 let fetch_start = Instant::now();
1042 let Some(chunk) = consolidator
1043 .next_chunk(
1044 cfg.compaction_yield_after_n_updates,
1045 cfg.batch.blob_target_size - total_bytes,
1046 )
1047 .await?
1048 else {
1049 break;
1050 };
1051 timings.part_fetching += fetch_start.elapsed();
1052 total_bytes += chunk.goodbytes();
1053 chunks.push(chunk);
1054 tokio::task::yield_now().await;
1055 }
1056
1057 let Some(updates) = Part::concat(&chunks).expect("compaction produces well-typed data")
1059 else {
1060 break;
1061 };
1062 batch.flush_part(desc.clone(), updates).await;
1063 }
1064 let mut batch = batch.finish(desc.clone()).await?;
1065
1066 let has_inline_parts = batch.batch.parts.iter().any(|x| x.is_inline());
1072 if has_inline_parts {
1073 error!(%shard_id, ?cfg, "compaction result unexpectedly had inline writes");
1074 let () = batch
1075 .flush_to_blob(
1076 &cfg.batch,
1077 &metrics.compaction.batch,
1078 &isolated_runtime,
1079 &write_schemas,
1080 )
1081 .await;
1082 }
1083
1084 timings.record(&metrics);
1085 Ok(batch.into_hollow_batch())
1086 }
1087
1088 fn validate_req(req: &CompactReq<T>) -> Result<(), anyhow::Error> {
1089 let mut frontier = req.desc.lower();
1090 for input in req.inputs.iter() {
1091 if PartialOrder::less_than(req.desc.since(), input.batch.desc.since()) {
1092 return Err(anyhow!(
1093 "output since {:?} must be at or in advance of input since {:?}",
1094 req.desc.since(),
1095 input.batch.desc.since()
1096 ));
1097 }
1098 if frontier != input.batch.desc.lower() {
1099 return Err(anyhow!(
1100 "invalid merge of non-consecutive batches {:?} vs {:?}",
1101 frontier,
1102 input.batch.desc.lower()
1103 ));
1104 }
1105 frontier = input.batch.desc.upper();
1106 }
1107 if frontier != req.desc.upper() {
1108 return Err(anyhow!(
1109 "invalid merge of non-consecutive batches {:?} vs {:?}",
1110 frontier,
1111 req.desc.upper()
1112 ));
1113 }
1114 Ok(())
1115 }
1116}
1117
1118#[derive(Debug, Default)]
1119struct Timings {
1120 part_fetching: Duration,
1121 heap_population: Duration,
1122}
1123
1124impl Timings {
1125 fn record(self, metrics: &Metrics) {
1126 let Timings {
1128 part_fetching,
1129 heap_population,
1130 } = self;
1131
1132 metrics
1133 .compaction
1134 .steps
1135 .part_fetch_seconds
1136 .inc_by(part_fetching.as_secs_f64());
1137 metrics
1138 .compaction
1139 .steps
1140 .heap_population_seconds
1141 .inc_by(heap_population.as_secs_f64());
1142 }
1143}
1144
1145#[cfg(test)]
1146mod tests {
1147 use mz_dyncfg::ConfigUpdates;
1148 use mz_ore::{assert_contains, assert_err};
1149 use mz_persist_types::codec_impls::StringSchema;
1150 use timely::progress::Antichain;
1151
1152 use crate::PersistLocation;
1153 use crate::batch::BLOB_TARGET_SIZE;
1154 use crate::internal::trace::SpineId;
1155 use crate::tests::{all_ok, expect_fetch_part, new_test_client_cache};
1156
1157 use super::*;
1158
1159 #[mz_persist_proc::test(tokio::test)]
1163 #[cfg_attr(miri, ignore)] async fn regression_minimum_since(dyncfgs: ConfigUpdates) {
1165 let data = vec![
1166 (("0".to_owned(), "zero".to_owned()), 0, 1),
1167 (("0".to_owned(), "zero".to_owned()), 1, -1),
1168 (("1".to_owned(), "one".to_owned()), 1, 1),
1169 ];
1170
1171 let cache = new_test_client_cache(&dyncfgs);
1172 cache.cfg.set_config(&BLOB_TARGET_SIZE, 100);
1173 let (mut write, _) = cache
1174 .open(PersistLocation::new_in_mem())
1175 .await
1176 .expect("client construction failed")
1177 .expect_open::<String, String, u64, i64>(ShardId::new())
1178 .await;
1179 let b0 = write
1180 .expect_batch(&data[..1], 0, 1)
1181 .await
1182 .into_hollow_batch();
1183 let b1 = write
1184 .expect_batch(&data[1..], 1, 2)
1185 .await
1186 .into_hollow_batch();
1187
1188 let req = CompactReq {
1189 shard_id: write.machine.shard_id(),
1190 desc: Description::new(
1191 b0.desc.lower().clone(),
1192 b1.desc.upper().clone(),
1193 Antichain::from_elem(10u64),
1194 ),
1195 inputs: vec![
1196 IdHollowBatch {
1197 batch: Arc::new(b0),
1198 id: SpineId(0, 1),
1199 },
1200 IdHollowBatch {
1201 batch: Arc::new(b1),
1202 id: SpineId(1, 2),
1203 },
1204 ],
1205 };
1206 let schemas = Schemas {
1207 id: None,
1208 key: Arc::new(StringSchema),
1209 val: Arc::new(StringSchema),
1210 };
1211 let res = Compactor::<String, String, u64, i64>::compact(
1212 CompactConfig::new(&write.cfg, write.shard_id()),
1213 Arc::clone(&write.blob),
1214 Arc::clone(&write.metrics),
1215 write.metrics.shards.shard(&write.machine.shard_id(), ""),
1216 Arc::new(IsolatedRuntime::new_for_tests()),
1217 req.clone(),
1218 schemas.clone(),
1219 )
1220 .await
1221 .expect("compaction failed");
1222
1223 assert_eq!(res.output.desc, req.desc);
1224 assert_eq!(res.output.len, 1);
1225 assert_eq!(res.output.part_count(), 1);
1226 let part = res.output.parts[0].expect_hollow_part();
1227 let (part, updates) = expect_fetch_part(
1228 write.blob.as_ref(),
1229 &part.key.complete(&write.machine.shard_id()),
1230 &write.metrics,
1231 &schemas,
1232 )
1233 .await;
1234 assert_eq!(part.desc, res.output.desc);
1235 assert_eq!(updates, all_ok(&data, 10));
1236 }
1237
1238 #[mz_persist_proc::test(tokio::test)]
1239 #[cfg_attr(miri, ignore)] async fn disable_compaction(dyncfgs: ConfigUpdates) {
1241 let data = [
1242 (("0".to_owned(), "zero".to_owned()), 0, 1),
1243 (("0".to_owned(), "zero".to_owned()), 1, -1),
1244 (("1".to_owned(), "one".to_owned()), 1, 1),
1245 ];
1246
1247 let cache = new_test_client_cache(&dyncfgs);
1248 cache.cfg.set_config(&BLOB_TARGET_SIZE, 100);
1249 let (mut write, _) = cache
1250 .open(PersistLocation::new_in_mem())
1251 .await
1252 .expect("client construction failed")
1253 .expect_open::<String, String, u64, i64>(ShardId::new())
1254 .await;
1255 let b0 = write
1256 .expect_batch(&data[..1], 0, 1)
1257 .await
1258 .into_hollow_batch();
1259 let b1 = write
1260 .expect_batch(&data[1..], 1, 2)
1261 .await
1262 .into_hollow_batch();
1263
1264 let req = CompactReq {
1265 shard_id: write.machine.shard_id(),
1266 desc: Description::new(
1267 b0.desc.lower().clone(),
1268 b1.desc.upper().clone(),
1269 Antichain::from_elem(10u64),
1270 ),
1271 inputs: vec![
1272 IdHollowBatch {
1273 batch: Arc::new(b0),
1274 id: SpineId(0, 1),
1275 },
1276 IdHollowBatch {
1277 batch: Arc::new(b1),
1278 id: SpineId(1, 2),
1279 },
1280 ],
1281 };
1282 write.cfg.set_config(&COMPACTION_HEURISTIC_MIN_INPUTS, 1);
1283 let compactor = write.compact.as_ref().expect("compaction hard disabled");
1284
1285 write.cfg.disable_compaction();
1286 let result = compactor
1287 .compact_and_apply_background(req.clone(), &write.machine)
1288 .expect("listener")
1289 .await
1290 .expect("channel closed");
1291 assert_err!(result);
1292 assert_contains!(result.unwrap_err().to_string(), "compaction disabled");
1293
1294 write.cfg.enable_compaction();
1295 compactor
1296 .compact_and_apply_background(req, &write.machine)
1297 .expect("listener")
1298 .await
1299 .expect("channel closed")
1300 .expect("compaction success");
1301
1302 let data2 = [
1304 (("2".to_owned(), "two".to_owned()), 2, 1),
1305 (("2".to_owned(), "two".to_owned()), 3, -1),
1306 (("3".to_owned(), "three".to_owned()), 3, 1),
1307 ];
1308
1309 let b2 = write
1310 .expect_batch(&data2[..1], 2, 3)
1311 .await
1312 .into_hollow_batch();
1313 let b3 = write
1314 .expect_batch(&data2[1..], 3, 4)
1315 .await
1316 .into_hollow_batch();
1317
1318 let req = CompactReq {
1319 shard_id: write.machine.shard_id(),
1320 desc: Description::new(
1321 b2.desc.lower().clone(),
1322 b3.desc.upper().clone(),
1323 Antichain::from_elem(20u64),
1324 ),
1325 inputs: vec![
1326 IdHollowBatch {
1327 batch: Arc::new(b2),
1328 id: SpineId(0, 1),
1329 },
1330 IdHollowBatch {
1331 batch: Arc::new(b3),
1332 id: SpineId(1, 2),
1333 },
1334 ],
1335 };
1336 let compactor = write.compact.as_ref().expect("compaction hard disabled");
1337
1338 write.cfg.set_config(&COMPACTION_CHECK_PROCESS_FLAG, false);
1340 write.cfg.disable_compaction();
1341 compactor
1343 .compact_and_apply_background(req, &write.machine)
1344 .expect("listener")
1345 .await
1346 .expect("channel closed")
1347 .expect("compaction success");
1348 }
1349}