Skip to main content

cli/util/
once_map.rs

1// SPDX-License-Identifier: Apache-2.0
2//! Process-lifetime per-key cache.
3//!
4//! `OnceMap<K, V>` is the shape that the CLI's daemon detection and mount
5//! lifecycle code all reach for independently: a process-wide `HashMap`
6//! whose entries are computed on first access and held until the process
7//! exits. The repeated `OnceLock<Mutex<HashMap<K, V>>>` ceremony at each
8//! call site disappears behind a single type.
9//!
10//! Two construction modes:
11//!
12//! - [`OnceMap::get_or_init_with`] for synchronous initializers (file-stat
13//!   probes, key derivation).
14//! - [`OnceMap::get_or_init_async`] for async initializers (gRPC channel
15//!   construction, network handshakes). Concurrent inserts for *different*
16//!   keys don't serialize because the lock is released across the `await`;
17//!   concurrent inserts for the *same* key may both run the init future
18//!   (last writer wins) — this matches the behavior of every call site
19//!   that previously used this pattern.
20//!
21//! Values are cloned on read. Use a cheaply-cloneable handle (`Arc<…>`,
22//! `tonic::transport::Channel`) when the underlying object is expensive
23//! to clone.
24
25use std::{
26    collections::HashMap,
27    hash::Hash,
28    sync::{Mutex, OnceLock},
29};
30
31use objects::sync::LockExt;
32
33/// A process-lifetime cache that maps `K → V` and computes each entry on
34/// first access. See module docs for semantics.
35pub struct OnceMap<K, V> {
36    inner: OnceLock<Mutex<HashMap<K, V>>>,
37}
38
39impl<K, V> OnceMap<K, V> {
40    /// Empty cache, suitable for `static` initializers.
41    pub const fn new() -> Self {
42        Self {
43            inner: OnceLock::new(),
44        }
45    }
46
47    fn map(&self) -> &Mutex<HashMap<K, V>> {
48        self.inner.get_or_init(|| Mutex::new(HashMap::new()))
49    }
50}
51
52impl<K: Eq + Hash + Clone, V: Clone> OnceMap<K, V> {
53    /// Return the value for `key`, computing and caching it with `init`
54    /// on first access. The lock is held across `init`, so concurrent
55    /// callers for the same key serialize.
56    pub fn get_or_init_with<F>(&self, key: &K, init: F) -> V
57    where
58        F: FnOnce() -> V,
59    {
60        let mut guard = self.map().lock_or_poisoned();
61        if let Some(v) = guard.get(key) {
62            return v.clone();
63        }
64        let v = init();
65        guard.insert(key.clone(), v.clone());
66        v
67    }
68
69    /// Async variant of [`Self::get_or_init_with`]. The lock is released
70    /// across the await, so different keys don't serialize. Two callers
71    /// for the same key may both run `init`; the last write wins.
72    pub async fn get_or_init_async<F, Fut>(&self, key: &K, init: F) -> V
73    where
74        F: FnOnce() -> Fut,
75        Fut: std::future::Future<Output = V>,
76    {
77        if let Some(v) = self.get(key) {
78            return v;
79        }
80        let v = init().await;
81        self.map().lock_or_poisoned().insert(key.clone(), v.clone());
82        v
83    }
84
85    /// Read without computing. Returns `None` if the key was never inserted.
86    pub fn get(&self, key: &K) -> Option<V> {
87        self.map().lock_or_poisoned().get(key).cloned()
88    }
89
90    /// Direct insert. Returns the previous value if any.
91    pub fn insert(&self, key: K, value: V) -> Option<V> {
92        self.map().lock_or_poisoned().insert(key, value)
93    }
94
95    /// Remove and return the value for `key`, if any. Used by call
96    /// sites that need to tear down a cached resource (the mount
97    /// registry hands the handle back so the caller can unmount it).
98    pub fn remove(&self, key: &K) -> Option<V> {
99        self.map().lock_or_poisoned().remove(key)
100    }
101}
102
103impl<K, V> Default for OnceMap<K, V> {
104    fn default() -> Self {
105        Self::new()
106    }
107}
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112
113    #[test]
114    fn computes_once_per_key() {
115        let map: OnceMap<String, i32> = OnceMap::new();
116        let mut calls = 0;
117        let mut init = |v: i32| {
118            calls += 1;
119            v
120        };
121        let a = map.get_or_init_with(&"a".to_string(), || init(1));
122        let a_again = map.get_or_init_with(&"a".to_string(), || init(2));
123        let b = map.get_or_init_with(&"b".to_string(), || init(3));
124        assert_eq!(a, 1);
125        assert_eq!(a_again, 1);
126        assert_eq!(b, 3);
127        assert_eq!(calls, 2);
128    }
129
130    #[test]
131    fn get_returns_none_when_missing() {
132        let map: OnceMap<String, i32> = OnceMap::new();
133        assert!(map.get(&"missing".to_string()).is_none());
134        map.insert("present".to_string(), 7);
135        assert_eq!(map.get(&"present".to_string()), Some(7));
136    }
137
138    #[tokio::test]
139    async fn async_init_caches_value() {
140        let map: OnceMap<String, i32> = OnceMap::new();
141        let v = map
142            .get_or_init_async(&"k".to_string(), || async { 42 })
143            .await;
144        assert_eq!(v, 42);
145        assert_eq!(map.get(&"k".to_string()), Some(42));
146    }
147}