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