Skip to main content

tuwunel_core/utils/
mutex_map.rs

1//! Per-key asynchronous mutual exclusion with automatic entry cleanup.
2//!
3//! Each key maps to a Tokio mutex shared by its current contenders. The last
4//! contender to release its claim removes the entry, whether it held the mutex
5//! or was canceled while waiting for it.
6
7use std::{
8	fmt::Debug,
9	hash::Hash,
10	sync::{Arc, TryLockError::WouldBlock},
11};
12
13use tokio::sync::OwnedMutexGuard as Omg;
14
15use crate::{Result, err};
16
17/// Provides independent asynchronous mutexes keyed by owned values.
18///
19/// Lock acquisition creates entries on demand, and callers contending for the
20/// same key serialize. An entry lives exactly as long as some caller holds or
21/// contends for it.
22#[derive(Debug)]
23pub struct MutexMap<Key, Val> {
24	map: Map<Key, Val>,
25}
26
27/// Keeps a keyed mutex locked until the guard is dropped.
28///
29/// The guard retains the parent map so cleanup remains possible. Dropping it
30/// releases the keyed mutex and then removes the entry when no other holder or
31/// contender references it.
32#[derive(Debug)]
33#[clippy::has_significant_drop]
34pub struct Guard<Key, Val> {
35	map: Map<Key, Val>,
36	entry: Option<Value<Val>>,
37	val: Option<Omg<Val>>,
38}
39
40type Map<Key, Val> = Arc<MapMutex<Key, Val>>;
41type MapMutex<Key, Val> = std::sync::Mutex<HashMap<Key, Val>>;
42type HashMap<Key, Val> = std::collections::HashMap<Key, Value<Val>>;
43type Value<Val> = Arc<tokio::sync::Mutex<Val>>;
44
45impl<Key, Val> MutexMap<Key, Val>
46where
47	Key: Clone + Eq + Hash + Send,
48	Val: Default + Send,
49{
50	/// Creates an empty keyed mutex map.
51	///
52	/// No per-key mutex is allocated until a lock method first sees its key.
53	/// The result is equivalent to [`Default::default`].
54	#[must_use]
55	pub fn new() -> Self {
56		Self {
57			map: Map::new(MapMutex::new(HashMap::new())),
58		}
59	}
60
61	/// Acquires the asynchronous mutex associated with a key.
62	///
63	/// The method creates an entry if absent and waits for the current holder
64	/// to release it. Cancellation while waiting releases the claim on the
65	/// entry, and a poisoned internal map mutex causes a panic.
66	#[tracing::instrument(level = "trace", skip(self))]
67	pub async fn lock<K>(&self, k: &K) -> Guard<Key, Val>
68	where
69		K: Debug + Send + ?Sized + Sync + ToOwned<Owned = Key>,
70	{
71		self.entry(k).lock().await
72	}
73
74	/// Attempts to acquire a key without waiting for its asynchronous mutex.
75	///
76	/// The key entry is created if absent, and contention returns an error
77	/// instead of yielding. Acquiring the internal map mutex can still block
78	/// and panics if that mutex is poisoned.
79	#[tracing::instrument(level = "trace", skip(self))]
80	pub fn try_lock<K>(&self, k: &K) -> Result<Guard<Key, Val>>
81	where
82		K: Debug + Send + ?Sized + Sync + ToOwned<Owned = Key>,
83	{
84		self.entry(k).try_lock()
85	}
86
87	/// Attempts to acquire a key without yielding, blocking only to release a
88	/// failed attempt.
89	///
90	/// Contention on either the internal map or keyed mutex returns an error.
91	/// The entry is created only after the map mutex is acquired, and releasing
92	/// a failed attempt blocks on that mutex again. A poisoned map mutex causes
93	/// a panic.
94	#[tracing::instrument(level = "trace", skip(self))]
95	pub fn try_try_lock<K>(&self, k: &K) -> Result<Guard<Key, Val>>
96	where
97		K: Debug + Send + ?Sized + Sync + ToOwned<Owned = Key>,
98	{
99		self.try_entry(k)?.try_lock()
100	}
101
102	/// Reports whether the map currently contains an entry for a key.
103	///
104	/// An entry represents a held mutex or contenders that still reference it.
105	/// The check locks the internal map and panics if that mutex is poisoned.
106	#[must_use]
107	pub fn contains(&self, k: &Key) -> bool { self.map.lock().expect("locked").contains_key(k) }
108
109	/// Reports whether no keyed mutex entries are currently tracked.
110	///
111	/// A false result implies at least one active holder or contender. The
112	/// check locks the internal map and panics if that mutex is poisoned.
113	#[must_use]
114	pub fn is_empty(&self) -> bool { self.map.lock().expect("locked").is_empty() }
115
116	/// Returns the number of keyed mutex entries currently tracked.
117	///
118	/// The count includes held mutexes and entries retained by contenders. The
119	/// check locks the internal map and panics if that mutex is poisoned.
120	#[must_use]
121	pub fn len(&self) -> usize { self.map.lock().expect("locked").len() }
122
123	fn entry<K>(&self, k: &K) -> Guard<Key, Val>
124	where
125		K: ?Sized + ToOwned<Owned = Key>,
126	{
127		let val = self
128			.map
129			.lock()
130			.expect("locked")
131			.entry(k.to_owned())
132			.or_default()
133			.clone();
134
135		self.pending(val)
136	}
137
138	fn try_entry<K>(&self, k: &K) -> Result<Guard<Key, Val>>
139	where
140		K: ?Sized + ToOwned<Owned = Key>,
141	{
142		let val = self
143			.map
144			.try_lock()
145			.map_err(|e| match e {
146				| WouldBlock => err!("would block"),
147				| _ => panic!("{e:?}"),
148			})?
149			.entry(k.to_owned())
150			.or_default()
151			.clone();
152
153		Ok(self.pending(val))
154	}
155
156	fn pending(&self, val: Value<Val>) -> Guard<Key, Val> {
157		Guard {
158			map: Arc::clone(&self.map),
159			entry: Some(val),
160			val: None,
161		}
162	}
163}
164
165impl<Key, Val> Default for MutexMap<Key, Val>
166where
167	Key: Clone + Eq + Hash + Send,
168	Val: Default + Send,
169{
170	fn default() -> Self { Self::new() }
171}
172
173impl<Key, Val> Guard<Key, Val> {
174	async fn lock(mut self) -> Self {
175		// The in-flight claim must release before this guard, so a cancellation
176		// leaves the entry unreferenced.
177		let val = self.claim();
178
179		self.val = Some(val.lock_owned().await);
180		self
181	}
182
183	fn try_lock(mut self) -> Result<Self> {
184		self.val = self
185			.claim()
186			.try_lock_owned()
187			.map_err(|_| err!("would yield"))
188			.map(Some)?;
189
190		Ok(self)
191	}
192
193	fn claim(&self) -> Value<Val> { Arc::clone(self.entry.as_ref().expect("claimed")) }
194}
195
196impl<Key, Val> Drop for Guard<Key, Val> {
197	#[tracing::instrument(name = "unlock", level = "trace", skip_all)]
198	fn drop(&mut self) {
199		self.val.take();
200
201		// Releasing the claim under the map lock elects the last one out.
202		let mut map = self.map.lock().expect("locked");
203
204		if self
205			.entry
206			.take()
207			.is_some_and(|val| Arc::strong_count(&val) <= 2)
208		{
209			map.retain(|_, val| Arc::strong_count(val) > 1);
210		}
211	}
212}