atagen

matrix-rust-sdk

public
Star0
Watch0

Commit 82e22202f3da

tarball

atagen <boss@atagen.co>2026-10-27 06:25 UTC

4 files changed+59-42
event-cache: stop a stalled event save from holding the room state

`RoomEventCache::save_events` held the room state write lock while it waited on a spawned `save_event`. On wasm that save can stall with no IndexedDB transaction open, and the lock holder report caught one at room open, behind a `Room` fetch that saves what it got, wedging the history restore and every later sync of that room behind it. The save only touches the store, whose single transaction keeps each read and write together, so it now runs after the lock is released. Every save also stops waiting after 10s, so the saves that still run under the lock during sync post-processing are bounded too.
Modified file: crates/matrix-sdk/src/event_cache/caches/mod.rs+42 additions−2 deletions
@@ -12,18 +12,26 @@
1212 // See the License for the specific language governing permissions and
1313 // limitations under the License.
1414
15-use std::sync::{Arc, atomic::AtomicU64};
15+use std::{
16+ sync::{Arc, atomic::AtomicU64},
17+ time::Duration,
18+};
1619
1720 use eyeball::SharedObservable;
1821 use eyeball_im::VectorDiff;
1922 use matrix_sdk_base::{
2023 ThreadingSupport,
21- event_cache::{Event, store::EventCacheStoreLock},
24+ event_cache::{
25+ Event,
26+ store::{EventCacheStoreLock, EventCacheStoreLockGuard},
27+ },
2228 linked_chunk::Position,
2329 sync::{JoinedRoomUpdate, LeftRoomUpdate},
2430 };
31+use matrix_sdk_common::{executor::spawn, timeout::timeout};
2532 use ruma::{OwnedRoomId, RoomId};
2633 use tokio::sync::{broadcast::Sender, mpsc};
34+use tracing::{debug, warn};
2735
2836 use super::{EventCacheError, EventsOrigin, Result};
2937 use crate::{
@@ -39,6 +47,38 @@ mod read_receipts;
3947 pub mod room;
4048 pub mod thread;
4149
50+/// How long a caller waits for events to reach the store.
51+const SAVE_WAIT: Duration = Duration::from_secs(10);
52+
53+/// Save events into the store, without notifying observers.
54+///
55+/// The save runs in its own task so cancelling the caller can't cut it short, and the caller
56+/// stops waiting after [`SAVE_WAIT`] so a save that never finishes can't hold a cache lock.
57+pub(super) async fn save_events(
58+ store: EventCacheStoreLockGuard,
59+ room_id: OwnedRoomId,
60+ events: Vec<Event>,
61+) -> Result<()> {
62+ let count = events.len();
63+ let saving = spawn({
64+ let room_id = room_id.clone();
65+ async move {
66+ debug!(%room_id, count, "saving events to the store");
67+ for event in events {
68+ store.save_event(&room_id, event).await?;
69+ }
70+ Result::Ok(())
71+ }
72+ });
73+ match timeout(saving, SAVE_WAIT).await {
74+ Ok(joined) => joined.expect("joining failed"),
75+ Err(_) => {
76+ warn!(%room_id, count, "events not saved to the store yet, no longer waiting");
77+ Ok(())
78+ }
79+ }
80+}
81+
4282 /// A type to hold all the caches for a given room.
4383 #[derive(Debug)]
4484 pub(super) struct Caches {
Modified file: crates/matrix-sdk/src/event_cache/caches/room/mod.rs+9 additions−7 deletions
@@ -429,16 +429,18 @@ impl RoomEventCache {
429429 /// Save some events in the event cache, for further retrieval with
430430 /// [`Self::event`].
431431 pub(crate) async fn save_events(&self, events: impl IntoIterator<Item = Event>) {
432- match self.inner.state.write().await {
433- Ok(mut state_guard) => {
434- if let Err(err) = state_guard.save_events(events).await {
435- warn!("couldn't save event in the event cache: {err}");
436- }
437- }
438-
432+ // the save touches only the store, which keeps each save's read and write in one
433+ // transaction, so holding the room's state across it only wedged sync behind it
434+ let store = match self.inner.state.read().await {
435+ Ok(state_guard) => state_guard.store_guard(),
439436 Err(err) => {
440437 warn!("couldn't save event in the event cache: {err}");
438+ return;
441439 }
440+ };
441+ let events = events.into_iter().collect();
442+ if let Err(err) = super::save_events(store, self.inner.room_id.clone(), events).await {
443+ warn!("couldn't save event in the event cache: {err}");
442444 }
443445 }
444446
Modified file: crates/matrix-sdk/src/event_cache/caches/room/state.rs+6 additions−16 deletions
@@ -36,7 +36,6 @@ use matrix_sdk_base::{
3636 serde_helpers::{extract_edit_target, extract_thread_root},
3737 sync::Timeline,
3838 };
39-use matrix_sdk_common::executor::spawn;
4039 use ruma::{
4140 EventId, OwnedEventId, OwnedRoomId, OwnedUserId, TransactionId,
4241 events::{
@@ -462,6 +461,10 @@ impl<'a> RoomEventCacheStateLockReadGuard<'a> {
462461 super::history::gap_positions(&self.state.room_linked_chunk)
463462 }
464463
464+ pub(super) fn store_guard(&self) -> EventCacheStoreLockGuard {
465+ self.store.clone()
466+ }
467+
465468 pub(super) fn has_sync_hole(&self) -> bool {
466469 self.state.sync_hole.is_some()
467470 }
@@ -1624,21 +1627,8 @@ impl<'a> RoomEventCacheStateLockWriteGuard<'a> {
16241627 &mut self,
16251628 events: impl IntoIterator<Item = Event>,
16261629 ) -> Result<(), EventCacheError> {
1627- let store = self.store.clone();
1628- let room_id = self.state.room_id.clone();
1629- let events = events.into_iter().collect::<Vec<_>>();
1630-
1631- // Spawn a task so the save is uninterrupted by task cancellation.
1632- spawn(async move {
1633- for event in events {
1634- store.save_event(&room_id, event).await?;
1635- }
1636- super::Result::Ok(())
1637- })
1638- .await
1639- .expect("joining failed")?;
1640-
1641- Ok(())
1630+ let events = events.into_iter().collect();
1631+ super::super::save_events(self.store.clone(), self.state.room_id.clone(), events).await
16421632 }
16431633
16441634 #[cfg(test)]
Modified file: crates/matrix-sdk/src/event_cache/caches/thread/state.rs+2 additions−17 deletions
@@ -19,7 +19,6 @@ use matrix_sdk_base::{
1919 event_cache::{Event, Gap, store::EventCacheStoreLock},
2020 linked_chunk::{OwnedLinkedChunkId, Position, Update},
2121 };
22-use matrix_sdk_common::executor::spawn;
2322 use ruma::{OwnedEventId, OwnedRoomId};
2423 use tokio::sync::broadcast::Sender;
2524 use tracing::instrument;
@@ -192,22 +191,8 @@ impl<'a> ThreadEventCacheStateLockWriteGuard<'a> {
192191
193192 /// Save events into the database, without notifying observers.
194193 pub async fn save_events(&mut self, events: impl IntoIterator<Item = Event>) -> Result<()> {
195- let store = self.store.clone();
196- let room_id = self.state.room_id.clone();
197- let events = events.into_iter().collect::<Vec<_>>();
198-
199- // Spawn a task so the save is uninterrupted by task cancellation.
200- spawn(async move {
201- for event in events {
202- store.save_event(&room_id, event).await?;
203- }
204-
205- Result::Ok(())
206- })
207- .await
208- .expect("joining failed")?;
209-
210- Ok(())
194+ let events = events.into_iter().collect();
195+ super::super::save_events(self.store.clone(), self.state.room_id.clone(), events).await
211196 }
212197
213198 /// Reset this data structure as if it were brand new.