Skip to content

Commit 958feb6

Browse files
antiguruclaude
andcommitted
compute: add ComputeRuntimeRole and role-labeled metrics
Introduce `ComputeRuntimeRole` (Solo, Maintenance, Interactive) and thread a role through `serve` into `ComputeMetrics::register_with`. Each named role stamps a distinct `role` const label on its metrics so that two compute runtimes sharing one process registry register distinct series rather than colliding, and gates the whole-registry `workload_class` postprocessor on the globals-owning role so it is installed exactly once. `Solo` is the single-runtime default and is behaviorally identical to compute before a second runtime existed: it emits no `role` label, so exact-match dashboards and alerts are byte-unchanged, and it owns the process globals as the sole runtime always has. Every call site passes `Solo` here. The maintenance and interactive runtimes that use the other roles arrive with the two-runtime work that builds on this. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CY7GHdrTBfAG4tgvSgPGJ9
1 parent f4b7f2d commit 958feb6

3 files changed

Lines changed: 225 additions & 69 deletions

File tree

src/clusterd/src/lib.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ use hyper_util::rt::TokioIo;
2121
use mz_build_info::{BuildInfo, build_info};
2222
use mz_cloud_resources::AwsExternalIdPrefix;
2323
use mz_cluster_client::client::TimelyConfig;
24-
use mz_compute::server::ComputeInstanceContext;
24+
use mz_compute::server::{ComputeInstanceContext, ComputeRuntimeRole};
2525
use mz_http_util::DynamicFilterTarget;
2626
use mz_orchestrator_tracing::{StaticTracingConfig, TracingCliArgs};
2727
use mz_ore::cli::{self, CliConfig};
@@ -470,6 +470,7 @@ async fn run(args: Args) -> Result<(), anyhow::Error> {
470470
// Start compute server.
471471
let compute_client_builder = mz_compute::server::serve(
472472
compute_timely_config,
473+
ComputeRuntimeRole::Solo,
473474
&metrics_registry,
474475
persist_clients,
475476
txns_ctx,

src/compute/src/metrics.rs

Lines changed: 160 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@ use std::sync::{Arc, Mutex};
1212
use mz_compute_client::metrics::{CommandMetrics, HistoryMetrics};
1313
use mz_ore::cast::CastFrom;
1414
use mz_ore::metric;
15-
use mz_ore::metrics::{MetricTag, MetricVisibility, MetricsRegistry, UIntGauge, raw};
15+
use mz_ore::metrics::{
16+
MakeCollectorOpts, MetricTag, MetricVisibility, MetricsRegistry, UIntGauge, raw,
17+
};
1618
use mz_repr::{GlobalId, SharedRow};
1719
use prometheus::core::{AtomicF64, GenericCounter};
1820
use prometheus::proto::LabelPair;
@@ -83,160 +85,190 @@ pub struct ComputeMetrics {
8385
subscribe_snapshots_skipped_total: IntCounter,
8486
}
8587

88+
/// Applies the per-role const label to `opts`, unless `role` is `Solo`.
89+
///
90+
/// The two named roles (maintenance, interactive) each get a distinct `role` label so a second
91+
/// compute runtime in the same process registers a distinct series rather than colliding with the
92+
/// first. `Solo` omits the label so a single-runtime deployment registers exactly as it did before
93+
/// a second runtime existed.
94+
fn with_role(
95+
mut opts: MakeCollectorOpts,
96+
role: crate::server::ComputeRuntimeRole,
97+
) -> MakeCollectorOpts {
98+
if let Some(label) = role.label() {
99+
opts.opts = opts.opts.const_label("role", label);
100+
}
101+
opts
102+
}
103+
86104
impl ComputeMetrics {
87-
pub fn register_with(registry: &MetricsRegistry) -> Self {
105+
/// Registers the compute metrics for `role` into `registry`.
106+
///
107+
/// The two named roles carry a `role` const label so that a second compute runtime in the same
108+
/// process registers a distinct series rather than colliding with the first. `Solo` carries no
109+
/// such label.
110+
pub fn register_with(
111+
registry: &MetricsRegistry,
112+
role: crate::server::ComputeRuntimeRole,
113+
) -> Self {
88114
let workload_class = Arc::new(Mutex::new(None));
89115

90116
// Apply a `workload_class` label to all metrics in the registry when we
91117
// have a known workload class.
92-
registry.register_postprocessor({
93-
let workload_class = Arc::clone(&workload_class);
94-
move |metrics| {
95-
let workload_class: Option<String> =
96-
workload_class.lock().expect("lock poisoned").clone();
97-
let Some(workload_class) = workload_class else {
98-
return;
99-
};
100-
for metric in metrics {
101-
for metric in metric.mut_metric() {
102-
let mut label = LabelPair::default();
103-
label.set_name("workload_class".into());
104-
label.set_value(workload_class.clone());
105-
106-
let mut labels = metric.take_label();
107-
labels.push(label);
108-
metric.set_label(labels);
118+
//
119+
// The postprocessor rewrites every metric in the whole registry, so only the maintenance
120+
// runtime registers it. A second registration from the interactive runtime would push the
121+
// label twice onto each metric and produce a duplicate-label scrape error.
122+
if role.owns_process_globals() {
123+
registry.register_postprocessor({
124+
let workload_class = Arc::clone(&workload_class);
125+
move |metrics| {
126+
let workload_class: Option<String> =
127+
workload_class.lock().expect("lock poisoned").clone();
128+
let Some(workload_class) = workload_class else {
129+
return;
130+
};
131+
for metric in metrics {
132+
for metric in metric.mut_metric() {
133+
let mut label = LabelPair::default();
134+
label.set_name("workload_class".into());
135+
label.set_value(workload_class.clone());
136+
137+
let mut labels = metric.take_label();
138+
labels.push(label);
139+
metric.set_label(labels);
140+
}
109141
}
110142
}
111-
}
112-
});
143+
});
144+
}
113145

114146
Self {
115147
workload_class,
116-
history_command_count: registry.register(metric!(
148+
history_command_count: registry.register(with_role(metric!(
117149
name: "mz_compute_replica_history_command_count",
118150
help: "The number of commands in the replica's command history.",
119151
var_labels: ["worker_id", "command_type"],
120-
)),
121-
history_dataflow_count: registry.register(metric!(
152+
), role)),
153+
history_dataflow_count: registry.register(with_role(metric!(
122154
name: "mz_compute_replica_history_dataflow_count",
123155
help: "The number of dataflows in the replica's command history.",
124156
var_labels: ["worker_id"],
125157
visibility: MetricVisibility::Public,
126158
tags: [MetricTag::Compute],
127-
)),
128-
reconciliation_reused_dataflows_count_total: registry.register(metric!(
159+
), role)),
160+
reconciliation_reused_dataflows_count_total: registry.register(with_role(metric!(
129161
name: "mz_compute_reconciliation_reused_dataflows_count_total",
130162
help: "The total number of dataflows that were reused during compute reconciliation.",
131163
var_labels: ["worker_id"],
132-
)),
133-
reconciliation_replaced_dataflows_count_total: registry.register(metric!(
164+
), role)),
165+
reconciliation_replaced_dataflows_count_total: registry.register(with_role(metric!(
134166
name: "mz_compute_reconciliation_replaced_dataflows_count_total",
135167
help: "The total number of dataflows that were replaced during compute reconciliation.",
136168
var_labels: ["worker_id", "reason"],
137-
)),
138-
arrangement_maintenance_seconds_total: registry.register(metric!(
169+
), role)),
170+
arrangement_maintenance_seconds_total: registry.register(with_role(metric!(
139171
name: "mz_arrangement_maintenance_seconds_total",
140172
help: "The total time spent maintaining arrangements.",
141173
var_labels: ["worker_id"],
142174
visibility: MetricVisibility::Public,
143175
tags: [MetricTag::Compute],
144-
)),
145-
arrangement_maintenance_active_info: registry.register(metric!(
176+
), role)),
177+
arrangement_maintenance_active_info: registry.register(with_role(metric!(
146178
name: "mz_arrangement_maintenance_active_info",
147179
help: "Whether maintenance is currently occuring.",
148180
var_labels: ["worker_id"],
149-
)),
150-
timely_step_duration_seconds: registry.register(metric!(
181+
), role)),
182+
timely_step_duration_seconds: registry.register(with_role(metric!(
151183
name: "mz_timely_step_duration_seconds",
152184
help: "The time spent in each compute step_or_park call",
153185
const_labels: {"cluster" => "compute"},
154186
var_labels: ["worker_id"],
155187
buckets: mz_ore::stats::histogram_seconds_buckets(0.000_128, 32.0),
156-
)),
157-
shared_row_heap_capacity_bytes: registry.register(metric!(
188+
), role)),
189+
shared_row_heap_capacity_bytes: registry.register(with_role(metric!(
158190
name: "mz_dataflow_shared_row_heap_capacity_bytes",
159191
help: "The heap capacity of the shared row.",
160192
var_labels: ["worker_id"],
161-
)),
162-
persist_peek_seconds: registry.register(metric!(
193+
), role)),
194+
persist_peek_seconds: registry.register(with_role(metric!(
163195
name: "mz_persist_peek_seconds",
164196
help: "Time spent in (experimental) Persist fast-path peeks.",
165197
var_labels: ["worker_id"],
166198
buckets: mz_ore::stats::histogram_seconds_buckets(0.000_128, 8.0),
167-
)),
168-
stashed_peek_seconds: registry.register(metric!(
199+
), role)),
200+
stashed_peek_seconds: registry.register(with_role(metric!(
169201
name: "mz_stashed_peek_seconds",
170202
help: "Time spent reading a peek result and stashing it in the peek result stash (aka. persist blob).",
171203
var_labels: ["worker_id"],
172204
buckets: mz_ore::stats::histogram_seconds_buckets(0.000_128, 8.0),
173-
)),
174-
handle_command_duration_seconds: registry.register(metric!(
205+
), role)),
206+
handle_command_duration_seconds: registry.register(with_role(metric!(
175207
name: "mz_cluster_handle_command_duration_seconds",
176208
help: "Time spent in handling commands.",
177209
const_labels: {"cluster" => "compute"},
178210
var_labels: ["worker_id", "command_type"],
179211
buckets: mz_ore::stats::histogram_seconds_buckets(0.000_128, 8.0),
180-
)),
181-
index_peek_total_seconds: registry.register(metric!(
212+
), role)),
213+
index_peek_total_seconds: registry.register(with_role(metric!(
182214
name: "mz_index_peek_total_seconds",
183215
help: "Total time processing index peeks, from process_peek entry to response. Excluding peeks that use the peek response stash.",
184216
buckets: mz_ore::stats::histogram_seconds_buckets(0.000_128, 8.0),
185-
)),
186-
index_peek_seek_fulfillment_seconds: registry.register(metric!(
217+
), role)),
218+
index_peek_seek_fulfillment_seconds: registry.register(with_role(metric!(
187219
name: "mz_index_peek_seek_fulfillment_seconds",
188220
help: "Time in seek_fulfillment method including frontier checks and data collection.",
189221
buckets: mz_ore::stats::histogram_seconds_buckets(0.000_128, 8.0),
190-
)),
191-
index_peek_error_scan_seconds: registry.register(metric!(
222+
), role)),
223+
index_peek_error_scan_seconds: registry.register(with_role(metric!(
192224
name: "mz_index_peek_error_scan_seconds",
193225
help: "Time scanning the error trace for errors.",
194226
buckets: mz_ore::stats::histogram_seconds_buckets(0.000_128, 8.0),
195-
)),
196-
index_peek_cursor_setup_seconds: registry.register(metric!(
227+
), role)),
228+
index_peek_cursor_setup_seconds: registry.register(with_role(metric!(
197229
name: "mz_index_peek_cursor_setup_seconds",
198230
help: "Time setting up cursor and literal constraints.",
199231
buckets: mz_ore::stats::histogram_seconds_buckets(0.000_128, 8.0),
200-
)),
201-
index_peek_row_iteration_seconds: registry.register(metric!(
232+
), role)),
233+
index_peek_row_iteration_seconds: registry.register(with_role(metric!(
202234
name: "mz_index_peek_row_iteration_seconds",
203235
help: "Time iterating rows and evaluating MFP.",
204236
buckets: mz_ore::stats::histogram_seconds_buckets(0.000_128, 8.0),
205-
)),
206-
index_peek_result_sort_seconds: registry.register(metric!(
237+
), role)),
238+
index_peek_result_sort_seconds: registry.register(with_role(metric!(
207239
name: "mz_index_peek_result_sort_seconds",
208240
help: "Time sorting intermediate results during peek collection.",
209241
buckets: mz_ore::stats::histogram_seconds_buckets(0.000_128, 8.0),
210-
)),
211-
index_peek_frontier_check_seconds: registry.register(metric!(
242+
), role)),
243+
index_peek_frontier_check_seconds: registry.register(with_role(metric!(
212244
name: "mz_index_peek_frontier_check_seconds",
213245
help: "Time checking trace frontiers.",
214246
buckets: mz_ore::stats::histogram_seconds_buckets(0.000_128, 8.0),
215-
)),
216-
index_peek_row_collection_seconds: registry.register(metric!(
247+
), role)),
248+
index_peek_row_collection_seconds: registry.register(with_role(metric!(
217249
name: "mz_index_peek_row_collection_seconds",
218250
help: "Time constructing RowCollection from peek results.",
219251
buckets: mz_ore::stats::histogram_seconds_buckets(0.000_128, 8.0),
220-
)),
221-
replica_expiration_timestamp_seconds: registry.register(metric!(
252+
), role)),
253+
replica_expiration_timestamp_seconds: registry.register(with_role(metric!(
222254
name: "mz_dataflow_replica_expiration_timestamp_seconds",
223255
help: "The replica expiration timestamp in seconds since epoch.",
224256
var_labels: ["worker_id"],
225-
)),
226-
replica_expiration_remaining_seconds: registry.register(metric!(
257+
), role)),
258+
replica_expiration_remaining_seconds: registry.register(with_role(metric!(
227259
name: "mz_dataflow_replica_expiration_remaining_seconds",
228260
help: "The remaining seconds until replica expiration. Can go negative, can lag behind.",
229261
var_labels: ["worker_id"],
230-
)),
231-
collection_count: registry.register(metric!(
262+
), role)),
263+
collection_count: registry.register(with_role(metric!(
232264
name: "mz_compute_collection_count",
233265
help: "The number and hydration status of maintained compute collections.",
234266
var_labels: ["worker_id", "type", "hydrated"],
235-
)),
236-
subscribe_snapshots_skipped_total: registry.register(metric!(
267+
), role)),
268+
subscribe_snapshots_skipped_total: registry.register(with_role(metric!(
237269
name: "mz_subscribe_snapshots_skipped_total",
238270
help: "The number of collection snapshots that were skipped by the subscribe snapshot optimization.",
239-
)),
271+
), role)),
240272
}
241273
}
242274

@@ -514,3 +546,64 @@ impl Drop for CollectionMetrics {
514546
.dec_collection_count(self.collection_type, self.collection_hydrated);
515547
}
516548
}
549+
550+
#[cfg(test)]
551+
mod tests {
552+
use std::collections::BTreeSet;
553+
554+
use mz_ore::metrics::MetricsRegistry;
555+
556+
use super::ComputeMetrics;
557+
use crate::server::ComputeRuntimeRole;
558+
559+
/// The `Solo` (single-runtime) role registers exactly as compute did before a second runtime
560+
/// existed: no metric carries a `role` label, so single-runtime dashboards and exact-match
561+
/// alerts are byte-unchanged.
562+
#[mz_ore::test]
563+
fn solo_runtime_omits_role_label() {
564+
let registry = MetricsRegistry::new();
565+
let metrics = ComputeMetrics::register_with(&registry, ComputeRuntimeRole::Solo);
566+
// Instantiate the per-worker children so the `*Vec` families emit rows to inspect.
567+
let _worker = metrics.for_worker(0);
568+
569+
for family in registry.gather() {
570+
for metric in family.get_metric() {
571+
for label in metric.get_label() {
572+
assert_ne!(
573+
label.name(),
574+
"role",
575+
"solo metric {} unexpectedly carries a role label",
576+
family.name(),
577+
);
578+
}
579+
}
580+
}
581+
}
582+
583+
/// The two named roles each carry their own `role` label, so two runtimes in one process
584+
/// register distinct series rather than colliding. Registering both on one registry also
585+
/// exercises the non-collision that lets them coexist.
586+
#[mz_ore::test]
587+
fn named_roles_carry_distinct_role_label() {
588+
let registry = MetricsRegistry::new();
589+
let maintenance = ComputeMetrics::register_with(&registry, ComputeRuntimeRole::Maintenance);
590+
let interactive = ComputeMetrics::register_with(&registry, ComputeRuntimeRole::Interactive);
591+
let _maintenance_worker = maintenance.for_worker(0);
592+
let _interactive_worker = interactive.for_worker(0);
593+
594+
let mut roles = BTreeSet::new();
595+
for family in registry.gather() {
596+
for metric in family.get_metric() {
597+
let role = metric
598+
.get_label()
599+
.iter()
600+
.find(|label| label.name() == "role")
601+
.unwrap_or_else(|| panic!("metric {} missing a role label", family.name()));
602+
roles.insert(role.value().to_string());
603+
}
604+
}
605+
606+
assert!(roles.contains("maintenance"), "roles seen: {roles:?}");
607+
assert!(roles.contains("interactive"), "roles seen: {roles:?}");
608+
}
609+
}

0 commit comments

Comments
 (0)