Skip to main content

mz_compute/sink/
correction.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 `Correction` data structure used by `persist_sink::write_batches` to stash updates before
11//! they are written into batches.
12
13use std::collections::BTreeMap;
14use std::fmt;
15use std::num::NonZeroIsize;
16use std::ops::{AddAssign, Bound, RangeBounds, SubAssign};
17
18use differential_dataflow::consolidation::{consolidate, consolidate_updates};
19use differential_dataflow::logging::{BatchEvent, DropEvent};
20use itertools::{Either, Itertools};
21use mz_compute_types::dyncfgs::{
22    CONSOLIDATING_VEC_GROWTH_DAMPENER, CORRECTION_V2_CHAIN_PROPORTIONALITY,
23    CORRECTION_V2_CHUNK_SIZE, ENABLE_CORRECTION_V2,
24};
25use mz_dyncfg::ConfigSet;
26use mz_persist_client::metrics::{SinkMetrics, SinkWorkerMetrics, UpdateDelta};
27use mz_repr::{Diff, Timestamp};
28use timely::PartialOrder;
29use timely::progress::Antichain;
30use tokio::sync::mpsc;
31
32use crate::logging::compute::{
33    ArrangementHeapAllocations, ArrangementHeapCapacity, ArrangementHeapSize,
34    ArrangementHeapSizeOperator, ArrangementHeapSizeOperatorDrop, ComputeEvent,
35    Logger as ComputeLogger,
36};
37use crate::sink::correction_v2::{CorrectionV2, Data};
38
39/// A data structure suitable for storing updates in a self-correcting persist sink.
40///
41/// Selects one of two correction buffer implementations. `V1` is the original simple
42/// implementation that stores updates in non-spillable memory. `V2` improves on `V1` by supporting
43/// spill-to-disk but is less battle-tested so for now we want to keep the option of reverting to
44/// `V1` in a pinch. The plan is to remove `V1` eventually.
45pub enum Correction<D: Data> {
46    /// Correction buffer based on a [`CorrectionV1`].
47    V1(CorrectionV1<D>),
48    /// Correction buffer based on a [`CorrectionV2`].
49    V2(CorrectionV2<D>),
50}
51
52impl<D: Data> Correction<D> {
53    /// Construct a new `Correction` instance.
54    pub fn new(
55        metrics: SinkMetrics,
56        worker_metrics: SinkWorkerMetrics,
57        logging: Option<ChannelLogging>,
58        config: &ConfigSet,
59    ) -> Self {
60        if ENABLE_CORRECTION_V2.get(config) {
61            let prop = CORRECTION_V2_CHAIN_PROPORTIONALITY.get(config);
62            let chunk_size = CORRECTION_V2_CHUNK_SIZE.get(config);
63            Self::V2(CorrectionV2::new(
64                metrics,
65                worker_metrics,
66                logging,
67                prop,
68                chunk_size,
69            ))
70        } else {
71            let growth_dampener = CONSOLIDATING_VEC_GROWTH_DAMPENER.get(config);
72            Self::V1(CorrectionV1::new(metrics, worker_metrics, growth_dampener))
73        }
74    }
75
76    /// Insert a batch of updates.
77    pub fn insert(&mut self, updates: &mut Vec<(D, Timestamp, Diff)>) {
78        match self {
79            Self::V1(c) => c.insert(updates),
80            Self::V2(c) => c.insert(updates),
81        }
82    }
83
84    /// Insert a batch of updates, after negating their diffs.
85    pub fn insert_negated(&mut self, updates: &mut Vec<(D, Timestamp, Diff)>) {
86        match self {
87            Self::V1(c) => c.insert_negated(updates),
88            Self::V2(c) => c.insert_negated(updates),
89        }
90    }
91
92    /// Consolidate and return updates before the given `upper`.
93    pub fn updates_before(
94        &mut self,
95        upper: &Antichain<Timestamp>,
96    ) -> Box<dyn Iterator<Item = (D, Timestamp, Diff)> + Send + '_> {
97        match self {
98            Self::V1(c) => Box::new(c.updates_before(upper)),
99            Self::V2(c) => Box::new(c.updates_before(upper)),
100        }
101    }
102
103    /// Consolidate the updates before the given `upper`.
104    ///
105    /// This is the expensive half of [`Correction::updates_before`], split out so callers that
106    /// must not run unbounded CPU work inline can perform it elsewhere.
107    pub fn consolidate_before(&mut self, upper: &Antichain<Timestamp>) {
108        match self {
109            Self::V1(c) => c.consolidate_before(upper),
110            Self::V2(c) => c.consolidate_before(upper),
111        }
112    }
113
114    /// Return the updates before the given `upper`, as consolidated by a preceding
115    /// [`Correction::consolidate_before`] call.
116    ///
117    /// The caller must have invoked `consolidate_before` with the same `upper` and must not have
118    /// mutated the buffer since. Otherwise the returned updates are neither consolidated nor
119    /// necessarily complete.
120    pub fn consolidated_updates_before<'a>(
121        &'a self,
122        upper: &Antichain<Timestamp>,
123    ) -> impl Iterator<Item = (D, Timestamp, Diff)> + Send + use<'a, D> {
124        match self {
125            Self::V1(c) => Either::Left(c.consolidated_updates_before(upper)),
126            Self::V2(c) => Either::Right(c.consolidated_updates_before(upper)),
127        }
128    }
129
130    /// Advance the since frontier.
131    ///
132    /// # Panics
133    ///
134    /// Panics if the given `since` is less than the current since frontier.
135    pub fn advance_since(&mut self, since: Antichain<Timestamp>) {
136        match self {
137            Self::V1(c) => c.advance_since(since),
138            Self::V2(c) => c.advance_since(since),
139        }
140    }
141
142    /// Consolidate all updates at the current `since`.
143    pub fn consolidate_at_since(&mut self) {
144        match self {
145            Self::V1(c) => c.consolidate_at_since(),
146            Self::V2(c) => c.consolidate_at_since(),
147        }
148    }
149}
150
151/// A collection holding `persist_sink` updates.
152///
153/// The `CorrectionV1` data structure is purpose-built for the `persist_sink::write_batches`
154/// operator:
155///
156///  * It stores updates by time, to enable efficient separation between updates that should
157///    be written to a batch and updates whose time has not yet arrived.
158///  * It eschews an interface for directly removing previously inserted updates. Instead, updates
159///    are removed by inserting them again, with negated diffs. Stored updates are continuously
160///    consolidated to give them opportunity to cancel each other out.
161///  * It provides an interface for advancing all contained updates to a given frontier.
162pub struct CorrectionV1<D> {
163    /// Stashed updates by time.
164    updates: BTreeMap<Timestamp, ConsolidatingVec<D>>,
165    /// Frontier to which all update times are advanced.
166    since: Antichain<Timestamp>,
167
168    /// Total length and capacity of vectors in `updates`.
169    ///
170    /// Tracked to maintain metrics.
171    total_size: LengthAndCapacity,
172    /// Global persist sink metrics.
173    metrics: SinkMetrics,
174    /// Per-worker persist sink metrics.
175    worker_metrics: SinkWorkerMetrics,
176    /// Configuration for `ConsolidatingVec` driving the growth rate down from doubling.
177    growth_dampener: usize,
178}
179
180impl<D> CorrectionV1<D> {
181    /// Construct a new `CorrectionV1` instance.
182    pub fn new(
183        metrics: SinkMetrics,
184        worker_metrics: SinkWorkerMetrics,
185        growth_dampener: usize,
186    ) -> Self {
187        Self {
188            updates: Default::default(),
189            since: Antichain::from_elem(Timestamp::MIN),
190            total_size: Default::default(),
191            metrics,
192            worker_metrics,
193            growth_dampener,
194        }
195    }
196
197    /// Update persist sink metrics to the given new length and capacity.
198    fn update_metrics(&mut self, new_size: LengthAndCapacity) {
199        let old_size = self.total_size;
200        let len_delta = UpdateDelta::new(new_size.length, old_size.length);
201        let cap_delta = UpdateDelta::new(new_size.capacity, old_size.capacity);
202        self.metrics
203            .report_correction_update_deltas(len_delta, cap_delta);
204        self.worker_metrics
205            .report_correction_update_totals(new_size.length, new_size.capacity);
206
207        self.total_size = new_size;
208    }
209}
210
211impl<D: Data> CorrectionV1<D> {
212    /// Insert a batch of updates.
213    pub fn insert(&mut self, updates: &mut Vec<(D, Timestamp, Diff)>) {
214        let Some(since_ts) = self.since.as_option() else {
215            // If the since frontier is empty, discard all updates.
216            updates.clear();
217            return;
218        };
219
220        for (_, time, _) in &mut *updates {
221            *time = std::cmp::max(*time, *since_ts);
222        }
223        self.insert_inner(updates);
224    }
225
226    /// Insert a batch of updates, after negating their diffs.
227    pub fn insert_negated(&mut self, updates: &mut Vec<(D, Timestamp, Diff)>) {
228        let Some(since_ts) = self.since.as_option() else {
229            // If the since frontier is empty, discard all updates.
230            updates.clear();
231            return;
232        };
233
234        for (_, time, diff) in &mut *updates {
235            *time = std::cmp::max(*time, *since_ts);
236            *diff = -*diff;
237        }
238        self.insert_inner(updates);
239    }
240
241    /// Insert a batch of updates.
242    ///
243    /// The given `updates` must all have been advanced by `self.since`.
244    fn insert_inner(&mut self, updates: &mut Vec<(D, Timestamp, Diff)>) {
245        consolidate_updates(updates);
246        updates.sort_unstable_by_key(|(_, time, _)| *time);
247
248        let mut new_size = self.total_size;
249        let mut updates = updates.drain(..).peekable();
250        while let Some(&(_, time, _)) = updates.peek() {
251            mz_ore::soft_assert_no_log!(
252                self.since.less_equal(&time),
253                "update not advanced by `since`"
254            );
255
256            let data = updates
257                .peeking_take_while(|(_, t, _)| *t == time)
258                .map(|(d, _, r)| (d, r));
259
260            use std::collections::btree_map::Entry;
261            match self.updates.entry(time) {
262                Entry::Vacant(entry) => {
263                    let mut vec: ConsolidatingVec<_> = data.collect();
264                    vec.growth_dampener = self.growth_dampener;
265                    new_size += (vec.len(), vec.capacity());
266                    entry.insert(vec);
267                }
268                Entry::Occupied(mut entry) => {
269                    let vec = entry.get_mut();
270                    new_size -= (vec.len(), vec.capacity());
271                    vec.extend(data);
272                    new_size += (vec.len(), vec.capacity());
273                }
274            }
275        }
276
277        self.update_metrics(new_size);
278    }
279
280    /// The range of stored times before the given `upper`.
281    fn range_before(upper: &Antichain<Timestamp>) -> (Bound<Timestamp>, Bound<Timestamp>) {
282        let start = Bound::Included(Timestamp::MIN);
283        let end = match upper.as_option() {
284            Some(ts) => Bound::Excluded(*ts),
285            None => Bound::Unbounded,
286        };
287        (start, end)
288    }
289
290    /// Consolidate the updates before the given `upper`.
291    pub fn consolidate_before(&mut self, upper: &Antichain<Timestamp>) {
292        let _ = self.consolidate(Self::range_before(upper));
293    }
294
295    /// Return the updates before the given `upper`, as consolidated by a preceding
296    /// [`CorrectionV1::consolidate_before`] call.
297    ///
298    /// The caller must have invoked `consolidate_before` with the same `upper` and must not have
299    /// mutated the buffer since. Otherwise the returned updates are not consolidated.
300    pub fn consolidated_updates_before<'a>(
301        &'a self,
302        upper: &Antichain<Timestamp>,
303    ) -> impl Iterator<Item = (D, Timestamp, Diff)> + Send + use<'a, D> {
304        self.updates
305            .range(Self::range_before(upper))
306            .flat_map(|(t, data)| data.iter().map(|(d, r)| (d.clone(), *t, *r)))
307    }
308
309    /// Consolidate and return updates before the given `upper`.
310    pub fn updates_before<'a>(
311        &'a mut self,
312        upper: &Antichain<Timestamp>,
313    ) -> impl Iterator<Item = (D, Timestamp, Diff)> + Send + use<'a, D> {
314        self.consolidate_before(upper);
315        self.consolidated_updates_before(upper)
316    }
317
318    /// Consolidate the updates at the times in the given range.
319    ///
320    /// Returns the number of updates remaining in the range afterwards.
321    fn consolidate<R>(&mut self, range: R) -> usize
322    where
323        R: RangeBounds<Timestamp>,
324    {
325        let mut new_size = self.total_size;
326
327        let updates = self.updates.range_mut(range);
328        let count = updates.fold(0, |acc, (_, data)| {
329            new_size -= (data.len(), data.capacity());
330            data.consolidate();
331            new_size += (data.len(), data.capacity());
332            acc + data.len()
333        });
334
335        self.update_metrics(new_size);
336        count
337    }
338
339    /// Advance the since frontier.
340    ///
341    /// # Panics
342    ///
343    /// Panics if the given `since` is less than the current since frontier.
344    pub fn advance_since(&mut self, since: Antichain<Timestamp>) {
345        assert!(PartialOrder::less_equal(&self.since, &since));
346
347        if since != self.since {
348            self.advance_by(&since);
349            self.since = since;
350        }
351    }
352
353    /// Advance all contained updates by the given frontier.
354    ///
355    /// If the given frontier is empty, all remaining updates are discarded.
356    pub fn advance_by(&mut self, frontier: &Antichain<Timestamp>) {
357        let Some(target_ts) = frontier.as_option() else {
358            self.updates.clear();
359            self.update_metrics(Default::default());
360            return;
361        };
362
363        let mut new_size = self.total_size;
364        while let Some((ts, data)) = self.updates.pop_first() {
365            if frontier.less_equal(&ts) {
366                // We have advanced all updates that can advance.
367                self.updates.insert(ts, data);
368                break;
369            }
370
371            use std::collections::btree_map::Entry;
372            match self.updates.entry(*target_ts) {
373                Entry::Vacant(entry) => {
374                    entry.insert(data);
375                }
376                Entry::Occupied(mut entry) => {
377                    let vec = entry.get_mut();
378                    new_size -= (data.len(), data.capacity());
379                    new_size -= (vec.len(), vec.capacity());
380                    vec.extend(data);
381                    new_size += (vec.len(), vec.capacity());
382                }
383            }
384        }
385
386        self.update_metrics(new_size);
387    }
388
389    /// Consolidate all updates at the current `since`.
390    pub fn consolidate_at_since(&mut self) {
391        let Some(since_ts) = self.since.as_option() else {
392            return;
393        };
394
395        let start = Bound::Included(*since_ts);
396        let end = match since_ts.try_step_forward() {
397            Some(ts) => Bound::Excluded(ts),
398            None => Bound::Unbounded,
399        };
400
401        self.consolidate((start, end));
402    }
403}
404
405impl<D> Drop for CorrectionV1<D> {
406    fn drop(&mut self) {
407        self.update_metrics(Default::default());
408    }
409}
410
411/// Helper type for convenient tracking of length and capacity together.
412#[derive(Clone, Copy, Debug, Default)]
413pub(super) struct LengthAndCapacity {
414    pub length: usize,
415    pub capacity: usize,
416}
417
418impl AddAssign<Self> for LengthAndCapacity {
419    fn add_assign(&mut self, size: Self) {
420        self.length += size.length;
421        self.capacity += size.capacity;
422    }
423}
424
425impl AddAssign<(usize, usize)> for LengthAndCapacity {
426    fn add_assign(&mut self, (len, cap): (usize, usize)) {
427        self.length += len;
428        self.capacity += cap;
429    }
430}
431
432impl SubAssign<(usize, usize)> for LengthAndCapacity {
433    fn sub_assign(&mut self, (len, cap): (usize, usize)) {
434        self.length -= len;
435        self.capacity -= cap;
436    }
437}
438
439/// A vector that consolidates its contents.
440///
441/// The vector is filled with updates until it reaches capacity. At this point, the updates are
442/// consolidated to free up space. This process repeats until the consolidation recovered less than
443/// half of the vector's capacity, at which point the capacity is doubled.
444#[derive(Debug)]
445pub(crate) struct ConsolidatingVec<D> {
446    data: Vec<(D, Diff)>,
447    /// A lower bound for how small we'll shrink the Vec's capacity. NB: The cap
448    /// might start smaller than this.
449    min_capacity: usize,
450    /// Dampener in the growth rate. 0 corresponds to doubling and in general `n` to `1+1/(n+1)`.
451    ///
452    /// If consolidation didn't free enough space, at least a linear amount, increase the capacity
453    /// Setting this to 0 results in doubling whenever the list is at least half full.
454    /// Larger numbers result in more conservative approaches that use more CPU, but less memory.
455    growth_dampener: usize,
456}
457
458impl<D: Ord> ConsolidatingVec<D> {
459    /// Return the length of the vector.
460    pub fn len(&self) -> usize {
461        self.data.len()
462    }
463
464    /// Return the capacity of the vector.
465    pub fn capacity(&self) -> usize {
466        self.data.capacity()
467    }
468
469    /// Pushes `item` into the vector.
470    ///
471    /// If the vector does not have sufficient capacity, we'll first consolidate and then increase
472    /// its capacity if the consolidated results still occupy a significant fraction of the vector.
473    ///
474    /// The worst-case cost of this function is O(n log n) in the number of items the vector stores,
475    /// but amortizes to O(log n).
476    pub fn push(&mut self, item: (D, Diff)) {
477        let capacity = self.data.capacity();
478        if self.data.len() == capacity {
479            // The vector is full. First, consolidate to try to recover some space.
480            self.consolidate();
481
482            // We may need more capacity if our current capacity is within `1+1/(n+1)` of the length.
483            // This corresponds to `cap < len + len/(n+1)`, which is the logic we use.
484            let length = self.data.len();
485            let dampener = self.growth_dampener;
486            if capacity < length + length / (dampener + 1) {
487                // We would like to increase the capacity by a factor of `1+1/(n+1)`, which involves
488                // determining the target capacity, and then reserving an amount that achieves this
489                // while working around the existing length.
490                let new_cap = capacity + capacity / (dampener + 1);
491                self.data.reserve_exact(new_cap - length);
492            }
493        }
494
495        self.data.push(item);
496    }
497
498    /// Consolidate the contents.
499    pub fn consolidate(&mut self) {
500        consolidate(&mut self.data);
501
502        // We may have the opportunity to reclaim allocated memory.
503        // Given that `push` will at most double the capacity when the vector is more than half full, and
504        // we want to avoid entering into a resizing cycle, we choose to only shrink if the
505        // vector's length is less than one fourth of its capacity.
506        if self.data.len() < self.data.capacity() / 4 {
507            self.data.shrink_to(self.min_capacity);
508        }
509    }
510
511    /// Return an iterator over the borrowed items.
512    pub fn iter(&self) -> impl Iterator<Item = &(D, Diff)> {
513        self.data.iter()
514    }
515}
516
517impl<D> IntoIterator for ConsolidatingVec<D> {
518    type Item = (D, Diff);
519    type IntoIter = std::vec::IntoIter<(D, Diff)>;
520
521    fn into_iter(self) -> Self::IntoIter {
522        self.data.into_iter()
523    }
524}
525
526impl<D> FromIterator<(D, Diff)> for ConsolidatingVec<D> {
527    fn from_iter<I>(iter: I) -> Self
528    where
529        I: IntoIterator<Item = (D, Diff)>,
530    {
531        Self {
532            data: Vec::from_iter(iter),
533            min_capacity: 0,
534            growth_dampener: 0,
535        }
536    }
537}
538
539impl<D: Ord> Extend<(D, Diff)> for ConsolidatingVec<D> {
540    fn extend<I>(&mut self, iter: I)
541    where
542        I: IntoIterator<Item = (D, Diff)>,
543    {
544        for item in iter {
545            self.push(item);
546        }
547    }
548}
549
550/// Helper type for convenient tracking of various size metrics together.
551#[derive(Clone, Copy, Debug, Default)]
552pub(super) struct SizeMetrics {
553    pub size: usize,
554    pub capacity: usize,
555    pub allocations: usize,
556}
557
558impl AddAssign<Self> for SizeMetrics {
559    fn add_assign(&mut self, other: Self) {
560        self.size += other.size;
561        self.capacity += other.capacity;
562        self.allocations += other.allocations;
563    }
564}
565
566/// A logging event sent from the Tokio task back to the Timely thread.
567#[derive(Debug)]
568pub enum LoggingEvent {
569    /// A chain with the given number of updates was created.
570    ChainCreated(usize),
571    /// A chain with the given number of updates was dropped.
572    ChainDropped(usize),
573    /// The heap size of the correction buffer changed by the given amount.
574    SizeDiff(NonZeroIsize),
575    /// The heap capacity of the correction buffer changed by the given amount.
576    CapacityDiff(NonZeroIsize),
577    /// The number of allocations of the correction buffer changed by the given amount.
578    AllocationsDiff(NonZeroIsize),
579}
580
581/// Channel-based logging for corrections on a Tokio task. `Send`-safe.
582///
583/// Sends logging events to the Timely thread, where they are applied to the real `Logging`
584/// instance. This allows corrections on the Tokio task to participate in introspection logging
585/// without holding `Rc<RefCell<..>>`.
586#[derive(Clone, Debug)]
587pub struct ChannelLogging(mpsc::UnboundedSender<LoggingEvent>);
588
589impl ChannelLogging {
590    /// Construct a new `ChannelLogging` sending events on the given channel.
591    pub fn new(tx: mpsc::UnboundedSender<LoggingEvent>) -> Self {
592        Self(tx)
593    }
594
595    /// Report the creation of a chain with the given number of updates.
596    pub fn chain_created(&self, updates: usize) {
597        let _ = self.0.send(LoggingEvent::ChainCreated(updates));
598    }
599
600    /// Report the dropping of a chain with the given number of updates.
601    pub fn chain_dropped(&self, updates: usize) {
602        let _ = self.0.send(LoggingEvent::ChainDropped(updates));
603    }
604
605    /// Report a change in heap size by the given amount.
606    pub fn report_size_diff(&self, diff: isize) {
607        if let Some(diff) = NonZeroIsize::new(diff) {
608            let _ = self.0.send(LoggingEvent::SizeDiff(diff));
609        }
610    }
611
612    /// Report a change in heap capacity by the given amount.
613    pub fn report_capacity_diff(&self, diff: isize) {
614        if let Some(diff) = NonZeroIsize::new(diff) {
615            let _ = self.0.send(LoggingEvent::CapacityDiff(diff));
616        }
617    }
618
619    /// Report a change in the number of allocations by the given amount.
620    pub fn report_allocations_diff(&self, diff: isize) {
621        if let Some(diff) = NonZeroIsize::new(diff) {
622            let _ = self.0.send(LoggingEvent::AllocationsDiff(diff));
623        }
624    }
625}
626
627/// State for correction buffer logging on the Timely thread.
628///
629/// Drains [`LoggingEvent`]s sent by [`ChannelLogging`] from the Tokio task and applies them
630/// to the compute and differential loggers. Emits `ArrangementHeapSizeOperator` on construction
631/// and `ArrangementHeapSizeOperatorDrop` on drop.
632// TODO: Correction buffer logging currently reuses the arrangement batch and size logging. This
633// isn't strictly correct as a correction buffer is not an arrangement. Consider refactoring this
634// to be about "operator sizes" instead.
635pub(super) struct CorrectionLogger {
636    compute_logger: ComputeLogger,
637    differential_logger: differential_dataflow::logging::Logger,
638    operator_id: usize,
639    rx: mpsc::UnboundedReceiver<LoggingEvent>,
640    /// Net number of batches logged (BatchEvent - DropEvent).
641    net_batches: isize,
642    /// Net number of records logged across all batch/drop/merge events.
643    net_records: isize,
644    /// Cumulative heap size delta, for retraction on drop.
645    net_size: isize,
646    /// Cumulative heap capacity delta, for retraction on drop.
647    net_capacity: isize,
648    /// Cumulative heap allocations delta, for retraction on drop.
649    net_allocations: isize,
650}
651
652impl fmt::Debug for CorrectionLogger {
653    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
654        f.debug_struct("CorrectionLogger")
655            .field("operator_id", &self.operator_id)
656            .finish_non_exhaustive()
657    }
658}
659
660impl CorrectionLogger {
661    pub fn new(
662        compute_logger: ComputeLogger,
663        differential_logger: differential_dataflow::logging::Logger,
664        operator_id: usize,
665        address: Vec<usize>,
666        rx: mpsc::UnboundedReceiver<LoggingEvent>,
667    ) -> Self {
668        compute_logger.log(&ComputeEvent::ArrangementHeapSizeOperator(
669            ArrangementHeapSizeOperator {
670                operator_id,
671                address,
672            },
673        ));
674
675        Self {
676            compute_logger,
677            differential_logger,
678            operator_id,
679            rx,
680            net_batches: 0,
681            net_records: 0,
682            net_size: 0,
683            net_capacity: 0,
684            net_allocations: 0,
685        }
686    }
687
688    /// Drain logging events from the channel and apply them locally.
689    pub fn apply_events(&mut self) {
690        use LoggingEvent::*;
691
692        while let Ok(event) = self.rx.try_recv() {
693            match event {
694                ChainCreated(length) => {
695                    self.net_batches += 1;
696                    self.net_records += isize::try_from(length).expect("must fit");
697                    self.differential_logger.log(BatchEvent {
698                        operator: self.operator_id,
699                        length,
700                    });
701                }
702                ChainDropped(length) => {
703                    self.net_batches -= 1;
704                    self.net_records -= isize::try_from(length).expect("must fit");
705                    self.differential_logger.log(DropEvent {
706                        operator: self.operator_id,
707                        length,
708                    });
709                }
710                SizeDiff(delta_size) => {
711                    self.net_size += delta_size.get();
712                    self.compute_logger.log(&ComputeEvent::ArrangementHeapSize(
713                        ArrangementHeapSize {
714                            operator_id: self.operator_id,
715                            delta_size: delta_size.get(),
716                        },
717                    ));
718                }
719                CapacityDiff(delta_capacity) => {
720                    self.net_capacity += delta_capacity.get();
721                    self.compute_logger
722                        .log(&ComputeEvent::ArrangementHeapCapacity(
723                            ArrangementHeapCapacity {
724                                operator_id: self.operator_id,
725                                delta_capacity: delta_capacity.get(),
726                            },
727                        ));
728                }
729                AllocationsDiff(delta_allocations) => {
730                    self.net_allocations += delta_allocations.get();
731                    self.compute_logger
732                        .log(&ComputeEvent::ArrangementHeapAllocations(
733                            ArrangementHeapAllocations {
734                                operator_id: self.operator_id,
735                                delta_allocations: delta_allocations.get(),
736                            },
737                        ));
738                }
739            }
740        }
741    }
742}
743
744impl Drop for CorrectionLogger {
745    fn drop(&mut self) {
746        // Drain any events that arrived before the drop. Note that the Tokio task
747        // may still be running (abort is async), so some events may not have arrived
748        // yet. We retract any remaining batch/record counts below.
749        self.apply_events();
750
751        // Retract any outstanding batch and record counts that weren't balanced by
752        // ChainDropped events. This handles the case where the Tokio task is aborted
753        // and its Correction destructors haven't run yet (abort is async).
754        //
755        // Each DropEvent retracts one batch and `length` records, so we emit one per
756        // outstanding batch, with the first carrying all outstanding records.
757        for i in 0..self.net_batches {
758            let length = if i == 0 {
759                usize::try_from(self.net_records).unwrap_or(0)
760            } else {
761                0
762            };
763            self.differential_logger.log(DropEvent {
764                operator: self.operator_id,
765                length,
766            });
767        }
768
769        // Retract any outstanding heap size/capacity/allocations deltas.
770        if self.net_size != 0 {
771            self.compute_logger
772                .log(&ComputeEvent::ArrangementHeapSize(ArrangementHeapSize {
773                    operator_id: self.operator_id,
774                    delta_size: -self.net_size,
775                }));
776        }
777        if self.net_capacity != 0 {
778            self.compute_logger
779                .log(&ComputeEvent::ArrangementHeapCapacity(
780                    ArrangementHeapCapacity {
781                        operator_id: self.operator_id,
782                        delta_capacity: -self.net_capacity,
783                    },
784                ));
785        }
786        if self.net_allocations != 0 {
787            self.compute_logger
788                .log(&ComputeEvent::ArrangementHeapAllocations(
789                    ArrangementHeapAllocations {
790                        operator_id: self.operator_id,
791                        delta_allocations: -self.net_allocations,
792                    },
793                ));
794        }
795
796        self.compute_logger
797            .log(&ComputeEvent::ArrangementHeapSizeOperatorDrop(
798                ArrangementHeapSizeOperatorDrop {
799                    operator_id: self.operator_id,
800                },
801            ));
802    }
803}