atuin_common/sync/
sharded_mutex.rs1use std::hash::{DefaultHasher, Hash, Hasher};
2use std::marker::PhantomData;
3use std::num::NonZeroUsize;
4
5#[derive(Debug)]
9pub struct Sharded<K, M> {
10 shards: Box<[M]>,
11 key: PhantomData<fn() -> K>,
12}
13
14impl<K: Hash, M: Default> Sharded<K, M> {
15 #[must_use]
17 pub fn new(shards: NonZeroUsize) -> Self {
18 Self {
19 shards: (0..shards.get()).map(|_| M::default()).collect(),
20 key: PhantomData,
21 }
22 }
23}
24
25impl<K: Hash, M> Sharded<K, M> {
26 #[must_use]
28 pub fn shard(&self, key: &K) -> &M {
29 &self.shards[self.shard_of(key)]
30 }
31
32 #[must_use]
34 pub fn shards(&self) -> usize {
35 self.shards.len()
36 }
37
38 fn shard_of(&self, key: &K) -> usize {
39 let mut hasher = DefaultHasher::new();
40 key.hash(&mut hasher);
41 let count = u64::try_from(self.shards.len()).expect("a shard count fits in u64");
42 usize::try_from(hasher.finish() % count).expect("a shard index is below the count")
43 }
44}
45
46impl<K: Hash, V> Sharded<K, tokio::sync::Mutex<V>> {
47 pub async fn lock(&self, key: &K) -> tokio::sync::MutexGuard<'_, V> {
50 self.shard(key).lock().await
51 }
52}
53
54impl<K: Hash, V> Sharded<K, parking_lot::Mutex<V>> {
55 pub fn lock(&self, key: &K) -> parking_lot::MutexGuard<'_, V> {
58 self.shard(key).lock()
59 }
60}
61
62pub type AsyncShardedMutex<K> = Sharded<K, tokio::sync::Mutex<()>>;
65
66pub type ShardedMutex<K> = Sharded<K, parking_lot::Mutex<()>>;
69
70#[cfg(test)]
71mod tests {
72 use std::num::NonZeroUsize;
73 use std::sync::Arc;
74 use std::time::Duration;
75
76 use rstest::rstest;
77
78 use super::{AsyncShardedMutex, Sharded, ShardedMutex};
79
80 fn shards(n: usize) -> NonZeroUsize {
81 NonZeroUsize::new(n).expect("test shard counts are non-zero")
82 }
83
84 #[rstest]
85 #[tokio::test]
86 async fn the_same_key_waits_for_its_holder() {
87 let mutex: Arc<AsyncShardedMutex<&str>> = Arc::new(AsyncShardedMutex::new(shards(16)));
88 let held = mutex.lock(&"key").await;
89
90 let contender = Arc::clone(&mutex);
91 let waiter = tokio::spawn(async move {
92 let _ = contender.lock(&"key").await;
93 });
94 assert!(
96 tokio::time::timeout(Duration::from_millis(50), waiter_ready(&waiter)).await.is_err()
97 );
98
99 drop(held);
100 waiter.await.expect("the waiter acquires the shard once it is released");
101 }
102
103 async fn waiter_ready(handle: &tokio::task::JoinHandle<()>) {
105 while !handle.is_finished() {
106 tokio::time::sleep(Duration::from_millis(1)).await;
107 }
108 }
109
110 #[rstest]
113 #[tokio::test]
114 async fn the_value_belongs_to_the_shard() {
115 let mutex: Sharded<u32, tokio::sync::Mutex<u32>> = Sharded::new(shards(1));
117 *mutex.lock(&1).await += 1;
118 *mutex.lock(&2).await += 1;
119 assert_eq!(*mutex.lock(&3).await, 2);
120 }
121
122 #[rstest]
123 #[tokio::test]
124 async fn the_guard_hands_out_the_stored_value() {
125 let mutex: Sharded<String, tokio::sync::Mutex<Vec<u8>>> = Sharded::new(shards(8));
126 mutex.lock(&"a".to_string()).await.push(7);
127 assert_eq!(*mutex.lock(&"a".to_string()).await, vec![7]);
128 }
129
130 #[rstest]
131 fn keys_spread_across_shards() {
132 let mutex: AsyncShardedMutex<u64> = AsyncShardedMutex::new(shards(64));
133 let used: std::collections::HashSet<usize> =
134 (0..256u64).map(|k| mutex.shard_of(&k)).collect();
135 assert_eq!(mutex.shards(), 64);
136 assert!(
137 used.len() >= 32,
138 "256 keys should land in at least half of 64 shards, got {}",
139 used.len()
140 );
141 }
142
143 #[rstest]
144 #[tokio::test]
145 async fn colliding_keys_serialise() {
146 let mutex: Arc<AsyncShardedMutex<u32>> = Arc::new(AsyncShardedMutex::new(shards(1)));
148 let held = mutex.lock(&1).await;
149
150 let contender = Arc::clone(&mutex);
151 let waiter = tokio::spawn(async move {
152 let _ = contender.lock(&2).await;
153 });
154 assert!(
155 tokio::time::timeout(Duration::from_millis(50), waiter_ready(&waiter)).await.is_err()
156 );
157
158 drop(held);
159 waiter.await.expect("the waiter acquires the shard once it is released");
160 }
161
162 #[rstest]
163 #[tokio::test]
164 async fn keys_on_different_shards_lock_independently() {
165 let mutex: AsyncShardedMutex<u64> = AsyncShardedMutex::new(shards(64));
166 let first = 0u64;
167 let other = (1..=1024u64)
168 .find(|k| mutex.shard_of(k) != mutex.shard_of(&first))
169 .expect("one of 1024 keys lands on another of 64 shards");
170 let _held = mutex.lock(&first).await;
171
172 assert!(tokio::time::timeout(Duration::from_millis(50), mutex.lock(&other)).await.is_ok());
174 }
175
176 #[rstest]
177 fn the_blocking_flavour_locks_the_same_shard_for_the_same_key() {
178 let mutex: Sharded<&str, parking_lot::Mutex<u32>> = Sharded::new(shards(16));
180 let mut held = mutex.lock(&"key");
181 *held += 1;
182 assert!(mutex.shard(&"key").try_lock().is_none());
184 drop(held);
185 assert_eq!(*mutex.lock(&"key"), 1);
186 }
187
188 #[rstest]
189 fn both_flavours_are_send_and_sync_regardless_of_the_key() {
190 fn assert_send_sync<T: Send + Sync>() {}
191 assert_send_sync::<AsyncShardedMutex<std::rc::Rc<u8>>>();
193 assert_send_sync::<ShardedMutex<std::rc::Rc<u8>>>();
194 }
195}