Skip to main content

mz_environmentd/
lib.rs

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#![recursion_limit = "256"]
11
12//! A SQL stream processor built on top of [timely dataflow] and
13//! [differential dataflow].
14//!
15//! [differential dataflow]: ../differential_dataflow/index.html
16//! [timely dataflow]: ../timely/index.html
17
18use ::http::HeaderValue;
19use std::collections::{BTreeMap, BTreeSet};
20use std::panic::AssertUnwindSafe;
21use std::path::PathBuf;
22use std::pin::Pin;
23use std::sync::{Arc, LazyLock, Mutex};
24use std::time::{Duration, Instant};
25use std::{env, io};
26
27use anyhow::{Context, anyhow};
28use derivative::Derivative;
29use futures::FutureExt;
30use ipnet::IpNet;
31use mz_adapter::config::{
32    SystemParameterSyncClientConfig, SystemParameterSyncConfig, system_parameter_sync,
33};
34use mz_adapter::webhook::WebhookConcurrencyLimiter;
35use mz_adapter::{
36    AdapterError, Client as AdapterClient, RemoteSystemParameters, load_remote_system_parameters,
37};
38use mz_adapter_types::bootstrap_builtin_cluster_config::BootstrapBuiltinClusterConfig;
39use mz_adapter_types::dyncfgs::{
40    ENABLE_0DT_DEPLOYMENT_PANIC_AFTER_TIMEOUT, WITH_0DT_DEPLOYMENT_DDL_CHECK_INTERVAL,
41    WITH_0DT_DEPLOYMENT_MAX_WAIT,
42};
43use mz_auth::password::Password;
44use mz_authenticator::GenericOidcAuthenticator;
45use mz_build_info::{BuildInfo, build_info};
46use mz_catalog::config::ClusterReplicaSizeMap;
47use mz_catalog::durable::BootstrapArgs;
48use mz_cloud_resources::CloudResourceController;
49use mz_controller::ControllerConfig;
50use mz_dyncfg::ConfigSet;
51use mz_frontegg_auth::Authenticator as FronteggAuthenticator;
52use mz_license_keys::ValidatedLicenseKey;
53use mz_ore::future::OreFutureExt;
54use mz_ore::metrics::MetricsRegistry;
55use mz_ore::now::NowFn;
56use mz_ore::tracing::TracingHandle;
57use mz_ore::url::SensitiveUrl;
58use mz_ore::{instrument, task};
59use mz_persist_client::cache::PersistClientCache;
60use mz_persist_client::usage::StorageUsageClient;
61use mz_pgwire::MetricsConfig;
62use mz_pgwire_common::ConnectionCounter;
63use mz_repr::strconv;
64use mz_secrets::SecretsController;
65use mz_server_core::listeners::v26_32_0::ListenersConfig;
66use mz_server_core::listeners::{HttpListenerConfig, ListenerConfig, SqlListenerConfig};
67use mz_server_core::{
68    ConnectionStream, ListenerHandle, ReloadTrigger, ReloadingSslContext, ServeConfig,
69    TlsCertConfig, TlsMode,
70};
71use mz_sql::catalog::EnvironmentId;
72use mz_sql::session::vars::{Value, VarInput};
73use tokio::sync::oneshot;
74use tower_http::cors::AllowOrigin;
75use tracing::{Instrument, info, info_span};
76
77use crate::deployment::preflight::{self, CatchupConfig};
78use crate::deployment::state::DeploymentState;
79use crate::http::{HttpConfig, HttpServer, InternalRouteConfig};
80
81pub use crate::http::{SqlResponse, WebSocketAuth, WebSocketResponse};
82
83mod deployment;
84pub mod environmentd;
85pub mod http;
86mod telemetry;
87#[cfg(feature = "test")]
88pub mod test_util;
89
90pub const BUILD_INFO: BuildInfo = build_info!();
91
92/// Configuration for an `environmentd` server.
93#[derive(Derivative)]
94#[derivative(Debug)]
95pub struct Config {
96    // === Special modes. ===
97    /// Whether to permit usage of unsafe features. This is never meant to run
98    /// in production.
99    pub unsafe_mode: bool,
100    /// Whether the environmentd is running on a local dev machine. This is
101    /// never meant to run in production or CI.
102    pub all_features: bool,
103
104    // === Connection options. ===
105    /// TLS encryption and authentication configuration.
106    pub tls: Option<TlsCertConfig>,
107    /// Trigger to attempt to reload TLS certififcates.
108    #[derivative(Debug = "ignore")]
109    pub tls_reload_certs: ReloadTrigger,
110    /// Password of the mz_system user.
111    pub external_login_password_mz_system: Option<Password>,
112    /// Frontegg JWT authenticator.
113    pub frontegg: Option<FronteggAuthenticator>,
114    /// Frontegg workspace URL advertised in MCP OAuth discovery.
115    pub frontegg_oauth_issuer_url: Option<String>,
116    /// Origins for which cross-origin resource sharing (CORS) for HTTP requests
117    /// is permitted.
118    pub cors_allowed_origin: AllowOrigin,
119    /// Raw list of allowed CORS origins. Retained alongside `cors_allowed_origin`
120    /// (which is the computed predicate) so that endpoints like MCP can perform
121    /// server-side Origin validation to defend against DNS rebinding attacks
122    /// (where same-origin requests bypass CORS enforcement).
123    pub cors_allowed_origin_list: Vec<HeaderValue>,
124    /// Public IP addresses which the cloud environment has configured for
125    /// egress.
126    pub egress_addresses: Vec<IpNet>,
127    /// The external host name to connect to the HTTP server of this
128    /// environment.
129    ///
130    /// Presently used to render webhook URLs for end users in notices and the
131    /// system catalog. Not used to establish connections directly.
132    pub http_host_name: Option<String>,
133    /// The URL of the Materialize console to proxy from the /internal-console
134    /// endpoint on the internal HTTP server.
135    pub internal_console_redirect_url: Option<String>,
136
137    // === Controller options. ===
138    /// Storage and compute controller configuration.
139    pub controller: ControllerConfig,
140    /// Secrets controller configuration.
141    pub secrets_controller: Arc<dyn SecretsController>,
142    /// VpcEndpoint controller configuration.
143    pub cloud_resource_controller: Option<Arc<dyn CloudResourceController>>,
144    /// The process-wide live system dyncfg set.
145    pub system_dyncfgs: Arc<ConfigSet>,
146
147    // === Storage options. ===
148    /// The interval at which to collect storage usage information.
149    pub storage_usage_collection_interval: Duration,
150    /// How long to retain storage usage records for.
151    pub storage_usage_retention_period: Option<Duration>,
152
153    // === Adapter options. ===
154    /// Catalog configuration.
155    pub catalog_config: CatalogConfig,
156    /// Availability zones in which storage and compute resources may be
157    /// deployed.
158    pub availability_zones: Vec<String>,
159    /// A map from size name to resource allocations for cluster replicas.
160    pub cluster_replica_sizes: ClusterReplicaSizeMap,
161    /// The PostgreSQL URL for the Postgres-backed timestamp oracle.
162    pub timestamp_oracle_url: Option<SensitiveUrl>,
163    /// An API key for Segment. Enables export of audit events to Segment.
164    pub segment_api_key: Option<String>,
165    /// Whether the Segment client is being used on the client side
166    /// (rather than the server side).
167    pub segment_client_side: bool,
168    /// Only create a dummy segment client, only to get more testing coverage.
169    pub test_only_dummy_segment_client: bool,
170    /// An SDK key for LaunchDarkly. Enables system parameter synchronization
171    /// with LaunchDarkly.
172    pub launchdarkly_sdk_key: Option<String>,
173    /// Overrides the LaunchDarkly service endpoints with a single base URL, as
174    /// for a relay proxy or a mock server in tests.
175    pub launchdarkly_base_uri: Option<String>,
176    /// An invertible map from system parameter names to LaunchDarkly feature
177    /// keys to use when propagating values from the latter to the former.
178    pub launchdarkly_key_map: BTreeMap<String, String>,
179    /// The duration at which the system parameter synchronization times out during startup.
180    pub config_sync_timeout: Duration,
181    /// The interval in seconds at which to synchronize system parameter values.
182    pub config_sync_loop_interval: Option<Duration>,
183    /// The path for file based config sync
184    pub config_sync_file_path: Option<PathBuf>,
185
186    // === Bootstrap options. ===
187    /// The cloud ID of this environment.
188    pub environment_id: EnvironmentId,
189    /// What role, if any, should be initially created with elevated privileges.
190    pub bootstrap_role: Option<String>,
191    /// The size of the default cluster replica if bootstrapping.
192    pub bootstrap_default_cluster_replica_size: String,
193    /// The default number of replicas if bootstrapping.
194    pub bootstrap_default_cluster_replication_factor: u32,
195    /// The config of the builtin system cluster replicas if bootstrapping.
196    pub bootstrap_builtin_system_cluster_config: BootstrapBuiltinClusterConfig,
197    /// The config of the builtin catalog server cluster replicas if bootstrapping.
198    pub bootstrap_builtin_catalog_server_cluster_config: BootstrapBuiltinClusterConfig,
199    /// The config of the builtin probe cluster replicas if bootstrapping.
200    pub bootstrap_builtin_probe_cluster_config: BootstrapBuiltinClusterConfig,
201    /// The config of the builtin support cluster replicas if bootstrapping.
202    pub bootstrap_builtin_support_cluster_config: BootstrapBuiltinClusterConfig,
203    /// The config of the builtin analytics cluster replicas if bootstrapping.
204    pub bootstrap_builtin_analytics_cluster_config: BootstrapBuiltinClusterConfig,
205    /// Values to set for system parameters, if those system parameters have not
206    /// already been set by the system user.
207    pub system_parameter_defaults: BTreeMap<String, String>,
208    /// Helm chart version
209    pub helm_chart_version: Option<String>,
210    /// Configuration managed by license keys
211    pub license_key: ValidatedLicenseKey,
212
213    // === AWS options. ===
214    /// The AWS account ID, which will be used to generate ARNs for
215    /// Materialize-controlled AWS resources.
216    pub aws_account_id: Option<String>,
217    /// Supported AWS PrivateLink availability zone ids.
218    pub aws_privatelink_availability_zones: Option<Vec<String>>,
219
220    // === Observability options. ===
221    /// The metrics registry to use.
222    pub metrics_registry: MetricsRegistry,
223    /// Handle to tracing.
224    pub tracing_handle: TracingHandle,
225
226    // === Testing options. ===
227    /// A now generation function for mocking time.
228    pub now: NowFn,
229    /// If `Some`, force running builtin schema migration using the specified
230    /// migration mechanism ("evolution" or "replacement").
231    pub force_builtin_schema_migration: Option<String>,
232}
233
234/// Configuration for the Catalog.
235#[derive(Debug, Clone)]
236pub struct CatalogConfig {
237    /// A process-global cache of (blob_uri, consensus_uri) -> PersistClient.
238    pub persist_clients: Arc<PersistClientCache>,
239    /// Persist catalog metrics.
240    pub metrics: Arc<mz_catalog::durable::Metrics>,
241}
242
243pub struct Listener<C> {
244    pub handle: ListenerHandle,
245    connection_stream: Pin<Box<dyn ConnectionStream>>,
246    config: C,
247}
248impl<C> Listener<C>
249where
250    C: ListenerConfig,
251{
252    /// Initializes network listeners for a later call to `serve` at the
253    /// specified addresses.
254    ///
255    /// Splitting this function out from `serve` has two benefits:
256    ///
257    ///   * It initializes network listeners as early as possible, so that the OS
258    ///     will queue incoming connections while the server is booting.
259    ///
260    ///   * It allows the caller to communicate with the server via the internal
261    ///     HTTP port while it is booting.
262    ///
263    async fn bind(config: C) -> Result<Self, io::Error> {
264        let (handle, connection_stream) = mz_server_core::listen(&config.addr()).await?;
265        Ok(Self {
266            handle,
267            connection_stream,
268            config,
269        })
270    }
271}
272
273impl Listener<SqlListenerConfig> {
274    #[instrument(name = "environmentd::serve_sql")]
275    pub async fn serve_sql(
276        self,
277        name: String,
278        active_connection_counter: ConnectionCounter,
279        tls_reloading_context: Option<ReloadingSslContext>,
280        frontegg: Option<FronteggAuthenticator>,
281        adapter_client: AdapterClient,
282        oidc: GenericOidcAuthenticator,
283        metrics: MetricsConfig,
284        helm_chart_version: Option<String>,
285    ) -> ListenerHandle {
286        let label = leak_listener_name(&name);
287        let tls = tls_reloading_context.map(|context| mz_server_core::ReloadingTlsConfig {
288            context,
289            mode: if self.config.enable_tls {
290                TlsMode::Require
291            } else {
292                TlsMode::Allow
293            },
294        });
295
296        task::spawn(|| format!("{}_sql_server", label), {
297            let sql_server = mz_pgwire::Server::new(mz_pgwire::Config {
298                label,
299                tls,
300                adapter_client,
301                authenticator_kind: self.config.authenticator_kind,
302                frontegg,
303                oidc,
304                metrics,
305                active_connection_counter,
306                helm_chart_version,
307                allowed_roles: self.config.allowed_roles,
308            });
309            mz_server_core::serve(ServeConfig {
310                conns: self.connection_stream,
311                server: sql_server,
312                // `environmentd` does not currently need to dynamically
313                // configure graceful termination behavior.
314                dyncfg: None,
315            })
316        });
317        self.handle
318    }
319}
320
321impl Listener<HttpListenerConfig> {
322    #[instrument(name = "environmentd::serve_http")]
323    pub async fn serve_http(self, config: HttpConfig) -> ListenerHandle {
324        let task_name = format!("{}_http_server", config.source);
325        task::spawn(|| task_name, {
326            let http_server = HttpServer::new(config);
327            mz_server_core::serve(ServeConfig {
328                conns: self.connection_stream,
329                server: http_server,
330                // `environmentd` does not currently need to dynamically
331                // configure graceful termination behavior.
332                dyncfg: None,
333            })
334        });
335        self.handle
336    }
337}
338
339/// The `&'static str` listener names handed to the metrics layers, kept so that
340/// a process which serves repeatedly reuses one allocation per name.
341static LISTENER_NAMES: LazyLock<Mutex<BTreeSet<&'static str>>> =
342    LazyLock::new(|| Mutex::new(BTreeSet::new()));
343
344/// Returns a `&'static str` for `name`, leaking it on first use.
345///
346/// The metrics layer a listener is wired into wants a `'static` label. Leaking
347/// is how it gets one, but an unconditional leak both grows with every restart
348/// in a process that serves more than once, such as `sqllogictest`, and reads
349/// to LeakSanitizer as a genuine leak that fails the process at exit. Holding
350/// the leaked strs in a static solves both: one allocation per distinct name,
351/// still reachable at exit.
352fn leak_listener_name(name: &str) -> &'static str {
353    let mut names = LISTENER_NAMES.lock().expect("lock poisoned");
354    if let Some(name) = names.get(name) {
355        return name;
356    }
357    let name: &'static str = Box::leak(name.to_owned().into_boxed_str());
358    names.insert(name);
359    name
360}
361
362pub struct Listeners {
363    pub http: BTreeMap<String, Listener<HttpListenerConfig>>,
364    pub sql: BTreeMap<String, Listener<SqlListenerConfig>>,
365}
366
367impl Listeners {
368    pub async fn bind(config: ListenersConfig) -> Result<Self, io::Error> {
369        let mut sql = BTreeMap::new();
370        for (name, config) in config.sql {
371            sql.insert(name, Listener::bind(config).await?);
372        }
373
374        let mut http = BTreeMap::new();
375        for (name, config) in config.http {
376            http.insert(name, Listener::bind(config).await?);
377        }
378
379        Ok(Listeners { http, sql })
380    }
381
382    /// Starts an `environmentd` server.
383    ///
384    /// Returns a handle to the server once it is fully booted.
385    #[instrument(name = "environmentd::serve")]
386    pub async fn serve(self, config: Config) -> Result<Server, AdapterError> {
387        let serve_start = Instant::now();
388        info!("startup: envd serve: beginning");
389        info!("startup: envd serve: preamble beginning");
390
391        // Validate TLS configuration, if present.
392        let tls_reloading_context = match config.tls {
393            Some(tls_config) => Some(tls_config.reloading_context(config.tls_reload_certs)?),
394            None => None,
395        };
396
397        let active_connection_counter = ConnectionCounter::default();
398        let (deployment_state, deployment_state_handle) = DeploymentState::new();
399
400        // Launch HTTP servers.
401        //
402        // We start these servers before we've completed initialization so that
403        // metrics are accessible during initialization. Some HTTP
404        // endpoints require the adapter to be initialized; requests to those
405        // endpoints block until the adapter client is installed.
406        // One of these endpoints is /api/readyz,
407        // which assumes we're ready when the adapter client exists.
408        let webhook_concurrency_limit = WebhookConcurrencyLimiter::default();
409        let internal_route_config = Arc::new(InternalRouteConfig {
410            deployment_state_handle,
411            internal_console_redirect_url: config.internal_console_redirect_url,
412        });
413
414        let (authenticator_oidc_tx, authenticator_oidc_rx) = oneshot::channel();
415        let authenticator_oidc_rx = authenticator_oidc_rx.shared();
416        let (adapter_client_tx, adapter_client_rx) = oneshot::channel();
417        let adapter_client_rx = adapter_client_rx.shared();
418
419        let metrics_registry = config.metrics_registry.clone();
420        let metrics = http::Metrics::register_into(&metrics_registry, "mz_http");
421        let mcp_metrics = http::mcp_metrics::McpMetrics::register_into(&metrics_registry);
422        let oauth_metadata_metrics =
423            http::oauth_metadata::OauthMetadataMetrics::register_into(&metrics_registry);
424        let mut http_listener_handles = BTreeMap::new();
425        for (name, listener) in self.http {
426            let authenticator_kind = listener.config.authenticator_kind();
427            let source = leak_listener_name(&name);
428            let tls = if listener.config.enable_tls() {
429                tls_reloading_context.clone()
430            } else {
431                None
432            };
433            let http_config = HttpConfig {
434                adapter_client_rx: adapter_client_rx.clone(),
435                active_connection_counter: active_connection_counter.clone(),
436                helm_chart_version: config.helm_chart_version.clone(),
437                http_host_name: config.http_host_name.clone(),
438                frontegg_oauth_issuer_url: config.frontegg_oauth_issuer_url.clone(),
439                source,
440                tls,
441                authenticator_kind,
442                frontegg: config.frontegg.clone(),
443                oidc_rx: authenticator_oidc_rx.clone(),
444                allowed_origin: config.cors_allowed_origin.clone(),
445                allowed_origin_list: config.cors_allowed_origin_list.clone(),
446                concurrent_webhook_req: webhook_concurrency_limit.semaphore(),
447                dyncfgs: Arc::clone(&config.system_dyncfgs),
448                metrics: metrics.clone(),
449                metrics_registry: metrics_registry.clone(),
450                mcp_metrics: mcp_metrics.clone(),
451                oauth_metadata_metrics: oauth_metadata_metrics.clone(),
452                internal_route_config: Arc::clone(&internal_route_config),
453                routes_enabled: listener.config.routes,
454                replica_http_locator: Arc::clone(&config.controller.replica_http_locator),
455            };
456            http_listener_handles.insert(name.clone(), listener.serve_http(http_config).await);
457        }
458
459        info!(
460            "startup: envd serve: preamble complete in {:?}",
461            serve_start.elapsed()
462        );
463
464        let catalog_init_start = Instant::now();
465        info!("startup: envd serve: catalog init beginning");
466
467        // Get the current timestamp so we can record when we booted.
468        let boot_ts = (config.now)().into();
469
470        let persist_client = config
471            .catalog_config
472            .persist_clients
473            .open(config.controller.persist_location.clone())
474            .await
475            .context("opening persist client")?;
476        let mut openable_adapter_storage = mz_catalog::durable::persist_backed_catalog_state(
477            persist_client.clone(),
478            config.environment_id.organization_id(),
479            BUILD_INFO.semver_version(),
480            Some(config.controller.deploy_generation),
481            Arc::clone(&config.catalog_config.metrics),
482        )
483        .await?;
484
485        info!(
486            "startup: envd serve: catalog init complete in {:?}",
487            catalog_init_start.elapsed()
488        );
489
490        let system_param_sync_start = Instant::now();
491        info!("startup: envd serve: system parameter sync beginning");
492        // Initialize the system parameter frontend
493        let system_parameter_sync_config =
494            match (config.launchdarkly_sdk_key, config.config_sync_file_path) {
495                (None, None) => None,
496                (None, Some(f)) => {
497                    info!("Using config file path {:?}", f);
498                    Some(SystemParameterSyncConfig::new(
499                        config.environment_id.clone(),
500                        &BUILD_INFO,
501                        &config.metrics_registry,
502                        config.launchdarkly_key_map,
503                        SystemParameterSyncClientConfig::File { path: f },
504                    ))
505                }
506                (Some(key), None) => Some(SystemParameterSyncConfig::new(
507                    config.environment_id.clone(),
508                    &BUILD_INFO,
509                    &config.metrics_registry,
510                    config.launchdarkly_key_map,
511                    SystemParameterSyncClientConfig::LaunchDarkly {
512                        sdk_key: key,
513                        base_uri: config.launchdarkly_base_uri,
514                        now_fn: config.now.clone(),
515                    },
516                )),
517
518                (Some(_), Some(_)) => {
519                    panic!("Cannot configure both file and Launchdarkly based config syncing")
520                }
521            };
522
523        let RemoteSystemParameters {
524            params: remote_system_parameters,
525            frontend: boot_frontend,
526        } = load_remote_system_parameters(
527            &mut openable_adapter_storage,
528            system_parameter_sync_config.clone(),
529            config.config_sync_timeout,
530        )
531        .await?;
532        info!(
533            "startup: envd serve: system parameter sync complete in {:?}",
534            system_param_sync_start.elapsed()
535        );
536
537        let preflight_checks_start = Instant::now();
538        info!("startup: envd serve: preflight checks beginning");
539
540        // Determine the maximum wait time when doing a 0dt deployment.
541        let with_0dt_deployment_max_wait = {
542            let cli_default = config
543                .system_parameter_defaults
544                .get(WITH_0DT_DEPLOYMENT_MAX_WAIT.name())
545                .map(|x| {
546                    Duration::parse(VarInput::Flat(x)).map_err(|err| {
547                        anyhow!(
548                            "failed to parse default for {}: {:?}",
549                            WITH_0DT_DEPLOYMENT_MAX_WAIT.name(),
550                            err
551                        )
552                    })
553                })
554                .transpose()?;
555            let compiled_default = WITH_0DT_DEPLOYMENT_MAX_WAIT.default().clone();
556            let ld = get_ld_value(
557                WITH_0DT_DEPLOYMENT_MAX_WAIT.name(),
558                &remote_system_parameters,
559                |x| {
560                    Duration::parse(VarInput::Flat(x)).map_err(|err| {
561                        format!(
562                            "failed to parse LD value {} for {}: {:?}",
563                            x,
564                            WITH_0DT_DEPLOYMENT_MAX_WAIT.name(),
565                            err
566                        )
567                    })
568                },
569            )?;
570            let catalog = openable_adapter_storage
571                .get_0dt_deployment_max_wait()
572                .await?;
573            let computed = ld.or(catalog).or(cli_default).unwrap_or(compiled_default);
574            info!(
575                ?computed,
576                ?ld,
577                ?catalog,
578                ?cli_default,
579                ?compiled_default,
580                "determined value for {} system parameter",
581                WITH_0DT_DEPLOYMENT_MAX_WAIT.name()
582            );
583            computed
584        };
585        // Determine the DDL check interval when doing a 0dt deployment.
586        let with_0dt_deployment_ddl_check_interval = {
587            let cli_default = config
588                .system_parameter_defaults
589                .get(WITH_0DT_DEPLOYMENT_DDL_CHECK_INTERVAL.name())
590                .map(|x| {
591                    Duration::parse(VarInput::Flat(x)).map_err(|err| {
592                        anyhow!(
593                            "failed to parse default for {}: {:?}",
594                            WITH_0DT_DEPLOYMENT_DDL_CHECK_INTERVAL.name(),
595                            err
596                        )
597                    })
598                })
599                .transpose()?;
600            let compiled_default = WITH_0DT_DEPLOYMENT_DDL_CHECK_INTERVAL.default().clone();
601            let ld = get_ld_value(
602                WITH_0DT_DEPLOYMENT_DDL_CHECK_INTERVAL.name(),
603                &remote_system_parameters,
604                |x| {
605                    Duration::parse(VarInput::Flat(x)).map_err(|err| {
606                        format!(
607                            "failed to parse LD value {} for {}: {:?}",
608                            x,
609                            WITH_0DT_DEPLOYMENT_DDL_CHECK_INTERVAL.name(),
610                            err
611                        )
612                    })
613                },
614            )?;
615            let catalog = openable_adapter_storage
616                .get_0dt_deployment_ddl_check_interval()
617                .await?;
618            let computed = ld.or(catalog).or(cli_default).unwrap_or(compiled_default);
619            info!(
620                ?computed,
621                ?ld,
622                ?catalog,
623                ?cli_default,
624                ?compiled_default,
625                "determined value for {} system parameter",
626                WITH_0DT_DEPLOYMENT_DDL_CHECK_INTERVAL.name()
627            );
628            computed
629        };
630
631        // Determine whether we should panic if we reach the maximum wait time
632        // without the preflight checks succeeding.
633        let enable_0dt_deployment_panic_after_timeout = {
634            let cli_default = config
635                .system_parameter_defaults
636                .get(ENABLE_0DT_DEPLOYMENT_PANIC_AFTER_TIMEOUT.name())
637                .map(|x| {
638                    strconv::parse_bool(x).map_err(|err| {
639                        anyhow!(
640                            "failed to parse default for {}: {}",
641                            ENABLE_0DT_DEPLOYMENT_PANIC_AFTER_TIMEOUT.name(),
642                            err
643                        )
644                    })
645                })
646                .transpose()?;
647            let compiled_default = ENABLE_0DT_DEPLOYMENT_PANIC_AFTER_TIMEOUT.default().clone();
648            let ld = get_ld_value(
649                "enable_0dt_deployment_panic_after_timeout",
650                &remote_system_parameters,
651                |x| strconv::parse_bool(x).map_err(|x| x.to_string()),
652            )?;
653            let catalog = openable_adapter_storage
654                .get_enable_0dt_deployment_panic_after_timeout()
655                .await?;
656            let computed = ld.or(catalog).or(cli_default).unwrap_or(compiled_default);
657            info!(
658                %computed,
659                ?ld,
660                ?catalog,
661                ?cli_default,
662                ?compiled_default,
663                "determined value for enable_0dt_deployment_panic_after_timeout system parameter",
664            );
665            computed
666        };
667
668        // Perform preflight checks.
669        //
670        // Preflight checks determine whether to boot in read-only mode or not.
671        let read_only = preflight::preflight_0dt(
672            openable_adapter_storage.as_mut(),
673            config.controller.deploy_generation,
674        )
675        .await?;
676
677        let bootstrap_args = BootstrapArgs {
678            default_cluster_replica_size: config.bootstrap_default_cluster_replica_size.clone(),
679            default_cluster_replication_factor: config.bootstrap_default_cluster_replication_factor,
680            bootstrap_role: config.bootstrap_role,
681            cluster_replica_size_map: config.cluster_replica_sizes.clone(),
682        };
683
684        let (caught_up_trigger, bootstrapped) = if read_only {
685            let (caught_up_trigger, caught_up_receiver) = mz_ore::channel::trigger::channel();
686            let (bootstrapped, bootstrapped_receiver) = tokio::sync::oneshot::channel();
687            let catchup_config = CatchupConfig {
688                boot_ts,
689                environment_id: config.environment_id.clone(),
690                persist_client,
691                deploy_generation: config.controller.deploy_generation,
692                deployment_state: deployment_state.clone(),
693                catalog_metrics: Arc::clone(&config.catalog_config.metrics),
694                caught_up_max_wait: with_0dt_deployment_max_wait,
695                panic_after_timeout: enable_0dt_deployment_panic_after_timeout,
696                bootstrap_args: bootstrap_args.clone(),
697                ddl_check_interval: with_0dt_deployment_ddl_check_interval,
698            };
699            preflight::spawn_catchup(catchup_config, caught_up_receiver, bootstrapped_receiver);
700            (Some(caught_up_trigger), Some(bootstrapped))
701        } else {
702            (None, None)
703        };
704
705        info!(
706            "startup: envd serve: preflight checks complete in {:?}",
707            preflight_checks_start.elapsed()
708        );
709
710        let catalog_open_start = Instant::now();
711        info!("startup: envd serve: durable catalog open beginning");
712
713        // Load the adapter durable storage.
714        let mut adapter_storage = if read_only {
715            // TODO: behavior of migrations when booting in savepoint mode is
716            // not well defined.
717            let adapter_storage = openable_adapter_storage
718                .open_savepoint(boot_ts, &bootstrap_args)
719                .await?;
720            // In read-only mode, we intentionally do not call `set_is_leader`,
721            // because we are by definition not the leader if we are in
722            // read-only mode.
723
724            adapter_storage
725        } else {
726            let adapter_storage = openable_adapter_storage
727                .open(boot_ts, &bootstrap_args)
728                .await?;
729
730            // Once we have successfully opened the adapter storage in
731            // read/write mode, we can announce we are the leader, as we've
732            // fenced out all other environments using the adapter storage.
733            deployment_state.set_is_leader();
734
735            adapter_storage
736        };
737
738        let bootstrapped = match bootstrapped {
739            Some(tx) => Some((tx, preflight::get_user_ids(adapter_storage.as_mut()).await?)),
740            None => None,
741        };
742
743        // Enable Persist compaction if we're not in read only.
744        if !read_only {
745            config.controller.persist_clients.cfg().enable_compaction();
746        }
747
748        info!(
749            "startup: envd serve: durable catalog open complete in {:?}",
750            catalog_open_start.elapsed()
751        );
752
753        let coord_init_start = Instant::now();
754        info!("startup: envd serve: coordinator init beginning");
755
756        if !config
757            .cluster_replica_sizes
758            .0
759            .contains_key(&config.bootstrap_default_cluster_replica_size)
760        {
761            return Err(anyhow!("bootstrap default cluster replica size is unknown").into());
762        }
763        let envd_epoch = adapter_storage.epoch();
764
765        // Initialize storage usage client.
766        let storage_usage_client = StorageUsageClient::open(
767            config
768                .controller
769                .persist_clients
770                .open(config.controller.persist_location.clone())
771                .await
772                .context("opening storage usage client")?,
773        );
774
775        // Initialize adapter.
776        let license_key = config.license_key.clone();
777        let segment_client = config.segment_api_key.map(|api_key| {
778            mz_segment::Client::new(mz_segment::Config {
779                api_key,
780                client_side: config.segment_client_side,
781            })
782        });
783        let connection_limiter = active_connection_counter.clone();
784        let connection_limit_callback = Box::new(move |limit, superuser_reserved| {
785            connection_limiter.update_limit(limit);
786            connection_limiter.update_superuser_reserved(superuser_reserved);
787        });
788
789        let (adapter_handle, adapter_client) = mz_adapter::serve(mz_adapter::Config {
790            connection_context: config.controller.connection_context.clone(),
791            connection_limit_callback,
792            controller_config: config.controller,
793            controller_envd_epoch: envd_epoch,
794            storage: adapter_storage,
795            timestamp_oracle_url: config.timestamp_oracle_url,
796            unsafe_mode: config.unsafe_mode,
797            all_features: config.all_features,
798            build_info: &BUILD_INFO,
799            environment_id: config.environment_id.clone(),
800            metrics_registry: config.metrics_registry.clone(),
801            now: config.now,
802            secrets_controller: config.secrets_controller,
803            cloud_resource_controller: config.cloud_resource_controller,
804            cluster_replica_sizes: config.cluster_replica_sizes,
805            builtin_system_cluster_config: config.bootstrap_builtin_system_cluster_config,
806            builtin_catalog_server_cluster_config: config
807                .bootstrap_builtin_catalog_server_cluster_config,
808            builtin_probe_cluster_config: config.bootstrap_builtin_probe_cluster_config,
809            builtin_support_cluster_config: config.bootstrap_builtin_support_cluster_config,
810            builtin_analytics_cluster_config: config.bootstrap_builtin_analytics_cluster_config,
811            availability_zones: config.availability_zones,
812            system_parameter_defaults: config.system_parameter_defaults,
813            storage_usage_client,
814            storage_usage_collection_interval: config.storage_usage_collection_interval,
815            storage_usage_retention_period: config.storage_usage_retention_period,
816            segment_client: segment_client.clone(),
817            egress_addresses: config.egress_addresses,
818            remote_system_parameters,
819            aws_account_id: config.aws_account_id,
820            aws_privatelink_availability_zones: config.aws_privatelink_availability_zones,
821            webhook_concurrency_limit: webhook_concurrency_limit.clone(),
822            http_host_name: config.http_host_name,
823            tracing_handle: config.tracing_handle,
824            read_only_controllers: read_only,
825            caught_up_trigger,
826            helm_chart_version: config.helm_chart_version.clone(),
827            license_key: config.license_key,
828            external_login_password_mz_system: config.external_login_password_mz_system,
829            force_builtin_schema_migration: config.force_builtin_schema_migration,
830        })
831        .instrument(info_span!("adapter::serve"))
832        .await?;
833
834        if let Some((tx, initial_ids)) = bootstrapped {
835            // The receiver is gone if an administrator already skipped catch-up.
836            let _ = tx.send(initial_ids);
837        }
838
839        // Initialize the OIDC authenticator, shared between the HTTP and SQL servers.
840        let oidc = GenericOidcAuthenticator::new(adapter_client.clone());
841
842        info!(
843            "startup: envd serve: coordinator init complete in {:?}",
844            coord_init_start.elapsed()
845        );
846
847        let serve_postamble_start = Instant::now();
848        info!("startup: envd serve: postamble beginning");
849
850        // Send adapter client and OIDC authenticator to the HTTP servers.
851        authenticator_oidc_tx
852            .send(oidc.clone())
853            .expect("rx known to be live");
854        adapter_client_tx
855            .send(adapter_client.clone())
856            .expect("internal HTTP server should not drop first");
857
858        let metrics = mz_pgwire::MetricsConfig::register_into(&config.metrics_registry);
859
860        // Launch SQL server.
861        let mut sql_listener_handles = BTreeMap::new();
862        for (name, listener) in self.sql {
863            sql_listener_handles.insert(
864                name.clone(),
865                listener
866                    .serve_sql(
867                        name,
868                        active_connection_counter.clone(),
869                        tls_reloading_context.clone(),
870                        config.frontegg.clone(),
871                        adapter_client.clone(),
872                        oidc.clone(),
873                        metrics.clone(),
874                        config.helm_chart_version.clone(),
875                    )
876                    .await,
877            );
878        }
879
880        // Start telemetry reporting loop.
881        if let Some(segment_client) = segment_client {
882            telemetry::start_reporting(telemetry::Config {
883                segment_client,
884                adapter_client: adapter_client.clone(),
885                environment_id: config.environment_id,
886                license_key: license_key.clone(),
887                helm_chart_version: config.helm_chart_version.clone(),
888                report_interval: Duration::from_secs(3600),
889            });
890        } else if config.test_only_dummy_segment_client {
891            // We only have access to a segment client in production but we
892            // still want to exercise the telemetry reporting code to a degree.
893            // So we create a dummy client and report telemetry into the void.
894            // This way we at least run the telemetry queries the way a
895            // production environment would.
896            tracing::debug!("starting telemetry reporting with a dummy segment client");
897            let segment_client = mz_segment::Client::new_dummy_client();
898            telemetry::start_reporting(telemetry::Config {
899                segment_client,
900                adapter_client: adapter_client.clone(),
901                environment_id: config.environment_id,
902                license_key: license_key.clone(),
903                helm_chart_version: config.helm_chart_version.clone(),
904                report_interval: Duration::from_secs(180),
905            });
906        }
907
908        // If system_parameter_sync_config and config_sync_loop_interval are present,
909        // start the system_parameter_sync loop.
910        if let Some(system_parameter_sync_config) = system_parameter_sync_config {
911            task::spawn(
912                || "system_parameter_sync",
913                AssertUnwindSafe(system_parameter_sync(
914                    system_parameter_sync_config,
915                    boot_frontend,
916                    adapter_client.clone(),
917                    config.config_sync_loop_interval,
918                ))
919                .ore_catch_unwind(),
920            );
921        }
922
923        info!(
924            "startup: envd serve: postamble complete in {:?}",
925            serve_postamble_start.elapsed()
926        );
927        info!(
928            "startup: envd serve: complete in {:?}",
929            serve_start.elapsed()
930        );
931
932        Ok(Server {
933            sql_listener_handles,
934            http_listener_handles,
935            #[cfg(feature = "test")]
936            adapter_client,
937            _adapter_handle: adapter_handle,
938        })
939    }
940}
941
942fn get_ld_value<V>(
943    name: &str,
944    remote_system_parameters: &Option<BTreeMap<String, String>>,
945    parse: impl Fn(&str) -> Result<V, String>,
946) -> Result<Option<V>, anyhow::Error> {
947    remote_system_parameters
948        .as_ref()
949        .and_then(|params| params.get(name))
950        .map(|x| {
951            parse(x).map_err(|err| anyhow!("failed to parse remote value for {}: {}", name, err))
952        })
953        .transpose()
954}
955
956/// A running `environmentd` server.
957pub struct Server {
958    // Drop order matters for these fields.
959    pub sql_listener_handles: BTreeMap<String, ListenerHandle>,
960    pub http_listener_handles: BTreeMap<String, ListenerHandle>,
961    #[cfg(feature = "test")]
962    adapter_client: AdapterClient,
963    _adapter_handle: mz_adapter::Handle,
964}
965
966impl Server {
967    /// A client for the adapter, letting tests drive the adapter API directly.
968    #[cfg(feature = "test")]
969    pub fn adapter_client(&self) -> &AdapterClient {
970        &self.adapter_client
971    }
972}