Skip to content

Commit 67d7101

Browse files
antiguruclaude
andcommitted
compute: turn the interactive runtime on in test configurations
`enable_compute_interactive_runtime` becomes a variable system parameter defaulting to on, which is what makes every preceding piece of this work reachable: sqllogictest, testdrive, and the mzcompose suites now provision replicas with two runtimes, so peeks and bounded transient dataflows route to the interactive runtime and maintenance publishes its indexes for it to read. Production keeps the dyncfg's own default, which is off. The clusterd mzcompose service grows the second runtime's port and `--interactive-compute-timely-config`, mirroring what the controller passes in a real deployment. Two clusterd-test-driver specs cover an index read and a query dataflow across the runtime boundary. The `ReadIsolationUnderHydration` parallel-benchmark scenario A/Bs the flag, measuring peek latency with the feature on against off while hydration saturates the maintenance workers. Three goldens move. `relations.slt` gains the publisher operators, which are real operators the maintenance runtime now installs on every published index. `introspection-sources.td` raises a coarse arrangement-size bound from 16 KiB to 32 KiB, because publication raises the reported size of a one-record index past the old bound. Whether that overhead is constant per arrangement or scales with size is not established and wants re-measuring, so the comment records the measurement without claiming a mechanism. `singlereplica_attribution_sources.slt` asserted a materialized view's timely dataflow id, which counted the peek dataflows rendered before it. Those now render on the interactive runtime, so the assertion is reduced to the id-to-collection pairing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VDm7opomJLxbNUEP3r9BLk
1 parent 861a6db commit 67d7101

12 files changed

Lines changed: 433 additions & 31 deletions

File tree

misc/python/materialize/mzcompose/__init__.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,11 @@ def get_variable_system_parameters(
364364
"true",
365365
["true", "false"],
366366
),
367+
VariableSystemParameter(
368+
"enable_compute_interactive_runtime",
369+
"true",
370+
["true", "false"],
371+
),
367372
VariableSystemParameter(
368373
"enable_upsert_v2",
369374
"false",
@@ -662,9 +667,6 @@ def get_default_system_parameters(
662667
# all. Only add it in UNINTERESTING_SYSTEM_PARAMETERS if none of the above
663668
# apply.
664669
UNINTERESTING_SYSTEM_PARAMETERS = [
665-
# Registered here rather than varied, because the interactive runtime cannot serve
666-
# index peeks yet. Moves to get_variable_system_parameters once it can.
667-
"enable_compute_interactive_runtime",
668670
"enable_compute_half_join2",
669671
"enable_mz_join_core",
670672
"linear_join_yielding",

misc/python/materialize/mzcompose/services/clusterd.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ def __init__(
3939
workers: int = 1,
4040
process_names: list[str] = [],
4141
mz_service: str = "materialized",
42+
interactive_compute: bool = False,
4243
) -> None:
4344
environment = [
4445
"CLUSTERD_LOG_FILTER",
@@ -78,6 +79,21 @@ def __init__(
7879
f"CLUSTERD_STORAGE_TIMELY_CONFIG={storage_timely_config}",
7980
]
8081

82+
# When set, clusterd runs a second, interactive compute runtime alongside the
83+
# maintenance one (see `--interactive-compute-timely-config` in
84+
# `src/clusterd/src/lib.rs`). It must span the same number of Timely peers as
85+
# the maintenance compute config, so it reuses `process_names`/`workers`; its
86+
# addresses use a distinct port (2104) so the two runtimes don't collide.
87+
ports = [2100, 2101, 6878]
88+
if interactive_compute:
89+
interactive_compute_timely_config = timely_config(
90+
process_names, 2104, workers, DEFAULT_COMPUTE_EXERT_PROPORTIONALITY
91+
)
92+
environment += [
93+
f"CLUSTERD_INTERACTIVE_COMPUTE_TIMELY_CONFIG={interactive_compute_timely_config}"
94+
]
95+
ports += [2104]
96+
8197
options = ["clusterd", f"--scratch-directory={scratch_directory}", *options]
8298

8399
config: ServiceConfig = {}
@@ -106,7 +122,7 @@ def __init__(
106122
config.update(
107123
{
108124
"command": options,
109-
"ports": [2100, 2101, 6878],
125+
"ports": ports,
110126
"environment": environment,
111127
"volumes": volumes or DEFAULT_MZ_VOLUMES,
112128
"restart": restart,

misc/python/materialize/parallel_workload/action.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3134,8 +3134,9 @@ def __init__(
31343134
BOOLEAN_FLAG_VALUES
31353135
)
31363136
self.flags_with_values["enable_upsert_v2"] = BOOLEAN_FLAG_VALUES
3137-
# Pinned off: the interactive runtime cannot serve index peeks yet.
3138-
self.flags_with_values["enable_compute_interactive_runtime"] = ["FALSE"]
3137+
self.flags_with_values["enable_compute_interactive_runtime"] = (
3138+
BOOLEAN_FLAG_VALUES
3139+
)
31393140
self.flags_with_values["enable_coalesce_case_transform"] = BOOLEAN_FLAG_VALUES
31403141
self.flags_with_values["enable_any_all_null_array_semantics"] = (
31413142
BOOLEAN_FLAG_VALUES

src/clusterd-test-driver/src/script.rs

Lines changed: 90 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,9 @@ pub enum ExplainTarget {
194194
exports: Vec<ExportSpec>,
195195
/// The dataflow's `as_of`.
196196
as_of: u64,
197+
/// The dataflow's `until`, if bounded. `as_of + 1` makes the dataflow single-time.
198+
#[serde(default)]
199+
until: Option<u64>,
197200
/// Run the MIR optimizer before lowering. Off by default.
198201
#[serde(default)]
199202
optimize: bool,
@@ -451,6 +454,11 @@ pub enum Command {
451454
/// The shard's exclusive write upper (see `PersistSource::upper`).
452455
upper: u64,
453456
},
457+
/// Submit a dataflow a prior `create-dataflow name=<name> defer` registered.
458+
SubmitDataflow {
459+
/// The `create-dataflow` name to submit.
460+
name: String,
461+
},
454462
/// Schedule a previously-submitted collection so it makes progress.
455463
Schedule {
456464
/// The collection's global id.
@@ -524,10 +532,17 @@ pub enum Command {
524532
exports: Vec<ExportSpec>,
525533
/// The dataflow's `as_of`.
526534
as_of: u64,
535+
/// The dataflow's `until`, if bounded. `as_of + 1` makes the dataflow single-time.
536+
#[serde(default)]
537+
until: Option<u64>,
527538
/// Run the MIR optimizer before lowering (needed for e.g. joins). Off by
528539
/// default, so the caller's MIR is lowered faithfully.
529540
#[serde(default)]
530541
optimize: bool,
542+
/// Register the dataflow's exports without submitting it, so a later dataflow
543+
/// can import them before `submit-dataflow` renders this one.
544+
#[serde(default)]
545+
defer: bool,
531546
},
532547
/// Render a dataflow's lowered LIR plan as text, the output assertion being the
533548
/// plan shape itself. It submits nothing and records no index, subscribe, or
@@ -631,6 +646,7 @@ struct DataflowSpec {
631646
builds: Vec<BuildSpec>,
632647
exports: Vec<ExportSpec>,
633648
as_of: u64,
649+
until: Option<u64>,
634650
optimize: bool,
635651
}
636652

@@ -801,6 +817,7 @@ impl ScriptState {
801817
builds: Vec<BuildSpec>,
802818
exports: Vec<ExportSpec>,
803819
as_of: u64,
820+
until: Option<u64>,
804821
optimize: bool,
805822
) -> anyhow::Result<(DataflowBuilder, PendingRegistrations)> {
806823
let mut builder =
@@ -937,9 +954,35 @@ impl ScriptState {
937954
}
938955
}
939956
builder.as_of(Timestamp::from(as_of));
957+
if let Some(until) = until {
958+
builder.until(Timestamp::from(until));
959+
}
940960
Ok((builder, registrations))
941961
}
942962

963+
/// Finishes and submits `builder`, then applies `registrations`.
964+
///
965+
/// Registers only after a successful submit, so a rejected dataflow leaves no dangling
966+
/// index entry or subscribe buffer.
967+
fn submit(
968+
&mut self,
969+
builder: DataflowBuilder,
970+
registrations: PendingRegistrations,
971+
) -> anyhow::Result<()> {
972+
let df = builder.finish()?;
973+
self.driver.submit_dataflow(df)?;
974+
for (index_id, entry) in registrations.indexes {
975+
self.indexes.insert(index_id, entry);
976+
}
977+
for sink_id in registrations.subscribes {
978+
self.driver.register_subscribe(sink_id);
979+
}
980+
for (sink_id, metadata) in registrations.mv_outputs {
981+
self.mv_outputs.insert(sink_id, metadata);
982+
}
983+
Ok(())
984+
}
985+
943986
/// Execute a single command, returning its golden output text.
944987
pub async fn execute(&mut self, cmd: Command) -> anyhow::Result<String> {
945988
match cmd {
@@ -1085,10 +1128,13 @@ impl ScriptState {
10851128
builds,
10861129
exports,
10871130
as_of,
1131+
until,
10881132
optimize,
1133+
defer,
10891134
} => {
10901135
// Record the spec under its name so `explain ref=<name>` can render
1091-
// this dataflow's plan later without repeating the body.
1136+
// this dataflow's plan later without repeating the body, and so
1137+
// `submit-dataflow` can submit a deferred one.
10921138
if let Some(name) = &name {
10931139
self.dataflows.insert(
10941140
name.clone(),
@@ -1097,39 +1143,63 @@ impl ScriptState {
10971143
builds: builds.clone(),
10981144
exports: exports.clone(),
10991145
as_of,
1146+
until,
11001147
optimize,
11011148
},
11021149
);
11031150
}
1104-
let (builder, registrations) =
1105-
self.configure_dataflow(name, imports, builds, exports, as_of, optimize)?;
1106-
let df = builder.finish()?;
1107-
self.driver.submit_dataflow(df)?;
1108-
// Register only after a successful submit, so a rejected dataflow
1109-
// leaves no dangling index entry or subscribe buffer.
1110-
for (index_id, entry) in registrations.indexes {
1111-
self.indexes.insert(index_id, entry);
1112-
}
1113-
for sink_id in registrations.subscribes {
1114-
self.driver.register_subscribe(sink_id);
1115-
}
1116-
for (sink_id, metadata) in registrations.mv_outputs {
1117-
self.mv_outputs.insert(sink_id, metadata);
1151+
if defer {
1152+
anyhow::ensure!(
1153+
name.is_some(),
1154+
"`defer` needs a name for the later `submit-dataflow`"
1155+
);
1156+
// Only the index registrations, which is what a later import
1157+
// resolves against. Everything else registers at submit.
1158+
let (_builder, registrations) = self.configure_dataflow(
1159+
name, imports, builds, exports, as_of, until, optimize,
1160+
)?;
1161+
for (index_id, entry) in registrations.indexes {
1162+
self.indexes.insert(index_id, entry);
1163+
}
1164+
return Ok("deferred".to_string());
11181165
}
1166+
let (builder, registrations) = self
1167+
.configure_dataflow(name, imports, builds, exports, as_of, until, optimize)?;
1168+
self.submit(builder, registrations)?;
1169+
Ok("ok".to_string())
1170+
}
1171+
Command::SubmitDataflow { name } => {
1172+
let spec = self.dataflows.get(&name).ok_or_else(|| {
1173+
anyhow::anyhow!(
1174+
"unknown dataflow {name:?}; declare it with \
1175+
create-dataflow name={name} defer first"
1176+
)
1177+
})?;
1178+
let (builder, registrations) = self.configure_dataflow(
1179+
Some(name.clone()),
1180+
spec.imports.clone(),
1181+
spec.builds.clone(),
1182+
spec.exports.clone(),
1183+
spec.as_of,
1184+
spec.until,
1185+
spec.optimize,
1186+
)?;
1187+
self.submit(builder, registrations)?;
11191188
Ok("ok".to_string())
11201189
}
11211190
Command::Explain { target } => {
11221191
// Resolve the target to a dataflow body: either given inline, or the
11231192
// spec a prior `create-dataflow name=<name>` recorded.
1124-
let (name, imports, builds, exports, as_of, optimize) = match target {
1193+
let (name, imports, builds, exports, as_of, until, optimize) = match target {
11251194
ExplainTarget::Inline {
11261195
name,
11271196
imports,
11281197
builds,
11291198
exports,
11301199
as_of,
1200+
until,
11311201
optimize,
1132-
} => (name, imports, builds, exports, as_of, optimize),
1202+
} => (name, imports, builds, exports, as_of, until, optimize),
11331203
ExplainTarget::Reference { name } => {
11341204
let spec = self.dataflows.get(&name).ok_or_else(|| {
11351205
anyhow::anyhow!(
@@ -1143,6 +1213,7 @@ impl ScriptState {
11431213
spec.builds.clone(),
11441214
spec.exports.clone(),
11451215
spec.as_of,
1216+
spec.until,
11461217
spec.optimize,
11471218
)
11481219
}
@@ -1151,8 +1222,8 @@ impl ScriptState {
11511222
// LIR plan instead of submitting it. The registrations are discarded:
11521223
// explain has no side effects, so it neither installs a dataflow nor
11531224
// records an index / subscribe / materialized-view output.
1154-
let (builder, _registrations) =
1155-
self.configure_dataflow(name, imports, builds, exports, as_of, optimize)?;
1225+
let (builder, _registrations) = self
1226+
.configure_dataflow(name, imports, builds, exports, as_of, until, optimize)?;
11561227
// The LIR render separates objects with blank lines; the `----` block
11571228
// preserves them via the doubled-separator form (see `crate::text`).
11581229
// Trim the trailing newline so the golden matches like every other

src/clusterd-test-driver/src/text.rs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -433,6 +433,7 @@ struct DataflowBody {
433433
builds: Vec<BuildSpec>,
434434
exports: Vec<ExportSpec>,
435435
as_of: u64,
436+
until: Option<u64>,
436437
optimize: bool,
437438
}
438439

@@ -446,6 +447,7 @@ fn parse_dataflow_body(
446447
) -> anyhow::Result<DataflowBody> {
447448
let name = opt_string(args, "name");
448449
let as_of = req_u64(args, "as-of")?;
450+
let until = opt_u64(args, "until")?;
449451
let optimize = flags.iter().any(|f| f == "optimize");
450452
let mut imports = Vec::new();
451453
let mut builds = Vec::new();
@@ -485,6 +487,7 @@ fn parse_dataflow_body(
485487
builds,
486488
exports,
487489
as_of,
490+
until,
488491
optimize,
489492
})
490493
}
@@ -568,6 +571,7 @@ fn parse_command(input: &str) -> anyhow::Result<Command> {
568571
builds,
569572
exports,
570573
as_of,
574+
until,
571575
optimize,
572576
} = parse_dataflow_body(&args, &flags, body)?;
573577
Command::CreateDataflow {
@@ -576,9 +580,14 @@ fn parse_command(input: &str) -> anyhow::Result<Command> {
576580
builds,
577581
exports,
578582
as_of,
583+
until,
579584
optimize,
585+
defer: flags.iter().any(|f| f == "defer"),
580586
}
581587
}
588+
"submit-dataflow" => Command::SubmitDataflow {
589+
name: req(&args, "name")?.to_string(),
590+
},
582591
"explain" => {
583592
// `explain ref=<name>` renders a previously declared dataflow; otherwise
584593
// the dataflow is given inline with the `create-dataflow` body.
@@ -595,6 +604,7 @@ fn parse_command(input: &str) -> anyhow::Result<Command> {
595604
builds,
596605
exports,
597606
as_of,
607+
until,
598608
optimize,
599609
} = parse_dataflow_body(&args, &flags, body)?;
600610
ExplainTarget::Inline {
@@ -603,6 +613,7 @@ fn parse_command(input: &str) -> anyhow::Result<Command> {
603613
builds,
604614
exports,
605615
as_of,
616+
until,
606617
optimize,
607618
}
608619
};
@@ -785,7 +796,9 @@ mod tests {
785796
key: vec![0]
786797
}],
787798
as_of: 0,
799+
until: None,
788800
optimize: false,
801+
defer: false,
789802
}
790803
);
791804

@@ -835,6 +848,7 @@ mod tests {
835848
key: vec![0],
836849
}],
837850
as_of: 0,
851+
until: None,
838852
optimize: true,
839853
}
840854
}
@@ -1019,4 +1033,21 @@ mod tests {
10191033
content
10201034
);
10211035
}
1036+
1037+
/// `defer` on the directive line is picked up, and `submit-dataflow` names the deferred
1038+
/// dataflow.
1039+
#[mz_ore::test]
1040+
fn parses_deferred_create_and_submit() {
1041+
let input = "create-dataflow name=d as-of=0 defer\n import index=1001\n build id=2000\n Get u1000\n export index=2001 on=2000 key=[0]";
1042+
assert!(matches!(
1043+
parse_command(input).unwrap(),
1044+
Command::CreateDataflow { defer: true, .. }
1045+
));
1046+
assert_eq!(
1047+
parse_command("submit-dataflow name=d").unwrap(),
1048+
Command::SubmitDataflow {
1049+
name: "d".to_string()
1050+
}
1051+
);
1052+
}
10221053
}

0 commit comments

Comments
 (0)