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/// A handle to a dynamically updatable configuration value.
69///
70/// This represents a strongly-typed named config of type `T`. It may be
71/// registered to a set of such configs with [ConfigSet::add] and then later
72/// used to retrieve the latest value at any time with [Self::get].
73///
74/// The supported types are [bool], [usize], [Duration], and [String], as well as [Option]
75/// variants of these as necessary.
76#[derive(Clone, Debug)]
77pub struct Config<D: ConfigDefault> {
78    name: &'static str,
79    desc: &'static str,
80    default: D,
81}
82
83impl<D: ConfigDefault> Config<D> {
84    /// Constructs a handle for a config of type `T`.
85    ///
86    /// It is best practice, but not strictly required, for the name to be
87    /// globally unique within a process.
88    ///
89    /// TODO(cfg): Add some sort of categorization of config purpose here: e.g.
90    /// limited-lifetime rollout flag, CYA, magic number that we never expect to
91    /// tune, magic number that we DO expect to tune, etc. This could be used to
92    /// power something like a `--future-default-flags` for CI, to replace part
93    /// or all of the manually maintained list.
94    ///
95    /// TODO(cfg): See if we can make this more Rust-y and take these params as
96    /// a struct (the obvious thing hits some issues with const combined with
97    /// Drop).
98    pub const fn new(name: &'static str, default: D, desc: &'static str) -> Self {
99        Config {
100            name,
101            default,
102            desc,
103        }
104    }
105
106    /// The name of this config.
107    pub fn name(&self) -> &str {
108        self.name
109    }
110
111    /// The description of this config.
112    pub fn desc(&self) -> &str {
113        self.desc
114    }
115
116    /// The default value of this config.
117    pub fn default(&self) -> &D {
118        &self.default
119    }
120
121    /// Returns the latest value of this config within the given set.
122    ///
123    /// Panics if this config was not previously registered to the set.
124    ///
125    /// TODO(cfg): Decide if this should be a method on `ConfigSet` instead to
126    /// match the precedent of `BTreeMap/HashMap::get` taking a key. It's like
127    /// this initially because it was thought that the `Config` definition was
128    /// the more important "noun" and also that rustfmt would maybe work better
129    /// on this ordering.
130    pub fn get(&self, set: &ConfigSet) -> D::ConfigType {
131        D::ConfigType::from_val(self.shared(set).load())
132    }
133
134    /// Returns a handle to the value of this config in the given set.
135    ///
136    /// This allows users to amortize the cost of the name lookup.
137    pub fn handle(&self, set: &ConfigSet) -> ConfigValHandle<D::ConfigType> {
138        ConfigValHandle {
139            val: self.shared(set).clone(),
140            _type: PhantomData,
141        }
142    }
143
144    /// Returns the shared value of this config in the given set.
145    fn shared<'a>(&self, set: &'a ConfigSet) -> &'a ConfigValAtomic {
146        &set.configs
147            .get(self.name)
148            .unwrap_or_else(|| panic!("config {} should be registered to set", self.name))
149            .val
150    }
151
152    /// Parse a string value for this config.
153    pub fn parse_val(&self, val: &str) -> Result<ConfigVal, String> {
154        let val = D::ConfigType::parse(val)?;
155        let val = Into::<ConfigVal>::into(val);
156        Ok(val)
157    }
158}
159
160/// A type usable as a [Config].
161pub trait ConfigType: Into<ConfigVal> + Clone + Sized {
162    /// Converts a type-erased enum value to this type.
163    ///
164    /// Panics if the enum's variant does not match this type.
165    fn from_val(val: ConfigVal) -> Self;
166
167    /// Parses this string slice into a [`ConfigType`].
168    fn parse(s: &str) -> Result<Self, String>;
169}
170
171/// A trait for a type that can be used as a default for a [`Config`].
172pub trait ConfigDefault: Clone {
173    type ConfigType: ConfigType;
174
175    /// Converts into the config type.
176    fn into_config_type(self) -> Self::ConfigType;
177}
178
179impl<T: ConfigType> ConfigDefault for T {
180    type ConfigType = T;
181
182    fn into_config_type(self) -> T {
183        self
184    }
185}
186
187impl<T: ConfigType> ConfigDefault for fn() -> T {
188    type ConfigType = T;
189
190    fn into_config_type(self) -> T {
191        (self)()
192    }
193}
194
195/// An set of [Config]s with values that may or may not be independent of other
196/// [ConfigSet]s.
197///
198/// When constructing a ConfigSet from scratch with [ConfigSet::default]
199/// followed by [ConfigSet::add], the values added to the ConfigSet will be
200/// independent of the values in all other ConfigSets.
201///
202/// When constructing a ConfigSet by cloning an existing ConfigSet, any values
203/// cloned from the original ConfigSet will be shared with the original
204/// ConfigSet. Updates to these values in one ConfigSet will be seen in the
205/// other ConfigSet, and vice versa. Any value added to the new ConfigSet via
206/// ConfigSet::add will be independent of values in the original ConfigSet,
207/// unless the new ConfigSet is later cloned.
208#[derive(Clone, Default)]
209pub struct ConfigSet {
210    configs: BTreeMap<String, ConfigEntry>,
211}
212
213impl ConfigSet {
214    /// Adds the given config to this set.
215    ///
216    /// Names are required to be unique within a set, but each set is entirely
217    /// independent. The same `Config` may be registered to multiple
218    /// [`ConfigSet`]s and thus have independent values (e.g. imagine a unit
219    /// test executing concurrently in the same process).
220    ///
221    /// Panics if a config with the same name has been previously registered
222    /// to this set.
223    pub fn add<D: ConfigDefault>(mut self, config: &Config<D>) -> Self {
224        let default = config.default.clone().into_config_type();
225        let default = Into::<ConfigVal>::into(default);
226        let config = ConfigEntry {
227            name: config.name,
228            desc: config.desc,
229            default: default.clone(),
230            val: ConfigValAtomic::from(default),
231        };
232        if let Some(prev) = self.configs.insert(config.name.to_owned(), config) {
233            panic!("{} registered twice", prev.name);
234        }
235        self
236    }
237
238    /// Returns the configs currently registered to this set.
239    pub fn entries(&self) -> impl Iterator<Item = &ConfigEntry> {
240        self.configs.values()
241    }
242
243    /// Returns the config with `name` registered to this set, if one exists.
244    pub fn entry(&self, name: &str) -> Option<&ConfigEntry> {
245        self.configs.get(name)
246    }
247}
248
249/// An entry for a config in a [ConfigSet].
250#[derive(Clone, Debug)]
251pub struct ConfigEntry {
252    name: &'static str,
253    desc: &'static str,
254    default: ConfigVal,
255    val: ConfigValAtomic,
256}
257
258impl ConfigEntry {
259    /// The name of this config.
260    pub fn name(&self) -> &'static str {
261        self.name
262    }
263
264    /// The description of this config.
265    pub fn desc(&self) -> &'static str {
266        self.desc
267    }
268
269    /// The default value of this config.
270    ///
271    /// This value is never updated.
272    pub fn default(&self) -> &ConfigVal {
273        &self.default
274    }
275
276    /// The value of this config in the set.
277    pub fn val(&self) -> ConfigVal {
278        self.val.load()
279    }
280}
281
282/// A handle to a configuration value in a [`ConfigSet`].
283///
284/// Allows users to amortize the lookup of a name within a set.
285///
286/// Handles can be cheaply cloned.
287#[derive(Debug, Clone)]
288pub struct ConfigValHandle<T> {
289    val: ConfigValAtomic,
290    _type: PhantomData<T>,
291}
292
293impl<T: ConfigType> ConfigValHandle<T> {
294    /// Returns the latest value of this config within the set associated with
295    /// the handle.
296    pub fn get(&self) -> T {
297        T::from_val(self.val.load())
298    }
299
300    /// Return a new handle that returns the constant value provided,
301    /// generally for testing.
302    pub fn disconnected<X>(value: X) -> Self
303    where
304        X: ConfigDefault<ConfigType = T>,
305    {
306        let config_val: ConfigVal = value.into_config_type().into();
307        Self {
308            val: config_val.into(),
309            _type: Default::default(),
310        }
311    }
312}
313
314/// A type-erased configuration value for when set of different types are stored
315/// in a collection.
316#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
317pub enum ConfigVal {
318    /// A `bool` value.
319    Bool(bool),
320    /// A `u32` value.
321    U32(u32),
322    /// A `usize` value.
323    Usize(usize),
324    /// An `Option<usize>` value.
325    OptUsize(Option<usize>),
326    /// An `f64` value.
327    F64(f64),
328    /// A `String` value.
329    String(String),
330    /// A `Duration` value.
331    Duration(Duration),
332    /// A JSON value.
333    #[serde(with = "serde_json_string")]
334    Json(serde_json::Value),
335}
336
337/// To make `ConfigVal` compatible with non-self-describing serialization formats like bincode,
338/// serialize JSON values as strings.
339mod serde_json_string {
340    use serde::de::{Deserialize, Deserializer, Error};
341    use serde::ser::Serializer;
342
343    pub fn serialize<S>(value: &serde_json::Value, serializer: S) -> Result<S::Ok, S::Error>
344    where
345        S: Serializer,
346    {
347        serializer.serialize_str(&value.to_string())
348    }
349
350    pub fn deserialize<'de, D>(deserializer: D) -> Result<serde_json::Value, D::Error>
351    where
352        D: Deserializer<'de>,
353    {
354        let s = String::deserialize(deserializer)?;
355        serde_json::from_str(&s).map_err(D::Error::custom)
356    }
357}
358
359/// An atomic version of [`ConfigVal`] to allow configuration values to be
360/// shared between configuration writers and readers.
361///
362/// TODO(cfg): Consider moving these Arcs to be a single one around the map in
363/// `ConfigSet` instead. That would mean less pointer-chasing in the common
364/// case, but would remove the possibility of amortizing the name lookup via
365/// [Config::handle].
366#[derive(Clone, Debug)]
367enum ConfigValAtomic {
368    Bool(Arc<AtomicBool>),
369    U32(Arc<AtomicU32>),
370    Usize(Arc<AtomicUsize>),
371    OptUsize(Arc<RwLock<Option<usize>>>),
372    // Shared via to_bits/from_bits so we can use the atomic instead of Mutex.
373    F64(Arc<AtomicU64>),
374    String(Arc<RwLock<String>>),
375    Duration(Arc<RwLock<Duration>>),
376    Json(Arc<RwLock<serde_json::Value>>),
377}
378
379impl From<ConfigVal> for ConfigValAtomic {
380    fn from(val: ConfigVal) -> ConfigValAtomic {
381        match val {
382            ConfigVal::Bool(x) => ConfigValAtomic::Bool(Arc::new(AtomicBool::new(x))),
383            ConfigVal::U32(x) => ConfigValAtomic::U32(Arc::new(AtomicU32::new(x))),
384            ConfigVal::Usize(x) => ConfigValAtomic::Usize(Arc::new(AtomicUsize::new(x))),
385            ConfigVal::OptUsize(x) => ConfigValAtomic::OptUsize(Arc::new(RwLock::new(x))),
386            ConfigVal::F64(x) => ConfigValAtomic::F64(Arc::new(AtomicU64::new(x.to_bits()))),
387            ConfigVal::String(x) => ConfigValAtomic::String(Arc::new(RwLock::new(x))),
388            ConfigVal::Duration(x) => ConfigValAtomic::Duration(Arc::new(RwLock::new(x))),
389            ConfigVal::Json(x) => ConfigValAtomic::Json(Arc::new(RwLock::new(x))),
390        }
391    }
392}
393
394impl ConfigValAtomic {
395    fn load(&self) -> ConfigVal {
396        match self {
397            ConfigValAtomic::Bool(x) => ConfigVal::Bool(x.load(SeqCst)),
398            ConfigValAtomic::U32(x) => ConfigVal::U32(x.load(SeqCst)),
399            ConfigValAtomic::Usize(x) => ConfigVal::Usize(x.load(SeqCst)),
400            ConfigValAtomic::OptUsize(x) => ConfigVal::OptUsize(*x.read().expect("lock poisoned")),
401            ConfigValAtomic::F64(x) => ConfigVal::F64(f64::from_bits(x.load(SeqCst))),
402            ConfigValAtomic::String(x) => {
403                ConfigVal::String(x.read().expect("lock poisoned").clone())
404            }
405            ConfigValAtomic::Duration(x) => ConfigVal::Duration(*x.read().expect("lock poisoned")),
406            ConfigValAtomic::Json(x) => ConfigVal::Json(x.read().expect("lock poisoned").clone()),
407        }
408    }
409
410    fn store(&self, val: ConfigVal) {
411        match (self, val) {
412            (ConfigValAtomic::Bool(x), ConfigVal::Bool(val)) => x.store(val, SeqCst),
413            (ConfigValAtomic::U32(x), ConfigVal::U32(val)) => x.store(val, SeqCst),
414            (ConfigValAtomic::Usize(x), ConfigVal::Usize(val)) => x.store(val, SeqCst),
415            (ConfigValAtomic::OptUsize(x), ConfigVal::OptUsize(val)) => {
416                *x.write().expect("lock poisoned") = val
417            }
418            (ConfigValAtomic::F64(x), ConfigVal::F64(val)) => x.store(val.to_bits(), SeqCst),
419            (ConfigValAtomic::String(x), ConfigVal::String(val)) => {
420                *x.write().expect("lock poisoned") = val
421            }
422            (ConfigValAtomic::Duration(x), ConfigVal::Duration(val)) => {
423                *x.write().expect("lock poisoned") = val
424            }
425            (ConfigValAtomic::Json(x), ConfigVal::Json(val)) => {
426                *x.write().expect("lock poisoned") = val
427            }
428            (ConfigValAtomic::Bool(_), val)
429            | (ConfigValAtomic::U32(_), val)
430            | (ConfigValAtomic::Usize(_), val)
431            | (ConfigValAtomic::OptUsize(_), val)
432            | (ConfigValAtomic::F64(_), val)
433            | (ConfigValAtomic::String(_), val)
434            | (ConfigValAtomic::Duration(_), val)
435            | (ConfigValAtomic::Json(_), val) => {
436                panic!("attempted to store {val:?} value in {self:?} parameter")
437            }
438        }
439    }
440}
441
442/// A batch of value updates to [Config]s in a [ConfigSet].
443///
444/// This may be sent across processes to apply the same value updates, but may not be durably
445/// written down.
446#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
447pub struct ConfigUpdates {
448    pub updates: BTreeMap<String, ConfigVal>,
449}
450
451impl ConfigUpdates {
452    /// Adds an update for the given config and value.
453    ///
454    /// If a value of the same config has previously been added to these
455    /// updates, replaces it.
456    pub fn add<T, U>(&mut self, config: &Config<T>, val: U)
457    where
458        T: ConfigDefault,
459        U: ConfigDefault<ConfigType = T::ConfigType>,
460    {
461        self.add_dynamic(config.name, val.into_config_type().into());
462    }
463
464    /// Adds an update for the given configuration name and value.
465    ///
466    /// It is the callers responsibility to ensure the value is of the
467    /// appropriate type for the configuration.
468    ///
469    /// If a value of the same config has previously been added to these
470    /// updates, replaces it.
471    pub fn add_dynamic(&mut self, name: &str, val: ConfigVal) {
472        self.updates.insert(name.to_owned(), val);
473    }
474
475    /// Adds the entries in `other` to `self`, with `other` taking precedence.
476    pub fn extend(&mut self, mut other: Self) {
477        self.updates.append(&mut other.updates)
478    }
479
480    /// Applies these config updates to the given [ConfigSet].
481    ///
482    /// This doesn't need to be the same set that the value updates were added
483    /// from. In fact, the primary use of this is propagating config updates
484    /// across processes.
485    ///
486    /// The value updates for any configs unknown by the given set are skipped.
487    /// Ditto for config type mismatches. However, this is unexpected usage at
488    /// present and so is logged to Sentry.
489    pub fn apply(&self, set: &ConfigSet) {
490        for (name, val) in self.updates.iter() {
491            let Some(config) = set.configs.get(name) else {
492                error!("config update {} {:?} not known set: {:?}", name, val, set);
493                continue;
494            };
495            config.val.store(val.clone());
496        }
497    }
498}
499
500mod impls {
501    use std::num::{ParseFloatError, ParseIntError};
502    use std::str::ParseBoolError;
503    use std::time::Duration;
504
505    use crate::{ConfigDefault, ConfigSet, ConfigType, ConfigVal};
506
507    impl ConfigType for bool {
508        fn from_val(val: ConfigVal) -> Self {
509            match val {
510                ConfigVal::Bool(x) => x,
511                x => panic!("expected bool value got {:?}", x),
512            }
513        }
514
515        fn parse(s: &str) -> Result<Self, String> {
516            match s {
517                "on" => return Ok(true),
518                "off" => return Ok(false),
519                _ => {}
520            }
521            s.parse().map_err(|e: ParseBoolError| e.to_string())
522        }
523    }
524
525    impl From<bool> for ConfigVal {
526        fn from(val: bool) -> ConfigVal {
527            ConfigVal::Bool(val)
528        }
529    }
530
531    impl ConfigType for u32 {
532        fn from_val(val: ConfigVal) -> Self {
533            match val {
534                ConfigVal::U32(x) => x,
535                x => panic!("expected u32 value got {:?}", x),
536            }
537        }
538
539        fn parse(s: &str) -> Result<Self, String> {
540            s.parse().map_err(|e: ParseIntError| e.to_string())
541        }
542    }
543
544    impl From<u32> for ConfigVal {
545        fn from(val: u32) -> ConfigVal {
546            ConfigVal::U32(val)
547        }
548    }
549
550    impl ConfigType for usize {
551        fn from_val(val: ConfigVal) -> Self {
552            match val {
553                ConfigVal::Usize(x) => x,
554                x => panic!("expected usize value got {:?}", x),
555            }
556        }
557
558        fn parse(s: &str) -> Result<Self, String> {
559            s.parse().map_err(|e: ParseIntError| e.to_string())
560        }
561    }
562
563    impl From<usize> for ConfigVal {
564        fn from(val: usize) -> ConfigVal {
565            ConfigVal::Usize(val)
566        }
567    }
568
569    impl ConfigType for Option<usize> {
570        fn from_val(val: ConfigVal) -> Self {
571            match val {
572                ConfigVal::OptUsize(x) => x,
573                x => panic!("expected usize value got {:?}", x),
574            }
575        }
576
577        fn parse(s: &str) -> Result<Self, String> {
578            if s.is_empty() {
579                Ok(None)
580            } else {
581                let val = s.parse().map_err(|e: ParseIntError| e.to_string())?;
582                Ok(Some(val))
583            }
584        }
585    }
586
587    impl From<Option<usize>> for ConfigVal {
588        fn from(val: Option<usize>) -> ConfigVal {
589            ConfigVal::OptUsize(val)
590        }
591    }
592
593    impl ConfigType for f64 {
594        fn from_val(val: ConfigVal) -> Self {
595            match val {
596                ConfigVal::F64(x) => x,
597                x => panic!("expected f64 value got {:?}", x),
598            }
599        }
600
601        fn parse(s: &str) -> Result<Self, String> {
602            s.parse().map_err(|e: ParseFloatError| e.to_string())
603        }
604    }
605
606    impl From<f64> for ConfigVal {
607        fn from(val: f64) -> ConfigVal {
608            ConfigVal::F64(val)
609        }
610    }
611
612    impl ConfigType for String {
613        fn from_val(val: ConfigVal) -> Self {
614            match val {
615                ConfigVal::String(x) => x,
616                x => panic!("expected String value got {:?}", x),
617            }
618        }
619
620        fn parse(s: &str) -> Result<Self, String> {
621            Ok(s.to_string())
622        }
623    }
624
625    impl From<String> for ConfigVal {
626        fn from(val: String) -> ConfigVal {
627            ConfigVal::String(val)
628        }
629    }
630
631    impl ConfigDefault for &str {
632        type ConfigType = String;
633
634        fn into_config_type(self) -> String {
635            self.into()
636        }
637    }
638
639    impl ConfigType for Duration {
640        fn from_val(val: ConfigVal) -> Self {
641            match val {
642                ConfigVal::Duration(x) => x,
643                x => panic!("expected Duration value got {:?}", x),
644            }
645        }
646
647        fn parse(s: &str) -> Result<Self, String> {
648            humantime::parse_duration(s).map_err(|e| e.to_string())
649        }
650    }
651
652    impl From<Duration> for ConfigVal {
653        fn from(val: Duration) -> ConfigVal {
654            ConfigVal::Duration(val)
655        }
656    }
657
658    impl ConfigType for serde_json::Value {
659        fn from_val(val: ConfigVal) -> Self {
660            match val {
661                ConfigVal::Json(x) => x,
662                x => panic!("expected JSON value got {:?}", x),
663            }
664        }
665
666        fn parse(s: &str) -> Result<Self, String> {
667            serde_json::from_str(s).map_err(|e| e.to_string())
668        }
669    }
670
671    impl From<serde_json::Value> for ConfigVal {
672        fn from(val: serde_json::Value) -> ConfigVal {
673            ConfigVal::Json(val)
674        }
675    }
676
677    impl std::fmt::Debug for ConfigSet {
678        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
679            let ConfigSet { configs } = self;
680            f.debug_map()
681                .entries(configs.iter().map(|(name, val)| (name, val.val())))
682                .finish()
683        }
684    }
685}
686
687#[cfg(test)]
688mod tests {
689    use super::*;
690
691    use mz_ore::assert_err;
692
693    const BOOL: Config<bool> = Config::new("bool", true, "");
694    const U32: Config<u32> = Config::new("u32", 4, "");
695    const USIZE: Config<usize> = Config::new("usize", 1, "");
696    const OPT_USIZE: Config<Option<usize>> = Config::new("opt_usize", Some(2), "");
697    const F64: Config<f64> = Config::new("f64", 5.0, "");
698    const STRING: Config<&str> = Config::new("string", "a", "");
699    const DURATION: Config<Duration> = Config::new("duration", Duration::from_nanos(3), "");
700    const JSON: Config<fn() -> serde_json::Value> =
701        Config::new("json", || serde_json::json!({}), "");
702
703    #[mz_ore::test]
704    fn all_types() {
705        let configs = ConfigSet::default()
706            .add(&BOOL)
707            .add(&USIZE)
708            .add(&U32)
709            .add(&OPT_USIZE)
710            .add(&F64)
711            .add(&STRING)
712            .add(&DURATION)
713            .add(&JSON);
714        assert_eq!(BOOL.get(&configs), true);
715        assert_eq!(U32.get(&configs), 4);
716        assert_eq!(USIZE.get(&configs), 1);
717        assert_eq!(OPT_USIZE.get(&configs), Some(2));
718        assert_eq!(F64.get(&configs), 5.0);
719        assert_eq!(STRING.get(&configs), "a");
720        assert_eq!(DURATION.get(&configs), Duration::from_nanos(3));
721        assert_eq!(JSON.get(&configs), serde_json::json!({}));
722
723        let mut updates = ConfigUpdates::default();
724        updates.add(&BOOL, false);
725        updates.add(&U32, 7);
726        updates.add(&USIZE, 2);
727        updates.add(&OPT_USIZE, None);
728        updates.add(&F64, 8.0);
729        updates.add(&STRING, "b");
730        updates.add(&DURATION, Duration::from_nanos(4));
731        updates.add(&JSON, serde_json::json!({"a": 1}));
732        updates.apply(&configs);
733
734        assert_eq!(BOOL.get(&configs), false);
735        assert_eq!(U32.get(&configs), 7);
736        assert_eq!(USIZE.get(&configs), 2);
737        assert_eq!(OPT_USIZE.get(&configs), None);
738        assert_eq!(F64.get(&configs), 8.0);
739        assert_eq!(STRING.get(&configs), "b");
740        assert_eq!(DURATION.get(&configs), Duration::from_nanos(4));
741        assert_eq!(JSON.get(&configs), serde_json::json!({"a": 1}));
742    }
743
744    #[mz_ore::test]
745    fn fn_default() {
746        const BOOL_FN_DEFAULT: Config<fn() -> bool> = Config::new("bool", || !true, "");
747        const STRING_FN_DEFAULT: Config<fn() -> String> =
748            Config::new("string", || "x".repeat(3), "");
749
750        let configs = ConfigSet::default()
751            .add(&BOOL_FN_DEFAULT)
752            .add(&STRING_FN_DEFAULT);
753        assert_eq!(BOOL_FN_DEFAULT.get(&configs), false);
754        assert_eq!(STRING_FN_DEFAULT.get(&configs), "xxx");
755    }
756
757    #[mz_ore::test]
758    fn config_set() {
759        let c0 = ConfigSet::default().add(&USIZE);
760        assert_eq!(USIZE.get(&c0), 1);
761        let mut updates = ConfigUpdates::default();
762        updates.add(&USIZE, 2);
763        updates.apply(&c0);
764        assert_eq!(USIZE.get(&c0), 2);
765
766        // Each ConfigSet is independent, even if they contain the same set of
767        // configs.
768        let c1 = ConfigSet::default().add(&USIZE);
769        assert_eq!(USIZE.get(&c1), 1);
770        let mut updates = ConfigUpdates::default();
771        updates.add(&USIZE, 3);
772        updates.apply(&c1);
773        assert_eq!(USIZE.get(&c1), 3);
774        assert_eq!(USIZE.get(&c0), 2);
775
776        // We can copy values from one to the other, though (envd -> clusterd).
777        let mut updates = ConfigUpdates::default();
778        for e in c0.entries() {
779            updates.add_dynamic(e.name, e.val());
780        }
781        assert_eq!(USIZE.get(&c1), 3);
782        updates.apply(&c1);
783        assert_eq!(USIZE.get(&c1), 2);
784    }
785
786    #[mz_ore::test]
787    fn config_updates_extend() {
788        // Regression test for database-issues#7793.
789        //
790        // Construct two ConfigUpdates with overlapping, but not identical, sets
791        // of configs. Combine them and assert that the expected number of
792        // updates is present.
793        let mut u1 = {
794            let c = ConfigSet::default().add(&USIZE).add(&STRING);
795            let mut x = ConfigUpdates::default();
796            for e in c.entries() {
797                x.add_dynamic(e.name(), e.val());
798            }
799            x
800        };
801        let u2 = {
802            let c = ConfigSet::default().add(&USIZE).add(&DURATION);
803            let mut updates = ConfigUpdates::default();
804            updates.add(&USIZE, 2);
805            updates.apply(&c);
806            let mut x = ConfigUpdates::default();
807            for e in c.entries() {
808                x.add_dynamic(e.name(), e.val());
809            }
810            x
811        };
812        assert_eq!(u1.updates.len(), 2);
813        assert_eq!(u2.updates.len(), 2);
814        u1.extend(u2);
815        assert_eq!(u1.updates.len(), 3);
816
817        // Assert that extend kept the correct (later) value for the overlapping
818        // config.
819        let c = ConfigSet::default().add(&USIZE);
820        u1.apply(&c);
821        assert_eq!(USIZE.get(&c), 2);
822    }
823
824    #[mz_ore::test]
825    fn config_parse() {
826        assert_eq!(BOOL.parse_val("true"), Ok(ConfigVal::Bool(true)));
827        assert_eq!(BOOL.parse_val("on"), Ok(ConfigVal::Bool(true)));
828        assert_eq!(BOOL.parse_val("false"), Ok(ConfigVal::Bool(false)));
829        assert_eq!(BOOL.parse_val("off"), Ok(ConfigVal::Bool(false)));
830        assert_err!(BOOL.parse_val("42"));
831        assert_err!(BOOL.parse_val("66.6"));
832        assert_err!(BOOL.parse_val("farragut"));
833        assert_err!(BOOL.parse_val(""));
834        assert_err!(BOOL.parse_val("5 s"));
835
836        assert_err!(U32.parse_val("true"));
837        assert_err!(U32.parse_val("false"));
838        assert_eq!(U32.parse_val("42"), Ok(ConfigVal::U32(42)));
839        assert_err!(U32.parse_val("66.6"));
840        assert_err!(U32.parse_val("farragut"));
841        assert_err!(U32.parse_val(""));
842        assert_err!(U32.parse_val("5 s"));
843
844        assert_err!(USIZE.parse_val("true"));
845        assert_err!(USIZE.parse_val("false"));
846        assert_eq!(USIZE.parse_val("42"), Ok(ConfigVal::Usize(42)));
847        assert_err!(USIZE.parse_val("66.6"));
848        assert_err!(USIZE.parse_val("farragut"));
849        assert_err!(USIZE.parse_val(""));
850        assert_err!(USIZE.parse_val("5 s"));
851
852        assert_err!(OPT_USIZE.parse_val("true"));
853        assert_err!(OPT_USIZE.parse_val("false"));
854        assert_eq!(OPT_USIZE.parse_val("42"), Ok(ConfigVal::OptUsize(Some(42))));
855        assert_err!(OPT_USIZE.parse_val("66.6"));
856        assert_err!(OPT_USIZE.parse_val("farragut"));
857        assert_eq!(OPT_USIZE.parse_val(""), Ok(ConfigVal::OptUsize(None)));
858        assert_err!(OPT_USIZE.parse_val("5 s"));
859
860        assert_err!(F64.parse_val("true"));
861        assert_err!(F64.parse_val("false"));
862        assert_eq!(F64.parse_val("42"), Ok(ConfigVal::F64(42.0)));
863        assert_eq!(F64.parse_val("66.6"), Ok(ConfigVal::F64(66.6)));
864        assert_err!(F64.parse_val("farragut"));
865        assert_err!(F64.parse_val(""));
866        assert_err!(F64.parse_val("5 s"));
867
868        assert_eq!(
869            STRING.parse_val("true"),
870            Ok(ConfigVal::String("true".to_string()))
871        );
872        assert_eq!(
873            STRING.parse_val("false"),
874            Ok(ConfigVal::String("false".to_string()))
875        );
876        assert_eq!(
877            STRING.parse_val("66.6"),
878            Ok(ConfigVal::String("66.6".to_string()))
879        );
880        assert_eq!(
881            STRING.parse_val("42"),
882            Ok(ConfigVal::String("42".to_string()))
883        );
884        assert_eq!(
885            STRING.parse_val("farragut"),
886            Ok(ConfigVal::String("farragut".to_string()))
887        );
888        assert_eq!(STRING.parse_val(""), Ok(ConfigVal::String("".to_string())));
889        assert_eq!(
890            STRING.parse_val("5 s"),
891            Ok(ConfigVal::String("5 s".to_string()))
892        );
893
894        assert_err!(DURATION.parse_val("true"));
895        assert_err!(DURATION.parse_val("false"));
896        assert_err!(DURATION.parse_val("42"));
897        assert_err!(DURATION.parse_val("66.6"));
898        assert_err!(DURATION.parse_val("farragut"));
899        assert_err!(DURATION.parse_val(""));
900        assert_eq!(
901            DURATION.parse_val("5 s"),
902            Ok(ConfigVal::Duration(Duration::from_secs(5)))
903        );
904
905        assert_eq!(
906            JSON.parse_val("true"),
907            Ok(ConfigVal::Json(serde_json::json!(true)))
908        );
909        assert_eq!(
910            JSON.parse_val("false"),
911            Ok(ConfigVal::Json(serde_json::json!(false)))
912        );
913        assert_eq!(
914            JSON.parse_val("42"),
915            Ok(ConfigVal::Json(serde_json::json!(42)))
916        );
917        assert_eq!(
918            JSON.parse_val("66.6"),
919            Ok(ConfigVal::Json(serde_json::json!(66.6)))
920        );
921        assert_err!(JSON.parse_val("farragut"));
922        assert_err!(JSON.parse_val(""));
923        assert_err!(JSON.parse_val("5 s"));
924        assert_eq!(
925            JSON.parse_val("{\"joe\": \"developer\"}"),
926            Ok(ConfigVal::Json(serde_json::json!({"joe": "developer"})))
927        );
928    }
929}