Skip to main content

mz_adapter/config/
frontend.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
10use std::collections::BTreeMap;
11use std::fs;
12use std::path::PathBuf;
13use std::sync::Arc;
14use std::time::Duration;
15
16use derivative::Derivative;
17use hyper_tls::HttpsConnector;
18use launchdarkly_server_sdk as ld;
19use mz_build_info::BuildInfo;
20use mz_cloud_provider::CloudProvider;
21use mz_cluster_client::ReplicaId;
22use mz_controller_types::ClusterId;
23use mz_ore::now::NowFn;
24use mz_sql::catalog::EnvironmentId;
25use serde_json::Value as JsonValue;
26use tokio::time;
27use tracing::warn;
28
29use crate::config::{
30    Metrics, SynchronizedParameters, SystemParameterSyncClientConfig, SystemParameterSyncConfig,
31};
32
33/// A frontend client for pulling [SynchronizedParameters] from LaunchDarkly.
34#[derive(Derivative)]
35#[derivative(Debug)]
36pub struct SystemParameterFrontend {
37    /// An SDK client to mediate interactions with the LaunchDarkly and json config file clients.
38    client: SystemParameterFrontendClient,
39    /// A map from parameter names to LaunchDarkly feature keys
40    /// to use when populating the [SynchronizedParameters]
41    /// instance in [SystemParameterFrontend::pull].
42    key_map: BTreeMap<String, String>,
43    /// The environment ID, used to build scoped (`cluster` / `replica`)
44    /// evaluation contexts.
45    env_id: EnvironmentId,
46    /// Build info, used to build scoped evaluation contexts.
47    build_info: &'static BuildInfo,
48    /// Frontend metrics.
49    metrics: Metrics,
50}
51
52#[derive(Derivative)]
53#[derivative(Debug)]
54pub enum SystemParameterFrontendClient {
55    File {
56        path: PathBuf,
57    },
58    LaunchDarkly {
59        /// An SDK client to mediate interactions with the LaunchDarkly client.
60        #[derivative(Debug = "ignore")]
61        client: ld::Client,
62        /// The context to use when querying LaunchDarkly using the SDK.
63        /// This scopes down queries to a specific key.
64        ctx: ld::Context,
65    },
66}
67
68impl SystemParameterFrontendClient {}
69
70impl SystemParameterFrontend {
71    /// Create a new [SystemParameterFrontend] initialize.
72    ///
73    /// This will create and initialize an [ld::Client] instance. The
74    /// [ld::Client::initialized_async] call will be attempted in a loop with an
75    /// exponential backoff with power `2s` and max duration `60s`.
76    pub async fn from(sync_config: &SystemParameterSyncConfig) -> Result<Self, anyhow::Error> {
77        match &sync_config.backend_config {
78            super::SystemParameterSyncClientConfig::File { path } => Ok(Self {
79                client: SystemParameterFrontendClient::File { path: path.clone() },
80                key_map: sync_config.key_map.clone(),
81                env_id: sync_config.env_id.clone(),
82                build_info: sync_config.build_info,
83                metrics: sync_config.metrics.clone(),
84            }),
85            SystemParameterSyncClientConfig::LaunchDarkly { sdk_key, now_fn } => Ok(Self {
86                client: SystemParameterFrontendClient::LaunchDarkly {
87                    client: ld_client(sdk_key, &sync_config.metrics, now_fn).await?,
88                    // The environment-wide context carries no cluster/replica
89                    // scope. Scoped evaluation passes a `cluster` or `replica`
90                    // context per pass via [`ld_ctx`].
91                    ctx: ld_ctx(&sync_config.env_id, sync_config.build_info, None, None)?,
92                },
93                env_id: sync_config.env_id.clone(),
94                build_info: sync_config.build_info,
95                metrics: sync_config.metrics.clone(),
96                key_map: sync_config.key_map.clone(),
97            }),
98        }
99    }
100
101    /// Pull the current values for all [SynchronizedParameters] from the
102    /// [SystemParameterFrontend] and return `true` iff at least one parameter
103    /// value was modified.
104    pub fn pull(&self, params: &mut SynchronizedParameters) -> bool {
105        let mut changed = false;
106        for param_name in params.synchronized().into_iter() {
107            let flag_name = self
108                .key_map
109                .get(param_name)
110                .map(|flag_name| flag_name.as_str())
111                .unwrap_or(param_name);
112
113            let flag_str = match self.client {
114                SystemParameterFrontendClient::LaunchDarkly {
115                    ref client,
116                    ref ctx,
117                } => {
118                    let flag_var = client.variation(ctx, flag_name, params.get(param_name));
119                    match flag_var {
120                        ld::FlagValue::Bool(v) => v.to_string(),
121                        ld::FlagValue::Str(v) => v,
122                        ld::FlagValue::Number(v) => v.to_string(),
123                        ld::FlagValue::Json(v) => v.to_string(),
124                    }
125                }
126                SystemParameterFrontendClient::File { ref path } => {
127                    let file_contents = fs::read_to_string(path)
128                        .inspect_err(|e| warn!("Could not open system paraemter sync file {}", e))
129                        .unwrap_or_default();
130                    let values: BTreeMap<String, JsonValue> = serde_json::from_str(&file_contents)
131                        .inspect_err(|e| warn!("Could not open system paraemter sync file {:?}", e))
132                        .unwrap_or_default();
133                    values
134                        .get(flag_name)
135                        .and_then(|o| match o {
136                            serde_json::Value::String(v) => Some(v.to_string()),
137                            serde_json::Value::Number(v) => Some(v.to_string()),
138                            serde_json::Value::Bool(v) => Some(v.to_string()),
139                            serde_json::Value::Object(_) => Some(o.to_string()),
140                            serde_json::Value::Array(_) => Some(o.to_string()),
141                            serde_json::Value::Null => None,
142                        })
143                        .unwrap_or_else(|| params.get(param_name))
144                }
145            };
146
147            let old = params.get(param_name);
148            let change = params.modify(param_name, flag_str.as_str());
149            if change {
150                tracing::debug!(
151                    %param_name, %old, new = %flag_str,
152                    "updating system param",
153                );
154            }
155            self.metrics.params_changed.inc_by(u64::from(change));
156            changed |= change;
157        }
158
159        changed
160    }
161
162    /// Evaluates the replica-local scoped parameters for each given replica and
163    /// returns, per cluster and replica, the parameter values that differ from
164    /// the environment-wide value held in `params`.
165    ///
166    /// Only the LaunchDarkly client performs scoped evaluation. The file
167    /// client returns an empty map (replicas fall back to the environment-wide value).
168    /// The returned map is sparse: replicas (and clusters) with no overriding
169    /// value are omitted.
170    pub fn pull_replica_overrides(
171        &self,
172        params: &SynchronizedParameters,
173        param_names: &[&'static str],
174        replicas: &[ReplicaEvalContext],
175    ) -> BTreeMap<ReplicaId, BTreeMap<String, String>> {
176        let mut out: BTreeMap<ReplicaId, BTreeMap<String, String>> = BTreeMap::new();
177
178        let SystemParameterFrontendClient::LaunchDarkly { client, .. } = &self.client else {
179            // The file client has no notion of scoped evaluation.
180            return out;
181        };
182
183        if param_names.is_empty() {
184            return out;
185        }
186
187        for replica in replicas {
188            let ctx = match ld_ctx(
189                &self.env_id,
190                self.build_info,
191                Some(&replica.cluster),
192                Some(&replica.replica),
193            ) {
194                Ok(ctx) => ctx,
195                Err(e) => {
196                    warn!(
197                        replica_id = %replica.replica.id,
198                        "could not build scoped LD context: {e}"
199                    );
200                    continue;
201                }
202            };
203
204            let overrides = self.evaluate_scoped_overrides(client, &ctx, params, param_names);
205            if !overrides.is_empty() {
206                out.insert(replica.replica_id, overrides);
207            }
208        }
209
210        out
211    }
212
213    /// Evaluates the cluster-coherent scoped parameters for each given cluster
214    /// and returns, per cluster, the parameter values that differ from the
215    /// environment-wide value held in `params`. Evaluated replica-free (the
216    /// `cluster` context kind), so the value cannot vary by replica.
217    ///
218    /// Only the LaunchDarkly client performs scoped evaluation. The file
219    /// client returns an empty map. The returned map is sparse.
220    pub fn pull_cluster_overrides(
221        &self,
222        params: &SynchronizedParameters,
223        param_names: &[&'static str],
224        clusters: &[ClusterEvalContext],
225    ) -> BTreeMap<ClusterId, BTreeMap<String, String>> {
226        let mut out: BTreeMap<ClusterId, BTreeMap<String, String>> = BTreeMap::new();
227
228        let SystemParameterFrontendClient::LaunchDarkly { client, .. } = &self.client else {
229            // The file client has no notion of scoped evaluation.
230            return out;
231        };
232
233        if param_names.is_empty() {
234            return out;
235        }
236
237        for cluster in clusters {
238            let ctx = match ld_ctx(&self.env_id, self.build_info, Some(&cluster.cluster), None) {
239                Ok(ctx) => ctx,
240                Err(e) => {
241                    warn!(
242                        cluster_id = %cluster.cluster.id,
243                        "could not build scoped LD context: {e}"
244                    );
245                    continue;
246                }
247            };
248
249            let overrides = self.evaluate_scoped_overrides(client, &ctx, params, param_names);
250            if !overrides.is_empty() {
251                out.insert(cluster.cluster_id, overrides);
252            }
253        }
254
255        out
256    }
257
258    /// Evaluates each of `param_names` against `ctx`, returning only the values
259    /// that differ from the environment-wide value held in `params`. Shared by
260    /// the cluster and replica passes, so the returned map is sparse.
261    ///
262    /// We record on the differs-from-env test, not the `variation_detail`
263    /// reason. The inline comment at the recording decision explains why.
264    fn evaluate_scoped_overrides(
265        &self,
266        client: &ld::Client,
267        ctx: &ld::Context,
268        params: &SynchronizedParameters,
269        param_names: &[&'static str],
270    ) -> BTreeMap<String, String> {
271        let mut overrides = BTreeMap::new();
272        for &param_name in param_names {
273            let flag_name = self
274                .key_map
275                .get(param_name)
276                .map(|flag_name| flag_name.as_str())
277                .unwrap_or(param_name);
278
279            let base = params.get(param_name);
280            // Evaluate with `base` as the default, so a silent LD (flag absent,
281            // off, error, failed prerequisite) resolves back to the env-wide
282            // value and is dropped by the difference test below.
283            let flag_var = client.variation(ctx, flag_name, base.clone());
284            let value = match flag_var {
285                ld::FlagValue::Bool(v) => v.to_string(),
286                ld::FlagValue::Str(v) => v,
287                ld::FlagValue::Number(v) => v.to_string(),
288                ld::FlagValue::Json(v) => v.to_string(),
289            };
290
291            // Record iff the scoped evaluation *differs* from the env-wide value.
292            // The `variation_detail` reason is the wrong signal: it cannot say
293            // which context kind's clause matched (an env-level rule and a
294            // cluster-specific rule both report `RuleMatch`), and `Fallthrough`
295            // serves the env-wide value to every object. Comparing against the
296            // env-wide baseline is the only signal that means "this scope context
297            // changed the answer", which is what must beat a manual `FEATURES`
298            // pin and what keeps the durable collections sparse. See the scoped
299            // feature flags design, §Resolution.
300            //
301            // Compare in the parameter's canonical encoding. `base` is the
302            // var-formatted env-wide value (a `bool` is `"on"`/`"off"`), whereas
303            // the raw LaunchDarkly value spells a boolean `"true"`/`"false"`, so a
304            // direct string compare would treat every boolean flag as differing,
305            // even on `Fallthrough`. We still *store* the raw `value` (downstream
306            // consumers parse `"true"`/`"false"`). Only the decision is canonical.
307            let differs = match params.canonicalize(param_name, &value) {
308                Some(canonical) => canonical != base,
309                // LaunchDarkly served a value that does not parse for this
310                // parameter's type (e.g. a malformed boolean like `"maybe"`).
311                // Never record it: storing an unparseable value would poison
312                // resolution. The optimizer's `bool` decode, for one, panics on
313                // every plan for a cluster-coherent override it cannot parse.
314                // Treat it as "no scoped opinion" and fall back to the env-wide
315                // value.
316                None => false,
317            };
318            if differs {
319                overrides.insert(param_name.to_string(), value);
320            }
321        }
322        overrides
323    }
324}
325
326/// The identity of a single live replica, used to evaluate replica-local scoped
327/// parameters in [`SystemParameterFrontend::pull_replica_overrides`].
328#[derive(Clone, Debug)]
329pub struct ReplicaEvalContext {
330    /// The owning cluster's id.
331    pub cluster_id: ClusterId,
332    /// The replica's id.
333    pub replica_id: ReplicaId,
334    /// The owning cluster's scope context (for the replica-free, cluster pass).
335    pub cluster: ClusterScopeContext,
336    /// The replica's scope context.
337    pub replica: ReplicaScopeContext,
338}
339
340/// The identity of a single live cluster, used to evaluate cluster-coherent
341/// scoped parameters in [`SystemParameterFrontend::pull_cluster_overrides`].
342#[derive(Clone, Debug)]
343pub struct ClusterEvalContext {
344    /// The cluster's id.
345    pub cluster_id: ClusterId,
346    /// The cluster's scope context (replica-free).
347    pub cluster: ClusterScopeContext,
348}
349
350fn ld_config(api_key: &str, metrics: &Metrics) -> ld::Config {
351    ld::ConfigBuilder::new(api_key)
352        .event_processor(
353            ld::EventProcessorBuilder::new()
354                .https_connector(HttpsConnector::new())
355                .on_success({
356                    let last_cse_time_seconds = metrics.last_cse_time_seconds.clone();
357                    Arc::new(move |result| {
358                        if let Ok(ts) = u64::try_from(result.time_from_server / 1000) {
359                            last_cse_time_seconds.set(ts);
360                        } else {
361                            tracing::warn!(
362                                "Cannot convert time_from_server / 1000 from u128 to u64"
363                            );
364                        }
365                    })
366                }),
367        )
368        .data_source(ld::StreamingDataSourceBuilder::new().https_connector(HttpsConnector::new()))
369        .build()
370        .expect("valid config")
371}
372
373async fn ld_client(
374    api_key: &str,
375    metrics: &Metrics,
376    now_fn: &NowFn,
377) -> Result<ld::Client, anyhow::Error> {
378    let ld_client = ld::Client::build(ld_config(api_key, metrics))?;
379    tracing::info!("waiting for SystemParameterFrontend to initialize");
380    // Start and initialize LD client for the frontend. The callback passed
381    // will export the last time when an SSE event from the LD server was
382    // received in a Prometheus metric.
383    ld_client.start_with_default_executor_and_callback({
384        let last_sse_time_seconds = metrics.last_sse_time_seconds.clone();
385        let now_fn = now_fn.clone();
386        Arc::new(move |_ev| {
387            let ts = now_fn() / 1000;
388            last_sse_time_seconds.set(ts);
389        })
390    });
391
392    let max_backoff = Duration::from_secs(60);
393    let mut backoff = Duration::from_secs(5);
394    let timeout = Duration::from_secs(10);
395
396    // TODO(materialize#32030): fix retry logic
397    loop {
398        match ld_client.wait_for_initialization(timeout).await {
399            Some(true) => break,
400            Some(false) => tracing::warn!("SystemParameterFrontend failed to initialize"),
401            None => tracing::warn!("SystemParameterFrontend initialization timed out"),
402        }
403
404        time::sleep(backoff).await;
405        backoff = (backoff * 2).min(max_backoff);
406    }
407
408    tracing::info!("successfully initialized SystemParameterFrontend");
409
410    Ok(ld_client)
411}
412
413/// Identity of a cluster, used to build a `cluster` context kind for
414/// cluster-coherent scoped feature flags.
415///
416/// Exposes both `id` and `name`: an LD rule that targets `cluster_id` is an
417/// incarnation pin that dies on drop/recreate (ids are never reused), while a
418/// rule targeting `cluster_name` / `is_builtin` is a durable role predicate
419/// that re-applies to any matching cluster. See the scoped feature flags
420/// design.
421#[derive(Clone, Debug)]
422pub struct ClusterScopeContext {
423    /// The cluster's catalog id, e.g. `s2` or `u1`.
424    pub id: String,
425    /// The cluster's name, e.g. `mz_catalog_server`.
426    pub name: String,
427    /// Whether the cluster is a builtin (system) cluster.
428    pub is_builtin: bool,
429}
430
431/// Identity of a replica, used to build a `replica` context kind for
432/// replica-local scoped feature flags.
433///
434/// Carries the owning cluster's identity as attributes so that replica-local
435/// flags can be cluster-targeted without a second evaluation, and the replica
436/// size and size *family* so flags can be keyed by size family (e.g. legacy
437/// sizes keep `lgalloc`). See the scoped feature flags design.
438#[derive(Clone, Debug)]
439pub struct ReplicaScopeContext {
440    /// The replica's catalog id.
441    pub id: String,
442    /// The replica's name.
443    pub name: String,
444    /// Whether the replica belongs to a builtin (system) cluster.
445    pub is_builtin: bool,
446    /// The replica's full size name, e.g. `D.1-xsmall` or a legacy t-shirt size
447    /// like `xsmall`. This is the fine-grained targeting axis. The coarse axis
448    /// is [`Self::size_family`]. The two are distinct: `D.1-xsmall` is a size,
449    /// `D` is its family.
450    pub size: String,
451    /// The replica's size family, e.g. `D` or `legacy`. The coarse targeting
452    /// axis, derived from the size map rather than the size name (see
453    /// [`Self::size`]).
454    pub size_family: String,
455    /// The owning cluster's catalog id.
456    pub cluster_id: String,
457    /// The owning cluster's name.
458    pub cluster_name: String,
459}
460
461/// Builds a single `cluster` context kind from a [`ClusterScopeContext`].
462///
463/// Deliberately replica-free: cluster-coherent flags must resolve identically
464/// across a cluster's replicas, so no replica/size attributes appear here.
465fn cluster_context(cluster: &ClusterScopeContext) -> Result<ld::Context, anyhow::Error> {
466    ld::ContextBuilder::new(cluster.id.as_str())
467        .anonymous(true) // keep the LD dashboard Contexts list clean
468        .kind("cluster")
469        .set_string("cluster_id", cluster.id.clone())
470        .set_string("cluster_name", cluster.name.clone())
471        .set_string("is_builtin", cluster.is_builtin.to_string())
472        .build()
473        .map_err(|e| anyhow::anyhow!(e))
474}
475
476/// Builds a single `replica` context kind from a [`ReplicaScopeContext`].
477///
478/// Includes the owning cluster's identity so a rule can combine both axes,
479/// e.g. "size family `D` *and* cluster `foo`".
480fn replica_context(replica: &ReplicaScopeContext) -> Result<ld::Context, anyhow::Error> {
481    ld::ContextBuilder::new(replica.id.as_str())
482        .anonymous(true) // keep the LD dashboard Contexts list clean
483        .kind("replica")
484        .set_string("replica_id", replica.id.clone())
485        .set_string("replica_name", replica.name.clone())
486        .set_string("is_builtin", replica.is_builtin.to_string())
487        .set_string("replica_size", replica.size.clone())
488        .set_string("replica_size_family", replica.size_family.clone())
489        .set_string("cluster_id", replica.cluster_id.clone())
490        .set_string("cluster_name", replica.cluster_name.clone())
491        .build()
492        .map_err(|e| anyhow::anyhow!(e))
493}
494
495/// Builds a multi-context for evaluating scoped feature flags.
496///
497/// Composes the base contexts (`environment` + `organization` + `build`) with:
498/// - a `cluster` context for cluster-coherent (replica-free) resolution, and/or
499/// - a `replica` context for replica-local resolution.
500///
501/// The environment-wide pass passes `None` for both. This is the single entry
502/// point the sync loop uses to evaluate each scoped pass.
503fn ld_ctx(
504    env_id: &EnvironmentId,
505    build_info: &'static BuildInfo,
506    cluster: Option<&ClusterScopeContext>,
507    replica: Option<&ReplicaScopeContext>,
508) -> Result<ld::Context, anyhow::Error> {
509    // Register multiple contexts for this client.
510    //
511    // Unfortunately, it seems that the order in which conflicting targeting
512    // rules are applied depends on the definition order of feature flag
513    // variations rather than on the order in which context are registered with
514    // the multi-context builder.
515    let mut ctx_builder = ld::MultiContextBuilder::new();
516
517    if env_id.cloud_provider() != &CloudProvider::Local {
518        ctx_builder.add_context(
519            ld::ContextBuilder::new(env_id.to_string())
520                .kind("environment")
521                .set_string("cloud_provider", env_id.cloud_provider().to_string())
522                .set_string("cloud_provider_region", env_id.cloud_provider_region())
523                .set_string("organization_id", env_id.organization_id().to_string())
524                .set_string("ordinal", env_id.ordinal().to_string())
525                .build()
526                .map_err(|e| anyhow::anyhow!(e))?,
527        );
528        ctx_builder.add_context(
529            ld::ContextBuilder::new(env_id.organization_id().to_string())
530                .kind("organization")
531                .build()
532                .map_err(|e| anyhow::anyhow!(e))?,
533        );
534    } else {
535        // If cloud_provider is 'local', use anonymous `environment` and
536        // `organization` contexts with fixed keys, as otherwise we will create
537        // a lot of additional contexts (which are the billable entity for
538        // LaunchDarkly).
539        ctx_builder.add_context(
540            ld::ContextBuilder::new("anonymous-dev@materialize.com")
541                .anonymous(true) // exclude this user from the dashboard
542                .kind("environment")
543                .set_string("cloud_provider", env_id.cloud_provider().to_string())
544                .set_string("cloud_provider_region", env_id.cloud_provider_region())
545                .set_string("organization_id", uuid::Uuid::nil().to_string())
546                .set_string("ordinal", env_id.ordinal().to_string())
547                .build()
548                .map_err(|e| anyhow::anyhow!(e))?,
549        );
550        ctx_builder.add_context(
551            ld::ContextBuilder::new(uuid::Uuid::nil().to_string())
552                .anonymous(true) // exclude this user from the dashboard
553                .kind("organization")
554                .build()
555                .map_err(|e| anyhow::anyhow!(e))?,
556        );
557    };
558
559    ctx_builder.add_context(
560        ld::ContextBuilder::new(build_info.sha)
561            .kind("build")
562            .set_string("semver_version", build_info.semver_version().to_string())
563            .build()
564            .map_err(|e| anyhow::anyhow!(e))?,
565    );
566
567    // Cluster-coherent resolution evaluates with a `cluster` context (no
568    // replica attributes). Replica-local resolution additionally carries a
569    // `replica` context. The environment-wide pass carries neither.
570    if let Some(cluster) = cluster {
571        ctx_builder.add_context(cluster_context(cluster)?);
572    }
573    if let Some(replica) = replica {
574        ctx_builder.add_context(replica_context(replica)?);
575    }
576
577    ctx_builder.build().map_err(|e| anyhow::anyhow!(e))
578}
579
580#[cfg(test)]
581mod tests {
582    use mz_build_info::DUMMY_BUILD_INFO;
583
584    use super::*;
585
586    fn env_id() -> EnvironmentId {
587        EnvironmentId::for_tests()
588    }
589
590    #[mz_ore::test]
591    fn builds_cluster_scoped_context() {
592        // Cluster-coherent resolution evaluates with a replica-free `cluster`
593        // context.
594        let cluster = ClusterScopeContext {
595            id: "s2".into(),
596            name: "mz_catalog_server".into(),
597            is_builtin: true,
598        };
599        ld_ctx(&env_id(), &DUMMY_BUILD_INFO, Some(&cluster), None)
600            .expect("cluster-scoped context builds");
601    }
602
603    #[mz_ore::test]
604    fn builds_replica_scoped_context() {
605        // Replica-local resolution carries both a `cluster` and a `replica`
606        // context so a rule can combine size family and cluster.
607        let cluster = ClusterScopeContext {
608            id: "u1".into(),
609            name: "quickstart".into(),
610            is_builtin: false,
611        };
612        let replica = ReplicaScopeContext {
613            id: "u1-replica-1".into(),
614            name: "r1".into(),
615            is_builtin: false,
616            size: "D.1-xsmall".into(),
617            size_family: "D".into(),
618            cluster_id: "u1".into(),
619            cluster_name: "quickstart".into(),
620        };
621        ld_ctx(&env_id(), &DUMMY_BUILD_INFO, Some(&cluster), Some(&replica))
622            .expect("replica-scoped context builds");
623    }
624
625    #[mz_ore::test]
626    fn environment_wide_context_is_unscoped() {
627        ld_ctx(&env_id(), &DUMMY_BUILD_INFO, None, None).expect("environment-wide context builds");
628    }
629}