Skip to main content

mz_dyncfg/
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//! Dynamically updatable configuration.
11//!
12//! Basic usage:
13//! - A type-safe static `Config` is defined near where it is used.
14//! - Once in the lifetime of a process, all interesting `Config`s are
15//!   registered to a `ConfigSet`. The values within a `ConfigSet` are shared,
16//!   though multiple `ConfigSet`s may be created and each are completely
17//!   independent (i.e. one in each unit test).
18//! - A `ConfigSet` is plumbed around as necessary and may be used to get or
19//!   set the value of `Config`.
20//!
21//! ```
22//! # use mz_dyncfg::{Config, ConfigSet, ParameterScope};
23//! const FOO: Config<bool> =
24//!     Config::new("foo", false, "description of foo", ParameterScope::Environment);
25//! fn bar(cfg: &ConfigSet) {
26//!     assert_eq!(FOO.get(&cfg), false);
27//! }
28//! fn main() {
29//!     let cfg = ConfigSet::default().add(&FOO);
30//!     bar(&cfg);
31//! }
32//! ```
33//!
34//! # Design considerations for this library
35//!
36//! - The primary motivation is minimal boilerplate. Runtime dynamic
37//!   configuration is one of the most powerful tools we have to quickly react
38//!   to incidents, etc in production. Adding and using them should be easy
39//!   enough that engineers feel empowered to use them generously.
40//!
41//!   The theoretical minimum boilerplate is 1) declare a config and 2) use a
42//!   config to get/set the value. These could be combined into one step if (2)
43//!   were based on global state, but that doesn't play well with testing. So
44//!   instead we accomplish (2) by constructing a shared bag of config values in
45//!   each `fn main`, amortizing the cost by plumbing it once to each component
46//!   (not once per config).
47//! - Config definitions are kept next to the usage. The common case is that a
48//!   config is used in only one place and this makes it easy to see the
49//!   associated documentation at the usage site. Configs that are used in
50//!   multiple places may be defined in some common place as appropriate.
51//! - Everything is type-safe.
52//! - Secondarily: set up the ability to get and use the latest values of
53//!   configs in tooling like `persistcli` and `stash debug`. As we've embraced
54//!   dynamic configuration, we've ended up in a situation where it's stressful
55//!   to run the read-write `persistcli admin` tooling with the defaults
56//!   compiled into code, but `persistcli` doesn't have access to the vars stuff
57//!   and doesn't want to instantiate a catalog impl.
58
59use std::collections::BTreeMap;
60use std::marker::PhantomData;
61use std::sync::atomic::Ordering::SeqCst;
62use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, AtomicUsize};
63use std::sync::{Arc, RwLock};
64use std::time::Duration;
65
66use serde::{Deserialize, Serialize};
67use tracing::error;
68
69/// The scope at which a synced parameter's value may be overridden.
70///
71/// Every synced parameter declares its scope class as part of its definition.
72/// The declaration is the single source of truth for which contexts the
73/// LaunchDarkly sync loop evaluates and where the resolved value may be
74/// overridden. See `doc/developer/design/20260609_scoped_feature_flags.md`.
75///
76/// [`Environment`] is the safe declaration, and the right one when in doubt. It
77/// preserves the unscoped behavior of a single value everywhere. A finer scope
78/// *enables* divergence, so declaring one asserts that divergence is both safe
79/// and useful for this config. Getting that wrong introduces a way to break an
80/// environment that did not previously exist, while erring toward
81/// [`Environment`] only forgoes a capability. Prefer forgoing the capability.
82///
83/// Where a config is *realized* is therefore a necessary condition for a finer
84/// scope, not a sufficient one. A config qualifies for [`Replica`] only if it is
85/// realized per replica, and should be declared [`Replica`] only if it also
86/// tunes that replica process's own resource usage (memory, CPU, I/O,
87/// concurrency, timing) and cannot change what a dataflow produces, what reaches
88/// durable state, or anything externally visible. `lgalloc`, the memory limiter,
89/// the spill and pager knobs, and the timely zero-copy settings are the shape to
90/// match.
91///
92/// In particular, declare [`Environment`] for a flag selecting a different
93/// implementation or code path whose output-equivalence is an assumption rather
94/// than a guarantee. Where the assumption holds, [`Environment`] costs nothing.
95/// Where it does not, a per-replica rollout turns one bug into query results
96/// that differ by which replica served them.
97///
98/// Which contexts a config can be realized in:
99///
100/// - A config realized inside a `clusterd` process is a [`Replica`] candidate.
101///   That covers the compute and storage worker config sets and `mz_metrics`,
102///   which the per-replica dyncfg push reaches. `environmentd` may read such a
103///   config too, for its own process. That read legitimately sees the
104///   environment-wide value.
105/// - A config that `environmentd` resolves for *one specific replica*, whether
106///   it ships the value there or acts on it itself, is also a candidate. The
107///   read site has to resolve that replica's override, either with
108///   [`Config::get_with_overrides`] or from a per-replica config set. Without
109///   that, the override never takes effect and the declaration is a silent
110///   no-op.
111/// - A config realized in `environmentd` with no single replica in scope is
112///   [`Environment`]. So is every `balancerd` config. `balancerd` syncs
113///   LaunchDarkly itself, against a `balancer` context keyed by cloud provider,
114///   region and build version, and has no environment, cluster or replica
115///   beneath it to target.
116/// - A config consumed at plan time, once per cluster, is [`Cluster`].
117///
118/// NOTE: a config whose value must agree across the replicas of one cluster
119/// (because they render the same dataflow and their outputs are compared) is
120/// [`Environment`] even when it is read on `clusterd`. Per-replica divergence in
121/// *how* a dataflow is rendered is fine, in *what* it produces is not.
122///
123/// NOTE: persist configs are [`Environment`] as a class, even though the persist
124/// client runs on `clusterd` and the per-replica push reaches its config set.
125/// The same client code also runs in `environmentd`, and every copy of it acts
126/// on shared durable state. A replica-scoped persist config could never reach
127/// the `environmentd` client, so a rollout targeting replicas would leave a
128/// shard's other writer on the old value indefinitely. [`Environment`] is the
129/// only scope covering every persist client in an environment.
130///
131/// [`Cluster`]: ParameterScope::Cluster
132/// [`Environment`]: ParameterScope::Environment
133/// [`Replica`]: ParameterScope::Replica
134#[derive(Clone, Copy, Debug, PartialEq, Eq)]
135pub enum ParameterScope {
136    /// Environment-wide only; no cluster/replica overrides. The default, so all
137    /// existing synced parameters are unchanged.
138    ///
139    /// NOTE: this names the coarsest targeting granularity, not the
140    /// `environmentd` process. A config a process other than `environmentd`
141    /// resolves for itself, with nothing finer beneath it, is `Environment`.
142    Environment,
143    /// Cluster-coherent: env-wide base plus per-cluster overrides. Evaluated
144    /// with the `cluster` context (replica-free) and resolved at plan time via
145    /// `OptimizerFeatureOverrides`. e.g. optimizer features.
146    Cluster,
147    /// Replica-local: env-wide base plus per-replica / per-size-family
148    /// overrides. Evaluated with the `replica` context and resolved at the
149    /// controller's per-replica dyncfg push. e.g. `lgalloc`, persist pager, LZ4.
150    Replica,
151}
152
153impl Default for ParameterScope {
154    fn default() -> Self {
155        Self::DEFAULT
156    }
157}
158
159impl ParameterScope {
160    /// The scope applied to a parameter that does not declare one: environment-
161    /// wide, i.e. no cluster/replica overrides. A `const` so it can be used in
162    /// the `const` contexts (system-var constructors, the `feature_flags!`
163    /// macro) where [`Default::default`] is unavailable.
164    pub const DEFAULT: ParameterScope = ParameterScope::Environment;
165
166    /// Returns the lowercase string name of this scope, as surfaced in
167    /// documentation and introspection.
168    pub const fn as_str(&self) -> &'static str {
169        match self {
170            ParameterScope::Environment => "environment",
171            ParameterScope::Cluster => "cluster",
172            ParameterScope::Replica => "replica",
173        }
174    }
175}
176
177/// A handle to a dynamically updatable configuration value.
178///
179/// This represents a strongly-typed named config of type `T`. It may be
180/// registered to a set of such configs with [ConfigSet::add] and then later
181/// used to retrieve the latest value at any time with [Self::get].
182///
183/// The supported types are [bool], [usize], [Duration], and [String], as well as [Option]
184/// variants of these as necessary.
185#[derive(Clone, Debug)]
186pub struct Config<D: ConfigDefault> {
187    name: &'static str,
188    desc: &'static str,
189    default: D,
190    scope: ParameterScope,
191}
192
193impl<D: ConfigDefault> Config<D> {
194    /// Constructs a handle for a config of type `T`.
195    ///
196    /// It is best practice, but not strictly required, for the name to be
197    /// globally unique within a process.
198    ///
199    /// `scope` declares where the config's value may be overridden. It is a
200    /// required parameter rather than a builder step so that every new config
201    /// makes the choice deliberately. [`ParameterScope`] documents how to pick
202    /// one from the config's read sites.
203    ///
204    /// TODO(cfg): Add some sort of categorization of config purpose here: e.g.
205    /// limited-lifetime rollout flag, CYA, magic number that we never expect to
206    /// tune, magic number that we DO expect to tune, etc. This could be used to
207    /// power something like a `--future-default-flags` for CI, to replace part
208    /// or all of the manually maintained list.
209    ///
210    /// TODO(cfg): See if we can make this more Rust-y and take these params as
211    /// a struct (the obvious thing hits some issues with const combined with
212    /// Drop).
213    pub const fn new(
214        name: &'static str,
215        default: D,
216        desc: &'static str,
217        scope: ParameterScope,
218    ) -> Self {
219        Config {
220            name,
221            default,
222            desc,
223            scope,
224        }
225    }
226
227    /// The name of this config.
228    pub fn name(&self) -> &str {
229        self.name
230    }
231
232    /// The description of this config.
233    pub fn desc(&self) -> &str {
234        self.desc
235    }
236
237    /// The [`ParameterScope`] of this config.
238    pub fn scope(&self) -> ParameterScope {
239        self.scope
240    }
241
242    /// The default value of this config.
243    pub fn default(&self) -> &D {
244        &self.default
245    }
246
247    /// Returns the latest value of this config within the given set.
248    ///
249    /// Panics if this config was not previously registered to the set.
250    ///
251    /// TODO(cfg): Decide if this should be a method on `ConfigSet` instead to
252    /// match the precedent of `BTreeMap/HashMap::get` taking a key. It's like
253    /// this initially because it was thought that the `Config` definition was
254    /// the more important "noun" and also that rustfmt would maybe work better
255    /// on this ordering.
256    pub fn get(&self, set: &ConfigSet) -> D::ConfigType {
257        D::ConfigType::from_val(self.shared(set).load())
258    }
259
260    /// Returns the value of this config within the given set, with `overrides`
261    /// layered on top.
262    ///
263    /// This is how `environmentd` must read a [`ParameterScope::Replica`] config
264    /// whose value it ships to one specific replica: the set holds the
265    /// environment-wide value and `overrides` holds that replica's scoped
266    /// overrides, which win. Reading such a config with [`Self::get`] instead
267    /// makes its scope declaration a silent no-op.
268    ///
269    /// Panics if this config was not previously registered to the set. An
270    /// override whose type does not match the config's is logged and ignored,
271    /// rather than panicking a read site that is often on a critical path.
272    pub fn get_with_overrides(
273        &self,
274        set: &ConfigSet,
275        overrides: Option<&ConfigUpdates>,
276    ) -> D::ConfigType {
277        let val = self.shared(set).load();
278        let val = match overrides.and_then(|o| o.updates.get(self.name)) {
279            None => val,
280            Some(o) if std::mem::discriminant(o) == std::mem::discriminant(&val) => o.clone(),
281            Some(o) => {
282                error!(
283                    "override {:?} for config {} does not match its type {:?}",
284                    o, self.name, val
285                );
286                val
287            }
288        };
289        D::ConfigType::from_val(val)
290    }
291
292    /// Returns a handle to the value of this config in the given set.
293    ///
294    /// This allows users to amortize the cost of the name lookup.
295    pub fn handle(&self, set: &ConfigSet) -> ConfigValHandle<D::ConfigType> {
296        ConfigValHandle {
297            val: self.shared(set).clone(),
298            _type: PhantomData,
299        }
300    }
301
302    /// Returns the shared value of this config in the given set.
303    fn shared<'a>(&self, set: &'a ConfigSet) -> &'a ConfigValAtomic {
304        &set.configs
305            .get(self.name)
306            .unwrap_or_else(|| panic!("config {} should be registered to set", self.name))
307            .val
308    }
309
310    /// Parse a string value for this config.
311    pub fn parse_val(&self, val: &str) -> Result<ConfigVal, String> {
312        let val = D::ConfigType::parse(val)?;
313        let val = Into::<ConfigVal>::into(val);
314        Ok(val)
315    }
316}
317
318/// A type usable as a [Config].
319pub trait ConfigType: Into<ConfigVal> + Clone + Sized {
320    /// Converts a type-erased enum value to this type.
321    ///
322    /// Panics if the enum's variant does not match this type.
323    fn from_val(val: ConfigVal) -> Self;
324
325    /// Parses this string slice into a [`ConfigType`].
326    fn parse(s: &str) -> Result<Self, String>;
327}
328
329/// A trait for a type that can be used as a default for a [`Config`].
330pub trait ConfigDefault: Clone {
331    type ConfigType: ConfigType;
332
333    /// Converts into the config type.
334    fn into_config_type(self) -> Self::ConfigType;
335}
336
337impl<T: ConfigType> ConfigDefault for T {
338    type ConfigType = T;
339
340    fn into_config_type(self) -> T {
341        self
342    }
343}
344
345impl<T: ConfigType> ConfigDefault for fn() -> T {
346    type ConfigType = T;
347
348    fn into_config_type(self) -> T {
349        (self)()
350    }
351}
352
353/// An set of [Config]s with values that may or may not be independent of other
354/// [ConfigSet]s.
355///
356/// When constructing a ConfigSet from scratch with [ConfigSet::default]
357/// followed by [ConfigSet::add], the values added to the ConfigSet will be
358/// independent of the values in all other ConfigSets.
359///
360/// When constructing a ConfigSet by cloning an existing ConfigSet, any values
361/// cloned from the original ConfigSet will be shared with the original
362/// ConfigSet. Updates to these values in one ConfigSet will be seen in the
363/// other ConfigSet, and vice versa. Any value added to the new ConfigSet via
364/// ConfigSet::add will be independent of values in the original ConfigSet,
365/// unless the new ConfigSet is later cloned.
366#[derive(Clone, Default)]
367pub struct ConfigSet {
368    configs: BTreeMap<String, ConfigEntry>,
369}
370
371impl ConfigSet {
372    /// Adds the given config to this set.
373    ///
374    /// Names are required to be unique within a set, but each set is entirely
375    /// independent. The same `Config` may be registered to multiple
376    /// [`ConfigSet`]s and thus have independent values (e.g. imagine a unit
377    /// test executing concurrently in the same process).
378    ///
379    /// Panics if a config with the same name has been previously registered
380    /// to this set.
381    pub fn add<D: ConfigDefault>(mut self, config: &Config<D>) -> Self {
382        let default = config.default.clone().into_config_type();
383        let default = Into::<ConfigVal>::into(default);
384        let config = ConfigEntry {
385            name: config.name,
386            desc: config.desc,
387            scope: config.scope,
388            default: default.clone(),
389            val: ConfigValAtomic::from(default),
390        };
391        if let Some(prev) = self.configs.insert(config.name.to_owned(), config) {
392            panic!("{} registered twice", prev.name);
393        }
394        self
395    }
396
397    /// Returns the configs currently registered to this set.
398    pub fn entries(&self) -> impl Iterator<Item = &ConfigEntry> {
399        self.configs.values()
400    }
401
402    /// Returns the config with `name` registered to this set, if one exists.
403    pub fn entry(&self, name: &str) -> Option<&ConfigEntry> {
404        self.configs.get(name)
405    }
406}
407
408/// An entry for a config in a [ConfigSet].
409#[derive(Clone, Debug)]
410pub struct ConfigEntry {
411    name: &'static str,
412    desc: &'static str,
413    scope: ParameterScope,
414    default: ConfigVal,
415    val: ConfigValAtomic,
416}
417
418impl ConfigEntry {
419    /// The name of this config.
420    pub fn name(&self) -> &'static str {
421        self.name
422    }
423
424    /// The description of this config.
425    pub fn desc(&self) -> &'static str {
426        self.desc
427    }
428
429    /// The [`ParameterScope`] of this config.
430    pub fn scope(&self) -> ParameterScope {
431        self.scope
432    }
433
434    /// The default value of this config.
435    ///
436    /// This value is never updated.
437    pub fn default(&self) -> &ConfigVal {
438        &self.default
439    }
440
441    /// Parses a string into a [`ConfigVal`] of this config's type.
442    ///
443    /// The type-erased analog of [`Config::parse_val`], dispatching on the
444    /// variant of this entry's value.
445    pub fn parse_val(&self, val: &str) -> Result<ConfigVal, String> {
446        match self.default {
447            ConfigVal::Bool(_) => <bool as ConfigType>::parse(val).map(Into::into),
448            ConfigVal::U32(_) => <u32 as ConfigType>::parse(val).map(Into::into),
449            ConfigVal::Usize(_) => <usize as ConfigType>::parse(val).map(Into::into),
450            ConfigVal::OptUsize(_) => <Option<usize> as ConfigType>::parse(val).map(Into::into),
451            ConfigVal::F64(_) => <f64 as ConfigType>::parse(val).map(Into::into),
452            ConfigVal::String(_) => <String as ConfigType>::parse(val).map(Into::into),
453            ConfigVal::OptString(_) => <Option<String> as ConfigType>::parse(val).map(Into::into),
454            ConfigVal::Duration(_) => <Duration as ConfigType>::parse(val).map(Into::into),
455            ConfigVal::Json(_) => <serde_json::Value as ConfigType>::parse(val).map(Into::into),
456        }
457    }
458
459    /// The value of this config in the set.
460    pub fn val(&self) -> ConfigVal {
461        self.val.load()
462    }
463}
464
465/// A handle to a configuration value in a [`ConfigSet`].
466///
467/// Allows users to amortize the lookup of a name within a set.
468///
469/// Handles can be cheaply cloned.
470#[derive(Debug, Clone)]
471pub struct ConfigValHandle<T> {
472    val: ConfigValAtomic,
473    _type: PhantomData<T>,
474}
475
476impl<T: ConfigType> ConfigValHandle<T> {
477    /// Returns the latest value of this config within the set associated with
478    /// the handle.
479    pub fn get(&self) -> T {
480        T::from_val(self.val.load())
481    }
482
483    /// Return a new handle that returns the constant value provided,
484    /// generally for testing.
485    pub fn disconnected<X>(value: X) -> Self
486    where
487        X: ConfigDefault<ConfigType = T>,
488    {
489        let config_val: ConfigVal = value.into_config_type().into();
490        Self {
491            val: config_val.into(),
492            _type: Default::default(),
493        }
494    }
495}
496
497/// A type-erased configuration value for when set of different types are stored
498/// in a collection.
499#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
500pub enum ConfigVal {
501    /// A `bool` value.
502    Bool(bool),
503    /// A `u32` value.
504    U32(u32),
505    /// A `usize` value.
506    Usize(usize),
507    /// An `Option<usize>` value.
508    OptUsize(Option<usize>),
509    /// An `f64` value.
510    F64(f64),
511    /// A `String` value.
512    String(String),
513    /// An `Option<String>` value
514    OptString(Option<String>),
515    /// A `Duration` value.
516    Duration(Duration),
517    /// A JSON value.
518    #[serde(with = "serde_json_string")]
519    Json(serde_json::Value),
520}
521
522/// To make `ConfigVal` compatible with non-self-describing serialization formats like bincode,
523/// serialize JSON values as strings.
524mod serde_json_string {
525    use serde::de::{Deserialize, Deserializer, Error};
526    use serde::ser::Serializer;
527
528    pub fn serialize<S>(value: &serde_json::Value, serializer: S) -> Result<S::Ok, S::Error>
529    where
530        S: Serializer,
531    {
532        serializer.serialize_str(&value.to_string())
533    }
534
535    pub fn deserialize<'de, D>(deserializer: D) -> Result<serde_json::Value, D::Error>
536    where
537        D: Deserializer<'de>,
538    {
539        let s = String::deserialize(deserializer)?;
540        serde_json::from_str(&s).map_err(D::Error::custom)
541    }
542}
543
544/// An atomic version of [`ConfigVal`] to allow configuration values to be
545/// shared between configuration writers and readers.
546///
547/// TODO(cfg): Consider moving these Arcs to be a single one around the map in
548/// `ConfigSet` instead. That would mean less pointer-chasing in the common
549/// case, but would remove the possibility of amortizing the name lookup via
550/// [Config::handle].
551#[derive(Clone, Debug)]
552enum ConfigValAtomic {
553    Bool(Arc<AtomicBool>),
554    U32(Arc<AtomicU32>),
555    Usize(Arc<AtomicUsize>),
556    OptUsize(Arc<RwLock<Option<usize>>>),
557    // Shared via to_bits/from_bits so we can use the atomic instead of Mutex.
558    F64(Arc<AtomicU64>),
559    String(Arc<RwLock<String>>),
560    OptString(Arc<RwLock<Option<String>>>),
561    Duration(Arc<RwLock<Duration>>),
562    Json(Arc<RwLock<serde_json::Value>>),
563}
564
565impl From<ConfigVal> for ConfigValAtomic {
566    fn from(val: ConfigVal) -> ConfigValAtomic {
567        match val {
568            ConfigVal::Bool(x) => ConfigValAtomic::Bool(Arc::new(AtomicBool::new(x))),
569            ConfigVal::U32(x) => ConfigValAtomic::U32(Arc::new(AtomicU32::new(x))),
570            ConfigVal::Usize(x) => ConfigValAtomic::Usize(Arc::new(AtomicUsize::new(x))),
571            ConfigVal::OptUsize(x) => ConfigValAtomic::OptUsize(Arc::new(RwLock::new(x))),
572            ConfigVal::F64(x) => ConfigValAtomic::F64(Arc::new(AtomicU64::new(x.to_bits()))),
573            ConfigVal::String(x) => ConfigValAtomic::String(Arc::new(RwLock::new(x))),
574            ConfigVal::OptString(x) => ConfigValAtomic::OptString(Arc::new(RwLock::new(x))),
575            ConfigVal::Duration(x) => ConfigValAtomic::Duration(Arc::new(RwLock::new(x))),
576            ConfigVal::Json(x) => ConfigValAtomic::Json(Arc::new(RwLock::new(x))),
577        }
578    }
579}
580
581impl ConfigValAtomic {
582    fn load(&self) -> ConfigVal {
583        match self {
584            ConfigValAtomic::Bool(x) => ConfigVal::Bool(x.load(SeqCst)),
585            ConfigValAtomic::U32(x) => ConfigVal::U32(x.load(SeqCst)),
586            ConfigValAtomic::Usize(x) => ConfigVal::Usize(x.load(SeqCst)),
587            ConfigValAtomic::OptUsize(x) => ConfigVal::OptUsize(*x.read().expect("lock poisoned")),
588            ConfigValAtomic::F64(x) => ConfigVal::F64(f64::from_bits(x.load(SeqCst))),
589            ConfigValAtomic::String(x) => {
590                ConfigVal::String(x.read().expect("lock poisoned").clone())
591            }
592            ConfigValAtomic::OptString(x) => {
593                ConfigVal::OptString(x.read().expect("lock poisoned").clone())
594            }
595            ConfigValAtomic::Duration(x) => ConfigVal::Duration(*x.read().expect("lock poisoned")),
596            ConfigValAtomic::Json(x) => ConfigVal::Json(x.read().expect("lock poisoned").clone()),
597        }
598    }
599
600    fn store(&self, val: ConfigVal) {
601        match (self, val) {
602            (ConfigValAtomic::Bool(x), ConfigVal::Bool(val)) => x.store(val, SeqCst),
603            (ConfigValAtomic::U32(x), ConfigVal::U32(val)) => x.store(val, SeqCst),
604            (ConfigValAtomic::Usize(x), ConfigVal::Usize(val)) => x.store(val, SeqCst),
605            (ConfigValAtomic::OptUsize(x), ConfigVal::OptUsize(val)) => {
606                *x.write().expect("lock poisoned") = val
607            }
608            (ConfigValAtomic::F64(x), ConfigVal::F64(val)) => x.store(val.to_bits(), SeqCst),
609            (ConfigValAtomic::String(x), ConfigVal::String(val)) => {
610                *x.write().expect("lock poisoned") = val
611            }
612            (ConfigValAtomic::OptString(x), ConfigVal::OptString(val)) => {
613                *x.write().expect("lock poisoned") = val
614            }
615            (ConfigValAtomic::Duration(x), ConfigVal::Duration(val)) => {
616                *x.write().expect("lock poisoned") = val
617            }
618            (ConfigValAtomic::Json(x), ConfigVal::Json(val)) => {
619                *x.write().expect("lock poisoned") = val
620            }
621            (ConfigValAtomic::Bool(_), val)
622            | (ConfigValAtomic::U32(_), val)
623            | (ConfigValAtomic::Usize(_), val)
624            | (ConfigValAtomic::OptUsize(_), val)
625            | (ConfigValAtomic::F64(_), val)
626            | (ConfigValAtomic::String(_), val)
627            | (ConfigValAtomic::OptString(_), val)
628            | (ConfigValAtomic::Duration(_), val)
629            | (ConfigValAtomic::Json(_), val) => {
630                panic!("attempted to store {val:?} value in {self:?} parameter")
631            }
632        }
633    }
634}
635
636/// A batch of value updates to [Config]s in a [ConfigSet].
637///
638/// This may be sent across processes to apply the same value updates, but may not be durably
639/// written down.
640#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
641pub struct ConfigUpdates {
642    pub updates: BTreeMap<String, ConfigVal>,
643}
644
645impl ConfigUpdates {
646    /// Adds an update for the given config and value.
647    ///
648    /// If a value of the same config has previously been added to these
649    /// updates, replaces it.
650    pub fn add<T, U>(&mut self, config: &Config<T>, val: U)
651    where
652        T: ConfigDefault,
653        U: ConfigDefault<ConfigType = T::ConfigType>,
654    {
655        self.add_dynamic(config.name, val.into_config_type().into());
656    }
657
658    /// Adds an update for the given configuration name and value.
659    ///
660    /// It is the callers responsibility to ensure the value is of the
661    /// appropriate type for the configuration.
662    ///
663    /// If a value of the same config has previously been added to these
664    /// updates, replaces it.
665    pub fn add_dynamic(&mut self, name: &str, val: ConfigVal) {
666        self.updates.insert(name.to_owned(), val);
667    }
668
669    /// Adds the entries in `other` to `self`, with `other` taking precedence.
670    pub fn extend(&mut self, mut other: Self) {
671        self.updates.append(&mut other.updates)
672    }
673
674    /// Applies these config updates to the given [ConfigSet].
675    ///
676    /// This doesn't need to be the same set that the value updates were added
677    /// from. In fact, the primary use of this is propagating config updates
678    /// across processes.
679    ///
680    /// The value updates for any configs unknown by the given set are skipped.
681    /// Ditto for config type mismatches. However, this is unexpected usage at
682    /// present and so is logged to Sentry.
683    pub fn apply(&self, set: &ConfigSet) {
684        for (name, val) in self.updates.iter() {
685            let Some(config) = set.configs.get(name) else {
686                error!("config update {} {:?} not known set: {:?}", name, val, set);
687                continue;
688            };
689            config.val.store(val.clone());
690        }
691    }
692}
693
694impl From<&ConfigSet> for ConfigUpdates {
695    /// Captures the current value of every config in the set as a dense set of
696    /// updates. Applying the result to another set seeded with the same configs
697    /// reproduces this set's values, regardless of which configs the other set
698    /// has previously had updated.
699    fn from(set: &ConfigSet) -> Self {
700        let mut updates = ConfigUpdates::default();
701        for entry in set.entries() {
702            updates.add_dynamic(entry.name(), entry.val());
703        }
704        updates
705    }
706}
707
708mod impls {
709    use std::num::{ParseFloatError, ParseIntError};
710    use std::str::ParseBoolError;
711    use std::time::Duration;
712
713    use crate::{ConfigDefault, ConfigSet, ConfigType, ConfigVal};
714
715    impl ConfigType for bool {
716        fn from_val(val: ConfigVal) -> Self {
717            match val {
718                ConfigVal::Bool(x) => x,
719                x => panic!("expected bool value got {:?}", x),
720            }
721        }
722
723        fn parse(s: &str) -> Result<Self, String> {
724            match s {
725                "on" => return Ok(true),
726                "off" => return Ok(false),
727                _ => {}
728            }
729            s.parse().map_err(|e: ParseBoolError| e.to_string())
730        }
731    }
732
733    impl From<bool> for ConfigVal {
734        fn from(val: bool) -> ConfigVal {
735            ConfigVal::Bool(val)
736        }
737    }
738
739    impl ConfigType for u32 {
740        fn from_val(val: ConfigVal) -> Self {
741            match val {
742                ConfigVal::U32(x) => x,
743                x => panic!("expected u32 value got {:?}", x),
744            }
745        }
746
747        fn parse(s: &str) -> Result<Self, String> {
748            s.parse().map_err(|e: ParseIntError| e.to_string())
749        }
750    }
751
752    impl From<u32> for ConfigVal {
753        fn from(val: u32) -> ConfigVal {
754            ConfigVal::U32(val)
755        }
756    }
757
758    impl ConfigType for usize {
759        fn from_val(val: ConfigVal) -> Self {
760            match val {
761                ConfigVal::Usize(x) => x,
762                x => panic!("expected usize value got {:?}", x),
763            }
764        }
765
766        fn parse(s: &str) -> Result<Self, String> {
767            s.parse().map_err(|e: ParseIntError| e.to_string())
768        }
769    }
770
771    impl From<usize> for ConfigVal {
772        fn from(val: usize) -> ConfigVal {
773            ConfigVal::Usize(val)
774        }
775    }
776
777    impl ConfigType for Option<usize> {
778        fn from_val(val: ConfigVal) -> Self {
779            match val {
780                ConfigVal::OptUsize(x) => x,
781                x => panic!("expected usize value got {:?}", x),
782            }
783        }
784
785        fn parse(s: &str) -> Result<Self, String> {
786            if s.is_empty() {
787                Ok(None)
788            } else {
789                let val = s.parse().map_err(|e: ParseIntError| e.to_string())?;
790                Ok(Some(val))
791            }
792        }
793    }
794
795    impl From<Option<usize>> for ConfigVal {
796        fn from(val: Option<usize>) -> ConfigVal {
797            ConfigVal::OptUsize(val)
798        }
799    }
800
801    impl ConfigType for f64 {
802        fn from_val(val: ConfigVal) -> Self {
803            match val {
804                ConfigVal::F64(x) => x,
805                x => panic!("expected f64 value got {:?}", x),
806            }
807        }
808
809        fn parse(s: &str) -> Result<Self, String> {
810            s.parse().map_err(|e: ParseFloatError| e.to_string())
811        }
812    }
813
814    impl From<f64> for ConfigVal {
815        fn from(val: f64) -> ConfigVal {
816            ConfigVal::F64(val)
817        }
818    }
819
820    impl ConfigType for String {
821        fn from_val(val: ConfigVal) -> Self {
822            match val {
823                ConfigVal::String(x) => x,
824                x => panic!("expected String value got {:?}", x),
825            }
826        }
827
828        fn parse(s: &str) -> Result<Self, String> {
829            Ok(s.to_string())
830        }
831    }
832
833    impl From<String> for ConfigVal {
834        fn from(val: String) -> ConfigVal {
835            ConfigVal::String(val)
836        }
837    }
838
839    impl ConfigDefault for &str {
840        type ConfigType = String;
841
842        fn into_config_type(self) -> String {
843            self.into()
844        }
845    }
846
847    impl ConfigType for Option<String> {
848        fn from_val(val: ConfigVal) -> Self {
849            match val {
850                ConfigVal::OptString(x) => x,
851                x => panic!("expected String value got {:?}", x),
852            }
853        }
854
855        fn parse(s: &str) -> Result<Self, String> {
856            Ok(Some(s.to_string()))
857        }
858    }
859
860    impl From<Option<String>> for ConfigVal {
861        fn from(val: Option<String>) -> ConfigVal {
862            ConfigVal::OptString(val)
863        }
864    }
865
866    impl ConfigDefault for Option<&str> {
867        type ConfigType = Option<String>;
868
869        fn into_config_type(self) -> Option<String> {
870            self.map(|s| s.to_string())
871        }
872    }
873
874    impl ConfigType for Duration {
875        fn from_val(val: ConfigVal) -> Self {
876            match val {
877                ConfigVal::Duration(x) => x,
878                x => panic!("expected Duration value got {:?}", x),
879            }
880        }
881
882        fn parse(s: &str) -> Result<Self, String> {
883            humantime::parse_duration(s).map_err(|e| e.to_string())
884        }
885    }
886
887    impl From<Duration> for ConfigVal {
888        fn from(val: Duration) -> ConfigVal {
889            ConfigVal::Duration(val)
890        }
891    }
892
893    impl ConfigType for serde_json::Value {
894        fn from_val(val: ConfigVal) -> Self {
895            match val {
896                ConfigVal::Json(x) => x,
897                x => panic!("expected JSON value got {:?}", x),
898            }
899        }
900
901        fn parse(s: &str) -> Result<Self, String> {
902            serde_json::from_str(s).map_err(|e| e.to_string())
903        }
904    }
905
906    impl From<serde_json::Value> for ConfigVal {
907        fn from(val: serde_json::Value) -> ConfigVal {
908            ConfigVal::Json(val)
909        }
910    }
911
912    impl std::fmt::Debug for ConfigSet {
913        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
914            let ConfigSet { configs } = self;
915            f.debug_map()
916                .entries(configs.iter().map(|(name, val)| (name, val.val())))
917                .finish()
918        }
919    }
920}
921
922#[cfg(test)]
923mod tests {
924    use super::*;
925
926    use mz_ore::assert_err;
927
928    const BOOL: Config<bool> = Config::new("bool", true, "", ParameterScope::Environment);
929    const U32: Config<u32> = Config::new("u32", 4, "", ParameterScope::Environment);
930    const USIZE: Config<usize> = Config::new("usize", 1, "", ParameterScope::Environment);
931    const OPT_USIZE: Config<Option<usize>> =
932        Config::new("opt_usize", Some(2), "", ParameterScope::Environment);
933    const F64: Config<f64> = Config::new("f64", 5.0, "", ParameterScope::Environment);
934    const STRING: Config<&str> = Config::new("string", "a", "", ParameterScope::Environment);
935    const OPT_STRING: Config<Option<&str>> =
936        Config::new("opt_string", Some("a"), "", ParameterScope::Environment);
937    const DURATION: Config<Duration> = Config::new(
938        "duration",
939        Duration::from_nanos(3),
940        "",
941        ParameterScope::Environment,
942    );
943    const JSON: Config<fn() -> serde_json::Value> = Config::new(
944        "json",
945        || serde_json::json!({}),
946        "",
947        ParameterScope::Environment,
948    );
949
950    #[mz_ore::test]
951    fn all_types() {
952        let configs = ConfigSet::default()
953            .add(&BOOL)
954            .add(&USIZE)
955            .add(&U32)
956            .add(&OPT_USIZE)
957            .add(&F64)
958            .add(&STRING)
959            .add(&OPT_STRING)
960            .add(&DURATION)
961            .add(&JSON);
962        assert_eq!(BOOL.get(&configs), true);
963        assert_eq!(U32.get(&configs), 4);
964        assert_eq!(USIZE.get(&configs), 1);
965        assert_eq!(OPT_USIZE.get(&configs), Some(2));
966        assert_eq!(F64.get(&configs), 5.0);
967        assert_eq!(STRING.get(&configs), "a");
968        assert_eq!(OPT_STRING.get(&configs), Some("a".to_string()));
969        assert_eq!(DURATION.get(&configs), Duration::from_nanos(3));
970        assert_eq!(JSON.get(&configs), serde_json::json!({}));
971
972        let mut updates = ConfigUpdates::default();
973        updates.add(&BOOL, false);
974        updates.add(&U32, 7);
975        updates.add(&USIZE, 2);
976        updates.add(&OPT_USIZE, None::<usize>);
977        updates.add(&F64, 8.0);
978        updates.add(&STRING, "b");
979        updates.add(&OPT_STRING, None::<String>);
980        updates.add(&DURATION, Duration::from_nanos(4));
981        updates.add(&JSON, serde_json::json!({"a": 1}));
982        updates.apply(&configs);
983
984        assert_eq!(BOOL.get(&configs), false);
985        assert_eq!(U32.get(&configs), 7);
986        assert_eq!(USIZE.get(&configs), 2);
987        assert_eq!(OPT_USIZE.get(&configs), None);
988        assert_eq!(F64.get(&configs), 8.0);
989        assert_eq!(STRING.get(&configs), "b");
990        assert_eq!(OPT_STRING.get(&configs), None);
991        assert_eq!(DURATION.get(&configs), Duration::from_nanos(4));
992        assert_eq!(JSON.get(&configs), serde_json::json!({"a": 1}));
993    }
994
995    #[mz_ore::test]
996    fn fn_default() {
997        const BOOL_FN_DEFAULT: Config<fn() -> bool> =
998            Config::new("bool", || !true, "", ParameterScope::Environment);
999        const STRING_FN_DEFAULT: Config<fn() -> String> =
1000            Config::new("string", || "x".repeat(3), "", ParameterScope::Environment);
1001
1002        const OPT_STRING_FN_DEFAULT: Config<fn() -> Option<String>> = Config::new(
1003            "opt_string",
1004            || Some("x".repeat(3)),
1005            "",
1006            ParameterScope::Environment,
1007        );
1008
1009        let configs = ConfigSet::default()
1010            .add(&BOOL_FN_DEFAULT)
1011            .add(&STRING_FN_DEFAULT)
1012            .add(&OPT_STRING_FN_DEFAULT);
1013        assert_eq!(BOOL_FN_DEFAULT.get(&configs), false);
1014        assert_eq!(STRING_FN_DEFAULT.get(&configs), "xxx");
1015        assert_eq!(OPT_STRING_FN_DEFAULT.get(&configs), Some("xxx".to_string()));
1016    }
1017
1018    #[mz_ore::test]
1019    fn config_set() {
1020        let c0 = ConfigSet::default().add(&USIZE);
1021        assert_eq!(USIZE.get(&c0), 1);
1022        let mut updates = ConfigUpdates::default();
1023        updates.add(&USIZE, 2);
1024        updates.apply(&c0);
1025        assert_eq!(USIZE.get(&c0), 2);
1026
1027        // Each ConfigSet is independent, even if they contain the same set of
1028        // configs.
1029        let c1 = ConfigSet::default().add(&USIZE);
1030        assert_eq!(USIZE.get(&c1), 1);
1031        let mut updates = ConfigUpdates::default();
1032        updates.add(&USIZE, 3);
1033        updates.apply(&c1);
1034        assert_eq!(USIZE.get(&c1), 3);
1035        assert_eq!(USIZE.get(&c0), 2);
1036
1037        // We can copy values from one to the other, though (envd -> clusterd).
1038        let mut updates = ConfigUpdates::default();
1039        for e in c0.entries() {
1040            updates.add_dynamic(e.name, e.val());
1041        }
1042        assert_eq!(USIZE.get(&c1), 3);
1043        updates.apply(&c1);
1044        assert_eq!(USIZE.get(&c1), 2);
1045    }
1046
1047    #[mz_ore::test]
1048    fn get_with_overrides() {
1049        let configs = ConfigSet::default().add(&USIZE).add(&STRING);
1050
1051        // No overrides at all, and an override map that doesn't mention the
1052        // config, both resolve to the set's value.
1053        assert_eq!(USIZE.get_with_overrides(&configs, None), 1);
1054        let mut overrides = ConfigUpdates::default();
1055        overrides.add(&STRING, "b");
1056        assert_eq!(USIZE.get_with_overrides(&configs, Some(&overrides)), 1);
1057
1058        // An override wins over the set's value, without disturbing it.
1059        overrides.add(&USIZE, 2);
1060        assert_eq!(USIZE.get_with_overrides(&configs, Some(&overrides)), 2);
1061        assert_eq!(USIZE.get(&configs), 1);
1062
1063        // An override of the wrong type is ignored rather than panicking.
1064        let mut mistyped = ConfigUpdates::default();
1065        mistyped.add_dynamic(USIZE.name(), ConfigVal::Bool(true));
1066        assert_eq!(USIZE.get_with_overrides(&configs, Some(&mistyped)), 1);
1067    }
1068
1069    #[mz_ore::test]
1070    fn config_updates_extend() {
1071        // Regression test for database-issues#7793.
1072        //
1073        // Construct two ConfigUpdates with overlapping, but not identical, sets
1074        // of configs. Combine them and assert that the expected number of
1075        // updates is present.
1076        let mut u1 = {
1077            let c = ConfigSet::default().add(&USIZE).add(&STRING);
1078            let mut x = ConfigUpdates::default();
1079            for e in c.entries() {
1080                x.add_dynamic(e.name(), e.val());
1081            }
1082            x
1083        };
1084        let u2 = {
1085            let c = ConfigSet::default().add(&USIZE).add(&DURATION);
1086            let mut updates = ConfigUpdates::default();
1087            updates.add(&USIZE, 2);
1088            updates.apply(&c);
1089            let mut x = ConfigUpdates::default();
1090            for e in c.entries() {
1091                x.add_dynamic(e.name(), e.val());
1092            }
1093            x
1094        };
1095        assert_eq!(u1.updates.len(), 2);
1096        assert_eq!(u2.updates.len(), 2);
1097        u1.extend(u2);
1098        assert_eq!(u1.updates.len(), 3);
1099
1100        // Assert that extend kept the correct (later) value for the overlapping
1101        // config.
1102        let c = ConfigSet::default().add(&USIZE);
1103        u1.apply(&c);
1104        assert_eq!(USIZE.get(&c), 2);
1105    }
1106
1107    #[mz_ore::test]
1108    fn config_parse() {
1109        assert_eq!(BOOL.parse_val("true"), Ok(ConfigVal::Bool(true)));
1110        assert_eq!(BOOL.parse_val("on"), Ok(ConfigVal::Bool(true)));
1111        assert_eq!(BOOL.parse_val("false"), Ok(ConfigVal::Bool(false)));
1112        assert_eq!(BOOL.parse_val("off"), Ok(ConfigVal::Bool(false)));
1113        assert_err!(BOOL.parse_val("42"));
1114        assert_err!(BOOL.parse_val("66.6"));
1115        assert_err!(BOOL.parse_val("farragut"));
1116        assert_err!(BOOL.parse_val(""));
1117        assert_err!(BOOL.parse_val("5 s"));
1118
1119        assert_err!(U32.parse_val("true"));
1120        assert_err!(U32.parse_val("false"));
1121        assert_eq!(U32.parse_val("42"), Ok(ConfigVal::U32(42)));
1122        assert_err!(U32.parse_val("66.6"));
1123        assert_err!(U32.parse_val("farragut"));
1124        assert_err!(U32.parse_val(""));
1125        assert_err!(U32.parse_val("5 s"));
1126
1127        assert_err!(USIZE.parse_val("true"));
1128        assert_err!(USIZE.parse_val("false"));
1129        assert_eq!(USIZE.parse_val("42"), Ok(ConfigVal::Usize(42)));
1130        assert_err!(USIZE.parse_val("66.6"));
1131        assert_err!(USIZE.parse_val("farragut"));
1132        assert_err!(USIZE.parse_val(""));
1133        assert_err!(USIZE.parse_val("5 s"));
1134
1135        assert_err!(OPT_USIZE.parse_val("true"));
1136        assert_err!(OPT_USIZE.parse_val("false"));
1137        assert_eq!(OPT_USIZE.parse_val("42"), Ok(ConfigVal::OptUsize(Some(42))));
1138        assert_err!(OPT_USIZE.parse_val("66.6"));
1139        assert_err!(OPT_USIZE.parse_val("farragut"));
1140        assert_eq!(OPT_USIZE.parse_val(""), Ok(ConfigVal::OptUsize(None)));
1141        assert_err!(OPT_USIZE.parse_val("5 s"));
1142
1143        assert_err!(F64.parse_val("true"));
1144        assert_err!(F64.parse_val("false"));
1145        assert_eq!(F64.parse_val("42"), Ok(ConfigVal::F64(42.0)));
1146        assert_eq!(F64.parse_val("66.6"), Ok(ConfigVal::F64(66.6)));
1147        assert_err!(F64.parse_val("farragut"));
1148        assert_err!(F64.parse_val(""));
1149        assert_err!(F64.parse_val("5 s"));
1150
1151        assert_eq!(
1152            STRING.parse_val("true"),
1153            Ok(ConfigVal::String("true".to_string()))
1154        );
1155        assert_eq!(
1156            STRING.parse_val("false"),
1157            Ok(ConfigVal::String("false".to_string()))
1158        );
1159        assert_eq!(
1160            STRING.parse_val("66.6"),
1161            Ok(ConfigVal::String("66.6".to_string()))
1162        );
1163        assert_eq!(
1164            STRING.parse_val("42"),
1165            Ok(ConfigVal::String("42".to_string()))
1166        );
1167        assert_eq!(
1168            STRING.parse_val("farragut"),
1169            Ok(ConfigVal::String("farragut".to_string()))
1170        );
1171        assert_eq!(STRING.parse_val(""), Ok(ConfigVal::String("".to_string())));
1172        assert_eq!(
1173            STRING.parse_val("5 s"),
1174            Ok(ConfigVal::String("5 s".to_string()))
1175        );
1176
1177        assert_eq!(
1178            OPT_STRING.parse_val("true"),
1179            Ok(ConfigVal::OptString(Some("true".to_string())))
1180        );
1181        assert_eq!(
1182            OPT_STRING.parse_val("false"),
1183            Ok(ConfigVal::OptString(Some("false".to_string())))
1184        );
1185        assert_eq!(
1186            OPT_STRING.parse_val("66.6"),
1187            Ok(ConfigVal::OptString(Some("66.6".to_string())))
1188        );
1189        assert_eq!(
1190            OPT_STRING.parse_val("42"),
1191            Ok(ConfigVal::OptString(Some("42".to_string())))
1192        );
1193        assert_eq!(
1194            OPT_STRING.parse_val("farragut"),
1195            Ok(ConfigVal::OptString(Some("farragut".to_string())))
1196        );
1197        assert_eq!(
1198            OPT_STRING.parse_val(""),
1199            Ok(ConfigVal::OptString(Some("".to_string())))
1200        );
1201        assert_eq!(
1202            OPT_STRING.parse_val("5 s"),
1203            Ok(ConfigVal::OptString(Some("5 s".to_string())))
1204        );
1205
1206        assert_err!(DURATION.parse_val("true"));
1207        assert_err!(DURATION.parse_val("false"));
1208        assert_err!(DURATION.parse_val("42"));
1209        assert_err!(DURATION.parse_val("66.6"));
1210        assert_err!(DURATION.parse_val("farragut"));
1211        assert_err!(DURATION.parse_val(""));
1212        assert_eq!(
1213            DURATION.parse_val("5 s"),
1214            Ok(ConfigVal::Duration(Duration::from_secs(5)))
1215        );
1216
1217        assert_eq!(
1218            JSON.parse_val("true"),
1219            Ok(ConfigVal::Json(serde_json::json!(true)))
1220        );
1221        assert_eq!(
1222            JSON.parse_val("false"),
1223            Ok(ConfigVal::Json(serde_json::json!(false)))
1224        );
1225        assert_eq!(
1226            JSON.parse_val("42"),
1227            Ok(ConfigVal::Json(serde_json::json!(42)))
1228        );
1229        assert_eq!(
1230            JSON.parse_val("66.6"),
1231            Ok(ConfigVal::Json(serde_json::json!(66.6)))
1232        );
1233        assert_err!(JSON.parse_val("farragut"));
1234        assert_err!(JSON.parse_val(""));
1235        assert_err!(JSON.parse_val("5 s"));
1236        assert_eq!(
1237            JSON.parse_val("{\"joe\": \"developer\"}"),
1238            Ok(ConfigVal::Json(serde_json::json!({"joe": "developer"})))
1239        );
1240    }
1241}