Skip to main content

nmbrs_metrics/
cells.rs

1// Copyright 2024-2026 Jonathan Shook
2// SPDX-License-Identifier: Apache-2.0
3
4//! Dimensional cells materialised from data.
5//!
6//! A metric whose series are one-per-instance of some dimension — a compaction
7//! tier, a keyspace, a node — does NOT get there by attaching labels to an
8//! instrument. The label set of a [`Component`] *is* the dimensional cell:
9//!
10//! > The component's `effective_labels` define the dimensional cell; the same
11//! > family on a different component is a different cell and produces no
12//! > collision.
13//!
14//! So one series per instance is one CHILD COMPONENT per instance, with the
15//! family registered exactly once on each. The duplicate-family rejection in
16//! [`Component::register_instrument`] is untouched and keeps catching what it
17//! exists to catch: two different instruments claiming one name inside one
18//! dimensional context.
19//!
20//! This registry is the resolve-or-create for those children. It is
21//! deliberately generic — it knows about coordinates and components, not about
22//! metrics — and it hangs off the component tree, so a cell's lifetime is its
23//! parent's: cells created under a phase component die when that phase's
24//! subtree is dropped.
25//!
26//! Coordinates are whole. A two-dimension coordinate resolves to ONE child
27//! carrying both labels, never to nested children — nesting would impose an
28//! arbitrary order on co-equal dimensions, and `a` inside `b` is a different
29//! tree shape from `b` inside `a` for no reason the data supports.
30
31use std::collections::HashMap;
32use std::sync::{Arc, Mutex, RwLock};
33
34use crate::component::Component;
35use crate::labels::Labels;
36
37/// Resolve-or-create map from a coordinate to the child component that
38/// represents it. Interior-mutable so it is reachable through a read guard on
39/// the parent, mirroring [`Component::controls`].
40#[derive(Default)]
41pub struct CellMap {
42    /// Keyed by the coordinate's canonical rendering. `Labels::to_prometheus`
43    /// is order-stable, so two spellings of one coordinate map to one cell.
44    inner: Mutex<HashMap<String, Arc<RwLock<Component>>>>,
45}
46
47impl CellMap {
48    pub fn new() -> Self {
49        Self {
50            inner: Mutex::new(HashMap::new()),
51        }
52    }
53
54    /// The cell for `coord` under `parent`, creating and attaching it on first
55    /// sight. Idempotent: the same coordinate yields the same component for as
56    /// long as the parent lives, so a caller may resolve per cycle and get a
57    /// registry write only once.
58    ///
59    /// `coord` carries only the dimension labels this cell adds. Inherited
60    /// labels come from the parent via [`crate::component::attach`], which also
61    /// enforces that no ancestor already owns these names.
62    pub fn resolve(
63        &self,
64        parent: &Arc<RwLock<Component>>,
65        coord: &Labels,
66    ) -> Arc<RwLock<Component>> {
67        let key = coord.to_prometheus();
68        let mut map = self.inner.lock().unwrap_or_else(|e| e.into_inner());
69        if let Some(existing) = map.get(&key) {
70            return existing.clone();
71        }
72        let mut component = Component::new(coord.clone(), HashMap::new());
73        // Running from birth. `Component::new` starts in `Starting`, and the
74        // cadence walk captures only `Running` components — a cell left
75        // `Starting` would accept samples and emit none of them. A cell exists
76        // because data is already flowing into it, so there is no window in
77        // which `Starting` would be the honest state.
78        component.set_state(crate::component::ComponentState::Running);
79        let child = Arc::new(RwLock::new(component));
80        crate::component::attach(parent, &child);
81        map.insert(key, child.clone());
82        child
83    }
84
85    /// Number of distinct coordinates materialised so far. For diagnostics and
86    /// tests — there is deliberately no cap: how many series a dimension has is
87    /// a modelling decision, and silently dropping data would be worse than
88    /// having many.
89    pub fn len(&self) -> usize {
90        self.inner.lock().unwrap_or_else(|e| e.into_inner()).len()
91    }
92
93    pub fn is_empty(&self) -> bool {
94        self.len() == 0
95    }
96}
97
98/// Resolve the cell for `coord` under `parent`, creating it on first sight.
99///
100/// Free function rather than a method so the lock discipline lives in ONE
101/// place: resolving attaches a child, which takes `parent`'s write lock, so a
102/// caller holding a read guard across the call self-deadlocks on the same
103/// `RwLock`. Binding the `Arc` first is what releases it.
104///
105/// `parent` must be **the component the metric registers on**, not an ambient
106/// one. A cell REFINES an identity: the coordinate adds dimensions to the label
107/// set that component already contributes. Sourcing the parent from ambient
108/// context instead composes identity from wherever the code happens to be
109/// running, which silently drops the parts the registration site owns (`op=`,
110/// most obviously) — a different metric identity wearing the same family name.
111pub fn resolve_under(parent: &Arc<RwLock<Component>>, coord: &Labels) -> Arc<RwLock<Component>> {
112    let cells = parent.read().unwrap_or_else(|e| e.into_inner()).cells();
113    cells.resolve(parent, coord)
114}
115
116#[cfg(test)]
117mod tests {
118    use super::*;
119
120    fn parent() -> Arc<RwLock<Component>> {
121        Arc::new(RwLock::new(Component::new(
122            Labels::of("phase", "finalize"),
123            HashMap::new(),
124        )))
125    }
126
127    /// The same coordinate must resolve to the SAME component, or a per-cycle
128    /// resolve would attach a new child every cycle and re-register the family
129    /// on each — the registry write this design exists to avoid.
130    #[test]
131    fn a_coordinate_resolves_to_one_stable_cell() {
132        let p = parent();
133        let cells = CellMap::new();
134        let a = cells.resolve(&p, &Labels::of("tier", "24"));
135        let b = cells.resolve(&p, &Labels::of("tier", "24"));
136        assert!(Arc::ptr_eq(&a, &b), "one coordinate must be one cell");
137        assert_eq!(cells.len(), 1);
138    }
139
140    /// Distinct values are distinct cells — which is what makes one family
141    /// registrable on each without colliding.
142    #[test]
143    fn distinct_values_are_distinct_cells() {
144        let p = parent();
145        let cells = CellMap::new();
146        let a = cells.resolve(&p, &Labels::of("tier", "24"));
147        let b = cells.resolve(&p, &Labels::of("tier", "25"));
148        assert!(!Arc::ptr_eq(&a, &b));
149        assert_eq!(cells.len(), 2);
150
151        // The registry invariant this whole design protects: the same family
152        // registers cleanly on both, because they are different cells.
153        for c in [&a, &b] {
154            let g = c.write().unwrap();
155            let mut g = g;
156            g.register_instrument(
157                "compaction_bytes_out",
158                crate::component::InstrumentRef::Gauge(Arc::new(
159                    crate::instruments::gauge::ValueGauge::new(Labels::default()),
160                )),
161            )
162            .expect("same family on a different cell must not collide");
163        }
164    }
165
166    /// A cell inherits its parent's labels and adds its own, so the emitted
167    /// series carries the full coordinate.
168    #[test]
169    fn a_cell_inherits_the_parent_dimensions() {
170        let p = parent();
171        let cells = CellMap::new();
172        let c = cells.resolve(&p, &Labels::of("tier", "24"));
173        let eff = c.read().unwrap().effective_labels().to_prometheus();
174        assert!(
175            eff.contains("phase=") && eff.contains("tier="),
176            "cell must carry inherited + own dimensions, got {eff}"
177        );
178    }
179
180    /// Two dimensions are ONE cell carrying both, not a nesting.
181    #[test]
182    fn a_multi_dimension_coordinate_is_a_single_child() {
183        let p = parent();
184        let cells = CellMap::new();
185        let coord = Labels::of("tier", "24").with("keyspace", "baselines");
186        let c = cells.resolve(&p, &coord);
187        assert_eq!(
188            c.read().unwrap().child_count(),
189            0,
190            "a coordinate must not nest one dimension inside another"
191        );
192        let eff = c.read().unwrap().effective_labels().to_prometheus();
193        assert!(
194            eff.contains("tier=") && eff.contains("keyspace="),
195            "both dimensions belong to one cell, got {eff}"
196        );
197        assert_eq!(p.read().unwrap().child_count(), 1);
198    }
199
200    /// Identity is the LABEL SET (with the family name promoted into it), so a
201    /// cell must add to the parent's dimensions, never stand in for them. If a
202    /// cell were parented somewhere else, the emitted identity would silently
203    /// lose whatever the registration site owned.
204    #[test]
205    fn a_cell_refines_the_parents_identity_rather_than_replacing_it() {
206        let phase = parent();
207        let op = Arc::new(RwLock::new(Component::new(
208            Labels::of("op", "read_history"),
209            HashMap::new(),
210        )));
211        crate::component::attach(&phase, &op);
212
213        let cell = resolve_under(&op, &Labels::of("tier", "24"));
214        let eff = cell.read().unwrap().effective_labels().to_prometheus();
215
216        for owned in ["phase=", "op=", "tier="] {
217            assert!(
218                eff.contains(owned),
219                "a cell must carry every dimension its ancestors own; {owned} \
220                 missing from {eff}"
221            );
222        }
223    }
224
225    /// Two siblings sharing a label set AT THE SAME TIME is the case that
226    /// breaks identity: each can register the same family, and the two
227    /// instruments then wear one identity with the per-component duplicate
228    /// check unable to see it.
229    #[test]
230    #[should_panic(expected = "sibling-identity violation")]
231    fn concurrent_siblings_cannot_share_a_label_set() {
232        let p = parent();
233        let _first = {
234            let c = Arc::new(RwLock::new(Component::new(
235                Labels::of("tier", "24"),
236                HashMap::new(),
237            )));
238            crate::component::attach(&p, &c);
239            c // kept alive and never stopped
240        };
241        let second = Arc::new(RwLock::new(Component::new(
242            Labels::of("tier", "24"),
243            HashMap::new(),
244        )));
245        crate::component::attach(&p, &second);
246    }
247
248    /// Sequential reuse must stay legal — an iteration whose values repeat
249    /// (fib yields `n=1` twice) re-materialises the SAME identity, which is one
250    /// identity sampled again over time. An unconditional check panicked here.
251    #[test]
252    fn a_label_set_may_be_reused_after_the_previous_component_stops() {
253        use crate::component::ComponentState;
254        let p = parent();
255        let first = Arc::new(RwLock::new(Component::new(
256            Labels::of("tier", "24"),
257            HashMap::new(),
258        )));
259        crate::component::attach(&p, &first);
260        first.write().unwrap().set_state(ComponentState::Stopped);
261
262        let second = Arc::new(RwLock::new(Component::new(
263            Labels::of("tier", "24"),
264            HashMap::new(),
265        )));
266        crate::component::attach(&p, &second); // must not panic
267        assert_eq!(p.read().unwrap().child_count(), 2);
268    }
269
270    /// The index must not keep a dead component alive, and must not leak a
271    /// phantom claim when a component is dropped without being stopped.
272    #[test]
273    fn a_dropped_component_releases_its_claim() {
274        let p = parent();
275        {
276            let c = Arc::new(RwLock::new(Component::new(
277                Labels::of("tier", "24"),
278                HashMap::new(),
279            )));
280            crate::component::attach(&p, &c);
281            // `children` holds a strong ref, so drop that too: this models a
282            // component detached and released rather than stopped.
283            crate::component::detach(&p, &c);
284        }
285        let again = Arc::new(RwLock::new(Component::new(
286            Labels::of("tier", "24"),
287            HashMap::new(),
288        )));
289        crate::component::attach(&p, &again); // must not panic
290    }
291
292    /// Distinct values attach as distinct siblings — the thing cells exist to
293    /// create.
294    #[test]
295    fn distinct_siblings_still_attach() {
296        let p = parent();
297        for v in ["24", "25", "26"] {
298            let c = Arc::new(RwLock::new(Component::new(
299                Labels::of("tier", v),
300                HashMap::new(),
301            )));
302            crate::component::attach(&p, &c);
303        }
304        assert_eq!(p.read().unwrap().child_count(), 3);
305    }
306
307    /// So the guarantee has to come from HERE: the same coordinate resolves to
308    /// the existing cell instead of attaching a twin, which is what keeps one
309    /// coordinate mapped to one identity.
310    #[test]
311    fn the_resolver_is_what_prevents_a_duplicated_cell_identity() {
312        let p = parent();
313        let coord = Labels::of("tier", "24");
314        for _ in 0..5 {
315            resolve_under(&p, &coord);
316        }
317        assert_eq!(
318            p.read().unwrap().child_count(),
319            1,
320            "repeated resolution must not multiply cells for one coordinate"
321        );
322    }
323
324    /// Cells route through the same check, so a resolver cannot be the only
325    /// thing standing between the tree and a duplicated identity.
326    #[test]
327    fn repeated_cell_resolution_does_not_trip_the_sibling_check() {
328        let p = parent();
329        let coord = Labels::of("tier", "24");
330        let first = resolve_under(&p, &coord);
331        let again = resolve_under(&p, &coord);
332        assert!(
333            Arc::ptr_eq(&first, &again),
334            "memoisation must return the existing cell rather than attach a twin"
335        );
336        assert_eq!(p.read().unwrap().child_count(), 1);
337    }
338}