atagen

matrix-rust-sdk

public
Star0
Watch0

Commit ae6eba54f24b

tarball

atagen <boss@atagen.co>2026-10-27 05:59 UTC

1 file changed+139-17
event-cache: name who holds a cache state lock when a wait on it runs long

A room open on wasm can wedge with the history restore never reaching its first IndexedDB call while no IndexedDB transaction is open, so it waits on the room state lock behind a holder suspended elsewhere, and nothing in a trace says which. Each guard now records its caller, and a wait past 5s warns with every current holder and how long each has held the lock.
Modified file: crates/matrix-sdk/src/event_cache/caches/lock.rs+139 additions−17 deletions
@@ -19,12 +19,21 @@
1919 //!
2020 //! [`RoomEventCache`]: super::super::RoomEventCache
2121
22-use std::ops::{Deref, DerefMut};
22+use std::{
23+ future::Future,
24+ ops::{Deref, DerefMut},
25+ panic::Location,
26+ sync::Mutex as StdMutex,
27+ time::Duration,
28+};
2329
2430 use matrix_sdk_base::event_cache::store::{
2531 EventCacheStoreLock, EventCacheStoreLockGuard, EventCacheStoreLockState,
2632 };
33+use matrix_sdk_common::timeout::timeout;
34+use ruma::time::Instant;
2735 use tokio::sync::{Mutex, RwLock, RwLockWriteGuard};
36+use tracing::warn;
2837
2938 use super::super::Result;
3039
@@ -42,12 +51,95 @@ pub struct StateLock<S> {
4251 /// Please see inline comment of [`Self::read`] to understand why it
4352 /// exists.
4453 state_lock_upgrade_mutex: Mutex<()>,
54+
55+ holders: StdMutex<Holders>,
56+}
57+
58+/// How long a wait on a state lock runs before it names whoever holds the lock.
59+const SLOW_WAIT: Duration = Duration::from_secs(5);
60+
61+#[derive(Default)]
62+struct Holders {
63+ next: u64,
64+ held: Vec<(u64, &'static Location<'static>, &'static str, Instant)>,
65+}
66+
67+struct Holder<'a> {
68+ id: u64,
69+ holders: &'a StdMutex<Holders>,
70+}
71+
72+impl Drop for Holder<'_> {
73+ fn drop(&mut self) {
74+ let mut holders = self.holders.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
75+ holders.held.retain(|(id, ..)| *id != self.id);
76+ }
4577 }
4678
4779 impl<S> StateLock<S> {
4880 /// Construct a new [`StateLock`].
4981 pub fn new_inner(state: S) -> Self {
50- Self { locked_state: RwLock::new(state), state_lock_upgrade_mutex: Mutex::new(()) }
82+ Self {
83+ locked_state: RwLock::new(state),
84+ state_lock_upgrade_mutex: Mutex::new(()),
85+ holders: Default::default(),
86+ }
87+ }
88+
89+ fn hold(&self, caller: &'static Location<'static>, kind: &'static str) -> Holder<'_> {
90+ let mut holders = self.holders.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
91+ holders.next += 1;
92+ let id = holders.next;
93+ holders.held.push((id, caller, kind, Instant::now()));
94+ Holder { id, holders: &self.holders }
95+ }
96+
97+ async fn waiting<T>(
98+ &self,
99+ caller: &'static Location<'static>,
100+ kind: &'static str,
101+ acquire: impl Future<Output = T>,
102+ ) -> T {
103+ let mut acquire = std::pin::pin!(acquire);
104+ match timeout(acquire.as_mut(), SLOW_WAIT).await {
105+ Ok(acquired) => acquired,
106+ Err(_) => {
107+ let held = {
108+ let holders =
109+ self.holders.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
110+ holders
111+ .held
112+ .iter()
113+ .map(|(_, at, kind, since)| {
114+ format!("{kind} at {at} for {:?}", since.elapsed())
115+ })
116+ .collect::<Vec<_>>()
117+ .join(", ")
118+ };
119+ warn!(%caller, kind, %held, "still waiting on a cache state lock");
120+ acquire.await
121+ }
122+ }
123+ }
124+
125+ /// Lock this [`StateLock`] with per-thread shared access.
126+ #[track_caller]
127+ pub fn read<'a>(&'a self) -> impl Future<Output = Result<StateLockReadGuard<'a, S>>> + 'a
128+ where
129+ S: Store,
130+ StateLockWriteGuard<'a, S>: Reload,
131+ {
132+ self.read_at(Location::caller())
133+ }
134+
135+ /// Lock this [`StateLock`] with exclusive per-thread write access.
136+ #[track_caller]
137+ pub fn write<'a>(&'a self) -> impl Future<Output = Result<StateLockWriteGuard<'a, S>>> + 'a
138+ where
139+ S: Store,
140+ StateLockWriteGuard<'a, S>: Reload,
141+ {
142+ self.write_at(Location::caller())
51143 }
52144
53145 /// Lock this [`StateLock`] with per-thread shared access.
@@ -59,7 +151,10 @@ impl<S> StateLock<S> {
59151 ///
60152 /// If the cross-process lock over the store is dirty (see
61153 /// [`EventCacheStoreLockState`]), the state is reset to the last chunk.
62- pub async fn read<'a>(&'a self) -> Result<StateLockReadGuard<'a, S>>
154+ async fn read_at<'a>(
155+ &'a self,
156+ caller: &'static Location<'static>,
157+ ) -> Result<StateLockReadGuard<'a, S>>
63158 where
64159 S: Store,
65160 StateLockWriteGuard<'a, S>: Reload,
@@ -110,15 +205,21 @@ impl<S> StateLock<S> {
110205 //
111206 // [^1]: https://docs.rs/lock_api/0.4.14/lock_api/struct.RwLock.html#method.upgradable_read
112207 // [^2]: https://docs.rs/async-lock/3.4.1/async_lock/struct.RwLock.html#method.upgradable_read
113- let _state_lock_upgrade_guard = self.state_lock_upgrade_mutex.lock().await;
208+ let (_state_lock_upgrade_guard, state_guard) = self
209+ .waiting(caller, "read", async {
210+ let upgrade = self.state_lock_upgrade_mutex.lock().await;
114211
115- // Obtain a read lock.
116- let state_guard = self.locked_state.read().await;
212+ // Obtain a read lock.
213+ (upgrade, self.locked_state.read().await)
214+ })
215+ .await;
117216
118217 match state_guard.store().lock().await? {
119- EventCacheStoreLockState::Clean(store_guard) => {
120- Ok(StateLockReadGuard { state: state_guard, store: store_guard })
121- }
218+ EventCacheStoreLockState::Clean(store_guard) => Ok(StateLockReadGuard {
219+ state: state_guard,
220+ store: store_guard,
221+ _holder: self.hold(caller, "read"),
222+ }),
122223 EventCacheStoreLockState::Dirty(store_guard) => {
123224 // Drop the read lock, and take a write lock to modify the state.
124225 // This is safe because only one reader at a time (see
@@ -126,7 +227,11 @@ impl<S> StateLock<S> {
126227 drop(state_guard);
127228 let state_guard = self.locked_state.write().await;
128229
129- let mut guard = StateLockWriteGuard { state: state_guard, store: store_guard };
230+ let mut guard = StateLockWriteGuard {
231+ state: state_guard,
232+ store: store_guard,
233+ _holder: self.hold(caller, "read"),
234+ };
130235
131236 // Reload the state.
132237 guard.reload().await?;
@@ -151,19 +256,28 @@ impl<S> StateLock<S> {
151256 ///
152257 /// If the cross-process lock over the store is dirty (see
153258 /// [`EventCacheStoreLockState`]), the state is reset to the last chunk.
154- pub async fn write<'a>(&'a self) -> Result<StateLockWriteGuard<'a, S>>
259+ async fn write_at<'a>(
260+ &'a self,
261+ caller: &'static Location<'static>,
262+ ) -> Result<StateLockWriteGuard<'a, S>>
155263 where
156264 S: Store,
157265 StateLockWriteGuard<'a, S>: Reload,
158266 {
159- let state_guard = self.locked_state.write().await;
267+ let state_guard = self.waiting(caller, "write", self.locked_state.write()).await;
160268
161269 match state_guard.store().lock().await? {
162- EventCacheStoreLockState::Clean(store_guard) => {
163- Ok(StateLockWriteGuard { state: state_guard, store: store_guard })
164- }
270+ EventCacheStoreLockState::Clean(store_guard) => Ok(StateLockWriteGuard {
271+ state: state_guard,
272+ store: store_guard,
273+ _holder: self.hold(caller, "write"),
274+ }),
165275 EventCacheStoreLockState::Dirty(store_guard) => {
166- let mut guard = StateLockWriteGuard { state: state_guard, store: store_guard };
276+ let mut guard = StateLockWriteGuard {
277+ state: state_guard,
278+ store: store_guard,
279+ _holder: self.hold(caller, "write"),
280+ };
167281
168282 // Reload the state.
169283 guard.reload().await?;
@@ -184,6 +298,8 @@ pub struct StateLockReadGuard<'a, S> {
184298
185299 /// The cross-process lock guard over the store.
186300 pub store: EventCacheStoreLockGuard,
301+
302+ _holder: Holder<'a>,
187303 }
188304
189305 impl<'a, S> Deref for StateLockReadGuard<'a, S> {
@@ -201,6 +317,8 @@ pub struct StateLockWriteGuard<'a, S> {
201317
202318 /// The cross-process lock guard over the store.
203319 pub store: EventCacheStoreLockGuard,
320+
321+ _holder: Holder<'a>,
204322 }
205323
206324 impl<'a, S> Deref for StateLockWriteGuard<'a, S> {
@@ -226,7 +344,11 @@ impl<'a, S> StateLockWriteGuard<'a, S> {
226344 /// It returns an RAII guard which will drop the read access to the
227345 /// state and to the store when dropped.
228346 fn downgrade(self) -> StateLockReadGuard<'a, S> {
229- StateLockReadGuard { state: self.state.downgrade(), store: self.store }
347+ StateLockReadGuard {
348+ state: self.state.downgrade(),
349+ store: self.store,
350+ _holder: self._holder,
351+ }
230352 }
231353 }