Skip to main content

mz_persist_client/internal/
trace.rs

1// Copyright Materialize, Inc. and contributors. All rights reserved.
2//
3// Use of this software is governed by the Business Source License
4// included in the LICENSE file.
5//
6// As of the Change Date specified in that file, in accordance with
7// the Business Source License, use of this software will be governed
8// by the Apache License, Version 2.0.
9
10//! An append-only collection of compactable update batches. The Spine below is
11//! a fork of Differential Dataflow's [Spine] with minimal modifications. The
12//! original Spine code is designed for incremental (via "fuel"ing) synchronous
13//! merge of in-memory batches. Persist doesn't want compaction to block
14//! incoming writes and, in fact, may in the future elect to push the work of
15//! compaction onto another machine entirely via RPC. As a result, we abuse the
16//! Spine code as follows:
17//!
18//! [Spine]: differential_dataflow::trace::implementations::spine_fueled::Spine
19//!
20//! - The normal Spine works in terms of batches. A batch is added to
21//!   the Spine. As progress is made, the Spine will merge two batches together
22//!   by: constructing a merger, giving it bits of fuel to
23//!   incrementally perform the merge (which spreads out the work, keeping
24//!   latencies even), and then once it's done fueling extracting the new single
25//!   output `Batch` and discarding the inputs.
26//! - Persist instead represents a batch of blob data with a [HollowBatch]
27//!   pointer which contains the normal batch metadata plus the keys necessary
28//!   to retrieve the updates.
29//! - [SpineBatch] wraps `HollowBatch` and has a [FuelingMerge] companion
30//!   (analogous to differential's merger) that allows us to represent a merge as it
31//!   is fueling. Normally, this would represent real incremental compaction
32//!   progress, but in persist, it's simply a bookkeeping mechanism. Once fully
33//!   fueled, the `FuelingMerge` is turned into a fueled [SpineBatch],
34//!   which to the Spine is indistinguishable from a merged batch. At this
35//!   point, it is eligible for asynchronous compaction and a `FueledMergeReq`
36//!   is generated.
37//! - At any later point, this request may be answered via
38//!   [Trace::apply_merge_res_checked] or [Trace::apply_merge_res_unchecked].
39//!   This internally replaces the`SpineBatch`, which has no
40//!   effect on the structure of `Spine` but replaces the metadata
41//!   in persist's state to point at the new batch.
42//! - `SpineBatch` is explictly allowed to accumulate a list of `HollowBatch`s.
43//!   This decouples compaction from Spine progress and also allows us to reduce
44//!   write amplification by merging `N` batches at once where `N` can be
45//!   greater than 2.
46//!
47
48use 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/// An append-only collection of compactable update batches.
89///
90/// In an effort to keep our fork of Spine as close as possible to the original,
91/// we push as many changes as possible into this wrapper.
92#[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        // Deconstruct self and other so we get a compile failure if new fields
102        // are added.
103        let Trace {
104            spine: _,
105            roundtrip_structure: _,
106        } = self;
107        let Trace {
108            spine: _,
109            roundtrip_structure: _,
110        } = other;
111
112        // Intentionally use HollowBatches for this comparison so we ignore
113        // differences in spine layers.
114        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    /// NB: this exists to validate legacy batch bounds during the migration;
133    /// it can be deleted once the roundtrip_structure flag is permanently rolled out.
134    pub(crate) descs: Vec<Description<T>>,
135}
136
137impl<T: PartialEq> PartialEq for ThinSpineBatch<T> {
138    fn eq(&self, other: &Self) -> bool {
139        // Ignore the temporary descs vector when comparing for equality.
140        (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/// This is a "flattened" representation of a Trace. Goals:
170/// - small updates to the trace should result in small differences in the `FlatTrace`;
171/// - two `FlatTrace`s should be efficient to diff;
172/// - converting to and from a `Trace` should be relatively straightforward.
173///
174/// These goals are all somewhat in tension, and the space of possible representations is pretty
175/// large. See individual fields for comments on some of the tradeoffs.
176#[derive(Clone, Debug)]
177pub struct FlatTrace<T> {
178    pub(crate) since: Antichain<T>,
179    /// Hollow batches without an associated ID. If this flattened trace contains spine batches,
180    /// we can figure out which legacy batch belongs in which spine batch by comparing the `desc`s.
181    /// Previously, we serialized a trace as just this list of batches. Keeping this data around
182    /// helps ensure backwards compatibility. In the near future, we may still keep some batches
183    /// here to help minimize the size of diffs -- rewriting all the hollow batches in a shard
184    /// can be prohibitively expensive. Eventually, we'd like to remove this in favour of the
185    /// collection below.
186    pub(crate) legacy_batches: BTreeMap<Arc<HollowBatch<T>>, ()>,
187    /// Hollow batches _with_ an associated ID. Spine batches can reference these hollow batches
188    /// by id directly.
189    pub(crate) hollow_batches: BTreeMap<SpineId, Arc<HollowBatch<T>>>,
190    /// Spine batches stored by ID. We reference hollow batches by ID, instead of inlining them,
191    /// to make differential updates smaller when two batches merge together. We also store the
192    /// level on the batch, instead of mapping from level to a list of batches... the level of a
193    /// spine batch doesn't change over time, but the list of batches at a particular level does.
194    pub(crate) spine_batches: BTreeMap<SpineId, ThinSpineBatch<T>>,
195    /// In-progress merges. We store this by spine id instead of level to prepare for some possible
196    /// generalizations to spine (merging N of M batches at a level). This is also a natural place
197    /// to store incremental merge progress in the future.
198    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                // Ideally, we'd like to put all batches in the hollow_batches collection, since
218                // tracking the spine id reduces ambiguity and makes diffing cheaper. However,
219                // we currently keep most batches in the legacy collection for backwards
220                // compatibility.
221                // As an exception, we add batches with empty time ranges to hollow_batches:
222                // they're otherwise not guaranteed to be unique, and since we only started writing
223                // them down recently there's no backwards compatibility risk.
224                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        // If the flattened representation has spine batches (or is empty)
285        // we know to preserve the structure for this trace.
286        let roundtrip_structure = !spine_batches.is_empty() || legacy_batches.is_empty();
287
288        // The flattened trace is decoded from an untrusted blob: any invariant
289        // a crafted or corrupted value can violate must surface as a decode
290        // error here, never as a panic in the spine code below.
291        //
292        // Bound the total logical len of all batches. The spine's maintenance
293        // arithmetic (`len.next_power_of_two()`, summing lens of merged
294        // batches) overflows on absurd lens, and no real trace has anywhere
295        // near this many updates.
296        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        // We need to look up legacy batches somehow, but we don't have a spine id for them.
308        // Instead, we rely on the fact that the spine must store them in antichain order.
309        // Our timestamp type may not be totally ordered, so we need to implement our own comparator
310        // here. Persist's invariants ensure that all the frontiers we're comparing are comparable,
311        // though.
312        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                    // We don't expect the desc's upper and lower to change for a given spine id.
330                    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                    // Due to the way thin spine batches are diffed, the sinces can be out of sync.
337                    // This should be rare, and hopefully impossible once we change how diffs work.
338                    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            // Empty legacy batches are not deterministic: different nodes may split them up
365            // in different ways. For now, we rearrange them such to match the spine data.
366            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 our current batch is too small, and there's another empty batch
370                // in the list, roll it in.
371                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 our current batch is too large, split it by the expected upper
384                // and preserve the remainder.
385                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        // Real spine levels are logarithmic in the total len of the trace, so
417        // this bound is far above any legitimate level while keeping the
418        // allocation below trivial.
419        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            // A spine batch's parts tile its id range (`SpineBatch::id`
452            // `debug_assert`s the endpoints). Real batches always have at least
453            // one part: an empty batch still carries an empty hollow batch.
454            // Validate the full tiling, not just the endpoints: downstream
455            // maintenance (`fueled_merge_reqs_before_ms` -> `id_range` in
456            // compaction, `apply_merge_res_checked`) `assert_eq!`s that the
457            // collected part ids are contiguous, so non-adjacent parts that
458            // happen to hit the right endpoints would panic later instead of
459            // here.
460            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            // If the structure wasn't actually serialized, we may have legacy batches left over.
519            for batch in legacy_batches.into_iter().rev() {
520                // `Spine::insert` asserts that pushed batches are non-empty
521                // and contiguous; check this here so that a corrupted batch
522                // results in a decode error instead of a panic.
523                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        // The same check that's `debug_assert`ed when mutating a trace we
543        // built ourselves; for a trace reconstructed from untrusted data it
544        // must be a hard error, both to keep corrupted state from being used
545        // and because the write side would panic on it anyway (e.g. `Spine`'s
546        // batch invariants and the full-level/merge correspondence).
547        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        // Spine::roll_up (internally used by insert) clears all batches out of
615        // levels below a target by walking up from level 0 and merging each
616        // level into the next (providing the necessary fuel). In practice, this
617        // means we'll get a series of requests like `(a, b), (a, b, c), ...`.
618        // It's a waste to do all of these (we'll throw away the results), so we
619        // filter out any that are entirely covered by some other request.
620        Self::remove_redundant_merge_reqs(merge_reqs)
621    }
622
623    pub fn claim_compaction(&mut self, id: SpineId, compaction: ActiveCompaction) {
624        // TODO: we ought to be able to look up the id for a batch by binary searching the levels.
625        // In the meantime, search backwards, since most compactions are for recent batches.
626        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    /// The same as [Self::push_batch] but without the `FueledMergeReq`s, which
635    /// account for a surprising amount of cpu in prod. database-issues#5411
636    pub(crate) fn push_batch_no_merge_reqs(&mut self, batch: HollowBatch<T>) {
637        self.spine.insert(batch, &mut SpineLog::Disabled);
638    }
639
640    /// Apply some amount of effort to trace maintenance.
641    ///
642    /// The units of effort are updates, and the method should be thought of as
643    /// analogous to inserting as many empty updates, where the trace is
644    /// permitted to perform proportionate work.
645    ///
646    /// Returns true if this did work and false if it left the spine unchanged.
647    #[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        // See the comment in [Self::push_batch].
658        let merge_reqs = Self::remove_redundant_merge_reqs(merge_reqs);
659        (merge_reqs, did_work)
660    }
661
662    /// Validates invariants.
663    ///
664    /// See `Spine::validate` for details.
665    pub fn validate(&self) -> Result<(), String> {
666        self.spine.validate()
667    }
668
669    /// Obtain all fueled merge reqs that either have no active compaction, or the previous
670    /// compaction was started at or before the threshold time, in order from oldest to newest.
671    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                // Either there's no active compaction, or the last active compaction
692                // is not after the timeout timestamp.
693                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    // This is only called with the results of one `insert` and so the length of
705    // `merge_reqs` is bounded by the number of levels in the spine (or possibly
706    // some small constant multiple?). The number of levels is logarithmic in the
707    // number of updates in the spine, so this number should stay very small. As
708    // a result, we simply use the naive O(n^2) algorithm here instead of doing
709    // anything fancy with e.g. interval trees.
710    fn remove_redundant_merge_reqs(
711        mut merge_reqs: Vec<FueledMergeReq<T>>,
712    ) -> Vec<FueledMergeReq<T>> {
713        // Returns true if b0 covers b1, false otherwise.
714        fn covers<T: PartialOrder>(b0: &FueledMergeReq<T>, b1: &FueledMergeReq<T>) -> bool {
715            // TODO: can we relax or remove this since check?
716            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        // In practice, merge_reqs will come in sorted such that the "large"
721        // requests are later. Take advantage of this by processing back to
722        // front.
723        while let Some(merge_req) = merge_reqs.pop() {
724            let covered = ret.iter().any(|r| covers(r, &merge_req));
725            if !covered {
726                // Now check if anything we've already staged is covered by this
727                // new req. In practice, the merge_reqs come in sorted and so
728                // this `retain` is a no-op.
729                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
787/// A log of what transitively happened during a Spine operation: e.g.
788/// FueledMergeReqs were generated.
789enum 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    /// We don't know what our inputs were; this should only be used for
799    /// unchecked legacy replacements.
800    Legacy,
801    /// This compaction output is a total replacement for all batches in this id range.
802    IdRange(SpineId),
803    /// This compaction output replaces the specified runs in this id range.
804    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
826/// Creates a `SpineId` that covers the range of ids in the set.
827pub 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    // A cached version of parts.iter().map(|x| x.len).sum()
867    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        // A compact batch has at most one run.
928        // This check used to be if there was at most one hollow batch with at most one run,
929        // but that was a bit too strict since introducing incremental compaction.
930        // Incremental compaction can result in a batch with a single run, but multiple empty
931        // hollow batches, which we still consider compact. As levels are merged, we
932        // will eventually clean up the empty hollow batches.
933        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        // NB: This is an upper bound on len for a non-compact batch; we won't know for sure until
950        // we compact it.
951        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    /// Get the diff sum across all runs of the given batch.
1057    ///
1058    /// Returns `None` if any parts don't have statistics, or if any run may
1059    /// hold updates outside the batch's registered desc: the statistics count
1060    /// those updates but readers filter them out, so no sum computed from the
1061    /// statistics can be compared against data seen through a read.
1062    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    /// Get the diff sum from the given batch for the given runs.
1077    /// Returns `None` if the runs aren't present, any parts don't have
1078    /// statistics, or any of the runs may hold updates outside the batch's
1079    /// registered desc (see [Self::batch_diffs_sum]).
1080    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        // The spine's and merge res's sinces don't need to match (which could occur if Spine
1191        // has been reloaded from state due to compare_and_set mismatch), but if so, the Spine
1192        // since must be in advance of the merge res since.
1193        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, it means that we didn't find any parts that match the id range.
1244        // We also check that the id matches the range of ids we found.
1245        // At scale, sometimes regular compaction will race forced compaction,
1246        // for things like the catalog. In that case, we may have a
1247        // replacement that no longer lines up with the spine batches.
1248        // I think this is because forced compaction ignores the active_compaction
1249        // and just goes for it. This is slightly annoying but probably the right behavior
1250        // for a functions whose prefix is `force_`, so we just return
1251        // NotAppliedNoMatch here.
1252        if ids.is_empty() || id != &id_range(ids) {
1253            return ApplyMergeResult::NotAppliedNoMatch;
1254        }
1255
1256        // This is the range of hollow batches that we will replace.
1257        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        // We need to replace a range of parts. Here we don't care about the run_indices
1265        // because we must be replacing the entire part(s)
1266        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    /// This is the "legacy" way of replacing a spine batch with a merge result.
1377    /// It is used in moments when we don't have the full compaction input
1378    /// information.
1379    /// Eventually we should strive to roundtrip Spine IDs everywhere and
1380    /// deprecate this method.
1381    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        // The spine's and merge res's sinces don't need to match (which could occur if Spine
1390        // has been reloaded from state due to compare_and_set mismatch), but if so, the Spine
1391        // since must be in advance of the merge res since.
1392        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        // If our merge result exactly matches a spine batch, we can swap it in directly
1399        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            // Spine internally has an invariant about a batch being at some level
1416            // or higher based on the len. We could end up violating this invariant
1417            // if we increased the length of the batch.
1418            //
1419            // A res output with length greater than the existing spine batch implies
1420            // a compaction has already been applied to this range, and with a higher
1421            // rate of consolidation than this one. This could happen as a result of
1422            // compaction's memory bound limiting the amount of consolidation possible.
1423            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        // Try subset replacement
1434        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    /// This is the even more legacy way of replacing a spine batch with a merge result.
1463    /// It is used in moments when we don't have the full compaction input
1464    /// information, and we don't have the diffs sum.
1465    /// Eventually we should strive to roundtrip Spine IDs and diffs sums everywhere and
1466    /// deprecate this method.
1467    fn maybe_replace_unchecked(&mut self, res: &FueledMergeRes<T>) -> ApplyMergeResult {
1468        // The spine's and merge res's sinces don't need to match (which could occur if Spine
1469        // has been reloaded from state due to compare_and_set mismatch), but if so, the Spine
1470        // since must be in advance of the merge res since.
1471        if !PartialOrder::less_equal(res.output.desc.since(), self.desc().since()) {
1472            return ApplyMergeResult::NotAppliedInvalidSince;
1473        }
1474
1475        // If our merge result exactly matches a spine batch, we can swap it in directly
1476        let exact_match = res.output.desc.lower() == self.desc().lower()
1477            && res.output.desc.upper() == self.desc().upper();
1478        if exact_match {
1479            // Spine internally has an invariant about a batch being at some level
1480            // or higher based on the len. We could end up violating this invariant
1481            // if we increased the length of the batch.
1482            //
1483            // A res output with length greater than the existing spine batch implies
1484            // a compaction has already been applied to this range, and with a higher
1485            // rate of consolidation than this one. This could happen as a result of
1486            // compaction's memory bound limiting the amount of consolidation possible.
1487            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        // Try subset replacement
1499        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    /// Find the range of parts that can be replaced by the merge result
1512    fn find_replacement_range(&self, desc: &Description<T>) -> Option<(SpineId, Range<usize>)> {
1513        // It is possible the structure of the spine has changed since the merge res
1514        // was created, such that it no longer exactly matches the description of a
1515        // spine batch. This can happen if another merge has happened in the interim,
1516        // or if spine needed to be rebuilt from state.
1517        //
1518        // When this occurs, we can still attempt to slot the merge res in to replace
1519        // the parts of a fueled merge. e.g. if the res is for `[1,3)` and the parts
1520        // are `[0,1),[1,2),[2,3),[3,4)`, we can swap out the middle two parts for res.
1521
1522        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    /// Perform the actual subset replacement
1546    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    /// Perform some amount of work, decrementing `fuel`.
1606    ///
1607    /// If `fuel` is non-zero after the call, the merging is complete and one
1608    /// should call `done` to extract the merged results.
1609    fn work(&mut self, _: &[SpineBatch<T>], fuel: &mut isize) {
1610        // A negative `fuel` means a caller already overspent, so there is
1611        // nothing to spend here. Reading it as a `usize` instead would wrap to a
1612        // huge budget, spend `remaining_work` against it, and then underflow the
1613        // subtraction below.
1614        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        // `used <= available <= isize::MAX`, so neither conversion nor the
1618        // subtraction can overflow.
1619        *fuel -= isize::try_from(used).expect("used is bounded by fuel");
1620    }
1621
1622    /// Extracts merged results.
1623    ///
1624    /// This method should only be called after `work` has been called and has
1625    /// not brought `fuel` to zero. Otherwise, the merge is still in progress.
1626    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        // Special case empty batches.
1640        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        // Pre-size the merged_parts Vec. Benchmarking has shown that, at least
1648        // in the worst case, the double iteration is absolutely worth having
1649        // merged_parts pre-sized.
1650        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        // Sanity check the pre-size code.
1659        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
1679/// The maximum number of batches per level in the spine.
1680/// In practice, we probably want a larger max and a configurable soft cap, but using a
1681/// stack-friendly data structure and keeping this number low makes this safer during the
1682/// initial rollout.
1683const BATCHES_PER_LEVEL: usize = 2;
1684
1685/// An append-only collection of update batches.
1686///
1687/// The `Spine` is a general-purpose trace implementation based on collection
1688/// and merging immutable batches of updates. It is generic with respect to the
1689/// batch type.
1690///
1691/// ## Design
1692///
1693/// This spine is represented as a list of layers, where each element in the
1694/// list is either
1695///
1696///   1. MergeState::Vacant  empty
1697///   2. MergeState::Single  a single batch
1698///   3. MergeState::Double  a pair of batches
1699///
1700/// Each "batch" has the option to be `None`, indicating a non-batch that
1701/// nonetheless acts as a number of updates proportionate to the level at which
1702/// it exists (for bookkeeping).
1703///
1704/// Each of the batches at layer i contains at most 2^i elements. The sequence
1705/// of batches should have the upper bound of one match the lower bound of the
1706/// next. Batches may be logically empty, with matching upper and lower bounds,
1707/// as a bookkeeping mechanism.
1708///
1709/// Each batch at layer i is treated as if it contains exactly 2^i elements,
1710/// even though it may actually contain fewer elements. This allows us to
1711/// decouple the physical representation from logical amounts of effort invested
1712/// in each batch. It allows us to begin compaction and to reduce the number of
1713/// updates, without compromising our ability to continue to move updates along
1714/// the spine. We are explicitly making the trade-off that while some batches
1715/// might compact at lower levels, we want to treat them as if they contained
1716/// their full set of updates for accounting reasons (to apply work to higher
1717/// levels).
1718///
1719/// We maintain the invariant that for any in-progress merge at level k there
1720/// should be fewer than 2^k records at levels lower than k. That is, even if we
1721/// were to apply an unbounded amount of effort to those records, we would not
1722/// have enough records to prompt a merge into the in-progress merge. Ideally,
1723/// we maintain the extended invariant that for any in-progress merge at level
1724/// k, the remaining effort required (number of records minus applied effort) is
1725/// less than the number of records that would need to be added to reach 2^k
1726/// records in layers below.
1727///
1728/// ## Mathematics
1729///
1730/// When a merge is initiated, there should be a non-negative *deficit* of
1731/// updates before the layers below could plausibly produce a new batch for the
1732/// currently merging layer. We must determine a factor of proportionality, so
1733/// that newly arrived updates provide at least that amount of "fuel" towards
1734/// the merging layer, so that the merge completes before lower levels invade.
1735///
1736/// ### Deficit:
1737///
1738/// A new merge is initiated only in response to the completion of a prior
1739/// merge, or the introduction of new records from outside. The latter case is
1740/// special, and will maintain our invariant trivially, so we will focus on the
1741/// former case.
1742///
1743/// When a merge at level k completes, assuming we have maintained our invariant
1744/// then there should be fewer than 2^k records at lower levels. The newly
1745/// created merge at level k+1 will require up to 2^k+2 units of work, and
1746/// should not expect a new batch until strictly more than 2^k records are
1747/// added. This means that a factor of proportionality of four should be
1748/// sufficient to ensure that the merge completes before a new merge is
1749/// initiated.
1750///
1751/// When new records get introduced, we will need to roll up any batches at
1752/// lower levels, which we treat as the introduction of records. Each of these
1753/// virtual records introduced should either be accounted for the fuel it should
1754/// contribute, as it results in the promotion of batches closer to in-progress
1755/// merges.
1756///
1757/// ### Fuel sharing
1758///
1759/// We like the idea of applying fuel preferentially to merges at *lower*
1760/// levels, under the idea that they are easier to complete, and we benefit from
1761/// fewer total merges in progress. This does delay the completion of merges at
1762/// higher levels, and may not obviously be a total win. If we choose to do
1763/// this, we should make sure that we correctly account for completed merges at
1764/// low layers: they should still extract fuel from new updates even though they
1765/// have completed, at least until they have paid back any "debt" to higher
1766/// layers by continuing to provide fuel as updates arrive.
1767#[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    /// All batches in the spine, oldest to newest.
1778    pub fn spine_batches(&self) -> impl Iterator<Item = &SpineBatch<T>> {
1779        self.merging.iter().rev().flat_map(|m| &m.batches)
1780    }
1781
1782    /// All (mutable) batches in the spine, oldest to newest.
1783    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    /// Allocates a fueled `Spine`.
1790    ///
1791    /// This trace will merge batches progressively, with each inserted batch
1792    /// applying a multiple of the batch's length in effort to each merge. The
1793    /// `effort` parameter is that multiplier. This value should be at least one
1794    /// for the merging to happen; a value of zero is not helpful.
1795    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    /// Apply some amount of effort to trace maintenance.
1806    ///
1807    /// The units of effort are updates, and the method should be thought of as
1808    /// analogous to inserting as many empty updates, where the trace is
1809    /// permitted to perform proportionate work.
1810    ///
1811    /// Returns true if this did work and false if it left the spine unchanged.
1812    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            // If any merges exist, we can directly call `apply_fuel`.
1821            self.apply_fuel(&fuel, log);
1822        } else {
1823            // Otherwise, we'll need to introduce fake updates to move merges
1824            // along.
1825
1826            // Introduce an empty batch with roughly *effort number of virtual updates.
1827            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    // Ideally, this method acts as insertion of `batch`, even if we are not yet
1850    // able to begin merging the batch. This means it is a good time to perform
1851    // amortized work proportional to the size of batch.
1852    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` and the most recently inserted batch are both empty,
1865        // we can just fuse them.
1866        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                    // Since we just inserted a batch, we should always have work to complete...
1871                    // but otherwise we just leave this layer vacant.
1872                    if let Some(merged) = self.complete_at(position, log) {
1873                        self.merging[position] = MergeState::single(merged);
1874                    }
1875                    return;
1876                }
1877            }
1878        }
1879
1880        // Normal insertion for the batch.
1881        let index = batch.len().next_power_of_two();
1882        self.introduce_batch(batch, usize::cast_from(index.trailing_zeros()), log);
1883    }
1884
1885    /// Returns true when the trace is considered *structurally reduced*.
1886    ///
1887    /// Reduced == the total number of runs (across every
1888    /// `SpineBatch` and all of their inner hollow batches) is < 2. In other
1889    /// words, there are either zero runs (fully empty) or exactly one logical
1890    /// run of data remaining.
1891    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    /// Describes the merge progress of layers in the trace.
1904    ///
1905    /// Intended for diagnostics rather than public consumption.
1906    #[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    /// Introduces a batch at an indicated level.
1915    ///
1916    /// The level indication is often related to the size of the batch, but it
1917    /// can also be used to artificially fuel the computation by supplying empty
1918    /// batches at non-trivial indices, to move merges along.
1919    fn introduce_batch(
1920        &mut self,
1921        batch: SpineBatch<T>,
1922        batch_index: usize,
1923        log: &mut SpineLog<'_, T>,
1924    ) {
1925        // Step 0.  Determine an amount of fuel to use for the computation.
1926        //
1927        //          Fuel is used to drive maintenance of the data structure,
1928        //          and in particular are used to make progress through merges
1929        //          that are in progress. The amount of fuel to use should be
1930        //          proportional to the number of records introduced, so that
1931        //          we are guaranteed to complete all merges before they are
1932        //          required as arguments to merges again.
1933        //
1934        //          The fuel use policy is negotiable, in that we might aim
1935        //          to use relatively less when we can, so that we return
1936        //          control promptly, or we might account more work to larger
1937        //          batches. Not clear to me which are best, of if there
1938        //          should be a configuration knob controlling this.
1939
1940        // The amount of fuel to use is proportional to 2^batch_index, scaled by
1941        // a factor of self.effort which determines how eager we are in
1942        // performing maintenance work. We need to ensure that each merge in
1943        // progress receives fuel for each introduced batch, and so multiply by
1944        // that as well.
1945        if batch_index > 32 {
1946            println!("Large batch index: {}", batch_index);
1947        }
1948
1949        // We believe that eight units of fuel is sufficient for each introduced
1950        // record, accounted as four for each record, and a potential four more
1951        // for each virtual record associated with promoting existing smaller
1952        // batches. We could try and make this be less, or be scaled to merges
1953        // based on their deficit at time of instantiation. For now, we remain
1954        // conservative.
1955        //
1956        // `batch_index` is derived from a batch's `len`, which for a trace
1957        // reconstructed from an untrusted blob can be large enough that
1958        // `8 << batch_index` overflows. Saturate at `isize::MAX` rather than
1959        // wrapping: fuel is a budget, so more of it only completes merges sooner,
1960        // whereas a wrapped value can land negative and starve them. The result
1961        // is an `isize` so a fuel shortfall stays observable.
1962        let fuel = u32::try_from(batch_index)
1963            .ok()
1964            .and_then(|shift| 8usize.checked_shl(shift))
1965            // Scale up by the effort parameter, which is calibrated to one as the
1966            // minimum amount of effort.
1967            .and_then(|fuel| fuel.checked_mul(self.effort))
1968            .and_then(|fuel| isize::try_from(fuel).ok())
1969            .unwrap_or(isize::MAX);
1970
1971        // Step 1.  Apply fuel to each in-progress merge.
1972        //
1973        //          Before we can introduce new updates, we must apply any
1974        //          fuel to in-progress merges, as this fuel is what ensures
1975        //          that the merges will be complete by the time we insert
1976        //          the updates.
1977        self.apply_fuel(&fuel, log);
1978
1979        // Step 2.  We must ensure the invariant that adjacent layers do not
1980        //          contain two batches will be satisfied when we insert the
1981        //          batch. We forcibly completing all merges at layers lower
1982        //          than and including `batch_index`, so that the new batch is
1983        //          inserted into an empty layer.
1984        //
1985        //          We could relax this to "strictly less than `batch_index`"
1986        //          if the layer above has only a single batch in it, which
1987        //          seems not implausible if it has been the focus of effort.
1988        //
1989        //          This should be interpreted as the introduction of some
1990        //          volume of fake updates, and we will need to fuel merges
1991        //          by a proportional amount to ensure that they are not
1992        //          surprised later on. The number of fake updates should
1993        //          correspond to the deficit for the layer, which perhaps
1994        //          we should track explicitly.
1995        self.roll_up(batch_index, log);
1996
1997        // Step 3. This insertion should be into an empty layer. It is a logical
1998        //         error otherwise, as we may be violating our invariant, from
1999        //         which all wonderment derives.
2000        self.insert_at(batch, batch_index);
2001
2002        // Step 4. Tidy the largest layers.
2003        //
2004        //         It is important that we not tidy only smaller layers,
2005        //         as their ascension is what ensures the merging and
2006        //         eventual compaction of the largest layers.
2007        self.tidy_layers();
2008    }
2009
2010    /// Ensures that an insertion at layer `index` will succeed.
2011    ///
2012    /// This method is subject to the constraint that all existing batches
2013    /// should occur at higher levels, which requires it to "roll up" batches
2014    /// present at lower levels before the method is called. In doing this, we
2015    /// should not introduce more virtual records than 2^index, as that is the
2016    /// amount of excess fuel we have budgeted for completing merges.
2017    fn roll_up(&mut self, index: usize, log: &mut SpineLog<'_, T>) {
2018        // Ensure entries sufficient for `index`.
2019        while self.merging.len() <= index {
2020            self.merging.push(MergeState::default());
2021        }
2022
2023        // We only need to roll up if there are non-vacant layers.
2024        if self.merging[..index].iter().any(|m| !m.is_vacant()) {
2025            // Collect and merge all batches at layers up to but not including
2026            // `index`.
2027            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            // The merged results should be introduced at level `index`, which
2036            // should be ready to absorb them (possibly creating a new merge at
2037            // the time).
2038            if let Some(merged) = merged {
2039                self.insert_at(merged, index);
2040            }
2041
2042            // If the insertion results in a merge, we should complete it to
2043            // ensure the upcoming insertion at `index` does not panic.
2044            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    /// Applies an amount of fuel to merges in progress.
2052    ///
2053    /// The supplied `fuel` is for each in progress merge, and if we want to
2054    /// spend the fuel non-uniformly (e.g. prioritizing merges at low layers) we
2055    /// could do so in order to maintain fewer batches on average (at the risk
2056    /// of completing merges of large batches later, but tbh probably not much
2057    /// later).
2058    pub fn apply_fuel(&mut self, fuel: &isize, log: &mut SpineLog<'_, T>) {
2059        // For the moment our strategy is to apply fuel independently to each
2060        // merge in progress, rather than prioritizing small merges. This sounds
2061        // like a great idea, but we need better accounting in place to ensure
2062        // that merges that borrow against later layers but then complete still
2063        // "acquire" fuel to pay back their debts.
2064        for index in 0..self.merging.len() {
2065            // Give each level independent fuel, for now.
2066            let mut fuel = *fuel;
2067            // Pass along various logging stuffs, in case we need to report
2068            // success.
2069            self.merging[index].work(&mut fuel);
2070            // `fuel` could have a deficit at this point, meaning we over-spent
2071            // when we took a merge step. We could ignore this, or maintain the
2072            // deficit and account future fuel against it before spending again.
2073            // It isn't clear why that would be especially helpful to do; we
2074            // might want to avoid overspends at multiple layers in the same
2075            // invocation (to limit latencies), but there is probably a rich
2076            // policy space here.
2077
2078            // If a merge completes, we can immediately merge it in to the next
2079            // level, which is "guaranteed" to be complete at this point, by our
2080            // fueling discipline.
2081            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    /// Inserts a batch at a specific location.
2089    ///
2090    /// This is a non-public internal method that can panic if we try and insert
2091    /// into a layer which already contains two batches (and is still in the
2092    /// process of merging).
2093    fn insert_at(&mut self, batch: SpineBatch<T>, index: usize) {
2094        // Ensure the spine is large enough.
2095        while self.merging.len() <= index {
2096            self.merging.push(MergeState::default());
2097        }
2098
2099        // Insert the batch at the location.
2100        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    /// Completes and extracts what ever is at layer `index`, leaving this layer vacant.
2109    fn complete_at(&mut self, index: usize, log: &mut SpineLog<'_, T>) -> Option<SpineBatch<T>> {
2110        self.merging[index].complete(log)
2111    }
2112
2113    /// Attempts to draw down large layers to size appropriate layers.
2114    fn tidy_layers(&mut self) {
2115        // If the largest layer is complete (not merging), we can attempt to
2116        // draw it down to the next layer. This is permitted if we can maintain
2117        // our invariant that below each merge there are at most half the
2118        // records that would be required to invade the merge.
2119        if !self.merging.is_empty() {
2120            let mut length = self.merging.len();
2121            if self.merging[length - 1].is_single() {
2122                // To move a batch down, we require that it contain few enough
2123                // records that the lower level is appropriate, and that moving
2124                // the batch would not create a merge violating our invariant.
2125                let appropriate_level = usize::cast_from(
2126                    self.merging[length - 1]
2127                        .len()
2128                        .next_power_of_two()
2129                        .trailing_zeros(),
2130                );
2131
2132                // Continue only as far as is appropriate
2133                while appropriate_level < length - 1 {
2134                    let current = &mut self.merging[length - 2];
2135                    if current.is_vacant() {
2136                        // Vacant batches can be absorbed.
2137                        self.merging.remove(length - 2);
2138                        length = self.merging.len();
2139                    } else {
2140                        if !current.is_full() {
2141                            // Single batches may initiate a merge, if sizes are
2142                            // within bounds, but terminate the loop either way.
2143
2144                            // Determine the number of records that might lead
2145                            // to a merge. Importantly, this is not the number
2146                            // of actual records, but the sum of upper bounds
2147                            // based on indices.
2148                            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                                // Remove the batch under consideration (shifting the deeper batches up a level),
2155                                // then merge in the single batch at the current level.
2156                                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    /// Checks invariants:
2171    /// - The lowers and uppers of all batches "line up".
2172    /// - The lower of the "minimum" batch is `antichain[T::minimum]`.
2173    /// - The upper of the "maximum" batch is `== self.upper`.
2174    /// - The since of each batch is `less_equal self.since`.
2175    /// - The `SpineIds` all "line up" and cover from `0` to `self.next_id`.
2176    /// - TODO: Verify fuel and level invariants.
2177    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            // TODO: Anything we can validate about x.merge? It'd
2207            // be nice to assert that it's bigger than the len of the
2208            // two batches, but apply_merge_res might swap those lengths
2209            // out from under us.
2210            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/// Describes the state of a layer.
2256///
2257/// A layer can be empty, contain a single batch, or contain a pair of batches
2258/// that are in the process of merging into a batch for the next layer.
2259#[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    /// An id that covers all the batches in the given merge state, assuming there are any.
2276    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    /// A new single-batch merge state.
2285    fn single(batch: SpineBatch<T>) -> Self {
2286        let mut state = Self::default();
2287        state.push_batch(batch);
2288        state
2289    }
2290
2291    /// Push a new batch at this level, checking invariants.
2292    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    /// Fallible version of [Self::push_batch], for [Trace::unflatten], where
2298    /// the batches were decoded from an untrusted blob and a violated
2299    /// invariant must be a decode error rather than a panic.
2300    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    /// The number of actual updates contained in the level.
2331    fn len(&self) -> usize {
2332        self.batches.iter().map(SpineBatch::len).sum()
2333    }
2334
2335    /// True if this merge state contains no updates.
2336    fn is_empty(&self) -> bool {
2337        self.batches.iter().all(SpineBatch::is_empty)
2338    }
2339
2340    /// True if this level contains no batches.
2341    fn is_vacant(&self) -> bool {
2342        self.batches.is_empty()
2343    }
2344
2345    /// True only for a single-batch state.
2346    fn is_single(&self) -> bool {
2347        self.batches.len() == 1
2348    }
2349
2350    /// True if this merge cannot hold any more batches.
2351    /// (i.e. for a binary merge tree, true if this layer holds two batches.)
2352    fn is_full(&self) -> bool {
2353        self.batches.is_full()
2354    }
2355
2356    /// Immediately complete any merge.
2357    ///
2358    /// The result is either a batch, if there is a non-trivial batch to return
2359    /// or `None` if there is no meaningful batch to return.
2360    ///
2361    /// There is the additional option of input batches.
2362    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            // Merge the remaining batches, regardless of whether we have a fully fueled merge.
2368            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    /// True iff the layer is a complete merge, ready for extraction.
2376    fn is_complete(&self) -> bool {
2377        match &self.merge {
2378            Some(IdFuelingMerge { merge, .. }) => merge.remaining_work == 0,
2379            None => false,
2380        }
2381    }
2382
2383    /// Performs a bounded amount of work towards a merge.
2384    fn work(&mut self, fuel: &mut isize) {
2385        // We only perform work for merges in progress.
2386        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    /// Shared state for a single [crate::internal::trace] [datadriven::TestFile].
2401    #[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                // Fix up the arbitrary HollowBatches so the lowers and uppers
2524                // align.
2525                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                    // Overall trace since has to be past each batch's since.
2529                    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)] // proptest is too heavy for miri!
2554    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        // Empty
2618        assert_eq!(Trace::<u64>::remove_redundant_merge_reqs(vec![]), vec![]);
2619
2620        // Single
2621        assert_eq!(
2622            Trace::remove_redundant_merge_reqs(vec![req(0, 1)]),
2623            vec![req(0, 1)]
2624        );
2625
2626        // Duplicate
2627        assert_eq!(
2628            Trace::remove_redundant_merge_reqs(vec![req(0, 1), req(0, 1)]),
2629            vec![req(0, 1)]
2630        );
2631
2632        // Nothing covered
2633        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        // Covered
2639        assert_eq!(
2640            Trace::remove_redundant_merge_reqs(vec![req(1, 2), req(0, 3)]),
2641            vec![req(0, 3)]
2642        );
2643
2644        // Covered, lower equal
2645        assert_eq!(
2646            Trace::remove_redundant_merge_reqs(vec![req(0, 2), req(0, 3)]),
2647            vec![req(0, 3)]
2648        );
2649
2650        // Covered, upper equal
2651        assert_eq!(
2652            Trace::remove_redundant_merge_reqs(vec![req(1, 3), req(0, 3)]),
2653            vec![req(0, 3)]
2654        );
2655
2656        // Covered, unexpected order (doesn't happen in practice)
2657        assert_eq!(
2658            Trace::remove_redundant_merge_reqs(vec![req(0, 3), req(1, 2)]),
2659            vec![req(0, 3)]
2660        );
2661
2662        // Partially overlapping
2663        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        // Partially overlapping, the other order
2669        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        // Different sinces (doesn't happen in practice)
2675        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)] // proptest is too heavy for miri!
2692    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); // One new part + one old part
2780        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    /// Inserting a batch whose `len` is large enough to saturate the fuel
2791    /// computation must not disturb an in-progress merge's accounting.
2792    ///
2793    /// `introduce_batch` derives its fuel from `8 << batch_index`, where
2794    /// `batch_index` is `len.next_power_of_two().trailing_zeros()`. A `len` near
2795    /// `2^60` pushes that past what an `isize` holds, and `Trace::unflatten`
2796    /// accepts such a `len` from an untrusted blob: its `MAX_TOTAL_LEN` guard
2797    /// only caps the total at `usize::MAX >> 3`.
2798    #[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        // Two same-size batches fill a level and begin a merge whose
2810        // `remaining_work` (the sum of their lens) far exceeds the `8 << 0` fuel
2811        // that a subsequent len-1 batch delivers, so the merge is still in
2812        // progress when the saturating batch arrives.
2813        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}