Skip to main content

dynamic_config/
registry.rs

1//! Per-monomorphization storage for generic configuration types.
2//!
3//! A non-generic config type keeps its snapshot in a `static`, which costs one
4//! atomic load to read. A generic one cannot: Rust has no generic statics, and
5//! `Config<Postgres>` and `Config<Mysql>` need separate snapshots.
6//!
7//! The way out is a registry keyed by [`TypeId`], which *is* per
8//! monomorphization. A `static Registry` inside a generic function is shared by
9//! every instantiation — items in a function body are not monomorphized — so
10//! one registry serves them all and the key tells them apart.
11//!
12//! ```text
13//! non-generic:  static CELL: ConfigCell<T>        →  one atomic load
14//! generic:      REGISTRY.entry::<Config<D>, _>()  →  read lock + hash + downcast
15//! ```
16//!
17//! That difference is why the macro emits the `static` whenever it can, and the
18//! registry only for the types that have no alternative. `benches/read_path.rs`
19//! measures both.
20
21use std::any::{Any, TypeId};
22use std::collections::HashMap;
23use std::hash::{BuildHasherDefault, Hasher};
24use std::sync::OnceLock;
25
26use arc_swap::ArcSwap;
27
28/// The table itself. Behind an [`ArcSwap`], so a read takes no lock at all.
29type Slots = HashMap<TypeId, &'static (dyn Any + Send + Sync), BuildHasherDefault<TypeIdHasher>>;
30
31/// Passes a [`TypeId`] straight through instead of hashing it.
32///
33/// `TypeId` is already a high-quality 128-bit value; running SipHash over it
34/// was measurably the largest part of a lookup, and buys nothing a compiler-
35/// generated identifier does not already have.
36#[derive(Default)]
37struct TypeIdHasher {
38    hash: u64,
39}
40
41impl Hasher for TypeIdHasher {
42    fn write(&mut self, bytes: &[u8]) {
43        // `TypeId`'s `Hash` writes its bytes in one go; taking the low eight is
44        // enough to spread the handful of keys a program actually has.
45        for chunk in bytes.chunks(8) {
46            let mut buffer = [0u8; 8];
47            buffer[..chunk.len()].copy_from_slice(chunk);
48
49            self.hash ^= u64::from_ne_bytes(buffer);
50        }
51    }
52
53    fn write_u64(&mut self, value: u64) {
54        self.hash ^= value;
55    }
56
57    fn write_u128(&mut self, value: u128) {
58        self.hash ^= (value as u64) ^ ((value >> 64) as u64);
59    }
60
61    fn finish(&self) -> u64 {
62        self.hash
63    }
64}
65
66/// One slot per type, allocated on first use and never freed.
67///
68/// Each generated accessor has its own `Registry`, so the snapshot, the two
69/// runtime layers and the diff baseline do not collide on a shared key.
70///
71/// # Example
72///
73/// ```
74/// use dynamic_config::{ConfigCell, Registry};
75///
76/// fn cell<T: Send + Sync + 'static>() -> &'static ConfigCell<T> {
77///     // Shared by every instantiation: a `static` in a function body is not
78///     // monomorphized.
79///     static REGISTRY: Registry = Registry::new();
80///
81///     REGISTRY.entry::<T, ConfigCell<T>>()
82/// }
83///
84/// cell::<u16>().store(8080);
85/// assert_eq!(*cell::<u16>().load().unwrap(), 8080);
86///
87/// // A different type is a different slot.
88/// assert!(cell::<String>().load().is_none());
89/// ```
90#[derive(Default)]
91pub struct Registry {
92    entries: OnceLock<ArcSwap<Slots>>,
93}
94
95impl Registry {
96    /// An empty registry.
97    #[must_use]
98    pub const fn new() -> Self {
99        Self {
100            entries: OnceLock::new(),
101        }
102    }
103
104    /// The slot for `K`, created on first use.
105    ///
106    /// The value is leaked deliberately. A configuration snapshot lives as long
107    /// as the process, and the alternative — handing out an `Arc` and reference
108    /// counting on every read — would cost more than the lookup it replaced.
109    /// The leak is bounded by the number of monomorphizations, which is fixed
110    /// at compile time.
111    ///
112    /// # Panics
113    ///
114    /// If one registry is used with two different `V` types for the same `K`
115    /// — the generated code never does this; hand-written callers must keep
116    /// one value type per registry.
117    pub fn entry<K, V>(&self) -> &'static V
118    where
119        K: 'static,
120        V: Default + Send + Sync + 'static,
121    {
122        let entries = self
123            .entries
124            .get_or_init(|| ArcSwap::from_pointee(Slots::default()));
125        let key = TypeId::of::<K>();
126
127        // The common case by far, and the one the benchmark measures: no lock,
128        // no hashing beyond a load, no allocation.
129        if let Some(found) = entries.load().get(&key) {
130            return downcast(*found);
131        }
132
133        // Missing: rebuild the table with the new slot in it. This happens once
134        // per monomorphization, so copying a table with a handful of entries is
135        // cheaper than making every read pay for a lock. A lost race leaks
136        // this one allocation — bounded by the same monomorphization count as
137        // the deliberate leak above, so it is a footnote, not a leak *rate*.
138        let leaked: &'static V = Box::leak(Box::new(V::default()));
139        let mut installed: Option<&'static (dyn Any + Send + Sync)> = None;
140
141        entries.rcu(|current| {
142            let mut next = Slots::clone(current);
143            // `or_insert` rather than `insert`: another thread may have won the
144            // race, and two slots for one type would mean two snapshots.
145            installed = Some(*next.entry(key).or_insert(leaked));
146
147            next
148        });
149
150        downcast(installed.expect("`rcu` runs its closure at least once"))
151    }
152}
153
154/// Recovers the concrete type a slot was created with.
155///
156/// The key is `TypeId::of::<K>()` and each registry serves exactly one `V`, so
157/// a mismatch would mean two different `V`s reached the same registry — a bug
158/// in the generated code rather than anything a caller can cause.
159fn downcast<V: 'static>(slot: &'static (dyn Any + Send + Sync)) -> &'static V {
160    slot.downcast_ref()
161        .expect("a registry slot holds exactly one type")
162}
163
164#[cfg(test)]
165mod tests {
166    use super::*;
167    use std::sync::atomic::{AtomicUsize, Ordering};
168    use std::thread;
169
170    #[derive(Default)]
171    struct Slot(AtomicUsize);
172
173    fn registry() -> &'static Registry {
174        static REGISTRY: Registry = Registry::new();
175
176        &REGISTRY
177    }
178
179    #[test]
180    fn the_same_key_always_returns_the_same_slot() {
181        let first = registry().entry::<u8, Slot>();
182        first.0.store(7, Ordering::SeqCst);
183
184        let second = registry().entry::<u8, Slot>();
185
186        assert!(std::ptr::eq(first, second));
187        assert_eq!(second.0.load(Ordering::SeqCst), 7);
188    }
189
190    #[test]
191    fn different_keys_get_different_slots() {
192        let one = registry().entry::<u16, Slot>();
193        let two = registry().entry::<u32, Slot>();
194
195        assert!(!std::ptr::eq(one, two));
196    }
197
198    #[test]
199    fn concurrent_first_use_hands_out_one_slot() {
200        struct Contended;
201
202        let handles: Vec<_> = (0..8)
203            .map(|_| {
204                thread::spawn(|| {
205                    // A raw pointer is not `Send`, so the address travels back
206                    // as an integer instead.
207                    registry().entry::<Contended, Slot>() as *const Slot as usize
208                })
209            })
210            .collect();
211
212        let slots: Vec<_> = handles.into_iter().map(|h| h.join().unwrap()).collect();
213
214        assert!(
215            slots.windows(2).all(|pair| pair[0] == pair[1]),
216            "every thread must see the same slot"
217        );
218    }
219}