Skip to main content

mz_compute/
logging.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//! Logging dataflows for events generated by various subsystems.
11
12pub mod compute;
13mod differential;
14pub(super) mod initialize;
15mod prometheus;
16mod reachability;
17mod resource_usage;
18mod timely;
19
20use std::any::Any;
21use std::collections::BTreeMap;
22use std::marker::PhantomData;
23use std::rc::Rc;
24use std::time::{Duration, Instant};
25
26use ::prometheus::IntCounter;
27use ::timely::container::{CapacityContainerBuilder, PushInto};
28use ::timely::dataflow::Stream;
29use ::timely::dataflow::channels::pact::Pipeline;
30use ::timely::dataflow::operators::capture::{Event, EventLink, EventPusher};
31use ::timely::dataflow::operators::generic::Session;
32use ::timely::dataflow::operators::{Capability, CapabilityTrait, InputCapability, Operator};
33use ::timely::progress::Stamp;
34use ::timely::progress::Timestamp as TimelyTimestamp;
35use ::timely::scheduling::Activator;
36use ::timely::{Container, ContainerBuilder};
37use differential_dataflow::trace::Batcher;
38use mz_compute_client::logging::{ComputeLog, DifferentialLog, LogVariant, TimelyLog};
39use mz_expr::{MirScalarExpr, permutation_for_arrangement};
40use mz_repr::{Datum, Diff, Row, RowPacker, RowRef, Timestamp};
41use mz_timely_util::activator::RcActivator;
42use mz_timely_util::columnar::builder::ColumnBuilder;
43use mz_timely_util::operator::{BatcherNew, consolidate_pact};
44
45use crate::logging::compute::Logger as ComputeLogger;
46use crate::typedefs::RowRowAgent;
47
48pub use crate::logging::initialize::initialize;
49
50/// An update of value `D` at a time and with a diff.
51pub(super) type Update<D> = (D, Timestamp, Diff);
52/// A pusher for containers `C`.
53/// An output session for the specified container builder.
54pub(super) type OutputSession<'a, 'b, CB> =
55    Session<'a, 'b, Timestamp, CB, InputCapability<Timestamp>>;
56/// An output session for vector-based containers of updates `D`, using a capacity container builder.
57pub(super) type OutputSessionVec<'a, 'b, D> =
58    OutputSession<'a, 'b, CapacityContainerBuilder<Vec<D>>>;
59/// An output session for columnar containers of updates `D`, using a column builder.
60pub(super) type OutputSessionColumnar<'a, 'b, D> = OutputSession<'a, 'b, ColumnBuilder<D>>;
61
62/// Logs events as a timely stream, with progress statements.
63struct BatchLogger<C, P>
64where
65    P: EventPusher<Timestamp, C>,
66{
67    /// Time in milliseconds of the current expressed capability.
68    time_ms: Timestamp,
69    /// Pushes events to the logging dataflow.
70    event_pusher: P,
71    /// Each time is advanced to the strictly next millisecond that is a multiple of this interval.
72    /// This means we should be able to perform the same action on timestamp capabilities, and only
73    /// flush buffers when this timestamp advances.
74    interval_ms: u128,
75    /// Counts the records of published batches.
76    records_total: IntCounter,
77    _marker: PhantomData<C>,
78}
79
80impl<C, P> BatchLogger<C, P>
81where
82    P: EventPusher<Timestamp, C>,
83{
84    /// Creates a new batch logger.
85    fn new(event_pusher: P, interval_ms: u128, records_total: IntCounter) -> Self {
86        BatchLogger {
87            time_ms: Timestamp::minimum(),
88            event_pusher,
89            interval_ms,
90            records_total,
91            _marker: PhantomData,
92        }
93    }
94}
95
96impl<C, P> BatchLogger<C, P>
97where
98    P: EventPusher<Timestamp, C>,
99    C: Container,
100{
101    /// Publishes a batch of logged events.
102    fn publish_batch(&mut self, data: C) {
103        let records = u64::try_from(data.record_count()).expect("record count is non-negative");
104        self.records_total.inc_by(records);
105        // One capability per logged batch: the stamp is a singleton.
106        self.event_pusher
107            .push(Event::Messages(Stamp::from_elem(self.time_ms), data));
108    }
109
110    /// Indicate progress up to `time`, advances the capability.
111    ///
112    /// Returns `true` if the capability was advanced.
113    fn report_progress(&mut self, time: Duration) -> bool {
114        let time_ms = ((time.as_millis() / self.interval_ms) + 1) * self.interval_ms;
115        let new_time_ms: Timestamp = time_ms.try_into().expect("must fit");
116        if self.time_ms < new_time_ms {
117            self.event_pusher
118                .push(Event::Progress(vec![(new_time_ms, 1), (self.time_ms, -1)]));
119            self.time_ms = new_time_ms;
120            true
121        } else {
122            false
123        }
124    }
125}
126
127impl<C, P> Drop for BatchLogger<C, P>
128where
129    P: EventPusher<Timestamp, C>,
130{
131    fn drop(&mut self) {
132        self.event_pusher
133            .push(Event::Progress(vec![(self.time_ms, -1)]));
134    }
135}
136
137/// Parts to connect a logging dataflows the timely runtime.
138///
139/// This is just a bundle-type intended to make passing around its contents in the logging
140/// initialization code more convenient.
141///
142/// The `N` type parameter specifies the number of links to create for the event queue. We need
143/// separate links for queues that feed from multiple loggers because the `EventLink` type is not
144/// multi-producer safe (it is a linked-list, and multiple writers would blindly append, replacing
145/// existing new data, and cutting off other writers).
146#[derive(Clone)]
147struct EventQueue<C, const N: usize = 1> {
148    links: [Rc<EventLink<Timestamp, C>>; N],
149    activator: RcActivator,
150}
151
152impl<C, const N: usize> EventQueue<C, N> {
153    fn new(name: &str) -> Self {
154        let activator_name = format!("{name}_activator");
155        let activate_after = 128;
156        Self {
157            links: [(); N].map(|_| Rc::new(EventLink::new())),
158            activator: RcActivator::new(activator_name, activate_after),
159        }
160    }
161}
162
163/// State shared between different logging dataflow fragments.
164#[derive(Default)]
165struct SharedLoggingState {
166    /// Activators for arrangement heap size operators.
167    arrangement_size_activators: BTreeMap<usize, Activator>,
168    /// Shared compute logger.
169    compute_logger: Option<ComputeLogger>,
170}
171
172/// Helper to pack collections of [`Datum`]s into key and value row.
173pub(crate) struct PermutedRowPacker {
174    key: Vec<usize>,
175    value: Vec<usize>,
176    key_row: Row,
177    value_row: Row,
178}
179
180impl PermutedRowPacker {
181    /// Construct based on the information within the log variant.
182    pub(crate) fn new<V: Into<LogVariant>>(variant: V) -> Self {
183        let variant = variant.into();
184        let key = variant.index_by();
185        let (_, value) = permutation_for_arrangement(
186            &key.iter()
187                .cloned()
188                .map(MirScalarExpr::column)
189                .collect::<Vec<_>>(),
190            variant.desc().arity(),
191        );
192        Self {
193            key,
194            value,
195            key_row: Row::default(),
196            value_row: Row::default(),
197        }
198    }
199
200    /// Pack a slice of datums suitable for the key columns in the log variant.
201    pub(crate) fn pack_slice(&mut self, datums: &[Datum]) -> (&RowRef, &RowRef) {
202        self.pack_by_index(|packer, index| packer.push(datums[index]))
203    }
204
205    /// Pack using a callback suitable for the key columns in the log variant.
206    pub(crate) fn pack_by_index<F: Fn(&mut RowPacker, usize)>(
207        &mut self,
208        logic: F,
209    ) -> (&RowRef, &RowRef) {
210        let mut packer = self.key_row.packer();
211        for index in &self.key {
212            logic(&mut packer, *index);
213        }
214
215        let mut packer = self.value_row.packer();
216        for index in &self.value {
217            logic(&mut packer, *index);
218        }
219
220        (&self.key_row, &self.value_row)
221    }
222}
223
224/// Downgrade `cap` to the next logging-interval boundary and schedule the operator's next
225/// activation there. Returns the time the capability now holds.
226///
227/// `now` and `start_offset` must be the ones the logging dataflow was constructed with, so that
228/// every collection in it reports on the same boundaries. Scheduling off the boundary rather than
229/// off a fixed delay keeps the output frontier progressing at the logging rate without drifting
230/// from wall-clock elapsed time.
231///
232/// NOTE: downgrading the capability asserts the collection is complete up to the new time, so an
233/// operator that samples less often than the logging interval publishes a stale value rather than
234/// withholding it.
235pub(super) fn downgrade_to_interval_boundary(
236    cap: &mut Capability<Timestamp>,
237    activator: &Activator,
238    now: Instant,
239    start_offset: Duration,
240    interval_ms: u128,
241) -> Timestamp {
242    let elapsed = now.elapsed().as_millis();
243    let time_ms: u128 = ((elapsed + start_offset.as_millis()) / interval_ms + 1) * interval_ms;
244    let ts: Timestamp = time_ms.try_into().expect("must fit");
245    cap.downgrade(&ts);
246
247    let next_boundary_ms = time_ms - start_offset.as_millis();
248    let next_activation =
249        now + Duration::from_millis(next_boundary_ms.try_into().expect("must fit"));
250    activator.activate_after(next_activation.saturating_duration_since(Instant::now()));
251
252    ts
253}
254
255/// Emit the difference between two snapshots of a sampled source as updates at `ts`.
256///
257/// A sampled source reports its whole state on every read, so a changed value has to be expressed
258/// as a retraction of the previous one paired with an insertion of the new one. A key absent from
259/// `current` is retracted without a replacement, so a source that stops being readable drops out of
260/// the collection rather than lingering at its last value.
261///
262/// `pack` takes the packer as an argument instead of closing over it, because it hands back rows
263/// borrowed from it.
264pub(super) fn emit_snapshot_diff<K, V, CB, P, F>(
265    session: &mut Session<'_, '_, Timestamp, CB, P>,
266    packer: &mut PermutedRowPacker,
267    prev: &BTreeMap<K, V>,
268    current: &BTreeMap<K, V>,
269    ts: Timestamp,
270    pack: F,
271) where
272    K: Ord,
273    V: PartialEq,
274    CB: ContainerBuilder + for<'a> PushInto<((&'a RowRef, &'a RowRef), Timestamp, Diff)>,
275    P: CapabilityTrait<Timestamp>,
276    F: for<'a> Fn(&'a mut PermutedRowPacker, &K, &V) -> (&'a RowRef, &'a RowRef),
277{
278    for (key, value) in prev {
279        if current.get(key) != Some(value) {
280            let row = pack(packer, key, value);
281            session.give((row, ts, Diff::MINUS_ONE));
282        }
283    }
284    for (key, value) in current {
285        if prev.get(key) != Some(value) {
286            let row = pack(packer, key, value);
287            session.give((row, ts, Diff::ONE));
288        }
289    }
290}
291
292/// Information about a collection exported from a logging dataflow.
293struct LogCollection {
294    /// Trace handle providing access to the logged records.
295    trace: RowRowAgent<Timestamp, Diff>,
296    /// Token that should be dropped to drop this collection.
297    token: Rc<dyn Any>,
298}
299
300/// A single-purpose function to consolidate and pack updates for log collection.
301///
302/// The function first consolidates worker-local updates using the [`Pipeline`] pact, then converts
303/// the updates into `(Row, Row)` pairs using the provided logic function. It is crucial that the
304/// data is not exchanged between workers, as the consolidation would not function as desired
305/// otherwise.
306pub(super) fn consolidate_and_pack<'scope, B, CB, L, F, C, Ch>(
307    input: Stream<'scope, Timestamp, C>,
308    log: L,
309    mut logic: F,
310) -> Stream<'scope, Timestamp, CB::Container>
311where
312    B: Batcher<C, Time = Timestamp, Output = Vec<Ch>> + BatcherNew + 'static,
313    C: Container + Clone + 'static,
314    Ch: Clone + 'static,
315    CB: ContainerBuilder,
316    L: Into<LogVariant>,
317    F: FnMut(Ch, &mut PermutedRowPacker, &mut OutputSession<CB>) + 'static,
318{
319    let log = log.into();
320    // TODO: Use something other than the debug representation of the log variant as a name.
321    let c_name = &format!("Consolidate {log:?}");
322    let u_name = &format!("ToRow {log:?}");
323    let mut packer = PermutedRowPacker::new(log);
324    let consolidated = consolidate_pact::<B, _, _>(input, Pipeline, c_name, B::new_batcher);
325    consolidated.unary::<CB, _, _, _>(Pipeline, u_name, |_, _| {
326        move |input, output| {
327            input.for_each_stamp(|time, data| {
328                let mut session = output.session_with_builder(&time);
329                for item in data.flatten().flat_map(|data| data.drain(..)) {
330                    logic(item, &mut packer, &mut session);
331                }
332            });
333        }
334    })
335}