Skip to main content

mz_storage/render/
sources.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//! Logic related to the creation of dataflow sources.
11//!
12//! See [`render_source`] for more details.
13
14use std::collections::BTreeMap;
15use std::iter;
16use std::sync::Arc;
17
18use differential_dataflow::{AsCollection, VecCollection};
19use mz_expr::MfpPlan;
20use mz_ore::cast::CastLossy;
21use mz_persist_client::operators::shard_source::SnapshotMode;
22use mz_repr::{Datum, Diff, GlobalId, Row, RowPacker};
23use mz_storage_operators::persist_source;
24use mz_storage_operators::persist_source::Subtime;
25use mz_storage_types::controller::CollectionMetadata;
26use mz_storage_types::dyncfgs;
27use mz_storage_types::errors::{
28    DataflowError, DecodeError, EnvelopeError, UpsertError, UpsertNullKeyError, UpsertValueError,
29};
30use mz_storage_types::parameters::StorageMaxInflightBytesConfig;
31use mz_storage_types::sources::envelope::{KeyEnvelope, NoneEnvelope, UpsertEnvelope, UpsertStyle};
32use mz_storage_types::sources::*;
33use mz_timely_util::builder_async::PressOnDropButton;
34use mz_timely_util::operator::CollectionExt;
35use mz_timely_util::order::refine_antichain;
36use serde::{Deserialize, Serialize};
37use timely::container::CapacityContainerBuilder;
38use timely::dataflow::StreamVec;
39use timely::dataflow::operators::vec::Map;
40use timely::dataflow::operators::{ConnectLoop, Feedback, Leave, OkErr};
41use timely::dataflow::scope::Scope;
42use timely::progress::{Antichain, Timestamp};
43
44use crate::decode::{render_decode_cdcv2, render_decode_delimited};
45use crate::healthcheck::{HealthStatusMessage, StatusNamespace};
46use crate::source::types::{DecodeResult, SourceOutput, SourceRender};
47use crate::source::{self, RawSourceCreationConfig, SourceExportCreationConfig};
48use crate::upsert::{UpsertKey, UpsertSourceTime, UpsertValue};
49
50/// _Renders_ complete _differential_ collections
51/// that represent the final source and its errors
52/// as requested by the original `CREATE SOURCE` statement,
53/// encapsulated in the passed `SourceInstanceDesc`.
54///
55/// The first element in the returned tuple is the pair of Collections,
56/// the second is a type-erased token that will keep the source
57/// alive as long as it is not dropped.
58///
59/// This function is intended to implement the recipe described here:
60/// <https://github.com/MaterializeInc/materialize/blob/main/doc/developer/platform/architecture-storage.md#source-ingestion>
61pub fn render_source<'scope, 'root, C>(
62    scope: Scope<'scope, mz_repr::Timestamp>,
63    root_scope: Scope<'root, ()>,
64    dataflow_debug_name: &String,
65    connection: C,
66    description: IngestionDescription<CollectionMetadata>,
67    committed_uppers: BTreeMap<GlobalId, StreamVec<'scope, mz_repr::Timestamp, ()>>,
68    storage_state: &crate::storage_state::StorageState,
69    base_source_config: RawSourceCreationConfig,
70) -> (
71    BTreeMap<
72        GlobalId,
73        (
74            VecCollection<'scope, mz_repr::Timestamp, Row, Diff>,
75            VecCollection<'scope, mz_repr::Timestamp, DataflowError, Diff>,
76        ),
77    >,
78    Vec<StreamVec<'root, (), HealthStatusMessage>>,
79    StreamVec<'scope, mz_repr::Timestamp, ()>,
80    Vec<PressOnDropButton>,
81)
82where
83    C: SourceConnection + SourceRender + 'static,
84    C::Time: UpsertSourceTime,
85{
86    // Tokens that we should return from the method.
87    let mut needed_tokens = Vec::new();
88
89    // Note that this `render_source` attaches a single _instance_ of a source
90    // to the passed `Scope`, and this instance may be disabled if the
91    // source type does not support multiple instances. `render_source`
92    // is called on each timely worker as part of
93    // [`super::build_storage_dataflow`].
94
95    // A set of channels (1 per worker) used to signal rehydration being finished
96    // to raw sources. These are channels and not timely streams because they
97    // have to cross a scope boundary.
98    //
99    // Note that these will be entirely subsumed by full `hydration` backpressure,
100    // once that is implemented.
101    let (starter, mut start_signal) = tokio::sync::mpsc::channel::<()>(1);
102    let start_signal = async move {
103        let _ = start_signal.recv().await;
104    };
105
106    // Build the _raw_ ok and error sources using `create_raw_source` and the
107    // correct `SourceReader` implementations
108    let (exports, health, remap_upper, source_tokens) = source::create_raw_source(
109        scope,
110        root_scope,
111        storage_state,
112        committed_uppers,
113        &base_source_config,
114        connection,
115        start_signal,
116    );
117
118    needed_tokens.extend(source_tokens);
119
120    let mut health_streams = Vec::with_capacity(exports.len() + 1);
121    health_streams.push(health);
122
123    let mut outputs = BTreeMap::new();
124    for (export_id, export) in exports {
125        type CB<C> = CapacityContainerBuilder<C>;
126        let (ok_stream, err_stream) =
127            export.map_fallible::<CB<_>, CB<_>, _, _, _>("export-demux-ok-err", |r| r);
128
129        // All sources should push their various error streams into this vector,
130        // whose contents will be concatenated and inserted along the collection.
131        // All subsources include the non-definite errors of the ingestion
132        let mut error_collections = Vec::new();
133
134        let data_config = base_source_config.source_exports[&export_id]
135            .data_config
136            .clone();
137        let (ok, extra_tokens, health_stream) = render_source_stream(
138            scope,
139            dataflow_debug_name,
140            export_id,
141            ok_stream,
142            data_config,
143            &description,
144            &mut error_collections,
145            storage_state,
146            &base_source_config,
147            starter.clone(),
148        );
149        needed_tokens.extend(extra_tokens);
150
151        // Flatten the error collections.
152        let err_collection = match error_collections.len() {
153            0 => err_stream,
154            _ => err_stream.concatenate(error_collections),
155        };
156
157        outputs.insert(export_id, (ok, err_collection));
158
159        health_streams.extend(health_stream.into_iter().map(|s| s.leave(root_scope)));
160    }
161    (outputs, health_streams, remap_upper, needed_tokens)
162}
163
164/// Completes the rendering of a particular source stream by applying decoding and envelope
165/// processing as necessary
166fn render_source_stream<'scope, FromTime>(
167    scope: Scope<'scope, mz_repr::Timestamp>,
168    dataflow_debug_name: &String,
169    export_id: GlobalId,
170    ok_source: VecCollection<'scope, mz_repr::Timestamp, SourceOutput<FromTime>, Diff>,
171    data_config: SourceExportDataConfig,
172    description: &IngestionDescription<CollectionMetadata>,
173    error_collections: &mut Vec<VecCollection<'scope, mz_repr::Timestamp, DataflowError, Diff>>,
174    storage_state: &crate::storage_state::StorageState,
175    base_source_config: &RawSourceCreationConfig,
176    rehydrated_token: impl std::any::Any + 'static,
177) -> (
178    VecCollection<'scope, mz_repr::Timestamp, Row, Diff>,
179    Vec<PressOnDropButton>,
180    Vec<StreamVec<'scope, mz_repr::Timestamp, HealthStatusMessage>>,
181)
182where
183    FromTime: Timestamp + Sync,
184    FromTime: UpsertSourceTime,
185{
186    let mut needed_tokens = vec![];
187
188    // Use the envelope and encoding configs for this particular source export
189    let SourceExportDataConfig { encoding, envelope } = data_config;
190
191    let SourceDesc {
192        connection: _,
193        timestamp_interval: _,
194    } = description.desc;
195
196    let (decoded_stream, decode_health) = match encoding {
197        None => (
198            ok_source.map(|r| DecodeResult {
199                // This is safe because the current set of sources produce
200                // either:
201                // 1. Non-nullable keys
202                // 2. No keys at all.
203                //
204                // Please see the comment on `key_envelope_no_encoding` in
205                // `mz_sql::plan::statement::ddl` for more details.
206                key: Some(Ok(r.key)),
207                value: Some(Ok(r.value)),
208                metadata: r.metadata,
209                from_time: r.from_time,
210            }),
211            None,
212        ),
213        Some(encoding) => {
214            let (decoded_stream, decode_health) = render_decode_delimited(
215                ok_source,
216                encoding.key,
217                encoding.value,
218                dataflow_debug_name.clone(),
219                storage_state.metrics.decode_defs.clone(),
220                storage_state.storage_configuration.clone(),
221            );
222            (decoded_stream, Some(decode_health))
223        }
224    };
225
226    // render envelopes
227    let (envelope_ok, envelope_health) = match &envelope {
228        SourceEnvelope::Upsert(upsert_envelope) => {
229            let upsert_input = upsert_commands(decoded_stream, upsert_envelope.clone());
230
231            let persist_clients = Arc::clone(&storage_state.persist_clients);
232            // TODO: Get this to work with the as_of.
233            let resume_upper = base_source_config.resume_uppers[&export_id].clone();
234
235            let upper_ts = resume_upper
236                .as_option()
237                .expect("resuming an already finished ingestion")
238                .clone();
239            let outer_mz_scope = scope.clone();
240            let (upsert, health_update) = scope.scoped(
241                &format!("upsert_rehydration_backpressure({})", export_id),
242                |scope| {
243                    let (
244                        previous_ok,
245                        previous_err,
246                        previous_token,
247                        feedback_handle,
248                        backpressure_metrics,
249                    ) = {
250                        let as_of = Antichain::from_elem(upper_ts.saturating_sub(1));
251
252                        let backpressure_max_inflight_bytes = get_backpressure_max_inflight_bytes(
253                            &storage_state
254                                .storage_configuration
255                                .parameters
256                                .storage_dataflow_max_inflight_bytes_config,
257                            &storage_state.instance_context.cluster_memory_limit,
258                        );
259
260                        let (feedback_handle, flow_control, backpressure_metrics) =
261                            if let Some(storage_dataflow_max_inflight_bytes) =
262                                backpressure_max_inflight_bytes
263                            {
264                                tracing::info!(
265                                    ?backpressure_max_inflight_bytes,
266                                    "timely-{} using backpressure in upsert for source {}",
267                                    base_source_config.worker_id,
268                                    export_id
269                                );
270                                if !storage_state
271                                    .storage_configuration
272                                    .parameters
273                                    .storage_dataflow_max_inflight_bytes_config
274                                    .disk_only
275                                    || storage_state.instance_context.scratch_directory.is_some()
276                                {
277                                    let (feedback_handle, feedback_data) =
278                                        scope.feedback(Default::default());
279
280                                    // TODO(guswynn): cleanup
281                                    let backpressure_metrics = Some(
282                                        base_source_config
283                                            .metrics
284                                            .get_backpressure_metrics(export_id, scope.index()),
285                                    );
286
287                                    (
288                                        Some(feedback_handle),
289                                        Some(persist_source::FlowControl {
290                                            progress_stream: feedback_data,
291                                            max_inflight_bytes: storage_dataflow_max_inflight_bytes,
292                                            summary: (Default::default(), Subtime::least_summary()),
293                                            metrics: backpressure_metrics
294                                                .as_ref()
295                                                .map(|m| m.operator_metrics()),
296                                        }),
297                                        backpressure_metrics,
298                                    )
299                                } else {
300                                    (None, None, None)
301                                }
302                            } else {
303                                (None, None, None)
304                            };
305
306                        let storage_metadata = description.source_exports[&export_id]
307                            .storage_metadata
308                            .clone();
309
310                        let error_handler =
311                            storage_state.error_handler("upsert_rehydration", export_id);
312
313                        let (ok_stream, err_stream, tok) = persist_source::persist_source_core(
314                            outer_mz_scope,
315                            scope,
316                            export_id,
317                            persist_clients,
318                            storage_metadata,
319                            None,
320                            Some(as_of),
321                            SnapshotMode::Include,
322                            Antichain::new(),
323                            None::<&mut MfpPlan>,
324                            flow_control,
325                            false.then_some(|| unreachable!()),
326                            async {},
327                            error_handler,
328                        );
329                        (
330                            ok_stream.as_collection(),
331                            err_stream.as_collection(),
332                            Some(tok),
333                            feedback_handle,
334                            backpressure_metrics,
335                        )
336                    };
337
338                    let export_statistics = storage_state
339                        .aggregated_statistics
340                        .get_source(&export_id)
341                        .expect("statistics initialized")
342                        .clone();
343                    let export_config = SourceExportCreationConfig {
344                        id: export_id,
345                        worker_id: base_source_config.worker_id,
346                        metrics: base_source_config.metrics.clone(),
347                        source_statistics: export_statistics,
348                    };
349                    // The chunked v2 stash spills only to swap, through the
350                    // buffer pool. On a replica that cannot swap, v1 keeps its
351                    // state in RocksDB on the scratch disk where one exists,
352                    // and v2's footprint without swap has not been measured.
353                    let use_upsert_v2 = dyncfgs::ENABLE_UPSERT_V2
354                        .get(storage_state.storage_configuration.config_set())
355                        && storage_state.instance_context.swap_available;
356                    let (upsert, health_update, snapshot_progress, upsert_token) = if use_upsert_v2
357                    {
358                        // Resolved here, at operator construction, so the
359                        // dataflow keeps one stash flavor for its whole
360                        // life even if the flag flips underneath it.
361                        let stash_flavor =
362                            crate::upsert_continual_feedback_v2::UpsertStashFlavor::from_config(
363                                storage_state.storage_configuration.config_set(),
364                            );
365                        crate::upsert::upsert_v2(
366                            upsert_input.enter(scope),
367                            upsert_envelope.clone(),
368                            refine_antichain(&resume_upper),
369                            previous_ok,
370                            previous_err,
371                            previous_token,
372                            export_config,
373                            backpressure_metrics,
374                            stash_flavor,
375                        )
376                    } else {
377                        crate::upsert::upsert(
378                            upsert_input.enter(scope),
379                            upsert_envelope.clone(),
380                            refine_antichain(&resume_upper),
381                            previous_ok,
382                            previous_err,
383                            previous_token,
384                            export_config,
385                            &storage_state.instance_context,
386                            &storage_state.storage_configuration,
387                            &storage_state.dataflow_parameters,
388                            backpressure_metrics,
389                        )
390                    };
391
392                    // Even though we register the `persist_sink` token at a top-level,
393                    // which will stop any data from being committed, we also register
394                    // a token for the `upsert` operator which may be in the middle of
395                    // rehydration processing the `persist_source` input above.
396                    needed_tokens.push(upsert_token);
397
398                    // If configured, delay raw sources until we rehydrate the upsert
399                    // source. Otherwise, drop the token, unblocking the sources at the
400                    // end rendering.
401                    if dyncfgs::DELAY_SOURCES_PAST_REHYDRATION
402                        .get(storage_state.storage_configuration.config_set())
403                    {
404                        crate::upsert::rehydration_finished(
405                            scope.clone(),
406                            base_source_config,
407                            rehydrated_token,
408                            refine_antichain(&resume_upper),
409                            snapshot_progress.clone(),
410                        );
411                    } else {
412                        drop(rehydrated_token)
413                    };
414
415                    // If backpressure from persist is enabled, we connect the upsert operator's
416                    // snapshot progress to the persist source feedback handle.
417                    if let Some(feedback_handle) = feedback_handle {
418                        snapshot_progress.connect_loop(feedback_handle);
419                    }
420
421                    (
422                        upsert.leave(outer_mz_scope),
423                        health_update
424                            .map(|(id, update)| HealthStatusMessage {
425                                id,
426                                namespace: StatusNamespace::Upsert,
427                                update,
428                            })
429                            .leave(outer_mz_scope),
430                    )
431                },
432            );
433
434            let (upsert_ok, upsert_err) = upsert.inner.ok_err(split_ok_err);
435            error_collections.push(upsert_err.as_collection());
436
437            (upsert_ok.as_collection(), Some(health_update))
438        }
439        SourceEnvelope::None(none_envelope) => {
440            let results = append_metadata_to_value(decoded_stream);
441
442            let flattened_stream = flatten_results_prepend_keys(none_envelope, results);
443
444            let (stream, errors) = flattened_stream.inner.ok_err(split_ok_err);
445
446            error_collections.push(errors.as_collection());
447            (stream.as_collection(), None)
448        }
449        SourceEnvelope::CdcV2 => {
450            let (oks, token) = render_decode_cdcv2(&decoded_stream);
451            needed_tokens.push(token);
452            (oks, None)
453        }
454    };
455
456    // Return the collections and any needed tokens.
457    let health = decode_health.into_iter().chain(envelope_health).collect();
458    (envelope_ok, needed_tokens, health)
459}
460
461// Returns the maximum limit of inflight bytes for backpressure based on given config
462// and the current cluster size
463fn get_backpressure_max_inflight_bytes(
464    inflight_bytes_config: &StorageMaxInflightBytesConfig,
465    cluster_memory_limit: &Option<usize>,
466) -> Option<usize> {
467    let StorageMaxInflightBytesConfig {
468        max_inflight_bytes_default,
469        max_inflight_bytes_cluster_size_fraction,
470        disk_only: _,
471    } = inflight_bytes_config;
472
473    // Will use backpressure only if the default inflight value is provided
474    if max_inflight_bytes_default.is_some() {
475        let current_cluster_max_bytes_limit =
476            cluster_memory_limit.as_ref().and_then(|cluster_memory| {
477                max_inflight_bytes_cluster_size_fraction.map(|fraction| {
478                    // We just need close the correct % of bytes here, so we just use lossy casts.
479                    usize::cast_lossy(f64::cast_lossy(*cluster_memory) * fraction)
480                })
481            });
482        current_cluster_max_bytes_limit.or(*max_inflight_bytes_default)
483    } else {
484        None
485    }
486}
487
488// TODO: Maybe we should finally move this to some central place and re-use. There seem to be
489// enough instances of this by now.
490fn split_ok_err<O, E, T, D>(x: (Result<O, E>, T, D)) -> Result<(O, T, D), (E, T, D)> {
491    match x {
492        (Ok(ok), ts, diff) => Ok((ok, ts, diff)),
493        (Err(err), ts, diff) => Err((err, ts, diff)),
494    }
495}
496
497/// After handling metadata insertion, we split streams into key/value parts for convenience
498#[derive(
499    Debug,
500    Clone,
501    Hash,
502    PartialEq,
503    Eq,
504    Ord,
505    PartialOrd,
506    Serialize,
507    Deserialize
508)]
509struct KV {
510    key: Option<Result<Row, DecodeError>>,
511    val: Option<Result<Row, DecodeError>>,
512}
513
514fn append_metadata_to_value<'scope, T: Timestamp, FromTime: Timestamp>(
515    results: VecCollection<'scope, T, DecodeResult<FromTime>, Diff>,
516) -> VecCollection<'scope, T, KV, Diff> {
517    results.map(move |res| {
518        let val = res.value.map(|val_result| {
519            val_result.map(|mut val| {
520                if !res.metadata.is_empty() {
521                    RowPacker::for_existing_row(&mut val).extend_by_row(&res.metadata);
522                }
523                val
524            })
525        });
526
527        KV { val, key: res.key }
528    })
529}
530
531/// Convert from streams of [`DecodeResult`] to UpsertCommands, inserting the Key according to [`KeyEnvelope`]
532fn upsert_commands<'scope, T: Timestamp, FromTime: Timestamp>(
533    input: VecCollection<'scope, T, DecodeResult<FromTime>, Diff>,
534    upsert_envelope: UpsertEnvelope,
535) -> VecCollection<'scope, T, (UpsertKey, Option<UpsertValue>, FromTime), Diff> {
536    let mut row_buf = Row::default();
537    input.map(move |result| {
538        let from_time = result.from_time;
539
540        let key = match result.key {
541            Some(Ok(key)) => Ok(key),
542            None => Err(UpsertError::NullKey(UpsertNullKeyError)),
543            Some(Err(err)) => Err(UpsertError::KeyDecode(err)),
544        };
545
546        // If we have a well-formed key we can continue, otherwise we're upserting an error
547        let key = match key {
548            Ok(key) => key,
549            Err(err) => match result.value {
550                Some(_) => {
551                    return (
552                        UpsertKey::from_key(Err(&err)),
553                        Some(Err(Box::new(err))),
554                        from_time,
555                    );
556                }
557                None => return (UpsertKey::from_key(Err(&err)), None, from_time),
558            },
559        };
560
561        // We can now apply the key envelope
562        let key_row = match upsert_envelope.style {
563            // flattened or debezium
564            UpsertStyle::Debezium { .. }
565            | UpsertStyle::Default(KeyEnvelope::Flattened)
566            | UpsertStyle::ValueErrInline {
567                key_envelope: KeyEnvelope::Flattened,
568                error_column: _,
569            } => key,
570            // named
571            UpsertStyle::Default(KeyEnvelope::Named(_))
572            | UpsertStyle::ValueErrInline {
573                key_envelope: KeyEnvelope::Named(_),
574                error_column: _,
575            } => {
576                if key.iter().nth(1).is_none() {
577                    key
578                } else {
579                    row_buf.packer().push_list(key.iter());
580                    row_buf.clone()
581                }
582            }
583            UpsertStyle::Default(KeyEnvelope::None)
584            | UpsertStyle::ValueErrInline {
585                key_envelope: KeyEnvelope::None,
586                error_column: _,
587            } => unreachable!(),
588        };
589
590        let key = UpsertKey::from_key(Ok(&key_row));
591
592        let metadata = result.metadata;
593
594        let value = match result.value {
595            Some(Ok(ref row)) => match upsert_envelope.style {
596                UpsertStyle::Debezium { after_idx } => match row.iter().nth(after_idx).unwrap() {
597                    Datum::List(after) => {
598                        let mut packer = row_buf.packer();
599                        packer.extend(after.iter());
600                        packer.extend_by_row(&metadata);
601                        Some(Ok(row_buf.clone()))
602                    }
603                    Datum::Null => None,
604                    d => panic!("type error: expected record, found {:?}", d),
605                },
606                UpsertStyle::Default(_) => {
607                    let mut packer = row_buf.packer();
608                    packer.extend_by_row(&key_row);
609                    packer.extend_by_row(row);
610                    packer.extend_by_row(&metadata);
611                    Some(Ok(row_buf.clone()))
612                }
613                UpsertStyle::ValueErrInline { .. } => {
614                    let mut packer = row_buf.packer();
615                    packer.extend_by_row(&key_row);
616                    // The 'error' column is null
617                    packer.push(Datum::Null);
618                    packer.extend_by_row(row);
619                    packer.extend_by_row(&metadata);
620                    Some(Ok(row_buf.clone()))
621                }
622            },
623            Some(Err(inner)) => {
624                match upsert_envelope.style {
625                    UpsertStyle::ValueErrInline { .. } => {
626                        let mut count = 0;
627                        // inline the error in the data output
628                        let err_string = inner.to_string();
629                        let mut packer = row_buf.packer();
630                        for datum in key_row.iter() {
631                            packer.push(datum);
632                            count += 1;
633                        }
634                        // The 'error' column is a record with a 'description' column
635                        packer.push_list(iter::once(Datum::String(&err_string)));
636                        count += 1;
637                        let metadata_len = metadata.as_row_ref().iter().count();
638                        // push nulls for all value columns
639                        packer.extend(
640                            iter::repeat(Datum::Null)
641                                .take(upsert_envelope.source_arity - count - metadata_len),
642                        );
643                        packer.extend_by_row(&metadata);
644                        Some(Ok(row_buf.clone()))
645                    }
646                    _ => Some(Err(Box::new(UpsertError::Value(UpsertValueError {
647                        for_key: key_row,
648                        inner,
649                    })))),
650                }
651            }
652            None => None,
653        };
654
655        (key, value, from_time)
656    })
657}
658
659/// Convert from streams of [`DecodeResult`] to Rows, inserting the Key according to [`KeyEnvelope`]
660fn flatten_results_prepend_keys<'scope, T: Timestamp>(
661    none_envelope: &NoneEnvelope,
662    results: VecCollection<'scope, T, KV, Diff>,
663) -> VecCollection<'scope, T, Result<Row, DataflowError>, Diff> {
664    let NoneEnvelope {
665        key_envelope,
666        key_arity,
667    } = none_envelope;
668
669    let null_key_columns = Row::pack_slice(&vec![Datum::Null; *key_arity]);
670
671    match key_envelope {
672        KeyEnvelope::None => {
673            results.flat_map(|KV { val, .. }| val.map(|result| result.map_err(Into::into)))
674        }
675        KeyEnvelope::Flattened => results
676            .flat_map(raise_key_value_errors)
677            .map(move |maybe_kv| {
678                maybe_kv.map(|(key, value)| {
679                    let mut key = key.unwrap_or_else(|| null_key_columns.clone());
680                    RowPacker::for_existing_row(&mut key).extend_by_row(&value);
681                    key
682                })
683            }),
684        KeyEnvelope::Named(_) => {
685            results
686                .flat_map(raise_key_value_errors)
687                .map(move |maybe_kv| {
688                    maybe_kv.map(|(key, value)| {
689                        let mut key = key.unwrap_or_else(|| null_key_columns.clone());
690                        // Named semantics rename a key that is a single column, and encode a
691                        // multi-column field as a struct with that name
692                        let row = if key.iter().nth(1).is_none() {
693                            RowPacker::for_existing_row(&mut key).extend_by_row(&value);
694                            key
695                        } else {
696                            let mut new_row = Row::default();
697                            let mut packer = new_row.packer();
698                            packer.push_list(key.iter());
699                            packer.extend_by_row(&value);
700                            new_row
701                        };
702                        row
703                    })
704                })
705        }
706    }
707}
708
709/// Handle possibly missing key or value portions of messages
710fn raise_key_value_errors(
711    KV { key, val }: KV,
712) -> Option<Result<(Option<Row>, Row), DataflowError>> {
713    match (key, val) {
714        (Some(Ok(key)), Some(Ok(value))) => Some(Ok((Some(key), value))),
715        (None, Some(Ok(value))) => Some(Ok((None, value))),
716        // always prioritize the value error if either or both have an error
717        (_, Some(Err(e))) => Some(Err(e.into())),
718        (Some(Err(e)), _) => Some(Err(e.into())),
719        (None, None) => None,
720        // TODO(petrosagg): these errors would be better grouped under an EnvelopeError enum
721        _ => Some(Err(DataflowError::from(EnvelopeError::Flat(
722            "Value not present for message".into(),
723        )))),
724    }
725}
726
727#[cfg(test)]
728mod test {
729    use super::*;
730
731    #[mz_ore::test]
732    fn test_no_default() {
733        let config = StorageMaxInflightBytesConfig {
734            max_inflight_bytes_default: None,
735            max_inflight_bytes_cluster_size_fraction: Some(0.5),
736            disk_only: false,
737        };
738        let memory_limit = Some(1000);
739
740        let backpressure_inflight_bytes_limit =
741            get_backpressure_max_inflight_bytes(&config, &memory_limit);
742
743        assert_eq!(backpressure_inflight_bytes_limit, None)
744    }
745
746    #[mz_ore::test]
747    fn test_no_matching_size() {
748        let config = StorageMaxInflightBytesConfig {
749            max_inflight_bytes_default: Some(10000),
750            max_inflight_bytes_cluster_size_fraction: Some(0.5),
751            disk_only: false,
752        };
753
754        let backpressure_inflight_bytes_limit = get_backpressure_max_inflight_bytes(&config, &None);
755
756        assert_eq!(
757            backpressure_inflight_bytes_limit,
758            config.max_inflight_bytes_default
759        )
760    }
761
762    #[mz_ore::test]
763    fn test_calculated_cluster_limit() {
764        let config = StorageMaxInflightBytesConfig {
765            max_inflight_bytes_default: Some(10000),
766            max_inflight_bytes_cluster_size_fraction: Some(0.5),
767            disk_only: false,
768        };
769        let memory_limit = Some(2000);
770
771        let backpressure_inflight_bytes_limit =
772            get_backpressure_max_inflight_bytes(&config, &memory_limit);
773
774        // the limit should be 50% of 2000 i.e. 1000
775        assert_eq!(backpressure_inflight_bytes_limit, Some(1000));
776    }
777}