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
31/// A process-lifetime cache that maps `K → V` and computes each entry on
32/// first access. See module docs for semantics.
33pub struct OnceMap<K, V> {
34    inner: OnceLock<Mutex<HashMap<K, V>>>,
35}
36
37impl<K, V> OnceMap<K, V> {
38    /// Empty cache, suitable for `static` initializers.
39    pub const fn new() -> Self {
40        Self {
41            inner: OnceLock::new(),
42        }
43    }
44
45    fn map(&self) -> &Mutex<HashMap<K, V>> {
46        self.inner.get_or_init(|| Mutex::new(HashMap::new()))
47    }
48}
49
50impl<K: Eq + Hash + Clone, V: Clone> OnceMap<K, V> {
51    /// Return the value for `key`, computing and caching it with `init`
52    /// on first access. The lock is held across `init`, so concurrent
53    /// callers for the same key serialize.
54    pub fn get_or_init_with<F>(&self, key: &K, init: F) -> V
55    where
56        F: FnOnce() -> V,
57    {
58        let mut guard = self.map().lock().expect("OnceMap mutex poisoned");
59        if let Some(v) = guard.get(key) {
60            return v.clone();
61        }
62        let v = init();
63        guard.insert(key.clone(), v.clone());
64        v
65    }
66
67    /// Async variant of [`Self::get_or_init_with`]. The lock is released
68    /// across the await, so different keys don't serialize. Two callers
69    /// for the same key may both run `init`; the last write wins.
70    pub async fn get_or_init_async<F, Fut>(&self, key: &K, init: F) -> V
71    where
72        F: FnOnce() -> Fut,
73        Fut: std::future::Future<Output = V>,
74    {
75        if let Some(v) = self.get(key) {
76            return v;
77        }
78        let v = init().await;
79        self.map()
80            .lock()
81            .expect("OnceMap mutex poisoned")
82            .insert(key.clone(), v.clone());
83        v
84    }
85
86    /// Read without computing. Returns `None` if the key was never inserted.
87    pub fn get(&self, key: &K) -> Option<V> {
88        self.map()
89            .lock()
90            .expect("OnceMap mutex poisoned")
91            .get(key)
92            .cloned()
93    }
94
95    /// Direct insert. Returns the previous value if any.
96    pub fn insert(&self, key: K, value: V) -> Option<V> {
97        self.map()
98            .lock()
99            .expect("OnceMap mutex poisoned")
100            .insert(key, value)
101    }
102
103    /// Remove and return the value for `key`, if any. Used by call
104    /// sites that need to tear down a cached resource (the mount
105    /// registry hands the handle back so the caller can unmount it).
106    pub fn remove(&self, key: &K) -> Option<V> {
107        self.map()
108            .lock()
109            .expect("OnceMap mutex poisoned")
110            .remove(key)
111    }
112}
113
114impl<K, V> Default for OnceMap<K, V> {
115    fn default() -> Self {
116        Self::new()
117    }
118}
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123
124    #[test]
125    fn computes_once_per_key() {
126        let map: OnceMap<String, i32> = OnceMap::new();
127        let mut calls = 0;
128        let mut init = |v: i32| {
129            calls += 1;
130            v
131        };
132        let a = map.get_or_init_with(&"a".to_string(), || init(1));
133        let a_again = map.get_or_init_with(&"a".to_string(), || init(2));
134        let b = map.get_or_init_with(&"b".to_string(), || init(3));
135        assert_eq!(a, 1);
136        assert_eq!(a_again, 1);
137        assert_eq!(b, 3);
138        assert_eq!(calls, 2);
139    }
140
141    #[test]
142    fn get_returns_none_when_missing() {
143        let map: OnceMap<String, i32> = OnceMap::new();
144        assert!(map.get(&"missing".to_string()).is_none());
145        map.insert("present".to_string(), 7);
146        assert_eq!(map.get(&"present".to_string()), Some(7));
147    }
148
149    #[tokio::test]
150    async fn async_init_caches_value() {
151        let map: OnceMap<String, i32> = OnceMap::new();
152        let v = map
153            .get_or_init_async(&"k".to_string(), || async { 42 })
154            .await;
155        assert_eq!(v, 42);
156        assert_eq!(map.get(&"k".to_string()), Some(42));
157    }
158}