1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
// Copyright Materialize, Inc. and contributors. All rights reserved.
//
// Use of this software is governed by the Business Source License
// included in the LICENSE file.
//
// As of the Change Date specified in that file, in accordance with
// the Business Source License, use of this software will be governed
// by the Apache License, Version 2.0.

//! Dynamically updatable configuration.
//!
//! Basic usage:
//! - A type-safe static `Config` is defined near where it is used.
//! - Once in the lifetime of a process, all interesting `Config`s are
//!   registered to a `ConfigSet`. The values within a `ConfigSet` are shared,
//!   though multiple `ConfigSet`s may be created and each are completely
//!   independent (i.e. one in each unit test).
//! - A `ConfigSet` is plumbed around as necessary and may be used to get or
//!   set the value of `Config`.
//!
//! ```
//! # use mz_dyncfg::{Config, ConfigSet};
//! const FOO: Config<bool> = Config::new("foo", false, "description of foo");
//! fn bar(cfg: &ConfigSet) {
//!     assert_eq!(FOO.get(&cfg), false);
//! }
//! fn main() {
//!     let cfg = ConfigSet::default().add(&FOO);
//!     bar(&cfg);
//! }
//! ```
//!
//! # Design considerations for this library
//!
//! - The primary motivation is minimal boilerplate. Runtime dynamic
//!   configuration is one of the most powerful tools we have to quickly react
//!   to incidents, etc in production. Adding and using them should be easy
//!   enough that engineers feel empowered to use them generously.
//!
//!   The theoretical minimum boilerplate is 1) declare a config and 2) use a
//!   config to get/set the value. These could be combined into one step if (2)
//!   were based on global state, but that doesn't play well with testing. So
//!   instead we accomplish (2) by constructing a shared bag of config values in
//!   each `fn main`, amortizing the cost by plumbing it once to each component
//!   (not once per config).
//! - Config definitions are kept next to the usage. The common case is that a
//!   config is used in only one place and this makes it easy to see the
//!   associated documentation at the usage site. Configs that are used in
//!   multiple places may be defined in some common place as appropriate.
//! - Everything is type-safe.
//! - Secondarily: set up the ability to get and use the latest values of
//!   configs in tooling like `persistcli` and `stash debug`. As we've embraced
//!   dynamic configuration, we've ended up in a situation where it's stressful
//!   to run the read-write `persistcli admin` tooling with the defaults
//!   compiled into code, but `persistcli` doesn't have access to the vars stuff
//!   and doesn't want to instantiate a catalog impl.

use std::collections::BTreeMap;
use std::marker::PhantomData;
use std::sync::atomic::Ordering::SeqCst;
use std::sync::atomic::{AtomicBool, AtomicU32, AtomicUsize};
use std::sync::{Arc, RwLock};
use std::time::Duration;

use tracing::error;

use mz_proto::{ProtoType, RustType};

include!(concat!(env!("OUT_DIR"), "/mz_dyncfg.rs"));

/// A handle to a dynamically updatable configuration value.
///
/// This represents a strongly-typed named config of type `T`. It may be
/// registered to a set of such configs with [ConfigSet::add] and then later
/// used to retrieve the latest value at any time with [Self::get].
///
/// The supported types are [bool], [usize], [Duration], and [String], as well as [Option]
/// variants of these as necessary.
#[derive(Clone, Debug)]
pub struct Config<D: ConfigDefault> {
    name: &'static str,
    desc: &'static str,
    default: D,
}

impl<D: ConfigDefault> Config<D> {
    /// Constructs a handle for a config of type `T`.
    ///
    /// It is best practice, but not strictly required, for the name to be
    /// globally unique within a process.
    ///
    /// TODO(cfg): Add some sort of categorization of config purpose here: e.g.
    /// limited-lifetime rollout flag, CYA, magic number that we never expect to
    /// tune, magic number that we DO expect to tune, etc. This could be used to
    /// power something like a `--future-default-flags` for CI, to replace part
    /// or all of the manually maintained list.
    ///
    /// TODO(cfg): See if we can make this more Rust-y and take these params as
    /// a struct (the obvious thing hits some issues with const combined with
    /// Drop).
    pub const fn new(name: &'static str, default: D, desc: &'static str) -> Self {
        Config {
            name,
            default,
            desc,
        }
    }

    /// The name of this config.
    pub fn name(&self) -> &str {
        self.name
    }

    /// The description of this config.
    pub fn desc(&self) -> &str {
        self.desc
    }

    /// The default value of this config.
    pub fn default(&self) -> &D {
        &self.default
    }

    /// Returns the latest value of this config within the given set.
    ///
    /// Panics if this config was not previously registered to the set.
    ///
    /// TODO(cfg): Decide if this should be a method on `ConfigSet` instead to
    /// match the precedent of `BTreeMap/HashMap::get` taking a key. It's like
    /// this initially because it was thought that the `Config` definition was
    /// the more important "noun" and also that rustfmt would maybe work better
    /// on this ordering.
    pub fn get(&self, set: &ConfigSet) -> D::ConfigType {
        D::ConfigType::from_val(self.shared(set).load())
    }

    /// Returns a handle to the value of this config in the given set.
    ///
    /// This allows users to amortize the cost of the name lookup.
    pub fn handle(&self, set: &ConfigSet) -> ConfigValHandle<D::ConfigType> {
        ConfigValHandle {
            val: self.shared(set).clone(),
            _type: PhantomData,
        }
    }

    /// Returns the shared value of this config in the given set.
    fn shared<'a>(&self, set: &'a ConfigSet) -> &'a ConfigValAtomic {
        &set.configs
            .get(self.name)
            .unwrap_or_else(|| panic!("config {} should be registered to set", self.name))
            .val
    }
}

/// A type usable as a [Config].
pub trait ConfigType: Into<ConfigVal> + Clone + Sized {
    /// Converts a type-erased enum value to this type.
    ///
    /// Panics if the enum's variant does not match this type.
    fn from_val(val: ConfigVal) -> Self;
}

/// A trait for a type that can be used as a default for a [`Config`].
pub trait ConfigDefault: Clone {
    type ConfigType: ConfigType;

    /// Converts into the config type.
    fn into_config_type(self) -> Self::ConfigType;
}

impl<T: ConfigType> ConfigDefault for T {
    type ConfigType = T;

    fn into_config_type(self) -> T {
        self
    }
}

impl<T: ConfigType> ConfigDefault for fn() -> T {
    type ConfigType = T;

    fn into_config_type(self) -> T {
        (self)()
    }
}

/// An set of [Config]s with values independent of other [ConfigSet]s (even if
/// they contain the same configs).
#[derive(Clone, Default)]
pub struct ConfigSet {
    configs: BTreeMap<String, ConfigEntry>,
}

impl ConfigSet {
    /// Adds the given config to this set.
    ///
    /// Names are required to be unique within a set, but each set is entirely
    /// independent. The same `Config` may be registered to multiple
    /// [`ConfigSet`]s and thus have independent values (e.g. imagine a unit
    /// test executing concurrently in the same process).
    ///
    /// Panics if a config with the same name has been previously registered
    /// to this set.
    pub fn add<D: ConfigDefault>(mut self, config: &Config<D>) -> Self {
        let default = config.default.clone().into_config_type();
        let default = Into::<ConfigVal>::into(default);
        let config = ConfigEntry {
            name: config.name,
            desc: config.desc,
            default: default.clone(),
            val: ConfigValAtomic::from(default),
        };
        if let Some(prev) = self.configs.insert(config.name.to_owned(), config) {
            panic!("{} registered twice", prev.name);
        }
        self
    }

    /// Returns the configs currently registered to this set.
    pub fn entries(&self) -> impl Iterator<Item = &ConfigEntry> {
        self.configs.values()
    }
}

/// An entry for a config in a [ConfigSet].
#[derive(Clone, Debug)]
pub struct ConfigEntry {
    name: &'static str,
    desc: &'static str,
    default: ConfigVal,
    val: ConfigValAtomic,
}

impl ConfigEntry {
    /// The name of this config.
    pub fn name(&self) -> &'static str {
        self.name
    }

    /// The description of this config.
    pub fn desc(&self) -> &'static str {
        self.desc
    }

    /// The default value of this config.
    ///
    /// This value is never updated.
    pub fn default(&self) -> &ConfigVal {
        &self.default
    }

    /// The value of this config in the set.
    pub fn val(&self) -> ConfigVal {
        self.val.load()
    }
}

/// A handle to a configuration value in a [`ConfigSet`].
///
/// Allows users to amortize the lookup of a name within a set.
///
/// Handles can be cheaply cloned.
#[derive(Debug, Clone)]
pub struct ConfigValHandle<T> {
    val: ConfigValAtomic,
    _type: PhantomData<T>,
}

impl<T: ConfigType> ConfigValHandle<T> {
    /// Returns the latest value of this config within the set associated with
    /// the handle.
    pub fn get(&self) -> T {
        T::from_val(self.val.load())
    }
}

/// A type-erased configuration value for when set of different types are stored
/// in a collection.
#[derive(Clone, Debug)]
pub enum ConfigVal {
    /// A `bool` value.
    Bool(bool),
    /// A `u32` value.
    U32(u32),
    /// A `usize` value.
    Usize(usize),
    /// An `Option<usize>` value.
    OptUsize(Option<usize>),
    /// A `String` value.
    String(String),
    /// A `Duration` value.
    Duration(Duration),
    /// A JSON value.
    Json(serde_json::Value),
}

/// An atomic version of [`ConfigVal`] to allow configuration values to be
/// shared between configuration writers and readers.
///
/// TODO(cfg): Consider moving these Arcs to be a single one around the map in
/// `ConfigSet` instead. That would mean less pointer-chasing in the common
/// case, but would remove the possibility of amortizing the name lookup via
/// [Config::handle].
#[derive(Clone, Debug)]
enum ConfigValAtomic {
    Bool(Arc<AtomicBool>),
    U32(Arc<AtomicU32>),
    Usize(Arc<AtomicUsize>),
    OptUsize(Arc<RwLock<Option<usize>>>),
    String(Arc<RwLock<String>>),
    Duration(Arc<RwLock<Duration>>),
    Json(Arc<RwLock<serde_json::Value>>),
}

impl From<ConfigVal> for ConfigValAtomic {
    fn from(val: ConfigVal) -> ConfigValAtomic {
        match val {
            ConfigVal::Bool(x) => ConfigValAtomic::Bool(Arc::new(AtomicBool::new(x))),
            ConfigVal::U32(x) => ConfigValAtomic::U32(Arc::new(AtomicU32::new(x))),
            ConfigVal::Usize(x) => ConfigValAtomic::Usize(Arc::new(AtomicUsize::new(x))),
            ConfigVal::OptUsize(x) => ConfigValAtomic::OptUsize(Arc::new(RwLock::new(x))),
            ConfigVal::String(x) => ConfigValAtomic::String(Arc::new(RwLock::new(x))),
            ConfigVal::Duration(x) => ConfigValAtomic::Duration(Arc::new(RwLock::new(x))),
            ConfigVal::Json(x) => ConfigValAtomic::Json(Arc::new(RwLock::new(x))),
        }
    }
}

impl ConfigValAtomic {
    fn load(&self) -> ConfigVal {
        match self {
            ConfigValAtomic::Bool(x) => ConfigVal::Bool(x.load(SeqCst)),
            ConfigValAtomic::U32(x) => ConfigVal::U32(x.load(SeqCst)),
            ConfigValAtomic::Usize(x) => ConfigVal::Usize(x.load(SeqCst)),
            ConfigValAtomic::OptUsize(x) => ConfigVal::OptUsize(*x.read().expect("lock poisoned")),
            ConfigValAtomic::String(x) => {
                ConfigVal::String(x.read().expect("lock poisoned").clone())
            }
            ConfigValAtomic::Duration(x) => ConfigVal::Duration(*x.read().expect("lock poisoned")),
            ConfigValAtomic::Json(x) => ConfigVal::Json(x.read().expect("lock poisoned").clone()),
        }
    }

    fn store(&self, val: ConfigVal) {
        match (self, val) {
            (ConfigValAtomic::Bool(x), ConfigVal::Bool(val)) => x.store(val, SeqCst),
            (ConfigValAtomic::U32(x), ConfigVal::U32(val)) => x.store(val, SeqCst),
            (ConfigValAtomic::Usize(x), ConfigVal::Usize(val)) => x.store(val, SeqCst),
            (ConfigValAtomic::OptUsize(x), ConfigVal::OptUsize(val)) => {
                *x.write().expect("lock poisoned") = val
            }
            (ConfigValAtomic::String(x), ConfigVal::String(val)) => {
                *x.write().expect("lock poisoned") = val
            }
            (ConfigValAtomic::Duration(x), ConfigVal::Duration(val)) => {
                *x.write().expect("lock poisoned") = val
            }
            (ConfigValAtomic::Json(x), ConfigVal::Json(val)) => {
                *x.write().expect("lock poisoned") = val
            }
            (ConfigValAtomic::Bool(_), val)
            | (ConfigValAtomic::U32(_), val)
            | (ConfigValAtomic::Usize(_), val)
            | (ConfigValAtomic::OptUsize(_), val)
            | (ConfigValAtomic::String(_), val)
            | (ConfigValAtomic::Duration(_), val)
            | (ConfigValAtomic::Json(_), val) => {
                panic!("attempted to store {val:?} value in {self:?} parameter")
            }
        }
    }
}

impl ConfigUpdates {
    /// Adds an update for the given config and value.
    ///
    /// If a value of the same config has previously been added to these
    /// updates, replaces it.
    pub fn add<T, U>(&mut self, config: &Config<T>, val: U)
    where
        T: ConfigDefault,
        U: ConfigDefault<ConfigType = T::ConfigType>,
    {
        self.add_dynamic(config.name, val.into_config_type().into());
    }

    /// Adds an update for the given configuration name and value.
    ///
    /// It is the callers responsibility to ensure the value is of the
    /// appropriate type for the configuration.
    ///
    /// If a value of the same config has previously been added to these
    /// updates, replaces it.
    pub fn add_dynamic(&mut self, name: &str, val: ConfigVal) {
        self.updates.insert(
            name.to_owned(),
            ProtoConfigVal {
                val: val.into_proto(),
            },
        );
    }

    /// Adds the entries in `other` to `self`, with `other` taking precedence.
    pub fn extend(&mut self, mut other: Self) {
        self.updates.append(&mut other.updates)
    }

    /// Applies these config updates to the given [ConfigSet].
    ///
    /// This doesn't need to be the same set that the value updates were added
    /// from. In fact, the primary use of this is propagating config updates
    /// across processes.
    ///
    /// The value updates for any configs unknown by the given set are skipped.
    /// Ditto for config type mismatches. However, this is unexpected usage at
    /// present and so is logged to Sentry.
    pub fn apply(&self, set: &ConfigSet) {
        for (name, ProtoConfigVal { val }) in self.updates.iter() {
            let Some(config) = set.configs.get(name) else {
                error!("config update {} {:?} not known set: {:?}", name, val, set);
                continue;
            };
            let val = match (val.clone()).into_rust() {
                Ok(x) => x,
                Err(err) => {
                    error!("config update {} decode error: {}", name, err);
                    continue;
                }
            };
            config.val.store(val);
        }
    }
}

mod impls {
    use std::time::Duration;

    use mz_ore::cast::CastFrom;
    use mz_proto::{ProtoType, RustType, TryFromProtoError};

    use crate::{
        proto_config_val, ConfigDefault, ConfigSet, ConfigType, ConfigVal, ProtoOptionU64,
    };

    impl ConfigType for bool {
        fn from_val(val: ConfigVal) -> Self {
            match val {
                ConfigVal::Bool(x) => x,
                x => panic!("expected bool value got {:?}", x),
            }
        }
    }

    impl From<bool> for ConfigVal {
        fn from(val: bool) -> ConfigVal {
            ConfigVal::Bool(val)
        }
    }

    impl ConfigType for u32 {
        fn from_val(val: ConfigVal) -> Self {
            match val {
                ConfigVal::U32(x) => x,
                x => panic!("expected u32 value got {:?}", x),
            }
        }
    }

    impl From<u32> for ConfigVal {
        fn from(val: u32) -> ConfigVal {
            ConfigVal::U32(val)
        }
    }

    impl ConfigType for usize {
        fn from_val(val: ConfigVal) -> Self {
            match val {
                ConfigVal::Usize(x) => x,
                x => panic!("expected usize value got {:?}", x),
            }
        }
    }

    impl From<usize> for ConfigVal {
        fn from(val: usize) -> ConfigVal {
            ConfigVal::Usize(val)
        }
    }

    impl ConfigType for Option<usize> {
        fn from_val(val: ConfigVal) -> Self {
            match val {
                ConfigVal::OptUsize(x) => x,
                x => panic!("expected usize value got {:?}", x),
            }
        }
    }

    impl From<Option<usize>> for ConfigVal {
        fn from(val: Option<usize>) -> ConfigVal {
            ConfigVal::OptUsize(val)
        }
    }

    impl ConfigType for String {
        fn from_val(val: ConfigVal) -> Self {
            match val {
                ConfigVal::String(x) => x,
                x => panic!("expected String value got {:?}", x),
            }
        }
    }

    impl From<String> for ConfigVal {
        fn from(val: String) -> ConfigVal {
            ConfigVal::String(val)
        }
    }

    impl ConfigDefault for &str {
        type ConfigType = String;

        fn into_config_type(self) -> String {
            self.into()
        }
    }

    impl ConfigType for Duration {
        fn from_val(val: ConfigVal) -> Self {
            match val {
                ConfigVal::Duration(x) => x,
                x => panic!("expected Duration value got {:?}", x),
            }
        }
    }

    impl From<Duration> for ConfigVal {
        fn from(val: Duration) -> ConfigVal {
            ConfigVal::Duration(val)
        }
    }

    impl ConfigType for serde_json::Value {
        fn from_val(val: ConfigVal) -> Self {
            match val {
                ConfigVal::Json(x) => x,
                x => panic!("expected JSON value got {:?}", x),
            }
        }
    }

    impl From<serde_json::Value> for ConfigVal {
        fn from(val: serde_json::Value) -> ConfigVal {
            ConfigVal::Json(val)
        }
    }

    impl RustType<Option<proto_config_val::Val>> for ConfigVal {
        fn into_proto(&self) -> Option<proto_config_val::Val> {
            use crate::proto_config_val::Val;
            let val = match self {
                ConfigVal::Bool(x) => Val::Bool(*x),
                ConfigVal::U32(x) => Val::U32(*x),
                ConfigVal::Usize(x) => Val::Usize(u64::cast_from(*x)),
                ConfigVal::OptUsize(x) => Val::OptUsize(ProtoOptionU64 {
                    val: x.map(u64::cast_from),
                }),
                ConfigVal::String(x) => Val::String(x.into_proto()),
                ConfigVal::Duration(x) => Val::Duration(x.into_proto()),
                ConfigVal::Json(x) => Val::Json(x.to_string()),
            };
            Some(val)
        }

        fn from_proto(proto: Option<proto_config_val::Val>) -> Result<Self, TryFromProtoError> {
            let val = match proto {
                Some(proto_config_val::Val::Bool(x)) => ConfigVal::Bool(x),
                Some(proto_config_val::Val::U32(x)) => ConfigVal::U32(x),
                Some(proto_config_val::Val::Usize(x)) => ConfigVal::Usize(usize::cast_from(x)),
                Some(proto_config_val::Val::OptUsize(ProtoOptionU64 { val })) => {
                    ConfigVal::OptUsize(val.map(usize::cast_from))
                }
                Some(proto_config_val::Val::String(x)) => ConfigVal::String(x),
                Some(proto_config_val::Val::Duration(x)) => ConfigVal::Duration(x.into_rust()?),
                Some(proto_config_val::Val::Json(x)) => ConfigVal::Json(serde_json::from_str(&x)?),
                None => {
                    return Err(TryFromProtoError::unknown_enum_variant(
                        "ProtoConfigVal::Val",
                    ))
                }
            };
            Ok(val)
        }
    }

    impl std::fmt::Debug for ConfigSet {
        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
            let ConfigSet { configs } = self;
            f.debug_map()
                .entries(configs.iter().map(|(name, val)| (name, val.val())))
                .finish()
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    const BOOL: Config<bool> = Config::new("bool", true, "");
    const USIZE: Config<usize> = Config::new("usize", 1, "");
    const OPT_USIZE: Config<Option<usize>> = Config::new("opt_usize", Some(2), "");
    const STRING: Config<&str> = Config::new("string", "a", "");
    const DURATION: Config<Duration> = Config::new("duration", Duration::from_nanos(3), "");
    const JSON: Config<fn() -> serde_json::Value> =
        Config::new("json", || serde_json::json!({}), "");

    #[mz_ore::test]
    fn all_types() {
        let configs = ConfigSet::default()
            .add(&BOOL)
            .add(&USIZE)
            .add(&OPT_USIZE)
            .add(&STRING)
            .add(&DURATION)
            .add(&JSON);
        assert_eq!(BOOL.get(&configs), true);
        assert_eq!(USIZE.get(&configs), 1);
        assert_eq!(OPT_USIZE.get(&configs), Some(2));
        assert_eq!(STRING.get(&configs), "a");
        assert_eq!(DURATION.get(&configs), Duration::from_nanos(3));
        assert_eq!(JSON.get(&configs), serde_json::json!({}));

        let mut updates = ConfigUpdates::default();
        updates.add(&BOOL, false);
        updates.add(&USIZE, 2);
        updates.add(&OPT_USIZE, None);
        updates.add(&STRING, "b");
        updates.add(&DURATION, Duration::from_nanos(4));
        updates.add(&JSON, serde_json::json!({"a": 1}));
        updates.apply(&configs);

        assert_eq!(BOOL.get(&configs), false);
        assert_eq!(USIZE.get(&configs), 2);
        assert_eq!(OPT_USIZE.get(&configs), None);
        assert_eq!(STRING.get(&configs), "b");
        assert_eq!(DURATION.get(&configs), Duration::from_nanos(4));
        assert_eq!(JSON.get(&configs), serde_json::json!({"a": 1}));
    }

    #[mz_ore::test]
    fn fn_default() {
        const BOOL_FN_DEFAULT: Config<fn() -> bool> = Config::new("bool", || !true, "");
        const STRING_FN_DEFAULT: Config<fn() -> String> =
            Config::new("string", || "x".repeat(3), "");

        let configs = ConfigSet::default()
            .add(&BOOL_FN_DEFAULT)
            .add(&STRING_FN_DEFAULT);
        assert_eq!(BOOL_FN_DEFAULT.get(&configs), false);
        assert_eq!(STRING_FN_DEFAULT.get(&configs), "xxx");
    }

    #[mz_ore::test]
    fn config_set() {
        let c0 = ConfigSet::default().add(&USIZE);
        assert_eq!(USIZE.get(&c0), 1);
        let mut updates = ConfigUpdates::default();
        updates.add(&USIZE, 2);
        updates.apply(&c0);
        assert_eq!(USIZE.get(&c0), 2);

        // Each ConfigSet is independent, even if they contain the same set of
        // configs.
        let c1 = ConfigSet::default().add(&USIZE);
        assert_eq!(USIZE.get(&c1), 1);
        let mut updates = ConfigUpdates::default();
        updates.add(&USIZE, 3);
        updates.apply(&c1);
        assert_eq!(USIZE.get(&c1), 3);
        assert_eq!(USIZE.get(&c0), 2);

        // We can copy values from one to the other, though (envd -> clusterd).
        let mut updates = ConfigUpdates::default();
        for e in c0.entries() {
            updates.add_dynamic(e.name, e.val());
        }
        assert_eq!(USIZE.get(&c1), 3);
        updates.apply(&c1);
        assert_eq!(USIZE.get(&c1), 2);
    }

    #[mz_ore::test]
    fn config_updates_extend() {
        // Regression test for #26196.
        //
        // Construct two ConfigUpdates with overlapping, but not identical, sets
        // of configs. Combine them and assert that the expected number of
        // updates is present.
        let mut u1 = {
            let c = ConfigSet::default().add(&USIZE).add(&STRING);
            let mut x = ConfigUpdates::default();
            for e in c.entries() {
                x.add_dynamic(e.name(), e.val());
            }
            x
        };
        let u2 = {
            let c = ConfigSet::default().add(&USIZE).add(&DURATION);
            let mut updates = ConfigUpdates::default();
            updates.add(&USIZE, 2);
            updates.apply(&c);
            let mut x = ConfigUpdates::default();
            for e in c.entries() {
                x.add_dynamic(e.name(), e.val());
            }
            x
        };
        assert_eq!(u1.updates.len(), 2);
        assert_eq!(u2.updates.len(), 2);
        u1.extend(u2);
        assert_eq!(u1.updates.len(), 3);

        // Assert that extend kept the correct (later) value for the overlapping
        // config.
        let c = ConfigSet::default().add(&USIZE);
        u1.apply(&c);
        assert_eq!(USIZE.get(&c), 2);
    }
}