1use std::cmp::Ordering;
49use std::collections::{BTreeMap, BTreeSet};
50use std::fmt::{Debug, Display};
51use std::mem;
52use std::ops::Range;
53use std::sync::Arc;
54
55use arrayvec::ArrayVec;
56use differential_dataflow::difference::Monoid;
57use differential_dataflow::lattice::Lattice;
58use differential_dataflow::trace::Description;
59use itertools::Itertools;
60use mz_ore::cast::CastFrom;
61use mz_persist::metrics::ColumnarMetrics;
62use mz_persist_types::Codec64;
63use serde::{Serialize, Serializer};
64use timely::PartialOrder;
65use timely::progress::frontier::AntichainRef;
66use timely::progress::{Antichain, Timestamp};
67use tracing::{error, warn};
68
69use crate::internal::paths::WriterKey;
70use crate::internal::state::{HollowBatch, RunId};
71
72use super::state::RunPart;
73
74#[derive(Debug, Clone, PartialEq)]
75pub struct FueledMergeReq<T> {
76 pub id: SpineId,
77 pub desc: Description<T>,
78 pub inputs: Vec<IdHollowBatch<T>>,
79}
80
81#[derive(Debug)]
82pub struct FueledMergeRes<T> {
83 pub output: HollowBatch<T>,
84 pub input: CompactionInput,
85 pub new_active_compaction: Option<ActiveCompaction>,
86}
87
88#[derive(Debug, Clone)]
93pub struct Trace<T> {
94 spine: Spine<T>,
95 pub(crate) roundtrip_structure: bool,
96}
97
98#[cfg(any(test, debug_assertions))]
99impl<T: PartialEq> PartialEq for Trace<T> {
100 fn eq(&self, other: &Self) -> bool {
101 let Trace {
104 spine: _,
105 roundtrip_structure: _,
106 } = self;
107 let Trace {
108 spine: _,
109 roundtrip_structure: _,
110 } = other;
111
112 self.batches().eq(other.batches())
115 }
116}
117
118impl<T: Timestamp + Lattice> Default for Trace<T> {
119 fn default() -> Self {
120 Self {
121 spine: Spine::new(),
122 roundtrip_structure: true,
123 }
124 }
125}
126
127#[derive(Clone, Debug, Serialize)]
128pub struct ThinSpineBatch<T> {
129 pub(crate) level: usize,
130 pub(crate) desc: Description<T>,
131 pub(crate) parts: Vec<SpineId>,
132 pub(crate) descs: Vec<Description<T>>,
135}
136
137impl<T: PartialEq> PartialEq for ThinSpineBatch<T> {
138 fn eq(&self, other: &Self) -> bool {
139 (self.level, &self.desc, &self.parts).eq(&(other.level, &other.desc, &other.parts))
141 }
142}
143
144#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
145pub struct ThinMerge<T> {
146 pub(crate) since: Antichain<T>,
147 pub(crate) remaining_work: usize,
148 pub(crate) active_compaction: Option<ActiveCompaction>,
149}
150
151impl<T: Clone> ThinMerge<T> {
152 fn fueling(merge: &FuelingMerge<T>) -> Self {
153 ThinMerge {
154 since: merge.since.clone(),
155 remaining_work: merge.remaining_work,
156 active_compaction: None,
157 }
158 }
159
160 fn fueled(batch: &SpineBatch<T>) -> Self {
161 ThinMerge {
162 since: batch.desc.since().clone(),
163 remaining_work: 0,
164 active_compaction: batch.active_compaction.clone(),
165 }
166 }
167}
168
169#[derive(Clone, Debug)]
177pub struct FlatTrace<T> {
178 pub(crate) since: Antichain<T>,
179 pub(crate) legacy_batches: BTreeMap<Arc<HollowBatch<T>>, ()>,
187 pub(crate) hollow_batches: BTreeMap<SpineId, Arc<HollowBatch<T>>>,
190 pub(crate) spine_batches: BTreeMap<SpineId, ThinSpineBatch<T>>,
195 pub(crate) merges: BTreeMap<SpineId, ThinMerge<T>>,
199}
200
201impl<T: Timestamp + Lattice> Trace<T> {
202 pub(crate) fn flatten(&self) -> FlatTrace<T> {
203 let since = self.spine.since.clone();
204 let mut legacy_batches = BTreeMap::new();
205 let mut hollow_batches = BTreeMap::new();
206 let mut spine_batches = BTreeMap::new();
207 let mut merges = BTreeMap::new();
208
209 let mut push_spine_batch = |level: usize, batch: &SpineBatch<T>| {
210 let id = batch.id();
211 let desc = batch.desc.clone();
212 let mut parts = Vec::with_capacity(batch.parts.len());
213 let mut descs = Vec::with_capacity(batch.parts.len());
214 for IdHollowBatch { id, batch } in &batch.parts {
215 parts.push(*id);
216 descs.push(batch.desc.clone());
217 if batch.desc.lower() == batch.desc.upper() {
225 hollow_batches.insert(*id, Arc::clone(batch));
226 } else {
227 legacy_batches.insert(Arc::clone(batch), ());
228 }
229 }
230
231 let spine_batch = ThinSpineBatch {
232 level,
233 desc,
234 parts,
235 descs,
236 };
237 spine_batches.insert(id, spine_batch);
238 };
239
240 for (level, state) in self.spine.merging.iter().enumerate() {
241 for batch in &state.batches {
242 push_spine_batch(level, batch);
243 if let Some(c) = &batch.active_compaction {
244 let previous = merges.insert(batch.id, ThinMerge::fueled(batch));
245 assert!(
246 previous.is_none(),
247 "recording a compaction for a batch that already exists! (level={level}, id={:?}, compaction={c:?})",
248 batch.id,
249 )
250 }
251 }
252 if let Some(IdFuelingMerge { id, merge }) = state.merge.as_ref() {
253 let previous = merges.insert(*id, ThinMerge::fueling(merge));
254 assert!(
255 previous.is_none(),
256 "fueling a merge for a batch that already exists! (level={level}, id={id:?}, merge={merge:?})"
257 )
258 }
259 }
260
261 if !self.roundtrip_structure {
262 assert!(hollow_batches.is_empty());
263 spine_batches.clear();
264 merges.clear();
265 }
266
267 FlatTrace {
268 since,
269 legacy_batches,
270 hollow_batches,
271 spine_batches,
272 merges,
273 }
274 }
275 pub(crate) fn unflatten(value: FlatTrace<T>) -> Result<Self, String> {
276 let FlatTrace {
277 since,
278 legacy_batches,
279 mut hollow_batches,
280 spine_batches,
281 mut merges,
282 } = value;
283
284 let roundtrip_structure = !spine_batches.is_empty() || legacy_batches.is_empty();
287
288 const MAX_TOTAL_LEN: usize = usize::MAX >> 3;
297 let mut total_len = 0usize;
298 for batch in legacy_batches.keys().chain(hollow_batches.values()) {
299 total_len = total_len
300 .checked_add(batch.len)
301 .filter(|len| *len <= MAX_TOTAL_LEN)
302 .ok_or_else(|| {
303 format!("total len of batches exceeds the maximum trace size: {batch:?}")
304 })?;
305 }
306
307 let compare_chains = |left: &Antichain<T>, right: &Antichain<T>| {
313 if PartialOrder::less_than(left, right) {
314 Ordering::Less
315 } else if PartialOrder::less_than(right, left) {
316 Ordering::Greater
317 } else {
318 Ordering::Equal
319 }
320 };
321 let mut legacy_batches: Vec<_> = legacy_batches.into_iter().map(|(k, _)| k).collect();
322 legacy_batches.sort_by(|a, b| compare_chains(a.desc.lower(), b.desc.lower()).reverse());
323
324 let mut pop_batch = |id: SpineId,
325 expected_desc: Option<&Description<T>>|
326 -> Result<_, String> {
327 if let Some(batch) = hollow_batches.remove(&id) {
328 if let Some(desc) = expected_desc {
329 if desc.lower() != batch.desc.lower() || desc.upper() != batch.desc.upper() {
331 return Err(format!(
332 "hollow batch desc {:?} did not match the spine batch desc {:?} for {id:?}",
333 batch.desc, desc
334 ));
335 }
336 if desc.since() != batch.desc.since() {
339 warn!(
340 "unexpected since out of sync for spine batch: {:?} != {:?}",
341 desc.since().elements(),
342 batch.desc.since().elements()
343 );
344 }
345 }
346 return Ok(IdHollowBatch { id, batch });
347 }
348 let mut batch = legacy_batches
349 .pop()
350 .ok_or_else(|| format!("missing referenced hollow batch {id:?}"))?;
351
352 let Some(expected_desc) = expected_desc else {
353 return Ok(IdHollowBatch { id, batch });
354 };
355
356 if expected_desc.lower() != batch.desc.lower() {
357 return Err(format!(
358 "hollow batch lower {:?} did not match expected lower {:?}",
359 batch.desc.lower().elements(),
360 expected_desc.lower().elements()
361 ));
362 }
363
364 if batch.parts.is_empty() && batch.run_splits.is_empty() && batch.len == 0 {
367 let mut new_upper = batch.desc.upper().clone();
368
369 while PartialOrder::less_than(&new_upper, expected_desc.upper()) {
372 let Some(next_batch) = legacy_batches.pop() else {
373 break;
374 };
375 if next_batch.is_empty() {
376 new_upper.clone_from(next_batch.desc.upper());
377 } else {
378 legacy_batches.push(next_batch);
379 break;
380 }
381 }
382
383 if PartialOrder::less_than(expected_desc.upper(), &new_upper) {
386 legacy_batches.push(Arc::new(HollowBatch::empty(Description::new(
387 expected_desc.upper().clone(),
388 new_upper.clone(),
389 batch.desc.since().clone(),
390 ))));
391 new_upper.clone_from(expected_desc.upper());
392 }
393 batch = Arc::new(HollowBatch::empty(Description::new(
394 batch.desc.lower().clone(),
395 new_upper,
396 batch.desc.since().clone(),
397 )))
398 }
399
400 if expected_desc.upper() != batch.desc.upper() {
401 return Err(format!(
402 "hollow batch upper {:?} did not match expected upper {:?}",
403 batch.desc.upper().elements(),
404 expected_desc.upper().elements()
405 ));
406 }
407
408 Ok(IdHollowBatch { id, batch })
409 };
410
411 let (upper, next_id) = if let Some((id, batch)) = spine_batches.last_key_value() {
412 (batch.desc.upper().clone(), id.1)
413 } else {
414 (Antichain::from_elem(T::minimum()), 0)
415 };
416 const MAX_LEVELS: usize = 256;
420 let levels = spine_batches
421 .first_key_value()
422 .map(|(_, batch)| batch.level.saturating_add(1))
423 .unwrap_or(0);
424 if levels > MAX_LEVELS {
425 return Err(format!(
426 "spine level {} exceeds the maximum {MAX_LEVELS}",
427 levels - 1
428 ));
429 }
430 let mut merging = vec![MergeState::default(); levels];
431 for (id, batch) in spine_batches {
432 let level = batch.level;
433
434 if batch.descs.len() > batch.parts.len() {
435 return Err(format!(
436 "spine batch {id:?} has more descs ({}) than parts ({})",
437 batch.descs.len(),
438 batch.parts.len()
439 ));
440 }
441 let descs = batch.descs.iter().map(Some).chain(std::iter::repeat_n(
442 None,
443 batch.parts.len() - batch.descs.len(),
444 ));
445 let parts = batch
446 .parts
447 .into_iter()
448 .zip_eq(descs)
449 .map(|(id, desc)| pop_batch(id, desc))
450 .collect::<Result<Vec<_>, _>>()?;
451 if parts.first().map(|x| x.id.0) != Some(id.0)
461 || parts.last().map(|x| x.id.1) != Some(id.1)
462 || parts.windows(2).any(|w| w[0].id.1 != w[1].id.0)
463 {
464 return Err(format!(
465 "spine batch {id:?} parts do not tile the batch's id range"
466 ));
467 }
468 let len = parts.iter().map(|p| (*p).batch.len).sum();
469 let active_compaction = merges.remove(&id).and_then(|m| m.active_compaction);
470 let batch = SpineBatch {
471 id,
472 desc: batch.desc,
473 parts,
474 active_compaction,
475 len,
476 };
477
478 let state = merging.get_mut(level).ok_or_else(|| {
479 format!("spine batch {id:?} level {level} out of bounds ({levels} levels)")
480 })?;
481
482 state.try_push_batch(batch)?;
483 if let Some(id) = state.id() {
484 if let Some(merge) = merges.remove(&id) {
485 state.merge = Some(IdFuelingMerge {
486 id,
487 merge: FuelingMerge {
488 since: merge.since,
489 remaining_work: merge.remaining_work,
490 },
491 })
492 }
493 }
494 }
495
496 let mut trace = Trace {
497 spine: Spine {
498 effort: 1,
499 next_id,
500 since,
501 upper,
502 merging,
503 },
504 roundtrip_structure,
505 };
506
507 fn check_empty(name: &str, len: usize) -> Result<(), String> {
508 if len != 0 {
509 Err(format!("{len} {name} left after reconstructing spine"))
510 } else {
511 Ok(())
512 }
513 }
514
515 if roundtrip_structure {
516 check_empty("legacy batches", legacy_batches.len())?;
517 } else {
518 for batch in legacy_batches.into_iter().rev() {
520 if batch.desc.lower() == batch.desc.upper() {
524 return Err(format!(
525 "legacy batch has an empty time range: {:?}",
526 batch.desc
527 ));
528 }
529 if batch.desc.lower() != trace.upper() {
530 return Err(format!(
531 "legacy batch lower {:?} does not match the trace upper {:?}",
532 batch.desc.lower().elements(),
533 trace.upper().elements()
534 ));
535 }
536 trace.push_batch_no_merge_reqs(Arc::unwrap_or_clone(batch));
537 }
538 }
539 check_empty("hollow batches", hollow_batches.len())?;
540 check_empty("merges", merges.len())?;
541
542 trace
548 .validate()
549 .map_err(|err| format!("reconstructed trace failed validation: {err}"))?;
550
551 Ok(trace)
552 }
553}
554
555#[derive(Clone, Debug, Default)]
556pub(crate) struct SpineMetrics {
557 pub compact_batches: u64,
558 pub compacting_batches: u64,
559 pub noncompact_batches: u64,
560}
561
562impl<T> Trace<T> {
563 pub fn since(&self) -> &Antichain<T> {
564 &self.spine.since
565 }
566
567 pub fn upper(&self) -> &Antichain<T> {
568 &self.spine.upper
569 }
570
571 pub fn map_batches<'a, F: FnMut(&'a HollowBatch<T>)>(&'a self, mut f: F) {
572 for batch in self.batches() {
573 f(batch);
574 }
575 }
576
577 pub fn batches(&self) -> impl Iterator<Item = &HollowBatch<T>> {
578 self.spine
579 .spine_batches()
580 .flat_map(|b| b.parts.as_slice())
581 .map(|b| &*b.batch)
582 }
583
584 pub fn num_spine_batches(&self) -> usize {
585 self.spine.spine_batches().count()
586 }
587
588 #[cfg(test)]
589 pub fn num_hollow_batches(&self) -> usize {
590 self.batches().count()
591 }
592
593 #[cfg(test)]
594 pub fn num_updates(&self) -> usize {
595 self.batches().map(|b| b.len).sum()
596 }
597}
598
599impl<T: Timestamp + Lattice> Trace<T> {
600 pub fn downgrade_since(&mut self, since: &Antichain<T>) {
601 self.spine.since.clone_from(since);
602 }
603
604 #[must_use]
605 pub fn push_batch(&mut self, batch: HollowBatch<T>) -> Vec<FueledMergeReq<T>> {
606 let mut merge_reqs = Vec::new();
607 self.spine.insert(
608 batch,
609 &mut SpineLog::Enabled {
610 merge_reqs: &mut merge_reqs,
611 },
612 );
613 debug_assert_eq!(self.spine.validate(), Ok(()), "{:?}", self);
614 Self::remove_redundant_merge_reqs(merge_reqs)
621 }
622
623 pub fn claim_compaction(&mut self, id: SpineId, compaction: ActiveCompaction) {
624 for batch in self.spine.spine_batches_mut().rev() {
627 if batch.id == id {
628 batch.active_compaction = Some(compaction);
629 break;
630 }
631 }
632 }
633
634 pub(crate) fn push_batch_no_merge_reqs(&mut self, batch: HollowBatch<T>) {
637 self.spine.insert(batch, &mut SpineLog::Disabled);
638 }
639
640 #[must_use]
648 pub fn exert(&mut self, fuel: usize) -> (Vec<FueledMergeReq<T>>, bool) {
649 let mut merge_reqs = Vec::new();
650 let did_work = self.spine.exert(
651 fuel,
652 &mut SpineLog::Enabled {
653 merge_reqs: &mut merge_reqs,
654 },
655 );
656 debug_assert_eq!(self.spine.validate(), Ok(()), "{:?}", self);
657 let merge_reqs = Self::remove_redundant_merge_reqs(merge_reqs);
659 (merge_reqs, did_work)
660 }
661
662 pub fn validate(&self) -> Result<(), String> {
666 self.spine.validate()
667 }
668
669 pub(crate) fn fueled_merge_reqs_before_ms(
672 &self,
673 threshold_ms: u64,
674 threshold_writer: Option<WriterKey>,
675 ) -> impl Iterator<Item = FueledMergeReq<T>> + '_ {
676 self.spine
677 .spine_batches()
678 .filter(move |b| {
679 let noncompact = !b.is_compact();
680 let old_writer = threshold_writer.as_ref().map_or(false, |min_writer| {
681 b.parts.iter().any(|b| {
682 b.batch
683 .parts
684 .iter()
685 .any(|p| p.writer_key().map_or(false, |writer| writer < *min_writer))
686 })
687 });
688 noncompact || old_writer
689 })
690 .filter(move |b| {
691 b.active_compaction
694 .as_ref()
695 .map_or(true, move |c| c.start_ms <= threshold_ms)
696 })
697 .map(|b| FueledMergeReq {
698 id: b.id,
699 desc: b.desc.clone(),
700 inputs: b.parts.clone(),
701 })
702 }
703
704 fn remove_redundant_merge_reqs(
711 mut merge_reqs: Vec<FueledMergeReq<T>>,
712 ) -> Vec<FueledMergeReq<T>> {
713 fn covers<T: PartialOrder>(b0: &FueledMergeReq<T>, b1: &FueledMergeReq<T>) -> bool {
715 b0.id.covers(b1.id) && b0.desc.since() == b1.desc.since()
717 }
718
719 let mut ret = Vec::<FueledMergeReq<T>>::with_capacity(merge_reqs.len());
720 while let Some(merge_req) = merge_reqs.pop() {
724 let covered = ret.iter().any(|r| covers(r, &merge_req));
725 if !covered {
726 ret.retain(|r| !covers(&merge_req, r));
730 ret.push(merge_req);
731 }
732 }
733 ret
734 }
735
736 pub fn spine_metrics(&self) -> SpineMetrics {
737 let mut metrics = SpineMetrics::default();
738 for batch in self.spine.spine_batches() {
739 if batch.is_compact() {
740 metrics.compact_batches += 1;
741 } else if batch.is_merging() {
742 metrics.compacting_batches += 1;
743 } else {
744 metrics.noncompact_batches += 1;
745 }
746 }
747 metrics
748 }
749}
750
751impl<T: Timestamp + Lattice + Codec64> Trace<T> {
752 pub fn apply_merge_res_checked<D: Codec64 + Monoid + PartialEq>(
753 &mut self,
754 res: &FueledMergeRes<T>,
755 metrics: &ColumnarMetrics,
756 ) -> ApplyMergeResult {
757 for batch in self.spine.spine_batches_mut().rev() {
758 let result = batch.maybe_replace_checked::<D>(res, metrics);
759 if result.matched() {
760 return result;
761 }
762 }
763 ApplyMergeResult::NotAppliedNoMatch
764 }
765
766 pub fn apply_merge_res_unchecked(&mut self, res: &FueledMergeRes<T>) -> ApplyMergeResult {
767 for batch in self.spine.spine_batches_mut().rev() {
768 let result = batch.maybe_replace_unchecked(res);
769 if result.matched() {
770 return result;
771 }
772 }
773 ApplyMergeResult::NotAppliedNoMatch
774 }
775
776 pub fn apply_tombstone_merge(&mut self, desc: &Description<T>) -> ApplyMergeResult {
777 for batch in self.spine.spine_batches_mut().rev() {
778 let result = batch.maybe_replace_with_tombstone(desc);
779 if result.matched() {
780 return result;
781 }
782 }
783 ApplyMergeResult::NotAppliedNoMatch
784 }
785}
786
787enum SpineLog<'a, T> {
790 Enabled {
791 merge_reqs: &'a mut Vec<FueledMergeReq<T>>,
792 },
793 Disabled,
794}
795
796#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
797pub enum CompactionInput {
798 Legacy,
801 IdRange(SpineId),
803 PartialBatch(SpineId, BTreeSet<RunId>),
805}
806
807#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
808pub struct SpineId(pub usize, pub usize);
809
810impl Display for SpineId {
811 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
812 write!(f, "[{}, {})", self.0, self.1)
813 }
814}
815
816impl Serialize for SpineId {
817 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
818 where
819 S: Serializer,
820 {
821 let SpineId(lo, hi) = self;
822 serializer.serialize_str(&format!("{lo}-{hi}"))
823 }
824}
825
826pub fn id_range(ids: BTreeSet<SpineId>) -> SpineId {
828 let mut id_iter = ids.iter().copied();
829 let Some(mut result) = id_iter.next() else {
830 panic!("at least one batch must be present")
831 };
832
833 for id in id_iter {
834 assert_eq!(
835 result.1, id.0,
836 "expected contiguous ids, but {result:?} is not adjacent to {id:?} in ids {ids:?}"
837 );
838 result.1 = id.1;
839 }
840 result
841}
842
843impl SpineId {
844 fn covers(self, other: SpineId) -> bool {
845 self.0 <= other.0 && other.1 <= self.1
846 }
847}
848
849#[derive(Debug, Clone, PartialEq)]
850pub struct IdHollowBatch<T> {
851 pub id: SpineId,
852 pub batch: Arc<HollowBatch<T>>,
853}
854
855#[derive(Debug, Clone, Eq, PartialEq, Serialize)]
856pub struct ActiveCompaction {
857 pub start_ms: u64,
858}
859
860#[derive(Debug, Clone, PartialEq)]
861struct SpineBatch<T> {
862 id: SpineId,
863 desc: Description<T>,
864 parts: Vec<IdHollowBatch<T>>,
865 active_compaction: Option<ActiveCompaction>,
866 len: usize,
868}
869
870impl<T> SpineBatch<T> {
871 fn merged(batch: IdHollowBatch<T>) -> Self
872 where
873 T: Clone,
874 {
875 Self {
876 id: batch.id,
877 desc: batch.batch.desc.clone(),
878 len: batch.batch.len,
879 parts: vec![batch],
880 active_compaction: None,
881 }
882 }
883}
884
885#[derive(Debug, Copy, Clone)]
886pub enum ApplyMergeResult {
887 AppliedExact,
888 AppliedSubset,
889 NotAppliedNoMatch,
890 NotAppliedInvalidSince,
891 NotAppliedTooManyUpdates,
892}
893
894impl ApplyMergeResult {
895 pub fn applied(&self) -> bool {
896 match self {
897 ApplyMergeResult::AppliedExact | ApplyMergeResult::AppliedSubset => true,
898 _ => false,
899 }
900 }
901 pub fn matched(&self) -> bool {
902 match self {
903 ApplyMergeResult::AppliedExact
904 | ApplyMergeResult::AppliedSubset
905 | ApplyMergeResult::NotAppliedTooManyUpdates => true,
906 _ => false,
907 }
908 }
909}
910
911impl<T: Timestamp + Lattice> SpineBatch<T> {
912 pub fn lower(&self) -> &Antichain<T> {
913 self.desc().lower()
914 }
915
916 pub fn upper(&self) -> &Antichain<T> {
917 self.desc().upper()
918 }
919
920 fn id(&self) -> SpineId {
921 mz_ore::soft_assert_eq_no_log!(self.parts.first().map(|x| x.id.0), Some(self.id.0));
922 mz_ore::soft_assert_eq_no_log!(self.parts.last().map(|x| x.id.1), Some(self.id.1));
923 self.id
924 }
925
926 pub fn is_compact(&self) -> bool {
927 self.parts
934 .iter()
935 .map(|p| p.batch.run_meta.len())
936 .sum::<usize>()
937 <= 1
938 }
939
940 pub fn is_merging(&self) -> bool {
941 self.active_compaction.is_some()
942 }
943
944 fn desc(&self) -> &Description<T> {
945 &self.desc
946 }
947
948 pub fn len(&self) -> usize {
949 debug_assert_eq!(
952 self.len,
953 self.parts.iter().map(|x| x.batch.len).sum::<usize>()
954 );
955 self.len
956 }
957
958 pub fn is_empty(&self) -> bool {
959 self.len() == 0
960 }
961
962 pub fn empty(
963 id: SpineId,
964 lower: Antichain<T>,
965 upper: Antichain<T>,
966 since: Antichain<T>,
967 ) -> Self {
968 SpineBatch::merged(IdHollowBatch {
969 id,
970 batch: Arc::new(HollowBatch::empty(Description::new(lower, upper, since))),
971 })
972 }
973
974 pub fn begin_merge(
975 bs: &[Self],
976 compaction_frontier: Option<AntichainRef<T>>,
977 ) -> Option<IdFuelingMerge<T>> {
978 let from = bs.first()?.id().0;
979 let until = bs.last()?.id().1;
980 let id = SpineId(from, until);
981 let mut sinces = bs.iter().map(|b| b.desc().since());
982 let mut since = sinces.next()?.clone();
983 for b in bs {
984 since.join_assign(b.desc().since())
985 }
986 if let Some(compaction_frontier) = compaction_frontier {
987 since.join_assign(&compaction_frontier.to_owned());
988 }
989 let remaining_work = bs.iter().map(|x| x.len()).sum();
990 Some(IdFuelingMerge {
991 id,
992 merge: FuelingMerge {
993 since,
994 remaining_work,
995 },
996 })
997 }
998
999 #[cfg(test)]
1000 fn describe(&self, extended: bool) -> String {
1001 let SpineBatch {
1002 id,
1003 parts,
1004 desc,
1005 active_compaction,
1006 len,
1007 } = self;
1008 let compaction = match active_compaction {
1009 None => "".to_owned(),
1010 Some(c) => format!(" (c@{})", c.start_ms),
1011 };
1012 match extended {
1013 false => format!(
1014 "[{}-{}]{:?}{:?}{}/{}{compaction}",
1015 id.0,
1016 id.1,
1017 desc.lower().elements(),
1018 desc.upper().elements(),
1019 parts.len(),
1020 len
1021 ),
1022 true => {
1023 format!(
1024 "[{}-{}]{:?}{:?}{:?} {}/{}{}{compaction}",
1025 id.0,
1026 id.1,
1027 desc.lower().elements(),
1028 desc.upper().elements(),
1029 desc.since().elements(),
1030 parts.len(),
1031 len,
1032 parts
1033 .iter()
1034 .flat_map(|x| x.batch.parts.iter())
1035 .map(|x| format!(" {}", x.printable_name()))
1036 .collect::<Vec<_>>()
1037 .join("")
1038 )
1039 }
1040 }
1041 }
1042}
1043
1044impl<T: Timestamp + Lattice + Codec64> SpineBatch<T> {
1045 fn diffs_sum<'a, D: Monoid + Codec64>(
1046 parts: impl IntoIterator<Item = &'a RunPart<T>>,
1047 metrics: &ColumnarMetrics,
1048 ) -> Option<D> {
1049 let mut sum = D::zero();
1050 for part in parts {
1051 sum.plus_equals(&part.diffs_sum::<D>(metrics)?);
1052 }
1053 Some(sum)
1054 }
1055
1056 fn batch_diffs_sum<D: Monoid + Codec64>(
1063 batch: &HollowBatch<T>,
1064 metrics: &ColumnarMetrics,
1065 ) -> Option<D> {
1066 let mut sum = D::zero();
1067 for (meta, run) in batch.runs() {
1068 if meta.bounds_truncated() {
1069 return None;
1070 }
1071 sum.plus_equals(&Self::diffs_sum(run, metrics)?);
1072 }
1073 Some(sum)
1074 }
1075
1076 fn diffs_sum_for_runs<D: Monoid + Codec64>(
1081 batch: &HollowBatch<T>,
1082 run_ids: &[RunId],
1083 metrics: &ColumnarMetrics,
1084 ) -> Option<D> {
1085 let mut run_ids = BTreeSet::from_iter(run_ids.iter().copied());
1086 let mut sum = D::zero();
1087
1088 for (meta, run) in batch.runs() {
1089 let id = meta.id?;
1090 if run_ids.remove(&id) {
1091 if meta.bounds_truncated() {
1092 return None;
1093 }
1094 sum.plus_equals(&Self::diffs_sum(run, metrics)?);
1095 }
1096 }
1097
1098 run_ids.is_empty().then_some(sum)
1099 }
1100
1101 fn maybe_replace_with_tombstone(&mut self, desc: &Description<T>) -> ApplyMergeResult {
1102 let exact_match =
1103 desc.lower() == self.desc().lower() && desc.upper() == self.desc().upper();
1104
1105 let empty_batch = HollowBatch::empty(desc.clone());
1106 if exact_match {
1107 *self = SpineBatch::merged(IdHollowBatch {
1108 id: self.id(),
1109 batch: Arc::new(empty_batch),
1110 });
1111 return ApplyMergeResult::AppliedExact;
1112 }
1113
1114 if let Some((id, range)) = self.find_replacement_range(desc) {
1115 self.perform_subset_replacement(&empty_batch, id, range, None)
1116 } else {
1117 ApplyMergeResult::NotAppliedNoMatch
1118 }
1119 }
1120
1121 fn construct_batch_with_runs_replaced(
1122 original: &HollowBatch<T>,
1123 run_ids: &[RunId],
1124 replacement: &HollowBatch<T>,
1125 ) -> Result<HollowBatch<T>, ApplyMergeResult> {
1126 if run_ids.is_empty() {
1127 return Err(ApplyMergeResult::NotAppliedNoMatch);
1128 }
1129
1130 let orig_run_ids: BTreeSet<_> = original.runs().filter_map(|(meta, _)| meta.id).collect();
1131 let run_ids: BTreeSet<_> = run_ids.iter().cloned().collect();
1132 if !orig_run_ids.is_superset(&run_ids) {
1133 return Err(ApplyMergeResult::NotAppliedNoMatch);
1134 }
1135
1136 let runs: Vec<_> = original
1137 .runs()
1138 .filter(|(meta, _)| {
1139 !run_ids.contains(&meta.id.expect("id should be present at this point"))
1140 })
1141 .chain(replacement.runs())
1142 .collect();
1143
1144 let len = runs.iter().filter_map(|(meta, _)| meta.len).sum::<usize>();
1145
1146 let run_meta = runs
1147 .iter()
1148 .map(|(meta, _)| *meta)
1149 .cloned()
1150 .collect::<Vec<_>>();
1151
1152 let parts = runs
1153 .iter()
1154 .flat_map(|(_, parts)| *parts)
1155 .cloned()
1156 .collect::<Vec<_>>();
1157
1158 let run_splits = {
1159 let mut splits = Vec::with_capacity(run_meta.len().saturating_sub(1));
1160 let mut pointer = 0;
1161 for (i, (_, parts)) in runs.into_iter().enumerate() {
1162 if parts.is_empty() {
1163 continue;
1164 }
1165 if i < run_meta.len() - 1 {
1166 splits.push(pointer + parts.len());
1167 }
1168 pointer += parts.len();
1169 }
1170 splits
1171 };
1172
1173 Ok(HollowBatch::new(
1174 replacement.desc.clone(),
1175 parts,
1176 len,
1177 run_meta,
1178 run_splits,
1179 ))
1180 }
1181
1182 fn maybe_replace_checked<D>(
1183 &mut self,
1184 res: &FueledMergeRes<T>,
1185 metrics: &ColumnarMetrics,
1186 ) -> ApplyMergeResult
1187 where
1188 D: Monoid + Codec64 + PartialEq + Debug,
1189 {
1190 if !PartialOrder::less_equal(res.output.desc.since(), self.desc().since()) {
1194 return ApplyMergeResult::NotAppliedInvalidSince;
1195 }
1196
1197 let new_diffs_sum = Self::diffs_sum(res.output.parts.iter(), metrics);
1198 let num_batches = self.parts.len();
1199
1200 let result = match &res.input {
1201 CompactionInput::IdRange(id) => {
1202 self.handle_id_range_replacement::<D>(res, id, new_diffs_sum, metrics)
1203 }
1204 CompactionInput::PartialBatch(id, runs) => {
1205 self.handle_partial_batch_replacement::<D>(res, *id, runs, new_diffs_sum, metrics)
1206 }
1207 CompactionInput::Legacy => self.maybe_replace_checked_classic::<D>(res, metrics),
1208 };
1209
1210 let num_batches_after = self.parts.len();
1211 assert!(
1212 num_batches_after <= num_batches,
1213 "replacing parts should not increase the number of batches"
1214 );
1215 result
1216 }
1217
1218 fn handle_id_range_replacement<D>(
1219 &mut self,
1220 res: &FueledMergeRes<T>,
1221 id: &SpineId,
1222 new_diffs_sum: Option<D>,
1223 metrics: &ColumnarMetrics,
1224 ) -> ApplyMergeResult
1225 where
1226 D: Monoid + Codec64 + PartialEq + Debug,
1227 {
1228 let range = self
1229 .parts
1230 .iter()
1231 .enumerate()
1232 .filter_map(|(i, p)| {
1233 if id.covers(p.id) {
1234 Some((i, p.id))
1235 } else {
1236 None
1237 }
1238 })
1239 .collect::<Vec<_>>();
1240
1241 let ids: BTreeSet<_> = range.iter().map(|(_, id)| *id).collect();
1242
1243 if ids.is_empty() || id != &id_range(ids) {
1253 return ApplyMergeResult::NotAppliedNoMatch;
1254 }
1255
1256 let (min, max) = match range.iter().map(|(i, _)| *i).minmax() {
1258 itertools::MinMaxResult::NoElements => return ApplyMergeResult::NotAppliedNoMatch,
1259 itertools::MinMaxResult::OneElement(elt) => (elt, elt),
1260 itertools::MinMaxResult::MinMax(min, max) => (min, max),
1261 };
1262 let replacement_range = min..max + 1;
1263
1264 let old_diffs_sum =
1267 self.parts[replacement_range.clone()]
1268 .iter()
1269 .try_fold(D::zero(), |mut sum, p| {
1270 sum.plus_equals(&Self::batch_diffs_sum::<D>(&p.batch, metrics)?);
1271 Some(sum)
1272 });
1273
1274 Self::validate_diffs_sum_match(old_diffs_sum, new_diffs_sum, "id range replacement");
1275
1276 self.perform_subset_replacement(
1277 &res.output,
1278 *id,
1279 replacement_range,
1280 res.new_active_compaction.clone(),
1281 )
1282 }
1283
1284 fn handle_partial_batch_replacement<D>(
1285 &mut self,
1286 res: &FueledMergeRes<T>,
1287 id: SpineId,
1288 runs: &BTreeSet<RunId>,
1289 new_diffs_sum: Option<D>,
1290 metrics: &ColumnarMetrics,
1291 ) -> ApplyMergeResult
1292 where
1293 D: Monoid + Codec64 + PartialEq + Debug,
1294 {
1295 if runs.is_empty() {
1296 return ApplyMergeResult::NotAppliedNoMatch;
1297 }
1298
1299 let part = self.parts.iter().enumerate().find(|(_, p)| p.id == id);
1300 let Some((i, batch)) = part else {
1301 return ApplyMergeResult::NotAppliedNoMatch;
1302 };
1303 let replacement_range = i..(i + 1);
1304
1305 let replacement_desc = &res.output.desc;
1306 let existing_desc = &batch.batch.desc;
1307 assert_eq!(
1308 replacement_desc.lower(),
1309 existing_desc.lower(),
1310 "batch lower should match, but {:?} != {:?}",
1311 replacement_desc.lower(),
1312 existing_desc.lower()
1313 );
1314 assert_eq!(
1315 replacement_desc.upper(),
1316 existing_desc.upper(),
1317 "batch upper should match, but {:?} != {:?}",
1318 replacement_desc.upper(),
1319 existing_desc.upper()
1320 );
1321 if !PartialOrder::less_equal(existing_desc.since(), replacement_desc.since()) {
1322 error!(
1323 "batch since should advance, but {:?} !<= {:?}",
1324 existing_desc.since(),
1325 replacement_desc.since()
1326 );
1327 return ApplyMergeResult::NotAppliedInvalidSince;
1328 }
1329
1330 let batch = &batch.batch;
1331 let run_ids = runs.iter().cloned().collect::<Vec<_>>();
1332
1333 match Self::construct_batch_with_runs_replaced(batch, &run_ids, &res.output) {
1334 Ok(new_batch) => {
1335 let old_diffs_sum = Self::diffs_sum_for_runs::<D>(batch, &run_ids, metrics);
1336 Self::validate_diffs_sum_match(
1337 old_diffs_sum,
1338 new_diffs_sum,
1339 "partial batch replacement",
1340 );
1341 let old_batch_diff_sum = Self::batch_diffs_sum::<D>(batch, metrics);
1342 let new_batch_diff_sum = Self::batch_diffs_sum::<D>(&new_batch, metrics);
1343 Self::validate_diffs_sum_match(
1344 old_batch_diff_sum,
1345 new_batch_diff_sum,
1346 "sanity checking diffs sum for replaced runs",
1347 );
1348 self.perform_subset_replacement(
1349 &new_batch,
1350 id,
1351 replacement_range,
1352 res.new_active_compaction.clone(),
1353 )
1354 }
1355 Err(err) => err,
1356 }
1357 }
1358
1359 fn validate_diffs_sum_match<D>(
1360 old_diffs_sum: Option<D>,
1361 new_diffs_sum: Option<D>,
1362 context: &str,
1363 ) where
1364 D: Monoid + Codec64 + PartialEq + Debug,
1365 {
1366 let new_diffs_sum = new_diffs_sum.unwrap_or_else(D::zero);
1367 if let Some(old_diffs_sum) = old_diffs_sum {
1368 assert_eq!(
1369 old_diffs_sum, new_diffs_sum,
1370 "merge res diffs sum ({:?}) did not match spine batch diffs sum ({:?}) ({})",
1371 new_diffs_sum, old_diffs_sum, context
1372 )
1373 }
1374 }
1375
1376 fn maybe_replace_checked_classic<D>(
1382 &mut self,
1383 res: &FueledMergeRes<T>,
1384 metrics: &ColumnarMetrics,
1385 ) -> ApplyMergeResult
1386 where
1387 D: Monoid + Codec64 + PartialEq + Debug,
1388 {
1389 if !PartialOrder::less_equal(res.output.desc.since(), self.desc().since()) {
1393 return ApplyMergeResult::NotAppliedInvalidSince;
1394 }
1395
1396 let new_diffs_sum = Self::diffs_sum(res.output.parts.iter(), metrics);
1397
1398 let exact_match = res.output.desc.lower() == self.desc().lower()
1400 && res.output.desc.upper() == self.desc().upper();
1401 if exact_match {
1402 let old_diffs_sum = self.parts.iter().try_fold(D::zero(), |mut sum, p| {
1403 sum.plus_equals(&Self::batch_diffs_sum::<D>(&p.batch, metrics)?);
1404 Some(sum)
1405 });
1406
1407 if let (Some(old_diffs_sum), Some(new_diffs_sum)) = (old_diffs_sum, new_diffs_sum) {
1408 assert_eq!(
1409 old_diffs_sum, new_diffs_sum,
1410 "merge res diffs sum ({:?}) did not match spine batch diffs sum ({:?})",
1411 new_diffs_sum, old_diffs_sum
1412 );
1413 }
1414
1415 if res.output.len > self.len() {
1424 return ApplyMergeResult::NotAppliedTooManyUpdates;
1425 }
1426 *self = SpineBatch::merged(IdHollowBatch {
1427 id: self.id(),
1428 batch: Arc::new(res.output.clone()),
1429 });
1430 return ApplyMergeResult::AppliedExact;
1431 }
1432
1433 if let Some((id, range)) = self.find_replacement_range(&res.output.desc) {
1435 let old_diffs_sum =
1436 self.parts[range.clone()]
1437 .iter()
1438 .try_fold(D::zero(), |mut sum, p| {
1439 sum.plus_equals(&Self::batch_diffs_sum::<D>(&p.batch, metrics)?);
1440 Some(sum)
1441 });
1442
1443 if let (Some(old_diffs_sum), Some(new_diffs_sum)) = (old_diffs_sum, new_diffs_sum) {
1444 assert_eq!(
1445 old_diffs_sum, new_diffs_sum,
1446 "merge res diffs sum ({:?}) did not match spine batch diffs sum ({:?})",
1447 new_diffs_sum, old_diffs_sum
1448 );
1449 }
1450
1451 self.perform_subset_replacement(
1452 &res.output,
1453 id,
1454 range,
1455 res.new_active_compaction.clone(),
1456 )
1457 } else {
1458 ApplyMergeResult::NotAppliedNoMatch
1459 }
1460 }
1461
1462 fn maybe_replace_unchecked(&mut self, res: &FueledMergeRes<T>) -> ApplyMergeResult {
1468 if !PartialOrder::less_equal(res.output.desc.since(), self.desc().since()) {
1472 return ApplyMergeResult::NotAppliedInvalidSince;
1473 }
1474
1475 let exact_match = res.output.desc.lower() == self.desc().lower()
1477 && res.output.desc.upper() == self.desc().upper();
1478 if exact_match {
1479 if res.output.len > self.len() {
1488 return ApplyMergeResult::NotAppliedTooManyUpdates;
1489 }
1490
1491 *self = SpineBatch::merged(IdHollowBatch {
1492 id: self.id(),
1493 batch: Arc::new(res.output.clone()),
1494 });
1495 return ApplyMergeResult::AppliedExact;
1496 }
1497
1498 if let Some((id, range)) = self.find_replacement_range(&res.output.desc) {
1500 self.perform_subset_replacement(
1501 &res.output,
1502 id,
1503 range,
1504 res.new_active_compaction.clone(),
1505 )
1506 } else {
1507 ApplyMergeResult::NotAppliedNoMatch
1508 }
1509 }
1510
1511 fn find_replacement_range(&self, desc: &Description<T>) -> Option<(SpineId, Range<usize>)> {
1513 let mut lower = None;
1523 let mut upper = None;
1524
1525 for (i, batch) in self.parts.iter().enumerate() {
1526 if batch.batch.desc.lower() == desc.lower() {
1527 lower = Some((i, batch.id.0));
1528 }
1529 if batch.batch.desc.upper() == desc.upper() {
1530 upper = Some((i, batch.id.1));
1531 }
1532 if lower.is_some() && upper.is_some() {
1533 break;
1534 }
1535 }
1536
1537 match (lower, upper) {
1538 (Some((lower_idx, id_lower)), Some((upper_idx, id_upper))) => {
1539 Some((SpineId(id_lower, id_upper), lower_idx..(upper_idx + 1)))
1540 }
1541 _ => None,
1542 }
1543 }
1544
1545 fn perform_subset_replacement(
1547 &mut self,
1548 res: &HollowBatch<T>,
1549 spine_id: SpineId,
1550 range: Range<usize>,
1551 new_active_compaction: Option<ActiveCompaction>,
1552 ) -> ApplyMergeResult {
1553 let SpineBatch {
1554 id,
1555 parts,
1556 desc,
1557 active_compaction: _,
1558 len: _,
1559 } = self;
1560
1561 let mut new_parts = vec![];
1562 new_parts.extend_from_slice(&parts[..range.start]);
1563 new_parts.push(IdHollowBatch {
1564 id: spine_id,
1565 batch: Arc::new(res.clone()),
1566 });
1567 new_parts.extend_from_slice(&parts[range.end..]);
1568
1569 let res = if range.len() == parts.len() {
1570 ApplyMergeResult::AppliedExact
1571 } else {
1572 ApplyMergeResult::AppliedSubset
1573 };
1574
1575 let new_spine_batch = SpineBatch {
1576 id: *id,
1577 desc: desc.to_owned(),
1578 len: new_parts.iter().map(|x| x.batch.len).sum(),
1579 parts: new_parts,
1580 active_compaction: new_active_compaction,
1581 };
1582
1583 if new_spine_batch.len() > self.len() {
1584 return ApplyMergeResult::NotAppliedTooManyUpdates;
1585 }
1586
1587 *self = new_spine_batch;
1588 res
1589 }
1590}
1591
1592#[derive(Debug, Clone, PartialEq, Serialize)]
1593pub struct FuelingMerge<T> {
1594 pub(crate) since: Antichain<T>,
1595 pub(crate) remaining_work: usize,
1596}
1597
1598#[derive(Debug, Clone, PartialEq, Serialize)]
1599pub struct IdFuelingMerge<T> {
1600 id: SpineId,
1601 merge: FuelingMerge<T>,
1602}
1603
1604impl<T: Timestamp + Lattice> FuelingMerge<T> {
1605 fn work(&mut self, _: &[SpineBatch<T>], fuel: &mut isize) {
1610 let available = usize::try_from(*fuel).unwrap_or(0);
1615 let used = std::cmp::min(available, self.remaining_work);
1616 self.remaining_work = self.remaining_work.saturating_sub(used);
1617 *fuel -= isize::try_from(used).expect("used is bounded by fuel");
1620 }
1621
1622 fn done(
1627 self,
1628 bs: ArrayVec<SpineBatch<T>, BATCHES_PER_LEVEL>,
1629 log: &mut SpineLog<'_, T>,
1630 ) -> Option<SpineBatch<T>> {
1631 let first = bs.first()?;
1632 let last = bs.last()?;
1633 let id = SpineId(first.id().0, last.id().1);
1634 assert!(id.0 < id.1);
1635 let lower = first.desc().lower().clone();
1636 let upper = last.desc().upper().clone();
1637 let since = self.since;
1638
1639 if bs.iter().all(SpineBatch::is_empty) {
1641 return Some(SpineBatch::empty(id, lower, upper, since));
1642 }
1643
1644 let desc = Description::new(lower, upper, since);
1645 let len = bs.iter().map(SpineBatch::len).sum();
1646
1647 let mut merged_parts_len = 0;
1651 for b in &bs {
1652 merged_parts_len += b.parts.len();
1653 }
1654 let mut merged_parts = Vec::with_capacity(merged_parts_len);
1655 for b in bs {
1656 merged_parts.extend(b.parts)
1657 }
1658 mz_ore::soft_assert_eq_no_log!(merged_parts.len(), merged_parts_len);
1660
1661 if let SpineLog::Enabled { merge_reqs } = log {
1662 merge_reqs.push(FueledMergeReq {
1663 id,
1664 desc: desc.clone(),
1665 inputs: merged_parts.clone(),
1666 });
1667 }
1668
1669 Some(SpineBatch {
1670 id,
1671 desc,
1672 len,
1673 parts: merged_parts,
1674 active_compaction: None,
1675 })
1676 }
1677}
1678
1679const BATCHES_PER_LEVEL: usize = 2;
1684
1685#[derive(Debug, Clone)]
1768struct Spine<T> {
1769 effort: usize,
1770 next_id: usize,
1771 since: Antichain<T>,
1772 upper: Antichain<T>,
1773 merging: Vec<MergeState<T>>,
1774}
1775
1776impl<T> Spine<T> {
1777 pub fn spine_batches(&self) -> impl Iterator<Item = &SpineBatch<T>> {
1779 self.merging.iter().rev().flat_map(|m| &m.batches)
1780 }
1781
1782 pub fn spine_batches_mut(&mut self) -> impl DoubleEndedIterator<Item = &mut SpineBatch<T>> {
1784 self.merging.iter_mut().rev().flat_map(|m| &mut m.batches)
1785 }
1786}
1787
1788impl<T: Timestamp + Lattice> Spine<T> {
1789 pub fn new() -> Self {
1796 Spine {
1797 effort: 1,
1798 next_id: 0,
1799 since: Antichain::from_elem(T::minimum()),
1800 upper: Antichain::from_elem(T::minimum()),
1801 merging: Vec::new(),
1802 }
1803 }
1804
1805 fn exert(&mut self, effort: usize, log: &mut SpineLog<'_, T>) -> bool {
1813 self.tidy_layers();
1814 if self.reduced() {
1815 return false;
1816 }
1817
1818 if self.merging.iter().any(|b| b.merge.is_some()) {
1819 let fuel = isize::try_from(effort).unwrap_or(isize::MAX);
1820 self.apply_fuel(&fuel, log);
1822 } else {
1823 let level = usize::cast_from(effort.next_power_of_two().trailing_zeros());
1828 let id = self.next_id();
1829 self.introduce_batch(
1830 SpineBatch::empty(
1831 id,
1832 self.upper.clone(),
1833 self.upper.clone(),
1834 self.since.clone(),
1835 ),
1836 level,
1837 log,
1838 );
1839 }
1840 true
1841 }
1842
1843 pub fn next_id(&mut self) -> SpineId {
1844 let id = self.next_id;
1845 self.next_id += 1;
1846 SpineId(id, self.next_id)
1847 }
1848
1849 pub fn insert(&mut self, batch: HollowBatch<T>, log: &mut SpineLog<'_, T>) {
1853 assert!(batch.desc.lower() != batch.desc.upper());
1854 assert_eq!(batch.desc.lower(), &self.upper);
1855
1856 let id = self.next_id();
1857 let batch = SpineBatch::merged(IdHollowBatch {
1858 id,
1859 batch: Arc::new(batch),
1860 });
1861
1862 self.upper.clone_from(batch.upper());
1863
1864 if batch.is_empty() {
1867 if let Some(position) = self.merging.iter().position(|m| !m.is_vacant()) {
1868 if self.merging[position].is_single() && self.merging[position].is_empty() {
1869 self.insert_at(batch, position);
1870 if let Some(merged) = self.complete_at(position, log) {
1873 self.merging[position] = MergeState::single(merged);
1874 }
1875 return;
1876 }
1877 }
1878 }
1879
1880 let index = batch.len().next_power_of_two();
1882 self.introduce_batch(batch, usize::cast_from(index.trailing_zeros()), log);
1883 }
1884
1885 fn reduced(&self) -> bool {
1892 self.spine_batches()
1893 .map(|b| {
1894 b.parts
1895 .iter()
1896 .map(|p| p.batch.run_meta.len())
1897 .sum::<usize>()
1898 })
1899 .sum::<usize>()
1900 < 2
1901 }
1902
1903 #[allow(dead_code)]
1907 fn describe(&self) -> Vec<(usize, usize)> {
1908 self.merging
1909 .iter()
1910 .map(|b| (b.batches.len(), b.len()))
1911 .collect()
1912 }
1913
1914 fn introduce_batch(
1920 &mut self,
1921 batch: SpineBatch<T>,
1922 batch_index: usize,
1923 log: &mut SpineLog<'_, T>,
1924 ) {
1925 if batch_index > 32 {
1946 println!("Large batch index: {}", batch_index);
1947 }
1948
1949 let fuel = u32::try_from(batch_index)
1963 .ok()
1964 .and_then(|shift| 8usize.checked_shl(shift))
1965 .and_then(|fuel| fuel.checked_mul(self.effort))
1968 .and_then(|fuel| isize::try_from(fuel).ok())
1969 .unwrap_or(isize::MAX);
1970
1971 self.apply_fuel(&fuel, log);
1978
1979 self.roll_up(batch_index, log);
1996
1997 self.insert_at(batch, batch_index);
2001
2002 self.tidy_layers();
2008 }
2009
2010 fn roll_up(&mut self, index: usize, log: &mut SpineLog<'_, T>) {
2018 while self.merging.len() <= index {
2020 self.merging.push(MergeState::default());
2021 }
2022
2023 if self.merging[..index].iter().any(|m| !m.is_vacant()) {
2025 let mut merged = None;
2028 for i in 0..index {
2029 if let Some(merged) = merged.take() {
2030 self.insert_at(merged, i);
2031 }
2032 merged = self.complete_at(i, log);
2033 }
2034
2035 if let Some(merged) = merged {
2039 self.insert_at(merged, index);
2040 }
2041
2042 if self.merging[index].is_full() {
2045 let merged = self.complete_at(index, log).expect("double batch");
2046 self.insert_at(merged, index + 1);
2047 }
2048 }
2049 }
2050
2051 pub fn apply_fuel(&mut self, fuel: &isize, log: &mut SpineLog<'_, T>) {
2059 for index in 0..self.merging.len() {
2065 let mut fuel = *fuel;
2067 self.merging[index].work(&mut fuel);
2070 if self.merging[index].is_complete() {
2082 let complete = self.complete_at(index, log).expect("complete batch");
2083 self.insert_at(complete, index + 1);
2084 }
2085 }
2086 }
2087
2088 fn insert_at(&mut self, batch: SpineBatch<T>, index: usize) {
2094 while self.merging.len() <= index {
2096 self.merging.push(MergeState::default());
2097 }
2098
2099 let merging = &mut self.merging[index];
2101 merging.push_batch(batch);
2102 if merging.batches.is_full() {
2103 let compaction_frontier = Some(self.since.borrow());
2104 merging.merge = SpineBatch::begin_merge(&merging.batches[..], compaction_frontier)
2105 }
2106 }
2107
2108 fn complete_at(&mut self, index: usize, log: &mut SpineLog<'_, T>) -> Option<SpineBatch<T>> {
2110 self.merging[index].complete(log)
2111 }
2112
2113 fn tidy_layers(&mut self) {
2115 if !self.merging.is_empty() {
2120 let mut length = self.merging.len();
2121 if self.merging[length - 1].is_single() {
2122 let appropriate_level = usize::cast_from(
2126 self.merging[length - 1]
2127 .len()
2128 .next_power_of_two()
2129 .trailing_zeros(),
2130 );
2131
2132 while appropriate_level < length - 1 {
2134 let current = &mut self.merging[length - 2];
2135 if current.is_vacant() {
2136 self.merging.remove(length - 2);
2138 length = self.merging.len();
2139 } else {
2140 if !current.is_full() {
2141 let mut smaller = 0;
2149 for (index, batch) in self.merging[..(length - 2)].iter().enumerate() {
2150 smaller += batch.batches.len() << index;
2151 }
2152
2153 if smaller <= (1 << length) / 8 {
2154 let state = self.merging.remove(length - 2);
2157 assert_eq!(state.batches.len(), 1);
2158 for batch in state.batches {
2159 self.insert_at(batch, length - 2);
2160 }
2161 }
2162 }
2163 break;
2164 }
2165 }
2166 }
2167 }
2168 }
2169
2170 fn validate(&self) -> Result<(), String> {
2178 let mut id = SpineId(0, 0);
2179 let mut frontier = Antichain::from_elem(T::minimum());
2180 for x in self.merging.iter().rev() {
2181 if x.is_full() != x.merge.is_some() {
2182 return Err(format!(
2183 "all (and only) full batches should have fueling merges (full={}, merge={:?})",
2184 x.is_full(),
2185 x.merge,
2186 ));
2187 }
2188
2189 if let Some(m) = &x.merge {
2190 if !x.is_full() {
2191 return Err(format!(
2192 "merge should only exist for full batches (len={:?}, merge={:?})",
2193 x.batches.len(),
2194 m.id,
2195 ));
2196 }
2197 if x.id() != Some(m.id) {
2198 return Err(format!(
2199 "merge id should match the range of the batch ids (batch={:?}, merge={:?})",
2200 x.id(),
2201 m.id,
2202 ));
2203 }
2204 }
2205
2206 for batch in &x.batches {
2211 if batch.id().0 != id.1 {
2212 return Err(format!(
2213 "batch id {:?} does not match the previous id {:?}: {:?}",
2214 batch.id(),
2215 id,
2216 self
2217 ));
2218 }
2219 id = batch.id();
2220 if batch.desc().lower() != &frontier {
2221 return Err(format!(
2222 "batch lower {:?} does not match the previous upper {:?}: {:?}",
2223 batch.desc().lower(),
2224 frontier,
2225 self
2226 ));
2227 }
2228 frontier.clone_from(batch.desc().upper());
2229 if !PartialOrder::less_equal(batch.desc().since(), &self.since) {
2230 return Err(format!(
2231 "since of batch {:?} past the spine since {:?}: {:?}",
2232 batch.desc().since(),
2233 self.since,
2234 self
2235 ));
2236 }
2237 }
2238 }
2239 if self.next_id != id.1 {
2240 return Err(format!(
2241 "spine next_id {:?} does not match the last batch's id {:?}: {:?}",
2242 self.next_id, id, self
2243 ));
2244 }
2245 if self.upper != frontier {
2246 return Err(format!(
2247 "spine upper {:?} does not match the last batch's upper {:?}: {:?}",
2248 self.upper, frontier, self
2249 ));
2250 }
2251 Ok(())
2252 }
2253}
2254
2255#[derive(Debug, Clone)]
2260struct MergeState<T> {
2261 batches: ArrayVec<SpineBatch<T>, BATCHES_PER_LEVEL>,
2262 merge: Option<IdFuelingMerge<T>>,
2263}
2264
2265impl<T> Default for MergeState<T> {
2266 fn default() -> Self {
2267 Self {
2268 batches: ArrayVec::new(),
2269 merge: None,
2270 }
2271 }
2272}
2273
2274impl<T: Timestamp + Lattice> MergeState<T> {
2275 fn id(&self) -> Option<SpineId> {
2277 if let (Some(first), Some(last)) = (self.batches.first(), self.batches.last()) {
2278 Some(SpineId(first.id().0, last.id().1))
2279 } else {
2280 None
2281 }
2282 }
2283
2284 fn single(batch: SpineBatch<T>) -> Self {
2286 let mut state = Self::default();
2287 state.push_batch(batch);
2288 state
2289 }
2290
2291 fn push_batch(&mut self, batch: SpineBatch<T>) {
2293 self.try_push_batch(batch)
2294 .unwrap_or_else(|err| panic!("invalid batch push: {err}"));
2295 }
2296
2297 fn try_push_batch(&mut self, batch: SpineBatch<T>) -> Result<(), String> {
2301 if let Some(last) = self.batches.last() {
2302 if last.id().1 != batch.id().0 {
2303 return Err(format!(
2304 "batch id {:?} does not chain with the previous id {:?}",
2305 batch.id(),
2306 last.id()
2307 ));
2308 }
2309 if last.upper() != batch.lower() {
2310 return Err(format!(
2311 "batch lower {:?} does not match the previous upper {:?}",
2312 batch.lower(),
2313 last.upper()
2314 ));
2315 }
2316 }
2317 if self.merge.is_some() {
2318 return Err(format!(
2319 "attempted to insert batch into incomplete merge! (batch={:?}, batch_count={})",
2320 batch.id,
2321 self.batches.len(),
2322 ));
2323 }
2324 if self.batches.try_push(batch).is_err() {
2325 return Err("attempted to insert batch into full layer!".to_string());
2326 }
2327 Ok(())
2328 }
2329
2330 fn len(&self) -> usize {
2332 self.batches.iter().map(SpineBatch::len).sum()
2333 }
2334
2335 fn is_empty(&self) -> bool {
2337 self.batches.iter().all(SpineBatch::is_empty)
2338 }
2339
2340 fn is_vacant(&self) -> bool {
2342 self.batches.is_empty()
2343 }
2344
2345 fn is_single(&self) -> bool {
2347 self.batches.len() == 1
2348 }
2349
2350 fn is_full(&self) -> bool {
2353 self.batches.is_full()
2354 }
2355
2356 fn complete(&mut self, log: &mut SpineLog<'_, T>) -> Option<SpineBatch<T>> {
2363 let mut this = mem::take(self);
2364 if this.batches.len() <= 1 {
2365 this.batches.pop()
2366 } else {
2367 let id_merge = this
2369 .merge
2370 .or_else(|| SpineBatch::begin_merge(&self.batches[..], None))?;
2371 id_merge.merge.done(this.batches, log)
2372 }
2373 }
2374
2375 fn is_complete(&self) -> bool {
2377 match &self.merge {
2378 Some(IdFuelingMerge { merge, .. }) => merge.remaining_work == 0,
2379 None => false,
2380 }
2381 }
2382
2383 fn work(&mut self, fuel: &mut isize) {
2385 if let Some(IdFuelingMerge { merge, .. }) = &mut self.merge {
2387 merge.work(&self.batches[..], fuel)
2388 }
2389 }
2390}
2391
2392#[cfg(test)]
2393pub mod datadriven {
2394 use mz_ore::fmt::FormatBuffer;
2395
2396 use crate::internal::datadriven::DirectiveArgs;
2397
2398 use super::*;
2399
2400 #[derive(Debug, Default)]
2402 pub struct TraceState {
2403 pub trace: Trace<u64>,
2404 pub merge_reqs: Vec<FueledMergeReq<u64>>,
2405 }
2406
2407 pub fn since_upper(
2408 datadriven: &TraceState,
2409 _args: DirectiveArgs,
2410 ) -> Result<String, anyhow::Error> {
2411 Ok(format!(
2412 "{:?}{:?}\n",
2413 datadriven.trace.since().elements(),
2414 datadriven.trace.upper().elements()
2415 ))
2416 }
2417
2418 pub fn batches(datadriven: &TraceState, _args: DirectiveArgs) -> Result<String, anyhow::Error> {
2419 let mut s = String::new();
2420 for b in datadriven.trace.spine.spine_batches() {
2421 s.push_str(b.describe(true).as_str());
2422 s.push('\n');
2423 }
2424 Ok(s)
2425 }
2426
2427 pub fn insert(
2428 datadriven: &mut TraceState,
2429 args: DirectiveArgs,
2430 ) -> Result<String, anyhow::Error> {
2431 for x in args
2432 .input
2433 .trim()
2434 .split('\n')
2435 .map(DirectiveArgs::parse_hollow_batch)
2436 {
2437 datadriven
2438 .merge_reqs
2439 .append(&mut datadriven.trace.push_batch(x));
2440 }
2441 Ok("ok\n".to_owned())
2442 }
2443
2444 pub fn downgrade_since(
2445 datadriven: &mut TraceState,
2446 args: DirectiveArgs,
2447 ) -> Result<String, anyhow::Error> {
2448 let since = args.expect("since");
2449 datadriven
2450 .trace
2451 .downgrade_since(&Antichain::from_elem(since));
2452 Ok("ok\n".to_owned())
2453 }
2454
2455 pub fn take_merge_req(
2456 datadriven: &mut TraceState,
2457 _args: DirectiveArgs,
2458 ) -> Result<String, anyhow::Error> {
2459 let mut s = String::new();
2460 for merge_req in std::mem::take(&mut datadriven.merge_reqs) {
2461 write!(
2462 s,
2463 "{:?}{:?}{:?} {}\n",
2464 merge_req.desc.lower().elements(),
2465 merge_req.desc.upper().elements(),
2466 merge_req.desc.since().elements(),
2467 merge_req
2468 .inputs
2469 .iter()
2470 .flat_map(|x| x.batch.parts.iter())
2471 .map(|x| x.printable_name())
2472 .collect::<Vec<_>>()
2473 .join(" ")
2474 );
2475 }
2476 Ok(s)
2477 }
2478
2479 pub fn apply_merge_res(
2480 datadriven: &mut TraceState,
2481 args: DirectiveArgs,
2482 ) -> Result<String, anyhow::Error> {
2483 let res = FueledMergeRes {
2484 output: DirectiveArgs::parse_hollow_batch(args.input),
2485 input: CompactionInput::Legacy,
2486 new_active_compaction: None,
2487 };
2488 match datadriven.trace.apply_merge_res_unchecked(&res) {
2489 ApplyMergeResult::AppliedExact => Ok("applied exact\n".into()),
2490 ApplyMergeResult::AppliedSubset => Ok("applied subset\n".into()),
2491 ApplyMergeResult::NotAppliedNoMatch => Ok("no-op\n".into()),
2492 ApplyMergeResult::NotAppliedInvalidSince => Ok("no-op invalid since\n".into()),
2493 ApplyMergeResult::NotAppliedTooManyUpdates => Ok("no-op too many updates\n".into()),
2494 }
2495 }
2496}
2497
2498#[cfg(test)]
2499pub(crate) mod tests {
2500 use std::ops::Range;
2501
2502 use proptest::prelude::*;
2503 use semver::Version;
2504
2505 use crate::internal::state::tests::{any_hollow_batch, any_hollow_batch_with_exact_runs};
2506
2507 use super::*;
2508
2509 pub fn any_trace<T: Arbitrary + Timestamp + Lattice>(
2510 num_batches: Range<usize>,
2511 ) -> impl Strategy<Value = Trace<T>> {
2512 Strategy::prop_map(
2513 (
2514 any::<Option<T>>(),
2515 proptest::collection::vec(any_hollow_batch::<T>(), num_batches),
2516 any::<bool>(),
2517 any::<u64>(),
2518 ),
2519 |(since, mut batches, roundtrip_structure, timeout_ms)| {
2520 let mut trace = Trace::<T>::default();
2521 trace.downgrade_since(&since.map_or_else(Antichain::new, Antichain::from_elem));
2522
2523 batches.sort_by(|x, y| x.desc.upper().elements().cmp(y.desc.upper().elements()));
2526 let mut lower = Antichain::from_elem(T::minimum());
2527 for mut batch in batches {
2528 if PartialOrder::less_than(trace.since(), batch.desc.since()) {
2530 trace.downgrade_since(batch.desc.since());
2531 }
2532 batch.desc = Description::new(
2533 lower.clone(),
2534 batch.desc.upper().clone(),
2535 batch.desc.since().clone(),
2536 );
2537 lower.clone_from(batch.desc.upper());
2538 let _merge_req = trace.push_batch(batch);
2539 }
2540 let reqs: Vec<_> = trace
2541 .fueled_merge_reqs_before_ms(timeout_ms, None)
2542 .collect();
2543 for req in reqs {
2544 trace.claim_compaction(req.id, ActiveCompaction { start_ms: 0 })
2545 }
2546 trace.roundtrip_structure = roundtrip_structure;
2547 trace
2548 },
2549 )
2550 }
2551
2552 #[mz_ore::test]
2553 #[cfg_attr(miri, ignore)] fn test_roundtrips() {
2555 fn check(trace: Trace<i64>) {
2556 trace.validate().unwrap();
2557 let flat = trace.flatten();
2558 let unflat = Trace::unflatten(flat).unwrap();
2559 assert_eq!(trace, unflat);
2560 }
2561
2562 proptest!(|(trace in any_trace::<i64>(1..10))| { check(trace) })
2563 }
2564
2565 #[mz_ore::test]
2566 fn fueled_merge_reqs() {
2567 let mut trace: Trace<u64> = Trace::default();
2568 let fueled_reqs = trace.push_batch(crate::internal::state::tests::hollow(
2569 0,
2570 10,
2571 &["n0011500/p3122e2a1-a0c7-429f-87aa-1019bf4f5f86"],
2572 1000,
2573 ));
2574
2575 assert!(fueled_reqs.is_empty());
2576 assert_eq!(
2577 trace.fueled_merge_reqs_before_ms(u64::MAX, None).count(),
2578 0,
2579 "no merge reqs when not filtering by version"
2580 );
2581 assert_eq!(
2582 trace
2583 .fueled_merge_reqs_before_ms(
2584 u64::MAX,
2585 Some(WriterKey::for_version(&Version::new(0, 50, 0)))
2586 )
2587 .count(),
2588 0,
2589 "zero batches are older than a past version"
2590 );
2591 assert_eq!(
2592 trace
2593 .fueled_merge_reqs_before_ms(
2594 u64::MAX,
2595 Some(WriterKey::for_version(&Version::new(99, 99, 0)))
2596 )
2597 .count(),
2598 1,
2599 "one batch is older than a future version"
2600 );
2601 }
2602
2603 #[mz_ore::test]
2604 fn remove_redundant_merge_reqs() {
2605 fn req(lower: u64, upper: u64) -> FueledMergeReq<u64> {
2606 FueledMergeReq {
2607 id: SpineId(usize::cast_from(lower), usize::cast_from(upper)),
2608 desc: Description::new(
2609 Antichain::from_elem(lower),
2610 Antichain::from_elem(upper),
2611 Antichain::new(),
2612 ),
2613 inputs: vec![],
2614 }
2615 }
2616
2617 assert_eq!(Trace::<u64>::remove_redundant_merge_reqs(vec![]), vec![]);
2619
2620 assert_eq!(
2622 Trace::remove_redundant_merge_reqs(vec![req(0, 1)]),
2623 vec![req(0, 1)]
2624 );
2625
2626 assert_eq!(
2628 Trace::remove_redundant_merge_reqs(vec![req(0, 1), req(0, 1)]),
2629 vec![req(0, 1)]
2630 );
2631
2632 assert_eq!(
2634 Trace::remove_redundant_merge_reqs(vec![req(0, 1), req(1, 2)]),
2635 vec![req(1, 2), req(0, 1)]
2636 );
2637
2638 assert_eq!(
2640 Trace::remove_redundant_merge_reqs(vec![req(1, 2), req(0, 3)]),
2641 vec![req(0, 3)]
2642 );
2643
2644 assert_eq!(
2646 Trace::remove_redundant_merge_reqs(vec![req(0, 2), req(0, 3)]),
2647 vec![req(0, 3)]
2648 );
2649
2650 assert_eq!(
2652 Trace::remove_redundant_merge_reqs(vec![req(1, 3), req(0, 3)]),
2653 vec![req(0, 3)]
2654 );
2655
2656 assert_eq!(
2658 Trace::remove_redundant_merge_reqs(vec![req(0, 3), req(1, 2)]),
2659 vec![req(0, 3)]
2660 );
2661
2662 assert_eq!(
2664 Trace::remove_redundant_merge_reqs(vec![req(0, 2), req(1, 3)]),
2665 vec![req(1, 3), req(0, 2)]
2666 );
2667
2668 assert_eq!(
2670 Trace::remove_redundant_merge_reqs(vec![req(1, 3), req(0, 2)]),
2671 vec![req(0, 2), req(1, 3)]
2672 );
2673
2674 let req015 = FueledMergeReq {
2676 id: SpineId(0, 1),
2677 desc: Description::new(
2678 Antichain::from_elem(0),
2679 Antichain::from_elem(1),
2680 Antichain::from_elem(5),
2681 ),
2682 inputs: vec![],
2683 };
2684 assert_eq!(
2685 Trace::remove_redundant_merge_reqs(vec![req(0, 1), req015.clone()]),
2686 vec![req015, req(0, 1)]
2687 );
2688 }
2689
2690 #[mz_ore::test]
2691 #[cfg_attr(miri, ignore)] fn construct_batch_with_runs_replaced_test() {
2693 let batch_strategy = any_hollow_batch::<u64>();
2694 let to_replace_strategy = any_hollow_batch_with_exact_runs::<u64>(1);
2695
2696 let combined_strategy = (batch_strategy, to_replace_strategy)
2697 .prop_filter("non-empty batch", |(batch, _)| batch.run_meta.len() >= 1);
2698
2699 let final_strategy = combined_strategy.prop_flat_map(|(batch, to_replace)| {
2700 let batch_len = batch.run_meta.len();
2701 let batch_clone = batch.clone();
2702 let to_replace_clone = to_replace.clone();
2703
2704 proptest::collection::vec(any::<bool>(), batch_len)
2705 .prop_filter("at least one run selected", |mask| mask.iter().any(|&x| x))
2706 .prop_map(move |mask| {
2707 let indices: Vec<usize> = mask
2708 .iter()
2709 .enumerate()
2710 .filter_map(|(i, &selected)| if selected { Some(i) } else { None })
2711 .collect();
2712 (batch_clone.clone(), to_replace_clone.clone(), indices)
2713 })
2714 });
2715
2716 proptest!(|(
2717 (batch, to_replace, runs) in final_strategy
2718 )| {
2719 let original_run_ids: Vec<_> = batch.run_meta.iter().map(|x|
2720 x.id.unwrap().clone()
2721 ).collect();
2722
2723 let run_ids = runs.iter().map(|&i| original_run_ids[i].clone()).collect::<Vec<_>>();
2724
2725 let new_batch = SpineBatch::construct_batch_with_runs_replaced(
2726 &batch,
2727 &run_ids,
2728 &to_replace,
2729 ).unwrap();
2730
2731 let expected_len = batch.run_meta.len() - runs.len()
2732 + to_replace.run_meta.len();
2733 prop_assert!(new_batch.run_meta.len() == expected_len);
2734 });
2735 }
2736
2737 #[mz_ore::test]
2738 fn test_perform_subset_replacement() {
2739 let batch1 = crate::internal::state::tests::hollow::<u64>(0, 10, &["a"], 10);
2740 let batch2 = crate::internal::state::tests::hollow::<u64>(10, 20, &["b"], 10);
2741 let batch3 = crate::internal::state::tests::hollow::<u64>(20, 30, &["c"], 10);
2742
2743 let id_batch1 = IdHollowBatch {
2744 id: SpineId(0, 1),
2745 batch: Arc::new(batch1.clone()),
2746 };
2747 let id_batch2 = IdHollowBatch {
2748 id: SpineId(1, 2),
2749 batch: Arc::new(batch2.clone()),
2750 };
2751 let id_batch3 = IdHollowBatch {
2752 id: SpineId(2, 3),
2753 batch: Arc::new(batch3.clone()),
2754 };
2755
2756 let spine_batch = SpineBatch {
2757 id: SpineId(0, 3),
2758 desc: Description::new(
2759 Antichain::from_elem(0),
2760 Antichain::from_elem(30),
2761 Antichain::from_elem(0),
2762 ),
2763 parts: vec![id_batch1, id_batch2, id_batch3],
2764 active_compaction: None,
2765 len: 30,
2766 };
2767
2768 let res_exact = crate::internal::state::tests::hollow::<u64>(0, 30, &["d"], 30);
2769 let mut sb_exact = spine_batch.clone();
2770 let result = sb_exact.perform_subset_replacement(&res_exact, SpineId(0, 3), 0..3, None);
2771 assert!(matches!(result, ApplyMergeResult::AppliedExact));
2772 assert_eq!(sb_exact.parts.len(), 1);
2773 assert_eq!(sb_exact.len(), 30);
2774
2775 let res_subset = crate::internal::state::tests::hollow::<u64>(0, 20, &["e"], 20);
2776 let mut sb_subset = spine_batch.clone();
2777 let result = sb_subset.perform_subset_replacement(&res_subset, SpineId(0, 2), 0..2, None);
2778 assert!(matches!(result, ApplyMergeResult::AppliedSubset));
2779 assert_eq!(sb_subset.parts.len(), 2); assert_eq!(sb_subset.len(), 30);
2781
2782 let res_too_big = crate::internal::state::tests::hollow::<u64>(0, 30, &["f"], 31);
2783 let mut sb_too_big = spine_batch.clone();
2784 let result = sb_too_big.perform_subset_replacement(&res_too_big, SpineId(0, 3), 0..3, None);
2785 assert!(matches!(result, ApplyMergeResult::NotAppliedTooManyUpdates));
2786 assert_eq!(sb_too_big.parts.len(), 3);
2787 assert_eq!(sb_too_big.len(), 30);
2788 }
2789
2790 #[mz_ore::test]
2799 fn spine_fuel_isize_overflow() {
2800 let mut trace = Trace::<u64>::default();
2801 let mut push = |lower, upper, key: &str, len| {
2802 trace.push_batch_no_merge_reqs(crate::internal::state::tests::hollow::<u64>(
2803 lower,
2804 upper,
2805 &[key],
2806 len,
2807 ));
2808 };
2809 push(0, 1, "a", 100);
2814 push(1, 2, "b", 100);
2815 push(2, 3, "c", 1);
2816 push(3, 4, "d", 1 << 60);
2817 }
2818}