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    pub fn entry<K, V>(&self) -> &'static V
112    where
113        K: 'static,
114        V: Default + Send + Sync + 'static,
115    {
116        let entries = self
117            .entries
118            .get_or_init(|| ArcSwap::from_pointee(Slots::default()));
119        let key = TypeId::of::<K>();
120
121        // The common case by far, and the one the benchmark measures: no lock,
122        // no hashing beyond a load, no allocation.
123        if let Some(found) = entries.load().get(&key) {
124            return downcast(*found);
125        }
126
127        // Missing: rebuild the table with the new slot in it. This happens once
128        // per monomorphization, so copying a table with a handful of entries is
129        // cheaper than making every read pay for a lock.
130        let leaked: &'static V = Box::leak(Box::new(V::default()));
131        let mut installed: Option<&'static (dyn Any + Send + Sync)> = None;
132
133        entries.rcu(|current| {
134            let mut next = Slots::clone(current);
135            // `or_insert` rather than `insert`: another thread may have won the
136            // race, and two slots for one type would mean two snapshots.
137            installed = Some(*next.entry(key).or_insert(leaked));
138
139            next
140        });
141
142        downcast(installed.expect("`rcu` runs its closure at least once"))
143    }
144}
145
146/// Recovers the concrete type a slot was created with.
147///
148/// The key is `TypeId::of::<K>()` and each registry serves exactly one `V`, so
149/// a mismatch would mean two different `V`s reached the same registry — a bug
150/// in the generated code rather than anything a caller can cause.
151fn downcast<V: 'static>(slot: &'static (dyn Any + Send + Sync)) -> &'static V {
152    slot.downcast_ref()
153        .expect("a registry slot holds exactly one type")
154}
155
156#[cfg(test)]
157mod tests {
158    use super::*;
159    use std::sync::atomic::{AtomicUsize, Ordering};
160    use std::thread;
161
162    #[derive(Default)]
163    struct Slot(AtomicUsize);
164
165    fn registry() -> &'static Registry {
166        static REGISTRY: Registry = Registry::new();
167
168        &REGISTRY
169    }
170
171    #[test]
172    fn the_same_key_always_returns_the_same_slot() {
173        let first = registry().entry::<u8, Slot>();
174        first.0.store(7, Ordering::SeqCst);
175
176        let second = registry().entry::<u8, Slot>();
177
178        assert!(std::ptr::eq(first, second));
179        assert_eq!(second.0.load(Ordering::SeqCst), 7);
180    }
181
182    #[test]
183    fn different_keys_get_different_slots() {
184        let one = registry().entry::<u16, Slot>();
185        let two = registry().entry::<u32, Slot>();
186
187        assert!(!std::ptr::eq(one, two));
188    }
189
190    #[test]
191    fn concurrent_first_use_hands_out_one_slot() {
192        struct Contended;
193
194        let handles: Vec<_> = (0..8)
195            .map(|_| {
196                thread::spawn(|| {
197                    // A raw pointer is not `Send`, so the address travels back
198                    // as an integer instead.
199                    registry().entry::<Contended, Slot>() as *const Slot as usize
200                })
201            })
202            .collect();
203
204        let slots: Vec<_> = handles.into_iter().map(|h| h.join().unwrap()).collect();
205
206        assert!(
207            slots.windows(2).all(|pair| pair[0] == pair[1]),
208            "every thread must see the same slot"
209        );
210    }
211}