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}