Skip to content

Commit cdc950c

Browse files
antiguruclaude
andcommitted
clusterd-test-driver: headless CTP harness over clusterd
New crate providing an alternate frontend to `clusterd` that stands in for environmentd's controller in compute tests: it hosts persist PubSub, connects the compute CTP channel, and drives dataflows directly, with no SQL layer, catalog, or timestamp oracle. This first piece is the harness library: connection-target resolution, the CTP connect + `Hello` handshake, persist-PubSub hosting, synthetic-row and direct persist writes, response demultiplexing (frontiers + peeks), the `Driver` (submit/schedule/await-frontier/peek), and `DataflowBuilder`, which lowers caller-supplied MIR to a shippable `RenderPlan` dataflow (optionally running the MIR optimizer). Exercised end-to-end by `tests/index_smoke.rs` against a real clusterd. The text script runner that drives this harness follows in a stacked PR. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent e6caffc commit cdc950c

13 files changed

Lines changed: 2602 additions & 0 deletions

File tree

Cargo.lock

Lines changed: 25 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ members = [
2525
"src/cluster",
2626
"src/cluster-client",
2727
"src/clusterd",
28+
"src/clusterd-test-driver",
2829
"src/compute",
2930
"src/compute-client",
3031
"src/compute-types",
@@ -154,6 +155,7 @@ default-members = [
154155
"src/cluster",
155156
"src/cluster-client",
156157
"src/clusterd",
158+
"src/clusterd-test-driver",
157159
"src/compute",
158160
"src/compute-client",
159161
"src/compute-types",

doc/developer/design/20260612_headless_clusterd_test_driver.md

Lines changed: 319 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
[package]
2+
name = "mz-clusterd-test-driver"
3+
description = "Headless frontend to clusterd for scripted compute tests."
4+
version = "0.0.0"
5+
edition.workspace = true
6+
rust-version.workspace = true
7+
publish = false
8+
9+
[lints]
10+
workspace = true
11+
12+
[dependencies]
13+
anyhow.workspace = true
14+
futures.workspace = true
15+
mz-compute-client = { path = "../compute-client" }
16+
mz-compute-types = { path = "../compute-types" }
17+
mz-dyncfg = { path = "../dyncfg" }
18+
mz-expr = { path = "../expr" }
19+
# default-features = false to avoid pulling mz-ore's default `tokio-console`
20+
# feature (pulled transitively via mz-compute-client); the harness only needs the
21+
# async, tracing, and metrics helpers.
22+
mz-ore = { path = "../ore", default-features = false, features = [
23+
"async",
24+
"tracing",
25+
"metrics",
26+
] }
27+
mz-persist-client = { path = "../persist-client" }
28+
mz-persist-types = { path = "../persist-types" }
29+
mz-repr = { path = "../repr" }
30+
mz-service = { path = "../service" }
31+
mz-storage-types = { path = "../storage-types" }
32+
# Optional MIR dataflow optimizer, run only by `DataflowBuilder::optimize`. Needed
33+
# for shapes that don't lower from raw MIR (notably joins, whose `implementation`
34+
# defaults to `Unimplemented`); the default path lowers faithfully without it.
35+
mz-transform = { path = "../transform" }
36+
timely.workspace = true
37+
tokio = { workspace = true, features = ["full"] }
38+
tokio-stream.workspace = true
39+
tracing.workspace = true
40+
uuid.workspace = true
41+
42+
[dev-dependencies]
43+
# The `test` feature provides the `#[mz_ore::test]` macro; it is only needed
44+
# when building tests, so keep it out of the non-test feature set (see the
45+
# [dependencies] mz-ore comment).
46+
mz-ore = { path = "../ore", features = ["test"] }
47+
tempfile.workspace = true
48+
49+
[package.metadata.cargo-udeps.ignore]
50+
normal = []
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
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+
//! Compute CTP connection and the `Hello` step of the controller handshake.
11+
//! Generic: it sends any `ComputeCommand` and receives any `ComputeResponse`.
12+
//!
13+
//! Only `Hello` (the transport/version step) happens here; the controller
14+
//! handshake proper — `CreateInstance`, `UpdateConfiguration`,
15+
//! `InitializationComplete` — is driven by explicit caller commands, so the
16+
//! caller controls the instance config, the peek-stash setting, and exactly when
17+
//! the reconciliation window opens and closes.
18+
19+
use std::time::Duration;
20+
21+
use mz_compute_client::protocol::command::ComputeCommand;
22+
use mz_compute_client::protocol::response::ComputeResponse;
23+
use mz_service::client::GenericClient;
24+
use mz_service::transport::{Client, NoopMetrics};
25+
use uuid::Uuid;
26+
27+
pub type ComputeCtpClient = Client<ComputeCommand, ComputeResponse>;
28+
29+
/// Connects to a clusterd compute controller address and sends `Hello`, leaving
30+
/// the controller handshake (`CreateInstance` onward) to the caller.
31+
///
32+
/// A reconnect re-runs exactly this: a fresh transport connection plus `Hello`.
33+
/// The reconciliation window then opens when the script sends `CreateInstance`
34+
/// and closes when it sends `InitializationComplete`; in between, the replica
35+
/// reconciles the replayed dataflows against its live ones rather than
36+
/// rehydrating.
37+
pub async fn connect_and_hello(compute_addr: &str) -> anyhow::Result<ComputeCtpClient> {
38+
// Use persist-client's BUILD_INFO: it is release-versioned (synced by
39+
// bin/bump-version), so it matches the clusterd we connect to. Our own
40+
// crate is `0.0.0`, which would fail the handshake's version check.
41+
let version = mz_persist_client::BUILD_INFO.semver_version();
42+
let mut client = Client::<ComputeCommand, ComputeResponse>::connect(
43+
compute_addr,
44+
version,
45+
Duration::from_secs(30),
46+
Duration::from_secs(60),
47+
NoopMetrics,
48+
)
49+
.await?;
50+
51+
client
52+
.send(ComputeCommand::Hello {
53+
nonce: Uuid::new_v4(),
54+
})
55+
.await?;
56+
57+
Ok(client)
58+
}
59+
60+
#[cfg(test)]
61+
mod tests {
62+
use super::*;
63+
use crate::target;
64+
65+
#[mz_ore::test(tokio::test)]
66+
#[cfg_attr(miri, ignore)]
67+
async fn hello_holds_connection() {
68+
if !target::e2e_enabled() {
69+
return;
70+
}
71+
let mut client = connect_and_hello(&target::compute_addr())
72+
.await
73+
.expect("hello");
74+
let r = tokio::time::timeout(Duration::from_millis(500), client.recv()).await;
75+
assert!(r.is_err() || r.unwrap().is_ok());
76+
}
77+
}

0 commit comments

Comments
 (0)