Skip to main content

mz_storage/source/
generator.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
10use std::collections::BTreeMap;
11use std::convert::Infallible;
12use std::ops::Rem;
13use std::sync::Arc;
14use std::time::Duration;
15
16use differential_dataflow::{AsCollection, Hashable};
17use futures::StreamExt;
18use itertools::Itertools;
19use mz_ore::cast::CastFrom;
20use mz_ore::iter::IteratorExt;
21use mz_ore::now::NowFn;
22use mz_repr::{Diff, GlobalId, Row};
23use mz_storage_types::errors::DataflowError;
24use mz_storage_types::sources::load_generator::{
25    Event, Generator, KeyValueLoadGenerator, LoadGenerator, LoadGeneratorOutput,
26    LoadGeneratorSourceConnection,
27};
28use mz_storage_types::sources::{MzOffset, SourceExportDetails, SourceTimestamp};
29use mz_timely_util::builder_async::{
30    Event as AsyncEvent, OperatorBuilder as AsyncOperatorBuilder, PressOnDropButton,
31};
32use mz_timely_util::containers::stack::FueledBuilder;
33use timely::container::CapacityContainerBuilder;
34use timely::dataflow::channels::pact::Pipeline;
35use timely::dataflow::operators::core::Partition;
36use timely::dataflow::{Scope, StreamVec};
37use timely::progress::{Antichain, Timestamp};
38use tokio::time::{Instant, interval_at};
39
40use crate::healthcheck::{HealthStatusMessage, HealthStatusUpdate, StatusNamespace};
41use crate::source::types::{
42    FuelSize, Probe, ResumeUppers, SignaledFuture, SourceRender, StackedCollection,
43};
44use crate::source::{RawSourceCreationConfig, SourceMessage};
45
46mod auction;
47mod clock;
48mod counter;
49mod datums;
50mod key_value;
51mod marketing;
52mod tpch;
53
54pub use auction::Auction;
55pub use clock::Clock;
56pub use counter::Counter;
57pub use datums::Datums;
58pub use tpch::Tpch;
59
60use self::marketing::Marketing;
61
62enum GeneratorKind {
63    Simple {
64        generator: Box<dyn Generator>,
65        tick_micros: Option<u64>,
66        as_of: u64,
67        up_to: u64,
68    },
69    KeyValue(KeyValueLoadGenerator),
70}
71
72impl GeneratorKind {
73    fn new(g: &LoadGenerator, tick_micros: Option<u64>, as_of: u64, up_to: u64) -> Self {
74        match g {
75            LoadGenerator::Auction => GeneratorKind::Simple {
76                generator: Box::new(Auction {}),
77                tick_micros,
78                as_of,
79                up_to,
80            },
81            LoadGenerator::Clock => GeneratorKind::Simple {
82                generator: Box::new(Clock {
83                    tick_ms: tick_micros
84                        .map(Duration::from_micros)
85                        .unwrap_or(Duration::from_secs(1))
86                        .as_millis()
87                        .try_into()
88                        .expect("reasonable tick interval"),
89                    as_of_ms: as_of,
90                }),
91                tick_micros,
92                as_of,
93                up_to,
94            },
95            LoadGenerator::Counter { max_cardinality } => GeneratorKind::Simple {
96                generator: Box::new(Counter {
97                    max_cardinality: max_cardinality.clone(),
98                }),
99                tick_micros,
100                as_of,
101                up_to,
102            },
103            LoadGenerator::Datums => GeneratorKind::Simple {
104                generator: Box::new(Datums {}),
105                tick_micros,
106                as_of,
107                up_to,
108            },
109            LoadGenerator::Marketing => GeneratorKind::Simple {
110                generator: Box::new(Marketing {}),
111                tick_micros,
112                as_of,
113                up_to,
114            },
115            LoadGenerator::Tpch {
116                count_supplier,
117                count_part,
118                count_customer,
119                count_orders,
120                count_clerk,
121            } => GeneratorKind::Simple {
122                generator: Box::new(Tpch {
123                    count_supplier: *count_supplier,
124                    count_part: *count_part,
125                    count_customer: *count_customer,
126                    count_orders: *count_orders,
127                    count_clerk: *count_clerk,
128                    // The default tick behavior 1s. For tpch we want to disable ticking
129                    // completely.
130                    tick: Duration::from_micros(tick_micros.unwrap_or(0)),
131                }),
132                tick_micros,
133                as_of,
134                up_to,
135            },
136            LoadGenerator::KeyValue(kv) => GeneratorKind::KeyValue(kv.clone()),
137        }
138    }
139
140    fn render<'scope>(
141        self,
142        scope: Scope<'scope, MzOffset>,
143        config: &RawSourceCreationConfig,
144        committed_uppers: impl futures::Stream<Item = ResumeUppers<MzOffset>> + 'static,
145        start_signal: impl std::future::Future<Output = ()> + 'static,
146    ) -> (
147        BTreeMap<
148            GlobalId,
149            StackedCollection<'scope, MzOffset, Result<SourceMessage, DataflowError>>,
150        >,
151        StreamVec<'scope, MzOffset, Infallible>,
152        StreamVec<'scope, MzOffset, HealthStatusMessage>,
153        Vec<PressOnDropButton>,
154    ) {
155        // figure out which output types from the generator belong to which output indexes
156        let mut output_map = BTreeMap::new();
157        // Maps the output index to export_id for statistics.
158        let mut idx_to_exportid = BTreeMap::new();
159        // Make sure that there's an entry for the default output, even if there are no exports
160        // that need data output. Certain implementations rely on it (at the time of this comment
161        // that includes the key-value load gen source).
162        output_map.insert(LoadGeneratorOutput::Default, Vec::new());
163        for (idx, (export_id, export)) in config.source_exports.iter().enumerate() {
164            let output_type = match &export.details {
165                SourceExportDetails::LoadGenerator(details) => details.output,
166                // This is an export that doesn't need any data output to it.
167                SourceExportDetails::None => continue,
168                _ => panic!("unexpected source export details: {:?}", export.details),
169            };
170            output_map
171                .entry(output_type)
172                .or_insert_with(Vec::new)
173                .push(idx);
174            idx_to_exportid.insert(idx, export_id.clone());
175        }
176
177        match self {
178            GeneratorKind::Simple {
179                tick_micros,
180                as_of,
181                up_to,
182                generator,
183            } => render_simple_generator(
184                generator,
185                tick_micros,
186                as_of.into(),
187                up_to.into(),
188                scope,
189                config,
190                committed_uppers,
191                output_map,
192            ),
193            GeneratorKind::KeyValue(kv) => key_value::render(
194                kv,
195                scope,
196                config.clone(),
197                committed_uppers,
198                start_signal,
199                output_map,
200                idx_to_exportid,
201            ),
202        }
203    }
204}
205
206impl SourceRender for LoadGeneratorSourceConnection {
207    type Time = MzOffset;
208
209    const STATUS_NAMESPACE: StatusNamespace = StatusNamespace::Generator;
210
211    fn render<'scope>(
212        self,
213        scope: Scope<'scope, MzOffset>,
214        config: &RawSourceCreationConfig,
215        committed_uppers: impl futures::Stream<Item = ResumeUppers<MzOffset>> + 'static,
216        start_signal: impl std::future::Future<Output = ()> + 'static,
217    ) -> (
218        BTreeMap<
219            GlobalId,
220            StackedCollection<'scope, MzOffset, Result<SourceMessage, DataflowError>>,
221        >,
222        StreamVec<'scope, MzOffset, HealthStatusMessage>,
223        StreamVec<'scope, MzOffset, Probe<MzOffset>>,
224        Vec<PressOnDropButton>,
225    ) {
226        let generator_kind = GeneratorKind::new(
227            &self.load_generator,
228            self.tick_micros,
229            self.as_of,
230            self.up_to,
231        );
232        let (updates, progress, health, button) =
233            generator_kind.render(scope, config, committed_uppers, start_signal);
234
235        let probe_stream = synthesize_probes(
236            config.id,
237            progress,
238            config.timestamp_interval,
239            config.now_fn.clone(),
240        );
241
242        (updates, health, probe_stream, button)
243    }
244}
245
246fn render_simple_generator<'scope>(
247    generator: Box<dyn Generator>,
248    tick_micros: Option<u64>,
249    as_of: MzOffset,
250    up_to: MzOffset,
251    scope: Scope<'scope, MzOffset>,
252    config: &RawSourceCreationConfig,
253    committed_uppers: impl futures::Stream<Item = ResumeUppers<MzOffset>> + 'static,
254    output_map: BTreeMap<LoadGeneratorOutput, Vec<usize>>,
255) -> (
256    BTreeMap<GlobalId, StackedCollection<'scope, MzOffset, Result<SourceMessage, DataflowError>>>,
257    StreamVec<'scope, MzOffset, Infallible>,
258    StreamVec<'scope, MzOffset, HealthStatusMessage>,
259    Vec<PressOnDropButton>,
260) {
261    let mut builder = AsyncOperatorBuilder::new(config.name.clone(), scope.clone());
262
263    let (data_output, stream) = builder.new_output::<FueledBuilder<
264        CapacityContainerBuilder<
265            Vec<(
266                (usize, Result<SourceMessage, DataflowError>),
267                MzOffset,
268                Diff,
269            )>,
270        >,
271    >>();
272    let export_ids: Vec<_> = config.source_exports.keys().copied().collect();
273    let partition_count = u64::cast_from(export_ids.len());
274    let data_streams: Vec<_> = stream.partition::<CapacityContainerBuilder<_>, _, _>(
275        partition_count,
276        |((output, data), time, diff): (
277            (usize, Result<SourceMessage, DataflowError>),
278            MzOffset,
279            Diff,
280        )| {
281            let output = u64::cast_from(output);
282            (output, (data, time, diff))
283        },
284    );
285    let mut data_collections = BTreeMap::new();
286    for (id, data_stream) in export_ids.iter().zip_eq(data_streams) {
287        data_collections.insert(*id, data_stream.as_collection());
288    }
289
290    let (_progress_output, progress_stream) = builder.new_output::<CapacityContainerBuilder<_>>();
291    let (health_output, health_stream) = builder.new_output::<CapacityContainerBuilder<_>>();
292
293    let busy_signal = Arc::clone(&config.busy_signal);
294    let source_resume_uppers = config.source_resume_uppers.clone();
295    // Only the active worker reports health and offset statistics, because
296    // offset statistics are summed across workers. It is the generator's
297    // primary worker, whose share has every progress event.
298    let is_active_worker = config.responsible_for(());
299    let worker_count = config.worker_count;
300    let generator_worker_index =
301        (config.worker_id + worker_count - config.responsible_worker(())) % worker_count;
302    let source_statistics = config.statistics.clone();
303    let button = builder.build(move |caps| {
304        SignaledFuture::new(busy_signal, async move {
305            let [mut cap, mut progress_cap, health_cap] = caps.try_into().unwrap();
306
307            // We only need this until we reported ourselves as Running.
308            let mut health_cap = is_active_worker.then_some(health_cap);
309
310            if !is_active_worker {
311                // Emit 0, to mark this worker as having started up correctly.
312                for stats in source_statistics.values() {
313                    stats.set_offset_known(0);
314                    stats.set_offset_committed(0);
315                }
316            }
317
318            let resume_upper = Antichain::from_iter(
319                source_resume_uppers
320                    .values()
321                    .flat_map(|f| f.iter().map(MzOffset::decode_row)),
322            );
323
324            let Some(resume_offset) = resume_upper.into_option() else {
325                return;
326            };
327
328            let now_fn = mz_ore::now::SYSTEM_TIME.clone();
329
330            let start_instant = {
331                // We want to have our interval start at a nice round number...
332                // for example, if our tick interval is one minute, to start at a minute boundary.
333                // However, the `Interval` type from tokio can't be "floored" in that way.
334                // Instead, figure out the amount we should step forward based on the wall clock,
335                // then apply that to our monotonic clock to make things start at approximately the
336                // right time.
337                let now_millis = now_fn();
338                let now_instant = Instant::now();
339                let delay_millis = tick_micros
340                    .map(|tick_micros| tick_micros / 1000)
341                    .filter(|tick_millis| *tick_millis > 0)
342                    .map(|tick_millis| tick_millis - now_millis.rem(tick_millis))
343                    .unwrap_or(0);
344                now_instant + Duration::from_millis(delay_millis)
345            };
346            let tick = Duration::from_micros(tick_micros.unwrap_or(1_000_000));
347            let mut tick_interval = interval_at(start_instant, tick);
348
349            let mut rows = generator.by_seed_for_worker(
350                now_fn,
351                None,
352                resume_offset,
353                generator_worker_index,
354                worker_count,
355            );
356
357            let mut committed_uppers = std::pin::pin!(committed_uppers);
358
359            // Each export's `offset_committed`. If we are just starting up, report 0.
360            let mut offsets_committed: BTreeMap<_, _> = if resume_offset.offset == 0 {
361                source_statistics.keys().map(|id| (*id, 0)).collect()
362            } else {
363                BTreeMap::new()
364            };
365
366            while let Some((output_type, event)) = rows.next() {
367                match event {
368                    Event::Message(mut offset, (value, diff)) => {
369                        // Fast forward any data before the requested as of.
370                        if offset <= as_of {
371                            offset = as_of;
372                        }
373
374                        // If the load generator produces data at or beyond the
375                        // requested `up_to`, drop it. We'll terminate the load
376                        // generator when the capability advances to the `up_to`,
377                        // but the load generator might produce data far in advance
378                        // of its capability.
379                        if offset >= up_to {
380                            continue;
381                        }
382
383                        // Once we see the load generator start producing data for some offset,
384                        // we report progress beyond that offset, to ensure that a binding can be
385                        // minted for the data and it doesn't accumulate in reclocking.
386                        let _ = progress_cap.try_downgrade(&(offset + 1));
387
388                        let outputs = match output_map.get(&output_type) {
389                            Some(outputs) => outputs,
390                            // We don't have an output index for this output type, so drop it
391                            None => continue,
392                        };
393
394                        let message: Result<SourceMessage, DataflowError> = Ok(SourceMessage {
395                            key: Row::default(),
396                            value,
397                            metadata: Row::default(),
398                        });
399
400                        // Some generators always reproduce their TVC from the beginning which can
401                        // generate a significant amount of data that will overwhelm the dataflow.
402                        // Since those are not required downstream we eagerly ignore them here.
403                        if resume_offset <= offset {
404                            for (&output, message) in outputs.iter().repeat_clone(message) {
405                                let update = ((output, message), offset, diff);
406                                let size = update.fuel_size();
407                                data_output.give_fueled(&cap, update, size).await;
408                            }
409                        }
410                    }
411                    Event::Progress(Some(offset)) => {
412                        if resume_offset <= offset && health_cap.is_some() {
413                            let health_cap = health_cap.take().expect("known to exist");
414                            let export_ids = export_ids.iter().copied();
415                            for id in export_ids.map(Some).chain(std::iter::once(None)) {
416                                health_output.give(
417                                    &health_cap,
418                                    HealthStatusMessage {
419                                        id,
420                                        namespace: StatusNamespace::Generator,
421                                        update: HealthStatusUpdate::running(),
422                                    },
423                                );
424                            }
425                        }
426
427                        // If we've reached the requested maximum offset, cease.
428                        if offset >= up_to {
429                            break;
430                        }
431
432                        // If the offset is at or below the requested `as_of`, don't
433                        // downgrade the capability.
434                        if offset <= as_of {
435                            continue;
436                        }
437
438                        cap.downgrade(&offset);
439                        let _ = progress_cap.try_downgrade(&offset);
440
441                        // We only sleep if we have surpassed the resume offset so that we can
442                        // quickly go over any historical updates that a generator might choose to
443                        // emit.
444                        // TODO(petrosagg): Remove the sleep below and make generators return an
445                        // async stream so that they can drive the rate of production directly
446                        if resume_offset < offset {
447                            loop {
448                                tokio::select! {
449                                    _tick = tick_interval.tick() => {
450                                        break;
451                                    }
452                                    Some(uppers) = committed_uppers.next() => {
453                                        for (id, frontier) in uppers.exports {
454                                            if let Some(offset) = frontier.into_option() {
455                                                // Offset N means we have committed N offsets (offsets are
456                                                // 0-indexed)
457                                                offsets_committed.insert(id, offset.offset);
458                                            }
459                                        }
460                                    }
461                                }
462                            }
463
464                            // TODO(guswynn): generators have various definitions of "snapshot", so
465                            // we are not going to implement snapshot progress statistics for them
466                            // right now, but will come back to it.
467                            for (id, offset_committed) in &offsets_committed {
468                                if let Some(stats) =
469                                    source_statistics.get(id).filter(|_| is_active_worker)
470                                {
471                                    stats.set_offset_committed(*offset_committed);
472                                    // technically we could have _known_ a larger offset
473                                    // than the one that has been committed, but we can
474                                    // never recover that known amount on restart, so we
475                                    // just advance these in lock step.
476                                    stats.set_offset_known(*offset_committed);
477                                }
478                            }
479                        }
480                    }
481                    Event::Progress(None) => return,
482                }
483            }
484        })
485    });
486
487    (
488        data_collections,
489        progress_stream,
490        health_stream,
491        vec![button.press_on_drop()],
492    )
493}
494
495/// Synthesizes a probe stream that produces the frontier of the given progress stream at the given
496/// interval.
497///
498/// This is used as a fallback for sources that don't support probing the frontier of the upstream
499/// system.
500fn synthesize_probes<'scope, T: timely::progress::Timestamp>(
501    source_id: GlobalId,
502    progress: StreamVec<'scope, T, Infallible>,
503    interval: Duration,
504    now_fn: NowFn,
505) -> StreamVec<'scope, T, Probe<T>> {
506    let scope = progress.scope();
507
508    let active_worker = usize::cast_from(source_id.hashed()) % scope.peers();
509    let is_active_worker = active_worker == scope.index();
510
511    let mut op = AsyncOperatorBuilder::new("synthesize_probes".into(), scope);
512    let (output, output_stream) = op.new_output::<CapacityContainerBuilder<_>>();
513    let mut input = op.new_input_for(progress, Pipeline, &output);
514
515    op.build(|caps| async move {
516        if !is_active_worker {
517            return;
518        }
519
520        let [cap] = caps.try_into().expect("one capability per output");
521
522        let mut ticker = super::probe::Ticker::new(move || interval, now_fn.clone());
523
524        let minimum_frontier = Antichain::from_elem(Timestamp::minimum());
525        let mut frontier = minimum_frontier.clone();
526        loop {
527            tokio::select! {
528                event = input.next() => match event {
529                    Some(AsyncEvent::Progress(progress)) => frontier = progress,
530                    Some(AsyncEvent::Data(..)) => unreachable!(),
531                    None => break,
532                },
533                // We only report a probe if the source upper frontier is not the minimum frontier.
534                // This makes it so the first remap binding corresponds to the snapshot of the
535                // source, and because the first binding always maps to the minimum *target*
536                // frontier we guarantee that the source will never appear empty.
537                probe_ts = ticker.tick(), if frontier != minimum_frontier => {
538                    let probe = Probe {
539                        probe_ts,
540                        upstream_frontier: frontier.clone(),
541                    };
542                    output.give(&cap, probe);
543                }
544            }
545        }
546
547        let probe = Probe {
548            probe_ts: now_fn().into(),
549            upstream_frontier: Antichain::new(),
550        };
551        output.give(&cap, probe);
552    });
553
554    output_stream
555}