Skip to main content

vtcode_commons/
lr_map.rs

1#![expect(
2    unused_results,
3    reason = "The left-right writer API returns fluent handles and prior values that are intentionally not needed here."
4)]
5
6//! Lock-free concurrent map built on [`left_right`].
7//!
8//! [`LrMap`] keeps two copies of a `HashMap` — readers see one copy while the
9//! writer mutates the other. On publish the copies swap, giving readers a
10//! consistent, wait-free snapshot.
11//!
12//! Best for read-heavy workloads with infrequent writes (caches, registries).
13
14use hashbrown::HashMap;
15use left_right::{Absorb, ReadHandleFactory, WriteHandle};
16use std::hash::Hash;
17use std::sync::Mutex;
18
19enum MapOp<K, V> {
20    Insert(K, V),
21    Clear,
22}
23
24impl<K: Eq + Hash + Clone, V: Clone> Absorb<MapOp<K, V>> for HashMap<K, V> {
25    fn absorb_first(&mut self, operation: &mut MapOp<K, V>, _other: &Self) {
26        match operation {
27            MapOp::Insert(k, v) => {
28                self.insert(k.clone(), v.clone());
29            }
30            MapOp::Clear => self.clear(),
31        }
32    }
33
34    fn sync_with(&mut self, first: &Self) {
35        self.clone_from(first);
36    }
37}
38
39/// A concurrent map optimized for read-heavy workloads.
40///
41/// Readers never block — not even while a write is in progress. Writers are
42/// serialized through an internal [`Mutex`].
43///
44/// Trade-off: doubled memory (two copies of the map).
45pub struct LrMap<K: Eq + Hash + Clone, V: Clone> {
46    reader_factory: ReadHandleFactory<HashMap<K, V>>,
47    writer: Mutex<WriteHandle<HashMap<K, V>, MapOp<K, V>>>,
48}
49
50impl<K, V> LrMap<K, V>
51where
52    K: Eq + Hash + Clone + Send + Sync,
53    V: Clone + Send + Sync,
54{
55    pub fn new() -> Self {
56        let (writer, reader) = left_right::new_from_empty(HashMap::new());
57        let factory = reader.factory();
58        Self {
59            reader_factory: factory,
60            writer: Mutex::new(writer),
61        }
62    }
63
64    /// Lock-free lookup returning a clone of the value.
65    pub fn get<Q>(&self, key: &Q) -> Option<V>
66    where
67        K: std::borrow::Borrow<Q>,
68        Q: Hash + Eq + ?Sized,
69    {
70        let reader = self.reader_factory.handle();
71        reader.enter().and_then(|map| map.get(key).cloned())
72    }
73
74    pub fn insert(&self, key: K, value: V) {
75        match self.writer.lock() {
76            Ok(mut w) => {
77                w.append(MapOp::Insert(key, value));
78                w.publish();
79            }
80            Err(e) => {
81                tracing::warn!("LrMap::insert failed due to poisoned mutex: {e}. Write dropped.");
82            }
83        }
84    }
85
86    pub fn clear(&self) {
87        match self.writer.lock() {
88            Ok(mut w) => {
89                w.append(MapOp::Clear);
90                w.publish();
91            }
92            Err(e) => {
93                tracing::warn!("LrMap::clear failed due to poisoned mutex: {e}. Operation dropped.");
94            }
95        }
96    }
97
98    pub fn len(&self) -> usize {
99        let reader = self.reader_factory.handle();
100        reader.enter().map(|m| m.len()).unwrap_or(0)
101    }
102
103    pub fn is_empty(&self) -> bool {
104        self.len() == 0
105    }
106}
107
108impl<K, V> Default for LrMap<K, V>
109where
110    K: Eq + Hash + Clone + Send + Sync,
111    V: Clone + Send + Sync,
112{
113    fn default() -> Self {
114        Self::new()
115    }
116}
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121    use std::sync::Arc;
122
123    #[test]
124    fn insert_and_get() {
125        let map: LrMap<String, i32> = LrMap::new();
126        map.insert("a".into(), 1);
127        assert_eq!(map.get("a"), Some(1));
128        assert_eq!(map.get("b"), None);
129    }
130
131    #[test]
132    fn overwrite_key() {
133        let map: LrMap<String, i32> = LrMap::new();
134        map.insert("a".into(), 1);
135        map.insert("a".into(), 2);
136        assert_eq!(map.get("a"), Some(2));
137    }
138
139    #[test]
140    fn clear_removes_all() {
141        let map: LrMap<String, i32> = LrMap::new();
142        map.insert("a".into(), 1);
143        map.insert("b".into(), 2);
144        map.clear();
145        assert!(map.is_empty());
146    }
147
148    #[test]
149    fn concurrent_reads() {
150        let map: Arc<LrMap<String, i32>> = Arc::new(LrMap::new());
151        map.insert("key".into(), 42);
152
153        let handles: Vec<_> = (0..4)
154            .map(|_| {
155                let m = Arc::clone(&map);
156                std::thread::spawn(move || {
157                    for _ in 0..100 {
158                        assert_eq!(m.get("key"), Some(42));
159                    }
160                })
161            })
162            .collect();
163
164        for h in handles {
165            h.join().expect("reader thread panicked");
166        }
167    }
168}