atagen

matrix-rust-sdk

public
Star0
Watch0

Commit f05c136873e9

tarball

atagen <boss@atagen.co>2026-10-27 04:45 UTC

5 files changed+61-5
sliding-sync: save pos only after the event cache handled the response

`RoomUpdates` reach the event cache through a broadcast, so `cache_to_storage` could persist a `pos` before the response's events were stored, and a kill in between resumed past them with no gap to back-paginate. The rooms connection now waits for `room_updates_task` to catch up with the client's send count, for at most 10s, after which pos is saved anyway as before.
Modified file: crates/matrix-sdk/src/client/mod.rs+18 additions−1 deletions
@@ -19,7 +19,10 @@ use std::{
1919 fmt::{self, Debug},
2020 future::{Future, ready},
2121 pin::Pin,
22- sync::{Arc, Mutex as StdMutex, RwLock as StdRwLock, Weak},
22+ sync::{
23+ Arc, Mutex as StdMutex, RwLock as StdRwLock, Weak,
24+ atomic::{AtomicU64, Ordering},
25+ },
2326 time::Duration,
2427 };
2528
@@ -348,6 +351,9 @@ pub(crate) struct ClientInner {
348351 /// sync response.
349352 pub(crate) room_updates_sender: broadcast::Sender<RoomUpdates>,
350353
354+ /// How many [`RoomUpdates`] have been sent on `room_updates_sender`.
355+ room_updates_sent: AtomicU64,
356+
351357 /// Whether the client should update its homeserver URL with the discovery
352358 /// information present in the login response.
353359 respect_login_well_known: bool,
@@ -461,6 +467,7 @@ impl ClientInner {
461467 // A single `RoomUpdates` is sent once per sync, so we assume that 32 is sufficient
462468 // ballast for all observers to catch up.
463469 room_updates_sender: broadcast::Sender::new(32),
470+ room_updates_sent: AtomicU64::new(0),
464471 respect_login_well_known,
465472 sync_beat: event_listener::Event::new(),
466473 event_cache,
@@ -1281,6 +1288,16 @@ impl Client {
12811288 self.inner.room_updates_sender.subscribe()
12821289 }
12831290
1291+ pub(crate) fn room_updates_sent(&self) -> u64 {
1292+ self.inner.room_updates_sent.load(Ordering::SeqCst)
1293+ }
1294+
1295+ pub(crate) fn send_room_updates(&self, rooms: RoomUpdates) {
1296+ self.inner.room_updates_sent.fetch_add(1, Ordering::SeqCst);
1297+ // Ignore errors when there are no receivers.
1298+ let _ = self.inner.room_updates_sender.send(rooms);
1299+ }
1300+
12841301 pub(crate) async fn notification_handlers(
12851302 &self,
12861303 ) -> RwLockReadGuard<'_, Vec<NotificationHandlerFn>> {
Modified file: crates/matrix-sdk/src/event_cache/mod.rs+28 additions−2 deletions
@@ -31,6 +31,7 @@ use std::{
3131 collections::HashMap,
3232 fmt,
3333 sync::{Arc, OnceLock, RwLock as StdRwLock, RwLockReadGuard, RwLockWriteGuard},
34+ time::Duration,
3435 };
3536
3637 use futures_util::future::try_join_all;
@@ -42,11 +43,12 @@ use matrix_sdk_base::{
4243 task_monitor::BackgroundTaskHandle,
4344 timer,
4445 };
46+use matrix_sdk_common::timeout::timeout;
4547 use ruma::{OwnedRoomId, RoomId};
4648 use tokio::sync::{
4749 Mutex, OwnedRwLockReadGuard, OwnedRwLockWriteGuard, RwLock,
4850 broadcast::{Receiver, Sender, channel},
49- mpsc,
51+ mpsc, watch,
5052 };
5153 use tracing::{debug, error, instrument, trace};
5254
@@ -226,6 +228,7 @@ impl EventCache {
226228 config: StdRwLock::new(EventCacheConfig::default()),
227229 store: event_cache_store,
228230 multiple_room_updates_lock: Default::default(),
231+ room_updates_handled: watch::Sender::new(0),
229232 by_room: Default::default(),
230233 drop_handles: Default::default(),
231234 auto_shrink_sender: Default::default(),
@@ -271,9 +274,11 @@ impl EventCache {
271274 let task_monitor = client.task_monitor();
272275
273276 // Spawn the task that will listen to all the room updates at once.
277+ let room_updates = client.subscribe_to_all_room_updates();
278+ self.inner.room_updates_handled.send_replace(client.room_updates_sent());
274279 let listen_updates_task = task_monitor.spawn_infinite_task("event_cache::room_updates_task", tasks::room_updates_task(
275280 self.inner.clone(),
276- client.subscribe_to_all_room_updates(),
281+ room_updates,
277282 )).abort_on_drop();
278283
279284 let ignore_user_list_update_task = task_monitor.spawn_infinite_task("event_cache::ignore_user_list_update_task", tasks::ignore_user_list_update_task(
@@ -358,6 +363,23 @@ impl EventCache {
358363 self.inner.handle_room_updates(updates).await
359364 }
360365
366+ /// Wait until every [`RoomUpdates`] the client had sent when called has
367+ /// been handled, for at most `limit`.
368+ ///
369+ /// Returns `false` if `limit` ran out first.
370+ pub(crate) async fn wait_for_room_updates(&self, limit: Duration) -> bool {
371+ let Ok(client) = self.inner.client() else {
372+ return true;
373+ };
374+ if !self.has_subscribed() {
375+ return true;
376+ }
377+ let target = client.room_updates_sent();
378+ let mut handled = self.inner.room_updates_handled.subscribe();
379+ let waited = timeout(handled.wait_for(|handled| *handled >= target), limit).await;
380+ waited.is_ok()
381+ }
382+
361383 /// Check whether [`EventCache::subscribe`] has been called.
362384 pub fn has_subscribed(&self) -> bool {
363385 self.inner.drop_handles.get().is_some()
@@ -488,6 +510,10 @@ struct EventCacheInner {
488510 // TODO: that's the place to add a cross-process lock!
489511 multiple_room_updates_lock: Mutex<()>,
490512
513+ /// How many of the client's [`RoomUpdates`] have been handled, counted in
514+ /// the client's `room_updates_sent` terms.
515+ room_updates_handled: watch::Sender<u64>,
516+
491517 /// Lazily-filled cache of live [`RoomEventCache`], once per room.
492518 //
493519 // It's behind an `Arc` to get owned locks.
Modified file: crates/matrix-sdk/src/event_cache/tasks.rs+2 additions
@@ -67,6 +67,7 @@ pub(super) async fn room_updates_task(
6767 }
6868 }
6969 }
70+ inner.room_updates_handled.send_modify(|handled| *handled += 1);
7071 }
7172
7273 Err(RecvError::Lagged(num_skipped)) => {
@@ -77,6 +78,7 @@ pub(super) async fn room_updates_task(
7778 if let Err(err) = inner.clear_all_rooms().await {
7879 error!("when clearing storage after lag in listen_task: {err}");
7980 }
81+ inner.room_updates_handled.send_modify(|handled| *handled += num_skipped);
8082 }
8183
8284 Err(RecvError::Closed) => {
Modified file: crates/matrix-sdk/src/sliding_sync/mod.rs+12 additions
@@ -53,6 +53,10 @@ pub use self::{builder::*, client::VersionBuilderError, error::*, list::*};
5353 use self::{cache::restore_sliding_sync_state, client::SlidingSyncResponseProcessor};
5454 use crate::{Client, Result, config::RequestConfig};
5555
56+/// How long a rooms response waits for the event cache before its `pos` is
57+/// saved anyway.
58+const ROOM_UPDATES_WAIT: Duration = Duration::from_secs(10);
59+
5660 /// The Sliding Sync instance.
5761 ///
5862 /// It is OK to clone this type as much as you need: cloning it is cheap.
@@ -649,6 +653,14 @@ impl SlidingSync {
649653 .handle_response(response, &mut position_guard, requested_required_states)
650654 .await?;
651655
656+ // A `pos` saved before the event cache stores this response's events
657+ // resumes past them after a kill, and no gap is left to fetch them.
658+ if this.must_process_rooms_response().await
659+ && !this.inner.client.event_cache().wait_for_room_updates(ROOM_UPDATES_WAIT).await
660+ {
661+ warn!("Saving the position before the event cache handled the response");
662+ }
663+
652664 this.cache_to_storage(&position_guard).await?;
653665
654666 // Release the position guard lock.
Modified file: crates/matrix-sdk/src/sync.rs+1 additions−2 deletions
@@ -188,8 +188,7 @@ impl Client {
188188 self.handle_sync_events(HandlerKind::Presence, None, presence).await?;
189189 self.handle_sync_to_device_events(to_device).await?;
190190
191- // Ignore errors when there are no receivers.
192- let _ = self.inner.room_updates_sender.send(rooms.clone());
191+ self.send_room_updates(rooms.clone());
193192
194193 for (room_id, room_info) in &rooms.joined {
195194 let Some(room) = self.get_room(room_id) else {