Skip to content

Commit 51bbf8a

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, and a parallel-benchmark scenario measures read isolation with the feature on against off. Two 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. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 40e6744 commit 51bbf8a

9 files changed

Lines changed: 386 additions & 10 deletions

File tree

misc/python/materialize/mzcompose/__init__.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,11 @@ def get_variable_system_parameters(
297297
"true",
298298
["true", "false"],
299299
),
300+
VariableSystemParameter(
301+
"enable_compute_interactive_runtime",
302+
"true",
303+
["true", "false"],
304+
),
300305
VariableSystemParameter(
301306
"enable_upsert_v2",
302307
"false",
@@ -584,9 +589,6 @@ def get_default_system_parameters(
584589
# all. Only add it in UNINTERESTING_SYSTEM_PARAMETERS if none of the above
585590
# apply.
586591
UNINTERESTING_SYSTEM_PARAMETERS = [
587-
# Registered here rather than varied, because the interactive runtime cannot serve
588-
# index peeks yet. Moves to get_variable_system_parameters once it can.
589-
"enable_compute_interactive_runtime",
590592
"enable_compute_half_join2",
591593
"enable_mz_join_core",
592594
"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_benchmark/scenarios.py

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1274,3 +1274,119 @@ def __init__(self, c: Composition, conn_infos: dict[str, PgConnInfo]):
12741274
],
12751275
conn_pool_size=100,
12761276
)
1277+
1278+
1279+
class HydrationChurn(Action):
1280+
"""Continuously builds a heavy maintained materialized view, forces it to
1281+
hydrate, then drops it, keeping the replica's maintenance runtime busy.
1282+
1283+
`SELECT count(*)` blocks until the view has hydrated, so each iteration does
1284+
real hydration work on the maintenance workers before the drop. Runs in a
1285+
`ClosedLoop`, which is single-threaded, so a fixed view name is safe.
1286+
"""
1287+
1288+
def __init__(self, conn_info: PgConnInfo, name: str, view_sql: str):
1289+
self.conn_info = conn_info
1290+
self.name = name
1291+
self.view_sql = view_sql
1292+
self.conn = conn_info.connect()
1293+
self.conn.autocommit = True
1294+
self.cur = self.conn.cursor()
1295+
1296+
def _run(self, conns: queue.Queue):
1297+
self.cur.execute(
1298+
f"CREATE MATERIALIZED VIEW {self.name} AS {self.view_sql}".encode()
1299+
)
1300+
# Force hydration to complete before we drop, so the maintenance runtime
1301+
# actually does the build work rather than cancelling it immediately.
1302+
self.cur.execute(f"SELECT count(*) FROM {self.name}".encode())
1303+
self.cur.fetchall()
1304+
self.cur.execute(f"DROP MATERIALIZED VIEW {self.name}".encode())
1305+
1306+
def __str__(self) -> str:
1307+
return f"hydration churn {self.name}"
1308+
1309+
1310+
class TwoRuntimeReadIsolation(Scenario):
1311+
"""Measures whether peeks stay fast while the replica's maintenance runtime
1312+
is saturated by continuous dataflow hydration.
1313+
1314+
Run the same scenario twice and compare the SELECT p50/p99/qps:
1315+
bin/mzcompose --find parallel-benchmark run default \
1316+
--scenario TwoRuntimeReadIsolation \
1317+
--this-params enable_compute_interactive_runtime=true
1318+
bin/mzcompose --find parallel-benchmark run default \
1319+
--scenario TwoRuntimeReadIsolation \
1320+
--this-params enable_compute_interactive_runtime=false
1321+
1322+
With two runtimes ON the interactive runtime serves the peeks off the shared
1323+
arrangements, isolated from the maintenance workers hydrating the churn MVs.
1324+
OFF, the peeks contend with hydration on the same workers.
1325+
1326+
The peeks run open-loop at a fixed rate, so a serving runtime that cannot
1327+
keep up under contention accumulates queue-wait latency the reported p50/p99
1328+
capture. That queue backlog is the signal: it is exactly what a user issuing
1329+
reads at a steady rate experiences when the maintenance runtime steals the
1330+
serving capacity.
1331+
"""
1332+
1333+
def __init__(self, c: Composition, conn_infos: dict[str, PgConnInfo]):
1334+
mz = conn_infos["materialized"]
1335+
# Heavy enough that one build takes real CPU, small enough that two
1336+
# concurrent churn loops do not exhaust memory.
1337+
churn_view_sql = "SELECT a, count(*) AS c FROM big GROUP BY a"
1338+
self.init(
1339+
[
1340+
TdPhase("""
1341+
> DROP TABLE IF EXISTS hot CASCADE
1342+
> DROP TABLE IF EXISTS big CASCADE
1343+
1344+
# The peek target: a small indexed table, pre-hydrated.
1345+
> CREATE TABLE hot (k int, v int)
1346+
> INSERT INTO hot SELECT n, n * 2 FROM generate_series(1, 100000) AS n
1347+
> CREATE INDEX hot_k ON hot (k)
1348+
1349+
# The contention source: a large table churned into heavy MVs.
1350+
> CREATE TABLE big (a int, b int)
1351+
> INSERT INTO big SELECT n, n % 1000 FROM generate_series(1, 1000000) AS n
1352+
1353+
# Wait for the hot index to hydrate before measuring.
1354+
> SELECT v FROM hot WHERE k = 42
1355+
84
1356+
"""),
1357+
LoadPhase(
1358+
duration=120,
1359+
actions=[
1360+
# Measured: fast-path index point lookup.
1361+
OpenLoop(
1362+
action=ReuseConnQuery(
1363+
"SELECT v FROM hot WHERE k = 42",
1364+
mz,
1365+
strict_serializable=False,
1366+
),
1367+
dist=Periodic(per_second=50),
1368+
report_regressions=False,
1369+
),
1370+
# Measured: slow-path range scan + reduce peek (more
1371+
# sensitive to serving-runtime contention).
1372+
OpenLoop(
1373+
action=ReuseConnQuery(
1374+
"SELECT count(*) FROM hot WHERE k < 50000",
1375+
mz,
1376+
strict_serializable=False,
1377+
),
1378+
dist=Periodic(per_second=12),
1379+
report_regressions=False,
1380+
),
1381+
]
1382+
+ [
1383+
# Contention: continuous hydration churn on the same replica.
1384+
ClosedLoop(
1385+
action=HydrationChurn(mz, f"churn_{i}", churn_view_sql),
1386+
report_regressions=False,
1387+
)
1388+
for i in range(2)
1389+
],
1390+
),
1391+
],
1392+
)

misc/python/materialize/parallel_workload/action.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3020,8 +3020,9 @@ def __init__(
30203020
BOOLEAN_FLAG_VALUES
30213021
)
30223022
self.flags_with_values["enable_upsert_v2"] = BOOLEAN_FLAG_VALUES
3023-
# Pinned off: the interactive runtime cannot serve index peeks yet.
3024-
self.flags_with_values["enable_compute_interactive_runtime"] = ["FALSE"]
3023+
self.flags_with_values["enable_compute_interactive_runtime"] = (
3024+
BOOLEAN_FLAG_VALUES
3025+
)
30253026
self.flags_with_values["enable_coalesce_case_transform"] = BOOLEAN_FLAG_VALUES
30263027
self.flags_with_values["enable_compute_sync_mv_sink"] = BOOLEAN_FLAG_VALUES
30273028
self.flags_with_values["enable_column_paged_batcher"] = BOOLEAN_FLAG_VALUES

test/clusterd-test-driver/mzcompose.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,14 @@ def __init__(self, name: str = "headless-driver") -> None:
9898
"create_time_config.spec",
9999
]
100100

101+
# Scenarios run against a clusterd with the interactive compute runtime enabled
102+
# (see `workflow_two_runtime_compute`). Kept separate from `SCRIPTS` so the
103+
# single-runtime scenarios above stay byte-for-byte unchanged.
104+
TWO_RUNTIME_SCRIPTS = [
105+
"two_runtime_index.spec",
106+
"two_runtime_query_dataflow.spec",
107+
]
108+
101109

102110
def workflow_default(c: Composition) -> None:
103111
c.up(METADATA_STORE, "minio", ServiceName("headless-driver", idle=True))
@@ -117,3 +125,32 @@ def workflow_default(c: Composition) -> None:
117125
env_extra={"DRIVER_SCRIPT": f"{SCRIPTS_DIR}/{script}"},
118126
use_aliases=True,
119127
)
128+
# Also exercise every other workflow (the two-runtime compute path) so the
129+
# CI job, which runs this composition's default workflow, covers them too.
130+
for name in c.workflows:
131+
if name == "default":
132+
continue
133+
with c.test_case(name):
134+
c.workflow(name)
135+
136+
137+
def workflow_two_runtime_compute(c: Composition) -> None:
138+
"""Run the two-runtime scenarios against a clusterd started with a second,
139+
interactive compute runtime configured. `Clusterd(interactive_compute=True)`
140+
sets `CLUSTERD_INTERACTIVE_COMPUTE_TIMELY_CONFIG`, which turns on the
141+
multiplexer that fronts both runtimes on the same `:2101` endpoint this
142+
driver already connects to, so no driver changes are needed to reach the
143+
interactive runtime.
144+
"""
145+
c.up(METADATA_STORE, "minio", ServiceName("headless-driver", idle=True))
146+
with c.override(Clusterd(mz_service="headless-driver", interactive_compute=True)):
147+
for i, script in enumerate(TWO_RUNTIME_SCRIPTS):
148+
ui.section(f"Running two-runtime scenario {script}")
149+
if i > 0:
150+
c.kill("clusterd")
151+
c.up("clusterd")
152+
c.run(
153+
"headless-driver",
154+
env_extra={"DRIVER_SCRIPT": f"{SCRIPTS_DIR}/{script}"},
155+
use_aliases=True,
156+
)
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
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 at the root of this repository.
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+
# two_runtime_index scenario: runs against a clusterd started with a second,
11+
# interactive compute runtime configured (see `Clusterd(interactive_compute=True)`
12+
# in the `two_runtime_compute` workflow). A multiplexer inside clusterd fronts both
13+
# runtimes on the single `:2101` endpoint this driver connects to, so the commands
14+
# below are unchanged from a single-runtime script; only the clusterd underneath
15+
# differs. The maintenance runtime renders and publishes the index below into the
16+
# shared arrangement-sharing registry; the multiplexer routes the `peek` to the
17+
# interactive runtime regardless, so a correct result proves the interactive
18+
# runtime served the rows from that shared registry rather than rendering its own
19+
# copy of the index.
20+
create-instance
21+
----
22+
ok
23+
24+
update-configuration
25+
----
26+
ok
27+
28+
initialization-complete
29+
----
30+
ok
31+
32+
write-rows shard=r ts=0
33+
1 alpha
34+
2 beta
35+
3 gamma
36+
----
37+
wrote 3
38+
39+
create-dataflow name=two-runtime-index as-of=0
40+
import source=1000 shard=r upper=1
41+
build id=2000
42+
Project (#0, #1)
43+
Get u1000
44+
export kind=index index=2001 on=2000 key=[0]
45+
----
46+
ok
47+
48+
schedule id=2001
49+
----
50+
ok
51+
52+
# Routed to the interactive runtime by the multiplexer.
53+
peek id=2001 ts=0
54+
----
55+
1 "alpha"
56+
2 "beta"
57+
3 "gamma"
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
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 at the root of this repository.
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+
# two_runtime_query_dataflow scenario: the acceptance test for the interactive
11+
# slow path (N1-N3). Unlike two_runtime_index.spec, which peeks a maintenance
12+
# index directly (the fast path), this creates a genuine INTERACTIVE QUERY
13+
# DATAFLOW: a `count(*)` reduce that imports a maintenance index and is itself
14+
# exported under a transient id (`t4000`), so the multiplexer routes its
15+
# `CreateDataflow` to the interactive runtime (see `mz_compute_client::multiplex`).
16+
#
17+
# The maintenance index is created but deliberately left unscheduled before the
18+
# query dataflow is submitted, so nothing has been published to the shared
19+
# arrangement-sharing registry yet. The interactive runtime builds the query
20+
# dataflow immediately anyway, binding its import to a real but empty publication
21+
# point (a placeholder) rather than deferring the build, which would break the
22+
# deterministic construction order every worker must follow. The query's own
23+
# `schedule` is then sent immediately, before the maintenance index is scheduled,
24+
# mirroring what the real compute controller always does for a transient
25+
# collection (`Instance::maybe_schedule_collection`: scheduled right away, without
26+
# waiting for its inputs). Only afterward is the maintenance index scheduled, which
27+
# renders it and adopts the placeholder in place; that publication (never a bare
28+
# poll) wakes the import, which begins producing. The result peek then returns the
29+
# correct reduced rows, proving the bind -> fill -> resolve path completed off the
30+
# maintenance worker.
31+
create-instance
32+
----
33+
ok
34+
35+
update-configuration
36+
----
37+
ok
38+
39+
initialization-complete
40+
----
41+
ok
42+
43+
write-rows shard=r ts=0
44+
1 alpha
45+
2 beta
46+
3 gamma
47+
----
48+
wrote 3
49+
50+
# The maintenance index over shard `r`. Registering it lets `import index=2001`
51+
# below reference it; its dataflow is intentionally left unscheduled, so no
52+
# arrangement is published yet.
53+
create-dataflow name=maint-index as-of=0
54+
import source=1000 shard=r upper=1
55+
build id=2000
56+
Project (#0, #1)
57+
Get u1000
58+
export kind=index index=2001 on=2000 key=[0]
59+
----
60+
ok
61+
62+
# A one-column schema for the count reduce's output (a single bigint).
63+
define-schema name=count_out
64+
count bigint
65+
----
66+
ok
67+
68+
# The interactive query dataflow: `count(*)` over the (still unpublished)
69+
# maintenance index, exported under a transient id (`t4000`) so it is routed to
70+
# the interactive runtime instead of maintenance.
71+
create-dataflow name=interactive-count as-of=0
72+
import index=2001
73+
build id=3000
74+
Reduce aggregates=[count(*)]
75+
Get u2000
76+
export index=t4000 on=3000 key=[0]
77+
----
78+
ok
79+
80+
# Scheduled before the maintenance index: the dataflow is built but its import is
81+
# still an empty placeholder at this point, so this `Schedule` races ahead of the
82+
# publication, exactly as the real controller's immediate-schedule-of-transient
83+
# behavior would.
84+
schedule id=t4000
85+
----
86+
ok
87+
88+
# Scheduling the maintenance index renders it and adopts the placeholder, waking
89+
# the interactive query dataflow's import via the publication notification.
90+
schedule id=2001
91+
----
92+
ok
93+
94+
# The interactive runtime ran the reduce once notified of the publication, off the
95+
# maintenance worker. The result is the correct count.
96+
peek id=t4000 schema=count_out ts=0
97+
----
98+
3

0 commit comments

Comments
 (0)