Skip to main content

helm_schema_k8s/diagnostic/
sink.rs

1use std::collections::BTreeMap;
2use std::sync::{Arc, Mutex};
3
4use super::diagnostic::{Diagnostic, DiagnosticKey};
5
6/// Thread-safe sink for [`Diagnostic`] events keyed by
7/// [`DiagnosticKey`]. Uses `BTreeMap` so iteration order is
8/// deterministic (driven by `DiagnosticKey`'s `Ord`) independent of
9/// insertion order. First writer per key wins; canonicalisation at
10/// insertion time means payloads for the same key are identical by
11/// construction.
12#[derive(Debug, Default, Clone)]
13pub struct DiagnosticSink {
14    inner: Arc<Mutex<BTreeMap<DiagnosticKey, Diagnostic>>>,
15}
16
17impl DiagnosticSink {
18    /// Creates an empty deterministic diagnostic sink.
19    #[must_use]
20    pub fn new() -> Self {
21        Self::default()
22    }
23
24    /// Insert a diagnostic if no diagnostic with the same key has been
25    /// inserted yet. Canonicalises the payload before insertion so
26    /// later equality checks are stable.
27    pub fn push(&self, mut diagnostic: Diagnostic) {
28        diagnostic.canonicalise();
29        if let Ok(mut guard) = self.inner.lock() {
30            guard.entry(diagnostic.key()).or_insert(diagnostic);
31        }
32    }
33
34    /// Run a closure over the `BTreeMap` iterator. Held under the mutex
35    /// for the duration of the closure; keep it short.
36    pub fn for_each<F: FnMut(&Diagnostic)>(&self, mut f: F) {
37        if let Ok(guard) = self.inner.lock() {
38            for diagnostic in guard.values() {
39                f(diagnostic);
40            }
41        }
42    }
43
44    /// Snapshot the diagnostics as a new `Vec`. Useful for tests; in
45    /// hot paths prefer `for_each`.
46    #[must_use]
47    pub fn snapshot(&self) -> Vec<Diagnostic> {
48        self.inner
49            .lock()
50            .map(|g| g.values().cloned().collect())
51            .unwrap_or_default()
52    }
53
54    /// Number of distinct diagnostic keys currently held.
55    #[must_use]
56    pub fn len(&self) -> usize {
57        self.inner.lock().map(|g| g.len()).unwrap_or(0)
58    }
59
60    /// True when no diagnostics have been emitted.
61    #[must_use]
62    pub fn is_empty(&self) -> bool {
63        self.len() == 0
64    }
65}