Skip to main content

handy/
collections.rs

1use crate::errors::ConcurrentCollectionError;
2use std::{
3    collections::{BTreeMap, HashMap},
4    hash::Hash,
5    sync::{Arc, RwLock},
6};
7
8/// A map that can be used to store key-value pairs.
9pub trait Map<K, V>: Default {
10    /// Creates a new empty map
11    fn new() -> Self;
12
13    /// Inserts a key-value pair into the map.
14    ///
15    /// ## Errors
16    ///
17    /// - [`ConcurrentCollectionError::Poison`]: The lock is poisoned
18    fn insert(&self, key: K, value: V) -> Result<(), ConcurrentCollectionError>;
19
20    /// Retrieves a value from the map.
21    fn get(&self, key: &K) -> Option<V>;
22
23    /// Removes a key-value pair from the map.
24    fn remove(&self, key: &K) -> Option<V>;
25
26    /// Returns the number of key-value pairs in the map
27    fn len(&self) -> usize;
28
29    /// Returns true if the map contains no key-value pairs
30    fn is_empty(&self) -> bool;
31
32    /// Returns true if the map contains the specified key
33    fn contains_key(&self, key: &K) -> bool;
34}
35
36/// A concurrent map that can be used to store key-value pairs.
37///
38/// ## Examples
39///
40/// ```rust,no_run
41/// use handy::collections::{ConcurrentHashMap, Map};
42///
43/// let map: ConcurrentHashMap<u32, &'static str> = ConcurrentHashMap::new();
44/// ```
45///
46/// ## Errors
47///
48/// - [`ConcurrentCollectionError::Poison`]: The lock is poisoned
49#[derive(Debug)]
50pub struct ConcurrentHashMap<K, V> {
51    map: Arc<RwLock<HashMap<K, V>>>,
52}
53
54impl<K, V> Default for ConcurrentHashMap<K, V> {
55    fn default() -> Self {
56        ConcurrentHashMap {
57            map: Arc::new(RwLock::new(HashMap::new())),
58        }
59    }
60}
61
62// impl<K, V> ConcurrentHashMap<K, V>
63impl<K, V> Map<K, V> for ConcurrentHashMap<K, V>
64where
65    K: Eq + Hash + Send + Sync,
66    V: Copy,
67{
68    /// Creates a new empty [`crate::collections::ConcurrentHashMap`]
69    ///
70    /// ## Examples
71    ///
72    /// ```rust
73    /// use handy::collections::{ConcurrentHashMap, Map};
74    ///
75    /// let map: ConcurrentHashMap<&'static str, &'static str> = ConcurrentHashMap::new();
76    /// ```
77    fn new() -> Self {
78        ConcurrentHashMap::default()
79    }
80
81    /// Inserts a key-value pair into the map.
82    ///
83    /// ## Examples
84    ///
85    /// ```rust,no_run
86    /// use handy::collections::{ConcurrentHashMap, Map};
87    ///
88    /// let map: ConcurrentHashMap<&'static str, &'static str> = ConcurrentHashMap::new();
89    ///
90    /// map.insert("key", "value").unwrap();
91    /// ```
92    ///
93    /// ## Errors
94    ///
95    /// - [`ConcurrentCollectionError::Poison`]: The lock is poisoned
96    fn insert(&self, key: K, value: V) -> Result<(), ConcurrentCollectionError> {
97        match self.map.write() {
98            Ok(mut guard) => {
99                guard.insert(key, value);
100                Ok(())
101            }
102            Err(_) => Err(ConcurrentCollectionError::Poison),
103        }
104    }
105
106    /// Retrieves a value from the map.
107    ///
108    /// ## Examples
109    ///
110    /// ```rust,no_run
111    /// use handy::collections::{ConcurrentHashMap, Map};
112    ///
113    /// let map: ConcurrentHashMap<&'static str, &'static str> = ConcurrentHashMap::new();
114    ///
115    /// map.get(&"key").unwrap();
116    /// ```
117    fn get(&self, key: &K) -> Option<V> {
118        match self.map.read() {
119            Ok(guard) => guard.get(key).copied(),
120            Err(_) => None,
121        }
122    }
123
124    /// Removes a key-value pair from the map.
125    ///
126    /// ## Examples
127    ///
128    /// ```rust,no_run
129    /// use handy::collections::{ConcurrentHashMap, Map};
130    ///
131    /// let map: ConcurrentHashMap<&'static str, &'static str> = ConcurrentHashMap::new();
132    ///
133    /// map.insert("key", "value").unwrap();
134    /// map.remove(&"key").unwrap();
135    /// ```
136    fn remove(&self, key: &K) -> Option<V> {
137        match self.map.write() {
138            Ok(mut guard) => guard.remove(key),
139            Err(_) => None,
140        }
141    }
142
143    /// Returns the number of key-value pairs in the map.
144    ///
145    /// ## Examples
146    ///
147    /// ```rust,no_run
148    /// use handy::collections::{ConcurrentHashMap, Map};
149    ///
150    /// let map: ConcurrentHashMap<&'static str, &'static str> = ConcurrentHashMap::new();
151    ///
152    /// map.insert("key", "value").unwrap();
153    /// assert_eq!(map.len(), 1);
154    fn len(&self) -> usize {
155        match self.map.read() {
156            Ok(guard) => guard.len(),
157            Err(_) => 0,
158        }
159    }
160
161    /// Returns true if the map contains no key-value pairs.
162    ///
163    /// ## Examples
164    ///
165    /// ```rust,no_run
166    /// use handy::collections::{ConcurrentHashMap, Map};
167    ///
168    /// let map: ConcurrentHashMap<&'static str, &'static str> = ConcurrentHashMap::new();
169    ///
170    /// assert!(map.is_empty());
171    ///
172    /// map.insert("key", "value").unwrap();
173    /// assert!(!map.is_empty());
174    /// ```
175    fn is_empty(&self) -> bool {
176        match self.map.read() {
177            Ok(guard) => guard.is_empty(),
178            Err(_) => false,
179        }
180    }
181
182    /// Returns true if the map contains the specified key.
183    ///
184    /// ## Examples
185    ///
186    /// ```rust,no_run
187    /// use handy::collections::{ConcurrentHashMap, Map};
188    ///
189    /// let map: ConcurrentHashMap<&'static str, &'static str> = ConcurrentHashMap::new();
190    ///
191    /// map.insert("key", "value").unwrap();
192    /// assert!(map.contains_key(&"key"));
193    /// ```
194    fn contains_key(&self, key: &K) -> bool {
195        match self.map.read() {
196            Ok(guard) => guard.contains_key(key),
197            Err(_) => false,
198        }
199    }
200}
201
202/// A concurrent map that can be used to store key-value pairs in a sorted order.
203///
204/// ## Examples
205///
206/// ```rust,no_run
207/// use handy::collections::{ConcurrentBTreeMap, Map};
208///
209/// let map: ConcurrentBTreeMap<usize, u32> = ConcurrentBTreeMap::new();
210/// ```
211///
212/// ## Errors
213///
214/// - [`ConcurrentCollectionError::Poison`]: The lock is poisoned
215#[derive(Debug)]
216pub struct ConcurrentBTreeMap<K, V> {
217    map: Arc<RwLock<BTreeMap<K, V>>>,
218}
219
220impl<K, V> Default for ConcurrentBTreeMap<K, V> {
221    fn default() -> Self {
222        ConcurrentBTreeMap {
223            map: Arc::new(RwLock::new(BTreeMap::new())),
224        }
225    }
226}
227
228impl<K, V> Map<K, V> for ConcurrentBTreeMap<K, V>
229where
230    K: Eq + Ord + Send + Sync,
231    V: Copy,
232{
233    /// Creates a new concurrent map.
234    ///
235    /// ## Examples
236    ///
237    /// ```rust,no_run
238    /// use handy::collections::{ConcurrentBTreeMap, Map};
239    ///
240    /// let map: ConcurrentBTreeMap<usize, u32> = ConcurrentBTreeMap::new();
241    /// ```
242    fn new() -> Self {
243        ConcurrentBTreeMap::default()
244    }
245
246    /// Inserts a key-value pair into the map.
247    ///
248    /// ## Examples
249    ///
250    /// ```rust,no_run
251    /// use handy::collections::{ConcurrentBTreeMap, Map};
252    ///
253    /// let map: ConcurrentBTreeMap<usize, u32> = ConcurrentBTreeMap::new();
254    ///
255    /// map.insert(1, 2).unwrap();
256    /// ```
257    ///
258    /// ## Errors
259    ///
260    /// - [`ConcurrentCollectionError::Poison`]: The lock is poisoned
261    fn insert(&self, key: K, value: V) -> Result<(), ConcurrentCollectionError> {
262        match self.map.write() {
263            Ok(mut guard) => {
264                guard.insert(key, value);
265                Ok(())
266            }
267            Err(_) => Err(ConcurrentCollectionError::Poison),
268        }
269    }
270
271    /// Retrieves the value associated with the specified key.
272    ///
273    /// ## Examples
274    ///
275    /// ```rust,no_run
276    /// use handy::collections::{ConcurrentBTreeMap, Map};
277    ///
278    /// let map: ConcurrentBTreeMap<usize, u32> = ConcurrentBTreeMap::new();
279    ///
280    /// if let Some(value) = map.get(&1) {
281    ///     // do something
282    /// }
283    /// ```
284    fn get(&self, key: &K) -> Option<V> {
285        match self.map.read() {
286            Ok(guard) => guard.get(key).copied(),
287            Err(_) => None,
288        }
289    }
290
291    /// Checks if the map contains the specified key.
292    ///
293    /// ## Examples
294    ///
295    /// ```rust,no_run
296    /// use handy::collections::{ConcurrentBTreeMap, Map};
297    ///
298    /// let map: ConcurrentBTreeMap<usize, u32> = ConcurrentBTreeMap::new();
299    ///
300    /// if map.contains_key(&1) {
301    ///     // do something
302    /// }
303    /// ```
304    fn contains_key(&self, key: &K) -> bool {
305        match self.map.read() {
306            Ok(guard) => guard.contains_key(key),
307            Err(_) => false,
308        }
309    }
310
311    /// Removes the value associated with the specified key.
312    ///
313    /// ## Examples
314    ///
315    /// ```rust,no_run
316    /// use handy::collections::{ConcurrentBTreeMap, Map};
317    ///
318    /// let map: ConcurrentBTreeMap<usize, u32> = ConcurrentBTreeMap::new();
319    ///
320    /// map.remove(&1).unwrap();
321    /// ```
322    fn remove(&self, key: &K) -> Option<V> {
323        match self.map.write() {
324            Ok(mut guard) => guard.remove(key),
325            Err(_) => None,
326        }
327    }
328
329    /// Returns the number of key-value pairs in the map.
330    ///
331    /// ## Example
332    ///
333    /// ```rust,no_run
334    /// use handy::collections::{ConcurrentBTreeMap, Map};
335    ///
336    /// let map: ConcurrentBTreeMap<usize, u32> = ConcurrentBTreeMap::new();
337    ///
338    /// map.insert(1, 2).unwrap();
339    /// assert_eq!(map.len(), 1);
340    /// ```
341    fn len(&self) -> usize {
342        match self.map.read() {
343            Ok(guard) => guard.len(),
344            Err(_) => 0,
345        }
346    }
347
348    /// Checks if the map is empty.
349    ///
350    /// ## Example
351    ///
352    /// ```rust,no_run
353    /// use handy::collections::{ConcurrentBTreeMap, Map};
354    ///
355    /// let map: ConcurrentBTreeMap<usize, u32> = ConcurrentBTreeMap::new();
356    ///
357    /// map.insert(1, 2).unwrap();
358    /// assert!(!map.is_empty());
359    /// ```
360    fn is_empty(&self) -> bool {
361        match self.map.read() {
362            Ok(guard) => guard.is_empty(),
363            Err(_) => false,
364        }
365    }
366}
367
368#[cfg(test)]
369mod tests {
370    use super::*;
371    use rand::Rng;
372    use rayon::prelude::*;
373
374    #[test]
375    fn test_concurrent_hash_map() {
376        assert_map_works::<ConcurrentHashMap<_, _>>();
377    }
378
379    #[test]
380    fn test_concurrent_btree_map() {
381        assert_map_works::<ConcurrentBTreeMap<_, _>>();
382    }
383
384    /// Asserts that a map works as expected.
385    ///
386    /// This function is only used for testing.
387    #[allow(clippy::cast_possible_truncation)]
388    fn assert_map_works<M>()
389    where
390        M: Map<usize, u32> + Send + Sync,
391    {
392        const NUM_THREADS: usize = 10;
393        const OPS_PER_THREAD: usize = 1000;
394        const CONTENTION_RANGE: usize = 100;
395
396        let map = M::new();
397
398        // insertions
399        (0..NUM_THREADS).into_par_iter().for_each(|thread_id| {
400            for i in 0..OPS_PER_THREAD {
401                let key = thread_id * OPS_PER_THREAD + i;
402                let value = (key * 2) as u32;
403                map.insert(key, value).unwrap();
404            }
405        });
406
407        assert_eq!(map.len(), NUM_THREADS * OPS_PER_THREAD);
408
409        for thread_id in 0..NUM_THREADS {
410            for i in 0..OPS_PER_THREAD {
411                let key = thread_id * OPS_PER_THREAD + i;
412                let value = (key * 2) as u32;
413                assert_eq!(map.get(&key), Some(value));
414            }
415        }
416
417        // reads and writes
418        (0..NUM_THREADS).into_par_iter().for_each(|_| {
419            let mut rng = rand::rng();
420            for _ in 0..OPS_PER_THREAD {
421                let key = rng.random_range(0..1000);
422                let operation = rng.random_range(0..3);
423
424                match operation {
425                    0 => if let Some(_value) = map.get(&key) {},
426                    1 => {
427                        let value: u32 = rng.random();
428                        map.insert(key, value).unwrap();
429                    }
430                    2 => if let Some(_value) = map.remove(&key) {},
431                    _ => unreachable!(),
432                }
433            }
434        });
435
436        // high contention
437        (0..NUM_THREADS).into_par_iter().for_each(|_| {
438            let mut rng = rand::rng();
439            for _ in 0..OPS_PER_THREAD {
440                let key = rng.random_range(0..CONTENTION_RANGE);
441                let operation = rng.random_range(0..2);
442
443                match operation {
444                    0 => if let Some(_value) = map.get(&key) {},
445                    1 => {
446                        let value: u32 = rng.random();
447                        map.insert(key, value).unwrap();
448                    }
449                    _ => unreachable!(),
450                }
451            }
452        });
453    }
454}