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
599impl From<&ConfigSet> for ConfigUpdates {
600    /// Captures the current value of every config in the set as a dense set of
601    /// updates. Applying the result to another set seeded with the same configs
602    /// reproduces this set's values, regardless of which configs the other set
603    /// has previously had updated.
604    fn from(set: &ConfigSet) -> Self {
605        let mut updates = ConfigUpdates::default();
606        for entry in set.entries() {
607            updates.add_dynamic(entry.name(), entry.val());
608        }
609        updates
610    }
611}
612
613mod impls {
614    use std::num::{ParseFloatError, ParseIntError};
615    use std::str::ParseBoolError;
616    use std::time::Duration;
617
618    use crate::{ConfigDefault, ConfigSet, ConfigType, ConfigVal};
619
620    impl ConfigType for bool {
621        fn from_val(val: ConfigVal) -> Self {
622            match val {
623                ConfigVal::Bool(x) => x,
624                x => panic!("expected bool value got {:?}", x),
625            }
626        }
627
628        fn parse(s: &str) -> Result<Self, String> {
629            match s {
630                "on" => return Ok(true),
631                "off" => return Ok(false),
632                _ => {}
633            }
634            s.parse().map_err(|e: ParseBoolError| e.to_string())
635        }
636    }
637
638    impl From<bool> for ConfigVal {
639        fn from(val: bool) -> ConfigVal {
640            ConfigVal::Bool(val)
641        }
642    }
643
644    impl ConfigType for u32 {
645        fn from_val(val: ConfigVal) -> Self {
646            match val {
647                ConfigVal::U32(x) => x,
648                x => panic!("expected u32 value got {:?}", x),
649            }
650        }
651
652        fn parse(s: &str) -> Result<Self, String> {
653            s.parse().map_err(|e: ParseIntError| e.to_string())
654        }
655    }
656
657    impl From<u32> for ConfigVal {
658        fn from(val: u32) -> ConfigVal {
659            ConfigVal::U32(val)
660        }
661    }
662
663    impl ConfigType for usize {
664        fn from_val(val: ConfigVal) -> Self {
665            match val {
666                ConfigVal::Usize(x) => x,
667                x => panic!("expected usize value got {:?}", x),
668            }
669        }
670
671        fn parse(s: &str) -> Result<Self, String> {
672            s.parse().map_err(|e: ParseIntError| e.to_string())
673        }
674    }
675
676    impl From<usize> for ConfigVal {
677        fn from(val: usize) -> ConfigVal {
678            ConfigVal::Usize(val)
679        }
680    }
681
682    impl ConfigType for Option<usize> {
683        fn from_val(val: ConfigVal) -> Self {
684            match val {
685                ConfigVal::OptUsize(x) => x,
686                x => panic!("expected usize value got {:?}", x),
687            }
688        }
689
690        fn parse(s: &str) -> Result<Self, String> {
691            if s.is_empty() {
692                Ok(None)
693            } else {
694                let val = s.parse().map_err(|e: ParseIntError| e.to_string())?;
695                Ok(Some(val))
696            }
697        }
698    }
699
700    impl From<Option<usize>> for ConfigVal {
701        fn from(val: Option<usize>) -> ConfigVal {
702            ConfigVal::OptUsize(val)
703        }
704    }
705
706    impl ConfigType for f64 {
707        fn from_val(val: ConfigVal) -> Self {
708            match val {
709                ConfigVal::F64(x) => x,
710                x => panic!("expected f64 value got {:?}", x),
711            }
712        }
713
714        fn parse(s: &str) -> Result<Self, String> {
715            s.parse().map_err(|e: ParseFloatError| e.to_string())
716        }
717    }
718
719    impl From<f64> for ConfigVal {
720        fn from(val: f64) -> ConfigVal {
721            ConfigVal::F64(val)
722        }
723    }
724
725    impl ConfigType for String {
726        fn from_val(val: ConfigVal) -> Self {
727            match val {
728                ConfigVal::String(x) => x,
729                x => panic!("expected String value got {:?}", x),
730            }
731        }
732
733        fn parse(s: &str) -> Result<Self, String> {
734            Ok(s.to_string())
735        }
736    }
737
738    impl From<String> for ConfigVal {
739        fn from(val: String) -> ConfigVal {
740            ConfigVal::String(val)
741        }
742    }
743
744    impl ConfigDefault for &str {
745        type ConfigType = String;
746
747        fn into_config_type(self) -> String {
748            self.into()
749        }
750    }
751
752    impl ConfigType for Option<String> {
753        fn from_val(val: ConfigVal) -> Self {
754            match val {
755                ConfigVal::OptString(x) => x,
756                x => panic!("expected String value got {:?}", x),
757            }
758        }
759
760        fn parse(s: &str) -> Result<Self, String> {
761            Ok(Some(s.to_string()))
762        }
763    }
764
765    impl From<Option<String>> for ConfigVal {
766        fn from(val: Option<String>) -> ConfigVal {
767            ConfigVal::OptString(val)
768        }
769    }
770
771    impl ConfigDefault for Option<&str> {
772        type ConfigType = Option<String>;
773
774        fn into_config_type(self) -> Option<String> {
775            self.map(|s| s.to_string())
776        }
777    }
778
779    impl ConfigType for Duration {
780        fn from_val(val: ConfigVal) -> Self {
781            match val {
782                ConfigVal::Duration(x) => x,
783                x => panic!("expected Duration value got {:?}", x),
784            }
785        }
786
787        fn parse(s: &str) -> Result<Self, String> {
788            humantime::parse_duration(s).map_err(|e| e.to_string())
789        }
790    }
791
792    impl From<Duration> for ConfigVal {
793        fn from(val: Duration) -> ConfigVal {
794            ConfigVal::Duration(val)
795        }
796    }
797
798    impl ConfigType for serde_json::Value {
799        fn from_val(val: ConfigVal) -> Self {
800            match val {
801                ConfigVal::Json(x) => x,
802                x => panic!("expected JSON value got {:?}", x),
803            }
804        }
805
806        fn parse(s: &str) -> Result<Self, String> {
807            serde_json::from_str(s).map_err(|e| e.to_string())
808        }
809    }
810
811    impl From<serde_json::Value> for ConfigVal {
812        fn from(val: serde_json::Value) -> ConfigVal {
813            ConfigVal::Json(val)
814        }
815    }
816
817    impl std::fmt::Debug for ConfigSet {
818        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
819            let ConfigSet { configs } = self;
820            f.debug_map()
821                .entries(configs.iter().map(|(name, val)| (name, val.val())))
822                .finish()
823        }
824    }
825}
826
827#[cfg(test)]
828mod tests {
829    use super::*;
830
831    use mz_ore::assert_err;
832
833    const BOOL: Config<bool> = Config::new("bool", true, "");
834    const U32: Config<u32> = Config::new("u32", 4, "");
835    const USIZE: Config<usize> = Config::new("usize", 1, "");
836    const OPT_USIZE: Config<Option<usize>> = Config::new("opt_usize", Some(2), "");
837    const F64: Config<f64> = Config::new("f64", 5.0, "");
838    const STRING: Config<&str> = Config::new("string", "a", "");
839    const OPT_STRING: Config<Option<&str>> = Config::new("opt_string", Some("a"), "");
840    const DURATION: Config<Duration> = Config::new("duration", Duration::from_nanos(3), "");
841    const JSON: Config<fn() -> serde_json::Value> =
842        Config::new("json", || serde_json::json!({}), "");
843
844    #[mz_ore::test]
845    fn all_types() {
846        let configs = ConfigSet::default()
847            .add(&BOOL)
848            .add(&USIZE)
849            .add(&U32)
850            .add(&OPT_USIZE)
851            .add(&F64)
852            .add(&STRING)
853            .add(&OPT_STRING)
854            .add(&DURATION)
855            .add(&JSON);
856        assert_eq!(BOOL.get(&configs), true);
857        assert_eq!(U32.get(&configs), 4);
858        assert_eq!(USIZE.get(&configs), 1);
859        assert_eq!(OPT_USIZE.get(&configs), Some(2));
860        assert_eq!(F64.get(&configs), 5.0);
861        assert_eq!(STRING.get(&configs), "a");
862        assert_eq!(OPT_STRING.get(&configs), Some("a".to_string()));
863        assert_eq!(DURATION.get(&configs), Duration::from_nanos(3));
864        assert_eq!(JSON.get(&configs), serde_json::json!({}));
865
866        let mut updates = ConfigUpdates::default();
867        updates.add(&BOOL, false);
868        updates.add(&U32, 7);
869        updates.add(&USIZE, 2);
870        updates.add(&OPT_USIZE, None::<usize>);
871        updates.add(&F64, 8.0);
872        updates.add(&STRING, "b");
873        updates.add(&OPT_STRING, None::<String>);
874        updates.add(&DURATION, Duration::from_nanos(4));
875        updates.add(&JSON, serde_json::json!({"a": 1}));
876        updates.apply(&configs);
877
878        assert_eq!(BOOL.get(&configs), false);
879        assert_eq!(U32.get(&configs), 7);
880        assert_eq!(USIZE.get(&configs), 2);
881        assert_eq!(OPT_USIZE.get(&configs), None);
882        assert_eq!(F64.get(&configs), 8.0);
883        assert_eq!(STRING.get(&configs), "b");
884        assert_eq!(OPT_STRING.get(&configs), None);
885        assert_eq!(DURATION.get(&configs), Duration::from_nanos(4));
886        assert_eq!(JSON.get(&configs), serde_json::json!({"a": 1}));
887    }
888
889    #[mz_ore::test]
890    fn fn_default() {
891        const BOOL_FN_DEFAULT: Config<fn() -> bool> = Config::new("bool", || !true, "");
892        const STRING_FN_DEFAULT: Config<fn() -> String> =
893            Config::new("string", || "x".repeat(3), "");
894
895        const OPT_STRING_FN_DEFAULT: Config<fn() -> Option<String>> =
896            Config::new("opt_string", || Some("x".repeat(3)), "");
897
898        let configs = ConfigSet::default()
899            .add(&BOOL_FN_DEFAULT)
900            .add(&STRING_FN_DEFAULT)
901            .add(&OPT_STRING_FN_DEFAULT);
902        assert_eq!(BOOL_FN_DEFAULT.get(&configs), false);
903        assert_eq!(STRING_FN_DEFAULT.get(&configs), "xxx");
904        assert_eq!(OPT_STRING_FN_DEFAULT.get(&configs), Some("xxx".to_string()));
905    }
906
907    #[mz_ore::test]
908    fn config_set() {
909        let c0 = ConfigSet::default().add(&USIZE);
910        assert_eq!(USIZE.get(&c0), 1);
911        let mut updates = ConfigUpdates::default();
912        updates.add(&USIZE, 2);
913        updates.apply(&c0);
914        assert_eq!(USIZE.get(&c0), 2);
915
916        // Each ConfigSet is independent, even if they contain the same set of
917        // configs.
918        let c1 = ConfigSet::default().add(&USIZE);
919        assert_eq!(USIZE.get(&c1), 1);
920        let mut updates = ConfigUpdates::default();
921        updates.add(&USIZE, 3);
922        updates.apply(&c1);
923        assert_eq!(USIZE.get(&c1), 3);
924        assert_eq!(USIZE.get(&c0), 2);
925
926        // We can copy values from one to the other, though (envd -> clusterd).
927        let mut updates = ConfigUpdates::default();
928        for e in c0.entries() {
929            updates.add_dynamic(e.name, e.val());
930        }
931        assert_eq!(USIZE.get(&c1), 3);
932        updates.apply(&c1);
933        assert_eq!(USIZE.get(&c1), 2);
934    }
935
936    #[mz_ore::test]
937    fn config_updates_extend() {
938        // Regression test for database-issues#7793.
939        //
940        // Construct two ConfigUpdates with overlapping, but not identical, sets
941        // of configs. Combine them and assert that the expected number of
942        // updates is present.
943        let mut u1 = {
944            let c = ConfigSet::default().add(&USIZE).add(&STRING);
945            let mut x = ConfigUpdates::default();
946            for e in c.entries() {
947                x.add_dynamic(e.name(), e.val());
948            }
949            x
950        };
951        let u2 = {
952            let c = ConfigSet::default().add(&USIZE).add(&DURATION);
953            let mut updates = ConfigUpdates::default();
954            updates.add(&USIZE, 2);
955            updates.apply(&c);
956            let mut x = ConfigUpdates::default();
957            for e in c.entries() {
958                x.add_dynamic(e.name(), e.val());
959            }
960            x
961        };
962        assert_eq!(u1.updates.len(), 2);
963        assert_eq!(u2.updates.len(), 2);
964        u1.extend(u2);
965        assert_eq!(u1.updates.len(), 3);
966
967        // Assert that extend kept the correct (later) value for the overlapping
968        // config.
969        let c = ConfigSet::default().add(&USIZE);
970        u1.apply(&c);
971        assert_eq!(USIZE.get(&c), 2);
972    }
973
974    #[mz_ore::test]
975    fn config_parse() {
976        assert_eq!(BOOL.parse_val("true"), Ok(ConfigVal::Bool(true)));
977        assert_eq!(BOOL.parse_val("on"), Ok(ConfigVal::Bool(true)));
978        assert_eq!(BOOL.parse_val("false"), Ok(ConfigVal::Bool(false)));
979        assert_eq!(BOOL.parse_val("off"), Ok(ConfigVal::Bool(false)));
980        assert_err!(BOOL.parse_val("42"));
981        assert_err!(BOOL.parse_val("66.6"));
982        assert_err!(BOOL.parse_val("farragut"));
983        assert_err!(BOOL.parse_val(""));
984        assert_err!(BOOL.parse_val("5 s"));
985
986        assert_err!(U32.parse_val("true"));
987        assert_err!(U32.parse_val("false"));
988        assert_eq!(U32.parse_val("42"), Ok(ConfigVal::U32(42)));
989        assert_err!(U32.parse_val("66.6"));
990        assert_err!(U32.parse_val("farragut"));
991        assert_err!(U32.parse_val(""));
992        assert_err!(U32.parse_val("5 s"));
993
994        assert_err!(USIZE.parse_val("true"));
995        assert_err!(USIZE.parse_val("false"));
996        assert_eq!(USIZE.parse_val("42"), Ok(ConfigVal::Usize(42)));
997        assert_err!(USIZE.parse_val("66.6"));
998        assert_err!(USIZE.parse_val("farragut"));
999        assert_err!(USIZE.parse_val(""));
1000        assert_err!(USIZE.parse_val("5 s"));
1001
1002        assert_err!(OPT_USIZE.parse_val("true"));
1003        assert_err!(OPT_USIZE.parse_val("false"));
1004        assert_eq!(OPT_USIZE.parse_val("42"), Ok(ConfigVal::OptUsize(Some(42))));
1005        assert_err!(OPT_USIZE.parse_val("66.6"));
1006        assert_err!(OPT_USIZE.parse_val("farragut"));
1007        assert_eq!(OPT_USIZE.parse_val(""), Ok(ConfigVal::OptUsize(None)));
1008        assert_err!(OPT_USIZE.parse_val("5 s"));
1009
1010        assert_err!(F64.parse_val("true"));
1011        assert_err!(F64.parse_val("false"));
1012        assert_eq!(F64.parse_val("42"), Ok(ConfigVal::F64(42.0)));
1013        assert_eq!(F64.parse_val("66.6"), Ok(ConfigVal::F64(66.6)));
1014        assert_err!(F64.parse_val("farragut"));
1015        assert_err!(F64.parse_val(""));
1016        assert_err!(F64.parse_val("5 s"));
1017
1018        assert_eq!(
1019            STRING.parse_val("true"),
1020            Ok(ConfigVal::String("true".to_string()))
1021        );
1022        assert_eq!(
1023            STRING.parse_val("false"),
1024            Ok(ConfigVal::String("false".to_string()))
1025        );
1026        assert_eq!(
1027            STRING.parse_val("66.6"),
1028            Ok(ConfigVal::String("66.6".to_string()))
1029        );
1030        assert_eq!(
1031            STRING.parse_val("42"),
1032            Ok(ConfigVal::String("42".to_string()))
1033        );
1034        assert_eq!(
1035            STRING.parse_val("farragut"),
1036            Ok(ConfigVal::String("farragut".to_string()))
1037        );
1038        assert_eq!(STRING.parse_val(""), Ok(ConfigVal::String("".to_string())));
1039        assert_eq!(
1040            STRING.parse_val("5 s"),
1041            Ok(ConfigVal::String("5 s".to_string()))
1042        );
1043
1044        assert_eq!(
1045            OPT_STRING.parse_val("true"),
1046            Ok(ConfigVal::OptString(Some("true".to_string())))
1047        );
1048        assert_eq!(
1049            OPT_STRING.parse_val("false"),
1050            Ok(ConfigVal::OptString(Some("false".to_string())))
1051        );
1052        assert_eq!(
1053            OPT_STRING.parse_val("66.6"),
1054            Ok(ConfigVal::OptString(Some("66.6".to_string())))
1055        );
1056        assert_eq!(
1057            OPT_STRING.parse_val("42"),
1058            Ok(ConfigVal::OptString(Some("42".to_string())))
1059        );
1060        assert_eq!(
1061            OPT_STRING.parse_val("farragut"),
1062            Ok(ConfigVal::OptString(Some("farragut".to_string())))
1063        );
1064        assert_eq!(
1065            OPT_STRING.parse_val(""),
1066            Ok(ConfigVal::OptString(Some("".to_string())))
1067        );
1068        assert_eq!(
1069            OPT_STRING.parse_val("5 s"),
1070            Ok(ConfigVal::OptString(Some("5 s".to_string())))
1071        );
1072
1073        assert_err!(DURATION.parse_val("true"));
1074        assert_err!(DURATION.parse_val("false"));
1075        assert_err!(DURATION.parse_val("42"));
1076        assert_err!(DURATION.parse_val("66.6"));
1077        assert_err!(DURATION.parse_val("farragut"));
1078        assert_err!(DURATION.parse_val(""));
1079        assert_eq!(
1080            DURATION.parse_val("5 s"),
1081            Ok(ConfigVal::Duration(Duration::from_secs(5)))
1082        );
1083
1084        assert_eq!(
1085            JSON.parse_val("true"),
1086            Ok(ConfigVal::Json(serde_json::json!(true)))
1087        );
1088        assert_eq!(
1089            JSON.parse_val("false"),
1090            Ok(ConfigVal::Json(serde_json::json!(false)))
1091        );
1092        assert_eq!(
1093            JSON.parse_val("42"),
1094            Ok(ConfigVal::Json(serde_json::json!(42)))
1095        );
1096        assert_eq!(
1097            JSON.parse_val("66.6"),
1098            Ok(ConfigVal::Json(serde_json::json!(66.6)))
1099        );
1100        assert_err!(JSON.parse_val("farragut"));
1101        assert_err!(JSON.parse_val(""));
1102        assert_err!(JSON.parse_val("5 s"));
1103        assert_eq!(
1104            JSON.parse_val("{\"joe\": \"developer\"}"),
1105            Ok(ConfigVal::Json(serde_json::json!({"joe": "developer"})))
1106        );
1107    }
1108}