1use std::path::PathBuf;
11use std::sync::Arc;
12use std::sync::LazyLock;
13use std::time::Duration;
14
15use anyhow::Context;
16use axum::http::StatusCode;
17use axum::routing;
18use fail::FailScenario;
19use futures::future;
20use hyper_util::rt::TokioIo;
21use mz_build_info::{BuildInfo, build_info};
22use mz_cloud_resources::AwsExternalIdPrefix;
23use mz_cluster_client::client::TimelyConfig;
24use mz_compute::server::{ComputeInstanceContext, ComputeRuntimeRole};
25use mz_http_util::DynamicFilterTarget;
26use mz_orchestrator_tracing::{StaticTracingConfig, TracingCliArgs};
27use mz_ore::cli::{self, CliConfig};
28use mz_ore::error::ErrorExt;
29use mz_ore::metrics::{MetricsRegistry, register_runtime_metrics};
30use mz_ore::netio::{Listener, SocketAddr};
31use mz_ore::now::SYSTEM_TIME;
32use mz_persist_client::cache::PersistClientCache;
33use mz_persist_client::cfg::PersistConfig;
34use mz_persist_client::rpc::{GrpcPubSubClient, PersistPubSubClient, PersistPubSubClientConfig};
35use mz_secrets_cli::SecretsReaderCliArgs;
36use mz_service::emit_boot_diagnostics;
37use mz_service::transport;
38use mz_service::transport::ClusterServerMetrics;
39use mz_storage::storage_state::StorageInstanceContext;
40use mz_storage_types::connections::ConnectionContext;
41use mz_txn_wal::operator::TxnsContext;
42use tokio::runtime::Handle;
43use tower::Service;
44use tracing::{Instrument, debug, error, info, info_span};
45
46mod usage_metrics;
47
48const BUILD_INFO: BuildInfo = build_info!();
49
50pub static VERSION: LazyLock<String> = LazyLock::new(|| BUILD_INFO.human_version(None));
51
52#[derive(clap::Parser)]
54#[clap(name = "clusterd", version = VERSION.as_str())]
55struct Args {
56 #[clap(
60 long,
61 env = "STORAGE_CONTROLLER_LISTEN_ADDR",
62 value_name = "HOST:PORT",
63 default_value = "127.0.0.1:2100"
64 )]
65 storage_controller_listen_addr: SocketAddr,
66 #[clap(
69 long,
70 env = "COMPUTE_CONTROLLER_LISTEN_ADDR",
71 value_name = "HOST:PORT",
72 default_value = "127.0.0.1:2101"
73 )]
74 compute_controller_listen_addr: SocketAddr,
75 #[clap(
77 long,
78 env = "INTERNAL_HTTP_LISTEN_ADDR",
79 value_name = "HOST:PORT",
80 default_value = "127.0.0.1:6878"
81 )]
82 internal_http_listen_addr: SocketAddr,
83 #[clap(long, env = "GRPC_HOST", value_name = "NAME")]
88 grpc_host: Option<String>,
89
90 #[clap(long, env = "STORAGE_TIMELY_CONFIG")]
93 storage_timely_config: TimelyConfig,
94 #[clap(long, env = "COMPUTE_TIMELY_CONFIG")]
96 compute_timely_config: TimelyConfig,
97 #[clap(long, env = "PROCESS")]
99 process: usize,
100
101 #[clap(
104 long,
105 env = "PERSIST_PUBSUB_URL",
106 value_name = "http://HOST:PORT",
107 default_value = "http://localhost:6879"
108 )]
109 persist_pubsub_url: String,
110
111 #[clap(long, env = "AWS_EXTERNAL_ID", value_name = "ID", value_parser = AwsExternalIdPrefix::new_from_cli_argument_or_environment_variable)]
116 aws_external_id_prefix: Option<AwsExternalIdPrefix>,
117
118 #[clap(long, env = "AWS_CONNECTION_ROLE_ARN")]
121 aws_connection_role_arn: Option<String>,
122
123 #[clap(flatten)]
125 secrets: SecretsReaderCliArgs,
126
127 #[clap(flatten)]
129 tracing: TracingCliArgs,
130
131 #[clap(long, env = "ENVIRONMENT_ID")]
135 environment_id: String,
136
137 #[clap(long, env = "SCRATCH_DIRECTORY", value_name = "PATH")]
139 scratch_directory: Option<PathBuf>,
140
141 #[clap(long)]
143 no_swap: bool,
144
145 #[clap(long)]
150 announce_memory_limit: Option<usize>,
151
152 #[clap(long)]
159 heap_limit: Option<usize>,
160
161 #[clap(long)]
164 is_cc: bool,
165
166 #[clap(long)]
172 worker_core_affinity: bool,
173
174 #[clap(long, env = "UNIFIED_CLUSTER")]
178 unified_cluster: bool,
179}
180
181fn process_ordinal_from_hostname(hostname: &str) -> Option<&str> {
190 let ordinal = hostname.rsplit('-').next()?;
191 ordinal.parse::<usize>().ok().map(|_| ordinal)
192}
193
194pub fn main() {
195 mz_ore::panic::install_enhanced_handler();
196
197 let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
204
205 if std::env::var("KUBERNETES_SERVICE_HOST").is_ok()
210 && std::env::var("CLUSTERD_PROCESS").is_err()
211 {
212 if let Ok(hostname) = std::env::var("HOSTNAME") {
213 if let Some(ordinal) = process_ordinal_from_hostname(&hostname) {
214 unsafe { std::env::set_var("CLUSTERD_PROCESS", ordinal) };
219 }
220 }
221 }
222
223 let args = cli::parse_args(CliConfig {
224 env_prefix: Some("CLUSTERD_"),
225 enable_version_flag: true,
226 });
227
228 let ncpus_useful = usize::max(1, std::cmp::min(num_cpus::get(), num_cpus::get_physical()));
229 let runtime = tokio::runtime::Builder::new_multi_thread()
230 .worker_threads(ncpus_useful)
231 .thread_stack_size(3 * 1024 * 1024) .thread_name_fn(|| {
237 use std::sync::atomic::{AtomicUsize, Ordering};
238 static ATOMIC_ID: AtomicUsize = AtomicUsize::new(0);
239 let id = ATOMIC_ID.fetch_add(1, Ordering::Relaxed);
240 format!("tokio:work-{}", id)
241 })
242 .enable_all()
243 .build()
244 .unwrap();
245 if let Err(err) = runtime.block_on(run(args)) {
246 panic!("clusterd: fatal: {}", err.display_with_causes());
247 }
248}
249
250async fn run(args: Args) -> Result<(), anyhow::Error> {
251 let metrics_registry = MetricsRegistry::new();
252 let tracing_handle = args
253 .tracing
254 .configure_tracing(
255 StaticTracingConfig {
256 service_name: "clusterd",
257 build_info: BUILD_INFO,
258 },
259 metrics_registry.clone(),
260 )
261 .await?;
262
263 let tracing_handle = Arc::new(tracing_handle);
264 register_runtime_metrics("main", Handle::current().metrics(), &metrics_registry);
265
266 mz_timely_util::panic::halt_on_timely_communication_panic();
269
270 let _failpoint_scenario = FailScenario::setup();
271
272 emit_boot_diagnostics!(&BUILD_INFO);
273
274 mz_alloc::register_metrics_into(&metrics_registry).await;
275 mz_metrics::register_metrics_into(
276 &metrics_registry,
277 mz_dyncfgs::all_dyncfgs(),
278 args.scratch_directory.clone(),
279 )
280 .await;
281
282 if let Some(heap_limit) = args.heap_limit {
283 mz_compute::memory_limiter::start_limiter(heap_limit, &metrics_registry);
284 } else {
285 info!("no heap limit announced; disabling memory limiter");
286 }
287
288 let secrets_reader = mz_secrets_loader::load(args.secrets)
289 .await
290 .context("loading secrets reader")?;
291
292 let usage_collector = Arc::new(usage_metrics::Collector {
293 disk_root: args.scratch_directory.clone(),
294 });
295
296 mz_ore::task::spawn(|| "clusterd_internal_http_server", {
297 let metrics_registry = metrics_registry.clone();
298 tracing::info!(
299 "serving internal HTTP server on {}",
300 args.internal_http_listen_addr
301 );
302 let listener = Listener::bind(args.internal_http_listen_addr).await?;
303 let mut make_service = mz_prof_http::router(&BUILD_INFO)
304 .route(
305 "/api/livez",
306 routing::get(mz_http_util::handle_liveness_check),
307 )
308 .route(
309 "/metrics",
310 routing::get(move |headers: axum::http::HeaderMap| async move {
311 mz_http_util::handle_prometheus(&metrics_registry, headers).await
312 }),
313 )
314 .route("/api/tracing", routing::get(mz_http_util::handle_tracing))
315 .route(
316 "/api/opentelemetry/config",
317 routing::put({
318 move |_: axum::Json<DynamicFilterTarget>| async {
319 (
320 StatusCode::BAD_REQUEST,
321 "This endpoint has been replaced. \
322 Use the `opentelemetry_filter` system variable."
323 .to_string(),
324 )
325 }
326 }),
327 )
328 .route(
329 "/api/stderr/config",
330 routing::put({
331 move |_: axum::Json<DynamicFilterTarget>| async {
332 (
333 StatusCode::BAD_REQUEST,
334 "This endpoint has been replaced. \
335 Use the `log_filter` system variable."
336 .to_string(),
337 )
338 }
339 }),
340 )
341 .route(
342 "/api/usage-metrics",
343 routing::get(async move || axum::Json(usage_collector.collect())),
344 )
345 .into_make_service();
346
347 async move {
350 loop {
351 let (conn, remote_addr) = match listener.accept().await {
352 Ok(peer) => peer,
353 Err(error) => {
354 if is_connection_error(&error) {
359 debug!("accepted connection already errored: {error:#}");
360 } else {
361 error!("internal_http accept error: {error:#}");
362 tokio::time::sleep(Duration::from_secs(1)).await;
363 }
364 continue;
365 }
366 };
367
368 let tower_service = make_service.call(&conn).await.expect("infallible");
369 let hyper_service =
370 hyper::service::service_fn(move |req| tower_service.clone().call(req));
371
372 mz_ore::task::spawn(
373 || format!("clusterd_internal_http_server:{remote_addr}"),
374 async move {
375 if let Err(error) = hyper::server::conn::http1::Builder::new()
376 .serve_connection(TokioIo::new(conn), hyper_service)
377 .await
378 {
379 info!("error serving internal_http connection: {error:#}");
383 }
384 },
385 );
386 }
387 }
388 });
389
390 let pubsub_caller_id = std::env::var("HOSTNAME")
391 .ok()
392 .or_else(|| args.tracing.log_prefix.clone())
393 .unwrap_or_default();
394 let mut persist_cfg =
395 PersistConfig::new(&BUILD_INFO, SYSTEM_TIME.clone(), mz_dyncfgs::all_dyncfgs());
396 persist_cfg.is_cc_active = args.is_cc;
397 persist_cfg.announce_memory_limit = args.announce_memory_limit;
398 persist_cfg.disable_compaction();
400
401 let persist_clients = Arc::new(PersistClientCache::new(
402 persist_cfg,
403 &metrics_registry,
404 |persist_cfg, metrics| {
405 let cfg = PersistPubSubClientConfig {
406 url: args.persist_pubsub_url,
407 caller_id: pubsub_caller_id,
408 persist_cfg: persist_cfg.clone(),
409 };
410 GrpcPubSubClient::connect(cfg, metrics)
411 },
412 ));
413 let txns_ctx = TxnsContext::default();
414
415 let connection_context = ConnectionContext::from_cli_args(
416 args.environment_id,
417 &args.tracing.startup_log_filter,
418 args.aws_external_id_prefix,
419 args.aws_connection_role_arn,
420 secrets_reader,
421 None,
422 );
423
424 let grpc_host = args.grpc_host.and_then(|h| (!h.is_empty()).then_some(h));
425 let cluster_server_metrics = ClusterServerMetrics::register_with(&metrics_registry);
426
427 let mut storage_timely_config = args.storage_timely_config;
428 storage_timely_config.process = args.process;
429 let mut compute_timely_config = args.compute_timely_config;
430 compute_timely_config.process = args.process;
431
432 assert_eq!(
434 storage_timely_config.workers, compute_timely_config.workers,
435 "storage and compute must have equal workers-per-process",
436 );
437
438 if args.unified_cluster {
439 info!("running with a unified timely cluster");
440
441 let (compute_client_builder, storage_client_builder) = mz_compute::server::serve_unified(
442 compute_timely_config,
443 ComputeRuntimeRole::Solo,
444 &metrics_registry,
445 persist_clients,
446 txns_ctx,
447 tracing_handle,
448 ComputeInstanceContext {
449 scratch_directory: args.scratch_directory.clone(),
450 worker_core_affinity: args.worker_core_affinity,
451 swap_available: !args.no_swap,
452 connection_context: connection_context.clone(),
453 },
454 SYSTEM_TIME.clone(),
455 connection_context,
456 StorageInstanceContext::new(
457 args.scratch_directory,
458 args.announce_memory_limit,
459 !args.no_swap,
460 ),
461 )
462 .await?;
463
464 info!(
465 "listening for storage controller connections on {}",
466 args.storage_controller_listen_addr
467 );
468 mz_ore::task::spawn(
469 || "storage_server",
470 transport::serve(
471 args.storage_controller_listen_addr,
472 BUILD_INFO.semver_version(),
473 grpc_host.clone(),
474 Duration::MAX,
475 storage_client_builder,
476 cluster_server_metrics.for_server("storage"),
477 )
478 .instrument(info_span!("ctp", name = "storage")),
479 );
480
481 info!(
482 "listening for compute controller connections on {}",
483 args.compute_controller_listen_addr
484 );
485 mz_ore::task::spawn(
486 || "compute_server",
487 transport::serve(
488 args.compute_controller_listen_addr,
489 BUILD_INFO.semver_version(),
490 grpc_host,
491 Duration::MAX,
492 compute_client_builder,
493 cluster_server_metrics.for_server("compute"),
494 )
495 .instrument(info_span!("ctp", name = "compute")),
496 );
497
498 return future::pending().await;
500 }
501
502 let storage_client_builder = mz_storage::serve(
504 storage_timely_config,
505 &metrics_registry,
506 Arc::clone(&persist_clients),
507 txns_ctx.clone(),
508 Arc::clone(&tracing_handle),
509 SYSTEM_TIME.clone(),
510 connection_context.clone(),
511 StorageInstanceContext::new(
512 args.scratch_directory.clone(),
513 args.announce_memory_limit,
514 !args.no_swap,
515 ),
516 )
517 .await?;
518 info!(
519 "listening for storage controller connections on {}",
520 args.storage_controller_listen_addr
521 );
522 mz_ore::task::spawn(
523 || "storage_server",
524 transport::serve(
525 args.storage_controller_listen_addr,
526 BUILD_INFO.semver_version(),
527 grpc_host.clone(),
528 Duration::MAX,
529 storage_client_builder,
530 cluster_server_metrics.for_server("storage"),
531 )
532 .instrument(info_span!("ctp", name = "storage")),
533 );
534
535 let compute_client_builder = mz_compute::server::serve(
537 compute_timely_config,
538 ComputeRuntimeRole::Solo,
539 &metrics_registry,
540 persist_clients,
541 txns_ctx,
542 tracing_handle,
543 ComputeInstanceContext {
544 scratch_directory: args.scratch_directory,
545 worker_core_affinity: args.worker_core_affinity,
546 swap_available: !args.no_swap,
547 connection_context,
548 },
549 )
550 .await?;
551 info!(
552 "listening for compute controller connections on {}",
553 args.compute_controller_listen_addr
554 );
555 mz_ore::task::spawn(
556 || "compute_server",
557 transport::serve(
558 args.compute_controller_listen_addr,
559 BUILD_INFO.semver_version(),
560 grpc_host.clone(),
561 Duration::MAX,
562 compute_client_builder,
563 cluster_server_metrics.for_server("compute"),
564 )
565 .instrument(info_span!("ctp", name = "compute")),
566 );
567
568 future::pending().await
572}
573
574fn is_connection_error(e: &std::io::Error) -> bool {
578 matches!(
579 e.kind(),
580 std::io::ErrorKind::ConnectionRefused
581 | std::io::ErrorKind::ConnectionAborted
582 | std::io::ErrorKind::ConnectionReset
583 )
584}
585
586#[cfg(test)]
587mod tests {
588 use super::*;
589
590 #[mz_ore::test]
591 fn test_process_ordinal_from_hostname() {
592 assert_eq!(
594 process_ordinal_from_hostname("mz5ncn-cluster-s1-replica-s1-gen-1-0"),
595 Some("0")
596 );
597 assert_eq!(
598 process_ordinal_from_hostname("mz5ncn-cluster-s1-replica-s1-gen-1-11"),
599 Some("11")
600 );
601 assert_eq!(process_ordinal_from_hostname("7"), Some("7"));
603
604 assert_eq!(process_ordinal_from_hostname("clusterd"), None);
608 assert_eq!(process_ordinal_from_hostname("replica-abc"), None);
609 assert_eq!(process_ordinal_from_hostname("replica-"), None);
610 assert_eq!(process_ordinal_from_hostname(""), None);
611 }
612}