Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
2361975
compute: stop error collections multiplying their cardinality
claude Aug 13, 2026
b9e9505
compute: note why distinct_errs collapses every form of a bundle
claude Aug 13, 2026
5e009b7
compute: scope enable_compute_error_distinct to replicas
claude Aug 13, 2026
2af5522
sqllogictest: drop redundant in-binary pin of enable_compute_error_di…
claude Aug 13, 2026
445fd0b
parallel-workload: flip enable_compute_error_distinct
claude Aug 13, 2026
f79c6ff
compute: keep LocalIds out of error arrangement names
claude Aug 13, 2026
cb96bf2
testdrive: record the error arrangements top-1-monotonic now sees
claude Aug 13, 2026
87eadbb
compute: collapse rec binding errors for forward reads
claude Aug 13, 2026
553b536
compute: scope enable_compute_error_distinct to the environment
claude Aug 14, 2026
ec3c28d
sqllogictest: make the diamond chain reach the overflow
claude Aug 18, 2026
5abfe40
compute: normalize error multiplicity at dataflow boundaries
claude Aug 18, 2026
d8a1cbf
compute: correct the scope note on enable_compute_error_distinct
claude Aug 18, 2026
5545b5b
compute: collapse exported errors after error-count logging
claude Aug 18, 2026
9957d62
compute: default enable_compute_error_distinct on
claude Aug 18, 2026
053c9d5
sqllogictest: record the export error collapse in introspection relat…
claude Aug 18, 2026
528abd9
compute: stop collapsing errors on exported index traces
claude Aug 18, 2026
888823e
testdrive: record the error collapse in the remaining goldens
claude Aug 18, 2026
9d542e4
compute: correct why exported errors are not collapsed
claude Aug 19, 2026
57dbe0d
compute: point the export_index note at its tracker
claude Aug 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions misc/python/materialize/mzcompose/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ def get_minimal_system_parameters(
"enable_s3_tables_region_check": "false",
"enable_statement_lifecycle_logging": "true",
"enable_storage_introspection_logs": "true",
"enable_compute_error_distinct": "true",
"enable_compute_temporal_bucketing": "true",
"enable_variadic_left_join_lowering": "true",
"enable_worker_core_affinity": "true",
Expand Down
1 change: 1 addition & 0 deletions misc/python/materialize/parallel_workload/action.py
Original file line number Diff line number Diff line change
Expand Up @@ -2986,6 +2986,7 @@ def __init__(
self.flags_with_values["enable_compute_temporal_bucketing"] = (
BOOLEAN_FLAG_VALUES
)
self.flags_with_values["enable_compute_error_distinct"] = BOOLEAN_FLAG_VALUES
self.flags_with_values["enable_alter_table_add_column"] = BOOLEAN_FLAG_VALUES
self.flags_with_values["enable_bounded_staleness_isolation"] = (
BOOLEAN_FLAG_VALUES
Expand Down
20 changes: 20 additions & 0 deletions src/compute-types/src/dyncfgs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,25 @@ pub const ENABLE_HALF_JOIN2: Config<bool> = Config::new(
"Whether compute should use `half_join2` rather than DD's `half_join` to render delta joins.",
);

/// Whether rendering should collapse error multiplicities to one where it arranges errors.
///
/// Error semantics depend only on whether an error is present, so its multiplicity carries no
/// information a consumer reads. Left uncollapsed, a shared collection contributes its errors once
/// per plan path that reads it, and because those factors apply again at each level of sharing they
/// compound multiplicatively until the `Diff` overflows.
///
/// Governs sharing within a dataflow only. Sharing across objects is bounded unconditionally, by
/// normalizing at every boundary another dataflow can read, so what this flag decides is never
/// durable state and two replicas rendering under different values still write the same thing.
/// Environment-scoped for now because nothing needs finer granularity, not because finer would be
/// unsafe.
pub const ENABLE_ERROR_DISTINCT: Config<bool> = Config::new(
Comment thread
antiguru marked this conversation as resolved.
Comment thread
antiguru marked this conversation as resolved.
"enable_compute_error_distinct",
true,
"Whether compute rendering should collapse error multiplicities to one where it arranges \
errors.",
);

/// Use the column-paged merge batcher code path at arrange sites. When
/// `true`, arrange operators use `Col2ValPagedBatcher` (in
/// `mz_timely_util::columnar`) and `RowRowColPagedBuilder` (in
Expand Down Expand Up @@ -548,6 +567,7 @@ pub const MV_SINK_ADVANCE_PERSIST_FRONTIERS: Config<bool> = Config::new(
pub fn all_dyncfgs(configs: ConfigSet) -> ConfigSet {
configs
.add(&ENABLE_HALF_JOIN2)
.add(&ENABLE_ERROR_DISTINCT)
.add(&ENABLE_MZ_JOIN_CORE)
.add(&ENABLE_SYNC_MV_SINK)
.add(&ENABLE_CORRECTION_V2)
Expand Down
40 changes: 39 additions & 1 deletion src/compute/src/render.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,8 @@ use mz_compute_types::dataflows::{DataflowDescription, IndexDesc};
use mz_compute_types::dyncfgs::{
COMPUTE_APPLY_COLUMN_DEMANDS, COMPUTE_LOGICAL_BACKPRESSURE_INFLIGHT_SLACK,
COMPUTE_LOGICAL_BACKPRESSURE_MAX_RETAINED_CAPABILITIES, ENABLE_COMPUTE_LOGICAL_BACKPRESSURE,
ENABLE_COMPUTE_TEMPORAL_BUCKETING, SUBSCRIBE_SNAPSHOT_OPTIMIZATION, TEMPORAL_BUCKETING_SUMMARY,
ENABLE_COMPUTE_TEMPORAL_BUCKETING, ENABLE_ERROR_DISTINCT, SUBSCRIBE_SNAPSHOT_OPTIMIZATION,
TEMPORAL_BUCKETING_SUMMARY,
};
use mz_compute_types::plan::render_plan::{
self, BindStage, LetBind, LetFreePlan, RecBind, RenderPlan,
Expand Down Expand Up @@ -711,6 +712,17 @@ impl<'g> Context<'g, mz_repr::Timestamp> {
let key = &idx.key;
match bundle.arrangement(key) {
Some(ArrangementFlavor::Local(mut oks, mut errs)) => {
// NOTE: Do not give an exported arrangement a second reader that holds a trace
// handle, such as a `reduce`. Such a reader pins the shared spine's physical
// frontier at its own lagging progress, and `ArrangementManager::maintenance` can
// then no longer advance it, so batches pile up in `Spine::pending`. A cursor is
// only checked for straddling over pending batches, so an importing dataflow's
// `cursor_through` eventually panics with `upper` straddles batch. Watching
// `errs.stream` in `output_probe` does not help, and neither does discarding the
// reader's output. Stream-level readers like `as_collection` are unaffected. This is
// why error multiplicity is not collapsed here, leaving multiplicity that crosses
// an index boundary unbounded. TODO(CPU-209): bound it without a trace reader.

// Ensure that the frontier does not advance past the expiration time, if set.
// Otherwise, we might write down incorrect data.
if let Some(&expiration) = self.dataflow_expiration.as_option() {
Expand Down Expand Up @@ -906,6 +918,7 @@ impl<'scope> Context<'scope, Product<mz_repr::Timestamp, PointStamp<u64>>> {
.render_letfree_plan(object_id, value, binding)
.leave_region(self.scope)
});
let bundle = self.distinct_binding_errs(bundle);
self.insert_id(Id::Local(id), bundle);
}

Expand Down Expand Up @@ -939,6 +952,11 @@ impl<'scope> Context<'scope, Product<mz_repr::Timestamp, PointStamp<u64>>> {
// here to cause that to happen.
let (oks, mut err) = bundle.collection.clone().unwrap();
let oks = oks.into_vec();
// Collapses what forward reads see. `err_v` below feeds reads rendered before this
// binding and is collapsed separately; without this, a `Get` in a later rec binding
// or in the body resolves to the bundle stored here and compounds level over level,
// which is exactly what the collapse prevents for non-recursive bindings.
let bundle = self.distinct_binding_errs(bundle);
self.insert_id(Id::Local(id), bundle);
let (oks_v, err_v) = variables.remove(&Id::Local(id)).unwrap();

Expand Down Expand Up @@ -1044,6 +1062,7 @@ impl<'scope, T: RenderTimestamp + MaybeBucketByTime> Context<'scope, T> {
.render_letfree_plan(object_id, value, binding)
.leave_region(self.scope)
});
let bundle = self.distinct_binding_errs(bundle);
self.insert_id(Id::Local(id), bundle);
}
}
Expand All @@ -1056,6 +1075,25 @@ impl<'scope, T: RenderTimestamp + MaybeBucketByTime> Context<'scope, T> {
})
}

/// Collapses a binding's error multiplicities.
///
/// Applied to every binding, not only the multiply-read ones. Gating on the reference count
/// would be a pure optimization, since collapsing a binding one `Get` reads is harmless, and
/// there is almost nothing to gate: `NormalizeLets` inlines single-use bindings, so the ones
/// reaching rendering are shared. See [`CollectionBundle::distinct_errs`] for why the collapse
/// is needed at all, and why a binding's definition is the place for it rather than the
/// multi-input operators where the duplicate copies happen to meet again.
fn distinct_binding_errs(
&self,
bundle: CollectionBundle<'scope, T>,
) -> CollectionBundle<'scope, T> {
if ENABLE_ERROR_DISTINCT.get(&self.config_set) {
bundle.distinct_errs()
} else {
bundle
}
}

/// Renders a let-free plan to a differential dataflow, producing the collection of results.
fn render_letfree_plan(
&self,
Expand Down
80 changes: 80 additions & 0 deletions src/compute/src/render/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ use timely::progress::{Antichain, Timestamp};

use crate::compute_state::ComputeState;
use crate::extensions::arrange::{KeyCollection, MzArrange, MzArrangeCore};
use crate::extensions::reduce::MzReduce;
use crate::render::columnar::CollectionEdge;
use crate::render::errors::{DataflowErrorSer, ErrorLogger};
use crate::render::{LinearJoinSpec, MaybeBucketByTime, RenderTimestamp};
Expand Down Expand Up @@ -435,6 +436,41 @@ impl<'scope, T: RenderTimestamp> ArrangementFlavor<'scope, T> {
}
}
}
/// Rewrites an arranged error collection to hold each of its errors once.
///
/// Sound because query semantics depend only on whether an error is present, and correct under
/// retraction only because it reads the accumulated collection: no pointwise function of the input
/// diffs (a saturating add, a sign) can collapse multiplicity and still cancel when the errors
/// retract.
///
/// NOTE: One consumer does read the multiplicity. Error-count introspection reports it as the
/// number of failing rows, so `log_dataflow_errors` must see the collection before this collapses
/// it, or a dataflow reports one error however many rows failed. Collapse after the logging, never
/// before.
pub(crate) fn distinct_arranged_errs<'a, T: RenderTimestamp>(
errs: Arranged<'a, ErrAgent<T, Diff>>,
name: &str,
) -> Arranged<'a, ErrAgent<T, Diff>> {
errs.mz_reduce_abelian::<_, ErrBuilder<_, _>, ErrSpine<_, _>, _>(
name,
|_err, _input, output| output.push(((), Diff::ONE)),
)
}

/// Rewrites an error collection to hold each of its errors once.
///
/// Costs an arrangement more than [`distinct_arranged_errs`], which reuses the arrangement its
/// input already has.
pub(crate) fn distinct_errs_collection<'a, T: RenderTimestamp>(
errs: VecCollection<'a, T, DataflowErrorSer, Diff>,
) -> VecCollection<'a, T, DataflowErrorSer, Diff> {
let errs: KeyCollection<_, _, _> = errs.into();
let errs = errs
.mz_arrange::<ColumnationChunker<_>, ErrBatcher<_, _>, ErrBuilder<_, _>, ErrSpine<_, _>>(
"Arrange errors",
);
distinct_arranged_errs(errs, "Distinct errors").as_collection(|err, _| err.clone())
}

/// A bundle of the various ways a collection can be represented.
///
Expand Down Expand Up @@ -507,6 +543,50 @@ impl<'scope, T: RenderTimestamp> CollectionBundle<'scope, T> {
}
}

/// Collapses the multiplicity of every error this bundle carries to one.
///
/// Belongs at the definition of a binding that more than one `Get` reads. Each reader
/// propagates the binding's errors independently, so a binding read `f` times contributes its
/// errors `f` times to the dataflow's error output, and those factors apply again at every
/// further level of sharing: a chain of diamonds multiplies rather than adds, and reaches
/// `Diff` overflow at a depth plans really do have. Collapsing at each definition holds the
/// dataflow's error multiplicity to the fan-out of a single level.
///
/// Sound because error semantics depend only on whether an error is present, and correct under
/// retraction only because it reads the accumulated collection: no pointwise function of the
/// input diffs (a saturating add, a sign) can collapse multiplicity and still cancel when the
/// errors retract.
///
/// Collapses every form the bundle offers, not just one. Each form carries its own error
/// stream, and those streams differ in content as well as identity: an arrangement's errors
/// include the key-formation errors that the raw collection's do not. Which form a consumer
/// reads is the consumer's choice, and a delta join reads both within one operator, so a
/// binding's definition cannot know which form to collapse.
///
/// NOTE: Leaves imported arrangements (`ArrangementFlavor::Trace`) alone, whose error traces
Comment thread
ggevay marked this conversation as resolved.
/// this dataflow cannot rewrite in place. Their errors arrive bounded by the exporting
/// dataflow's last level of sharing rather than collapsed to one, since nothing collapses at an
/// export. A global read more than once within one dataflow is not collapsed either, because
/// only local bindings reach this.
pub fn distinct_errs(mut self) -> Self {
if let Some((oks, errs)) = self.collection.take() {
self.collection = Some((oks, distinct_errs_collection(errs)));
}
for (key, flavor) in std::mem::take(&mut self.arranged) {
let flavor = match flavor {
ArrangementFlavor::Local(oks, errs) => {
// Names the key, not the binding: an operator name carrying a `LocalId` would
// churn the introspection goldens every time the optimizer renumbers locals.
let name = format!("Distinct errors[{key:?}]");
ArrangementFlavor::Local(oks, distinct_arranged_errs(errs, &name))
}
flavor @ ArrangementFlavor::Trace(..) => flavor,
};
self.arranged.insert(key, flavor);
}
self
}

/// Brings the collection bundle into a region.
pub fn enter_region<'inner>(&self, region: Scope<'inner, T>) -> CollectionBundle<'inner, T> {
CollectionBundle {
Expand Down
Loading
Loading