Skip to main content

atuin_common/sync/
sharded_mutex.rs

1use std::hash::{DefaultHasher, Hash, Hasher};
2use std::marker::PhantomData;
3use std::num::NonZeroUsize;
4
5/// A fixed number of locks of type `M`, selected by hashing a `K`.
6///
7/// Reach it through [`AsyncShardedMutex`] (tokio) or [`ShardedMutex`] (parking_lot).
8#[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    /// `shards` locks, each starting at `M::default()`.
16    #[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    /// The lock `key` hashes to.
27    #[must_use]
28    pub fn shard(&self, key: &K) -> &M {
29        &self.shards[self.shard_of(key)]
30    }
31
32    /// How many shards there are.
33    #[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    /// Lock the shard `key` hashes to, waiting while another task holds it. The guard may be held
48    /// across `.await`s.
49    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    /// Lock the shard `key` hashes to, blocking the thread while another holds it. Never hold the
56    /// guard across an `.await`.
57    pub fn lock(&self, key: &K) -> parking_lot::MutexGuard<'_, V> {
58        self.shard(key).lock()
59    }
60}
61
62/// `Sharded` over [`tokio::sync::Mutex`] used purely as a lock (it guards `()`, carrying no
63/// value): `lock` is `async`, and its guard may be held across `.await`s.
64pub type AsyncShardedMutex<K> = Sharded<K, tokio::sync::Mutex<()>>;
65
66/// `Sharded` over [`parking_lot::Mutex`] used purely as a lock (it guards `()`, carrying no
67/// value): `lock` blocks the calling thread, so never hold its guard across an `.await`.
68pub 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        // While the guard is held, the second lock of the same key cannot complete.
95        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    /// Resolves once `handle`'s task has finished; used to observe "still blocked".
104    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    // The public aliases guard `()`, but `Sharded` itself still carries a value per shard; these
111    // exercise that generality directly.
112    #[rstest]
113    #[tokio::test]
114    async fn the_value_belongs_to_the_shard() {
115        // One shard: every key shares it, and therefore shares its value.
116        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        // One shard: two different keys collide, so the second waits for the first.
147        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        // Not one big lock: a key on another shard is acquired immediately.
173        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        // `Sharded` directly so the guard still carries a value, exercising the parking_lot path.
179        let mutex: Sharded<&str, parking_lot::Mutex<u32>> = Sharded::new(shards(16));
180        let mut held = mutex.lock(&"key");
181        *held += 1;
182        // The shard is taken: a second acquisition of the same key cannot succeed right now.
183        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        // `Rc` is neither; the key marker must not drag the key's auto traits into the mutex.
192        assert_send_sync::<AsyncShardedMutex<std::rc::Rc<u8>>>();
193        assert_send_sync::<ShardedMutex<std::rc::Rc<u8>>>();
194    }
195}