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