mz_storage/upsert_continual_feedback_v2/stash.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//! The upsert-v2 source stash: the consolidating diff, the stash batcher
11//! types, and the per-flavor [`UpsertStashArm`]s.
12
13use differential_dataflow::batcher::Extract;
14use differential_dataflow::difference::{IsZero, Semigroup};
15use differential_dataflow::lattice::Lattice;
16use differential_dataflow::operators::arrange::agent::TraceAgent;
17use differential_dataflow::trace::TraceReader;
18use differential_dataflow::trace::chunk::ChunkMerger;
19use differential_dataflow::trace::implementations::spine_fueled::SpineBatch;
20use mz_repr::{Diff, GlobalId, Row};
21use mz_row_spine::FundedValRowSpine;
22use mz_timely_util::columnar::Column;
23use mz_timely_util::columnar::batcher::ColumnChunker;
24use mz_timely_util::columnar::chunk::{ChunkChunker, ColumnChunk, StorageSpill};
25use mz_timely_util::columnar::merge_batcher::{ColumnMergeBatcher, PagedChunker};
26use mz_timely_util::operator::ConsolidatingBatcher;
27use timely::container::{ContainerBuilder, PushInto};
28use timely::dataflow::operators::Capability;
29use timely::order::TotalOrder;
30use timely::progress::frontier::AntichainRef;
31use timely::progress::timestamp::Refines;
32use timely::progress::{Antichain, Timestamp};
33
34use super::UpsertOutputHandle;
35use super::drain::{DrainStats, drain_sealed_input_chunked, drain_sealed_input_paged};
36use super::feedback::FeedbackSpine;
37use crate::upsert::UpsertKey;
38
39// The source stash carries the upsert payload in a custom diff type so the
40// merge batcher consolidates by (key, time), keeping the update with the
41// highest `FromTime` (latest source offset) per group. The diff is `Columnar`
42// so the paged merge batcher can store it in a `Column` and page it out of RSS.
43//
44// The value is a tag-encoded `Row` (see `upsert_value_to_row`) rather than an
45// `UpsertValue`: folding both the `Ok` and `Err` arms into one `Row` lets the
46// value share a single columnar byte container, and `Row` already implements
47// `Columnar`. `None` is a deletion tombstone.
48
49// Derive ordering on the generated `UpsertDiffReference` too: the paged merge
50// batcher requires `Ref: Ord` to sort the `(key, time, diff)` columns it
51// consolidates. The derived order (by `from_time`, then `value`) is fine —
52// "max FromTime wins" can tie only between equal `from_time`s, and a source
53// never emits two distinct values for the same `(key, time, from_time)`, so
54// the consolidated result doesn't depend on the fold order of equal
55// `(key, time)` runs.
56#[derive(Clone, Debug, Default, columnar::Columnar)]
57#[columnar(derive(PartialEq, Eq, PartialOrd, Ord))]
58pub(super) struct UpsertDiff<O> {
59 pub(super) from_time: O,
60 pub(super) value: Option<Row>,
61}
62
63impl<O> IsZero for UpsertDiff<O> {
64 fn is_zero(&self) -> bool {
65 false
66 }
67}
68
69impl<O: Ord + Clone> Semigroup for UpsertDiff<O> {
70 fn plus_equals(&mut self, rhs: &Self) {
71 if rhs.from_time > self.from_time {
72 *self = rhs.clone();
73 }
74 }
75}
76
77// Accumulate a borrowed columnar reference: the paged merge batcher consolidates
78// `Column`-resident diffs through this path on every fold of an equal
79// `(key, time)` run. Materialize only the order key to decide the "max FromTime
80// wins" comparison — copying the value `Row` out of the column solely when `rhs`
81// wins. Losing folds (the common case for a repeatedly-updated key) then pay no
82// `Row` copy at all.
83impl<'a, O> Semigroup<columnar::Ref<'a, UpsertDiff<O>>> for UpsertDiff<O>
84where
85 O: columnar::Columnar + Ord + Clone,
86{
87 fn plus_equals(&mut self, rhs: &columnar::Ref<'a, UpsertDiff<O>>) {
88 let rhs_from_time = <O as columnar::Columnar>::into_owned(rhs.from_time);
89 if rhs_from_time > self.from_time {
90 self.from_time = rhs_from_time;
91 self.value = <Option<Row> as columnar::Columnar>::into_owned(rhs.value);
92 }
93 }
94}
95
96/// One source-stash update: a key, its dataflow time, and the payload diff.
97/// `O` is the columnar order key projected from the source `FromTime` (see
98/// [`UpsertSourceTime`](crate::upsert::UpsertSourceTime)).
99pub(super) type UpsertUpdate<T, O> = (UpsertKey, T, UpsertDiff<O>);
100
101/// One chunked-flavor stash update: [`UpsertUpdate`] with the time copied
102/// into the data, so the batcher sorts by `(time, key)`.
103///
104/// A seal at an upper then cuts a prefix of the batcher's chains. Its extract
105/// passes chunks wholly on either side of the upper through by their time
106/// bounds, so it rewrites only the chunk the upper splits. Its merges pass
107/// chunks through when their data ranges are disjoint, which holds between
108/// chains at different times, so data at a later time does not rewrite the
109/// stashed backlog; data at the same time merges as in any batcher (see
110/// [`ChunkedArm`] for what a pass-through still costs). Sorted by key, every
111/// chunk spans the stash's whole time range, so each seal would load and
112/// rewrite every chunk, which makes catching up on a tail of `n` timestamps
113/// quadratic in the tail. Within one time, entries stay sorted by key, which
114/// the drain's monotone probes rely on.
115pub(super) type TimeMajorUpdate<T, O> = ((T, UpsertKey), T, UpsertDiff<O>);
116
117/// One chunked-flavor stash chunk: a sorted, consolidated run of time-major
118/// updates, resident or spilled to the buffer pool.
119type UpsertChunk<T, O> = ColumnChunk<(T, UpsertKey), T, UpsertDiff<O>, StorageSpill>;
120
121/// The chunked flavor's stash: differential's chunk merge batcher over
122/// `ColumnChunk`s. Data is pushed in unsorted. The batcher maintains
123/// geometrically-sized sorted chains and consolidates via the UpsertDiff
124/// Semigroup automatically. Committed chunks spill their bodies to the
125/// process buffer pool (see `mz_timely_util::columnar::chunk`), so the
126/// not-yet-eligible backlog (the snapshot / persist-lag window) pages out of
127/// RSS instead of growing it.
128type UpsertChunkBatcher<T, O> = ConsolidatingBatcher<
129 ChunkChunker<(T, UpsertKey), T, UpsertDiff<O>, StorageSpill>,
130 ChunkMerger<UpsertChunk<T, O>>,
131>;
132
133/// The paged flavor's stash: the paged columnar merge batcher, consolidating
134/// like [`UpsertChunkBatcher`] but storing each chain entry as a `Column`
135/// routed through the storage-owned pager, which pages cold chains out of
136/// RSS.
137type UpsertPagedBatcher<T, O> =
138 ColumnMergeBatcher<PagedChunker<UpsertUpdate<T, O>>, UpsertKey, T, UpsertDiff<O>, ()>;
139
140/// The chunker that sorts and consolidates raw input into the `Column` chunks
141/// the paged stash batcher consumes.
142type UpsertChunker<T, O> = ColumnChunker<UpsertUpdate<T, O>>;
143
144/// [`UpsertChunker`]'s time-major counterpart, for the chunked stash.
145type TimeMajorChunker<T, O> = ColumnChunker<TimeMajorUpdate<T, O>>;
146
147/// The start of the `mz_repr::Timestamp` after `persist_upper`'s, capped at
148/// `input_upper` so no unfinalized time is extracted.
149///
150/// Only entries at `persist_upper` are eligible in a round, and the drain
151/// returns every extracted entry past `persist_upper` as a resident `Vec`,
152/// outside the buffer pool budget, to be re-pushed through the chunker.
153/// Sealing at `input_upper` instead would do that for the whole backlog
154/// between the two frontiers once per persist round trip, which is the entire
155/// source tail while the operator catches up from behind.
156pub(super) fn seal_upper<T>(
157 persist_upper: &Antichain<T>,
158 input_upper: &Antichain<T>,
159) -> Antichain<T>
160where
161 T: Timestamp + TotalOrder + Refines<mz_repr::Timestamp>,
162{
163 match persist_upper.as_option().and_then(next_outer) {
164 Some(next) if !input_upper.less_equal(&next) => Antichain::from_elem(next),
165 _ => input_upper.clone(),
166 }
167}
168
169/// The least time past every time sharing `ts`'s `mz_repr::Timestamp`, or
170/// `None` when that timestamp is the maximum.
171fn next_outer<T: Refines<mz_repr::Timestamp>>(ts: &T) -> Option<T> {
172 ts.clone().to_outer().try_step_forward().map(T::to_inner)
173}
174
175/// The flavor-specific pieces of the upsert-v2 operator: the stash batcher,
176/// the feedback arrangement's spine, and how the drain reads prior state.
177/// [`build_upsert_operator`](super::build_upsert_operator) holds the
178/// flavor-independent operator loop and calls through this trait at the few
179/// points where the flavors diverge.
180pub(super) trait UpsertStashArm<T, O>
181where
182 T: Timestamp + Lattice + mz_timely_util::columnar::chunk::PoolColumnar + Default,
183 for<'a> columnar::Ref<'a, T>: Copy + Ord,
184 O: mz_timely_util::columnar::chunk::PoolColumnar
185 + Default
186 + Ord
187 + Clone
188 + Send
189 + Sync
190 + 'static,
191 for<'a> columnar::Ref<'a, O>: Ord + Copy,
192{
193 /// The feedback arrangement's spine. `'static` because the operator
194 /// future owns a trace agent for it.
195 type Spine: TraceReader<Time = T, Batch: SpineBatch> + 'static;
196 /// The source-stash batcher. `'static` because the operator future owns
197 /// it.
198 type Batcher: 'static;
199 /// The chunk representation the stash batcher accumulates into.
200 type Chunk;
201
202 /// Sorts and consolidates raw updates into the batcher's chunks.
203 type Chunker: Default + 'static;
204
205 /// A new stash batcher for one source dataflow.
206 fn new_batcher() -> Self::Batcher;
207
208 /// Carve the chain of updates `upper` unblocks out of the stash.
209 ///
210 /// Returns the chunks and a lower bound on the times of what stays behind. The stash keeps
211 /// the chunks as they are: a caller that wanted a trace batch would have to take it apart
212 /// again.
213 fn extract(
214 batcher: &mut Self::Batcher,
215 upper: AntichainRef<T>,
216 ) -> (Vec<Self::Chunk>, Antichain<T>);
217
218 /// Consolidate `updates` through `chunker` into chunks and push them into
219 /// `batcher`, emptying `updates` (keeping its capacity). The chunker
220 /// readies a fully-consolidated chunk per `push_into`, so nothing stays
221 /// pending in it between calls.
222 fn flush(
223 updates: &mut Vec<UpsertUpdate<T, O>>,
224 chunker: &mut Self::Chunker,
225 batcher: &mut Self::Batcher,
226 );
227
228 /// Classify one sealed stash against `persist_upper` and emit eligible
229 /// output; see [`DrainStats`].
230 async fn drain(
231 sealed: Vec<Self::Chunk>,
232 ineligible: &mut Vec<UpsertUpdate<T, O>>,
233 output_handle: &UpsertOutputHandle<T>,
234 output_cap: &Capability<T>,
235 persist_upper: &Antichain<T>,
236 trace: &mut TraceAgent<Self::Spine>,
237 worker_id: usize,
238 source_id: GlobalId,
239 ) -> DrainStats;
240}
241
242/// The shared body of the arms' `flush`: writes each update into a raw
243/// column through `record`, consolidates it through `chunker`, and hands each
244/// readied chunk to `push`.
245fn flush_through<T, O, U, C>(
246 updates: &mut Vec<UpsertUpdate<T, O>>,
247 chunker: &mut C,
248 mut record: impl FnMut(UpsertUpdate<T, O>, &mut Column<U>),
249 mut push: impl FnMut(C::Container),
250) where
251 U: columnar::Columnar,
252 C: ContainerBuilder + for<'a> PushInto<&'a mut Column<U>>,
253 C::Container: Default,
254{
255 if updates.is_empty() {
256 return;
257 }
258 let mut raw: Column<U> = Default::default();
259 for update in updates.drain(..) {
260 record(update, &mut raw);
261 }
262 chunker.push_into(&mut raw);
263 while let Some(chunk) = chunker.extract() {
264 push(std::mem::take(chunk));
265 }
266}
267
268/// The chunked flavor: time-major [`UpsertChunkBatcher`] stash,
269/// [`FeedbackSpine`] feedback arrangement, bulk-probe drain.
270///
271/// NOTE: the seal's chain merge loads the bodies of the chunks it actually
272/// merges. Chunks whose data ranges are disjoint pass through, and the
273/// partition against the upper passes chunks wholly on one side of it
274/// through, so only chunks the upper splits are rewritten (see
275/// [`TimeMajorUpdate`]). Two costs remain per seal. A spilled body that ages
276/// to `compress_min_depth` through a pass-through is loaded once and
277/// re-spilled compressed. A resident chunk's time bounds are recomputed by
278/// scanning its time column, so with spilling off a seal still reads every
279/// stashed time. The drain then loads sealed chunks one at a time, so a large
280/// drain (a frontier advance releasing a snapshot's worth of stash at once)
281/// holds at most one chunk resident rather than the whole backlog.
282pub(super) struct ChunkedArm;
283
284impl<T, O> UpsertStashArm<T, O> for ChunkedArm
285where
286 T: Timestamp + TotalOrder + Lattice + Sync,
287 T: columnation::Columnation + mz_timely_util::columnar::chunk::PoolColumnar + Default,
288 for<'a> columnar::Ref<'a, T>: Copy + Ord,
289 O: mz_timely_util::columnar::chunk::PoolColumnar
290 + Default
291 + Ord
292 + Clone
293 + Send
294 + Sync
295 + 'static,
296 for<'a> columnar::Ref<'a, O>: Ord + Copy,
297{
298 type Spine = FeedbackSpine<T>;
299 type Batcher = UpsertChunkBatcher<T, O>;
300 type Chunk = UpsertChunk<T, O>;
301 type Chunker = TimeMajorChunker<T, O>;
302
303 fn new_batcher() -> Self::Batcher {
304 ConsolidatingBatcher::new(None, 0)
305 }
306
307 fn extract(
308 batcher: &mut Self::Batcher,
309 upper: AntichainRef<T>,
310 ) -> (Vec<Self::Chunk>, Antichain<T>) {
311 let (chain, frontier) = batcher.extract(upper);
312 (chain.unwrap_or_default(), frontier.to_owned())
313 }
314
315 fn flush(
316 updates: &mut Vec<UpsertUpdate<T, O>>,
317 chunker: &mut Self::Chunker,
318 batcher: &mut Self::Batcher,
319 ) {
320 flush_through(
321 updates,
322 chunker,
323 |(key, ts, diff), raw| raw.push_into(&((ts.clone(), key), ts, diff)),
324 |chunk| batcher.push_chunk(ColumnChunk::from_body(chunk)),
325 );
326 }
327
328 async fn drain(
329 sealed: Vec<UpsertChunk<T, O>>,
330 ineligible: &mut Vec<UpsertUpdate<T, O>>,
331 output_handle: &UpsertOutputHandle<T>,
332 output_cap: &Capability<T>,
333 persist_upper: &Antichain<T>,
334 trace: &mut TraceAgent<Self::Spine>,
335 worker_id: usize,
336 source_id: GlobalId,
337 ) -> DrainStats {
338 drain_sealed_input_chunked(
339 sealed.into_iter().map(ColumnChunk::into_body),
340 ineligible,
341 output_handle,
342 output_cap,
343 persist_upper,
344 trace,
345 worker_id,
346 source_id,
347 )
348 .await
349 }
350}
351
352/// The paged flavor: [`UpsertPagedBatcher`] stash, `ValRowSpine` feedback
353/// arrangement, cursor drain. Cold chains page out of RSS through the
354/// storage-owned column pager, captured once per batcher at construction.
355pub(super) struct PagedArm;
356
357impl<T, O> UpsertStashArm<T, O> for PagedArm
358where
359 T: Timestamp + TotalOrder + Lattice + Sync,
360 T: columnation::Columnation + mz_timely_util::columnar::chunk::PoolColumnar + Default,
361 for<'a> columnar::Ref<'a, T>: Copy + Ord,
362 O: mz_timely_util::columnar::chunk::PoolColumnar
363 + Default
364 + Ord
365 + Clone
366 + Send
367 + Sync
368 + 'static,
369 for<'a> columnar::Ref<'a, O>: Ord + Copy,
370{
371 type Spine = FundedValRowSpine<UpsertKey, T, Diff>;
372 type Batcher = UpsertPagedBatcher<T, O>;
373 type Chunk = Column<UpsertUpdate<T, O>>;
374 type Chunker = UpsertChunker<T, O>;
375
376 fn new_batcher() -> Self::Batcher {
377 let mut batcher = UpsertPagedBatcher::<T, O>::new(None, 0);
378 batcher.set_pager(crate::upsert::upsert_stash_pager::pager());
379 batcher
380 }
381
382 fn extract(
383 batcher: &mut Self::Batcher,
384 upper: AntichainRef<T>,
385 ) -> (Vec<Self::Chunk>, Antichain<T>) {
386 let (chain, frontier) = batcher.extract_chain(upper);
387 (chain, frontier.to_owned())
388 }
389
390 fn flush(
391 updates: &mut Vec<UpsertUpdate<T, O>>,
392 chunker: &mut Self::Chunker,
393 batcher: &mut Self::Batcher,
394 ) {
395 flush_through(
396 updates,
397 chunker,
398 |update, raw| raw.push_into(&update),
399 // The pager's chains are edge containers, so the body goes back
400 // onto one.
401 |chunk| batcher.push_chunk(Column::from(chunk)),
402 );
403 }
404
405 async fn drain(
406 sealed: Vec<Column<UpsertUpdate<T, O>>>,
407 ineligible: &mut Vec<UpsertUpdate<T, O>>,
408 output_handle: &UpsertOutputHandle<T>,
409 output_cap: &Capability<T>,
410 persist_upper: &Antichain<T>,
411 trace: &mut TraceAgent<Self::Spine>,
412 worker_id: usize,
413 source_id: GlobalId,
414 ) -> DrainStats {
415 drain_sealed_input_paged(
416 sealed,
417 ineligible,
418 output_handle,
419 output_cap,
420 persist_upper,
421 trace,
422 worker_id,
423 source_id,
424 )
425 .await
426 }
427}