atagen

matrix-rust-sdk

public
Star0
Watch0

Commit 19e9256c1d96

tarball

atagen <boss@atagen.co>2026-10-28 06:01 UTC

3 files changed+158-52
sliding-sync: save pos behind the sync loop, without rewinding the live connection

Waiting up to 10s per response for the event cache stalled live updates all through a catch-up, and since the room list shares its pos through the store, every skipped save rewound the running connection to the last saved pos and refetched everything since. A response's pos now waits in HeldPositions until the event cache has handled its room updates, and a background task saves it, so the sync loop never waits. share_pos only adopts a stored pos this instance didn't write itself.
Modified file: crates/matrix-sdk/src/event_cache/mod.rs+11 additions−18 deletions
@@ -382,24 +382,17 @@ impl EventCache {
382382 self.inner.handle_room_updates(updates).await
383383 }
384384
385- /// Wait until every [`RoomUpdates`] the client had sent when called has
386- /// been handled, for at most `limit`.
387- ///
388- /// Returns `false` if `limit` ran out first.
389- pub(crate) async fn wait_for_room_updates(&self, limit: Duration) -> bool {
390- let Ok(client) = self.inner.client() else {
391- return true;
392- };
393- if !self.has_subscribed() {
394- return true;
395- }
396- if self.inner.dropped_room_updates.load(Ordering::SeqCst) {
397- return false;
398- }
399- let target = client.room_updates_sent();
400- let mut handled = self.inner.room_updates_handled.subscribe();
401- let waited = timeout(handled.wait_for(|handled| *handled >= target), limit).await;
402- waited.is_ok()
385+ /// Follows how many of the client's [`RoomUpdates`] have been handled, in
386+ /// the client's `room_updates_sent` terms, or `None` if this cache never
387+ /// subscribed and so has nothing to catch up on.
388+ pub(crate) fn room_updates_handled(&self) -> Option<watch::Receiver<u64>> {
389+ self.has_subscribed().then(|| self.inner.room_updates_handled.subscribe())
390+ }
391+
392+ /// Whether a room update was dropped, after which nothing counts as
393+ /// handled until a restart.
394+ pub(crate) fn dropped_room_updates(&self) -> bool {
395+ self.inner.dropped_room_updates.load(Ordering::SeqCst)
403396 }
404397
405398 /// Check whether [`EventCache::subscribe`] has been called.
Modified file: crates/matrix-sdk/src/sliding_sync/builder.rs+8 additions−4 deletions
@@ -11,8 +11,8 @@ use ruma::{OwnedRoomId, api::client::sync::sync_events::v5 as http};
1111 use tokio::sync::{Mutex as AsyncMutex, RwLock as AsyncRwLock, broadcast::channel};
1212
1313 use super::{
14- Error, ResponseCommitHook, SlidingSync, SlidingSyncInner, SlidingSyncListBuilder,
15- SlidingSyncPositionMarkers, Version, cache::format_storage_key_prefix,
14+ Error, HeldPositions, ResponseCommitHook, SlidingSync, SlidingSyncInner,
15+ SlidingSyncListBuilder, SlidingSyncPositionMarkers, Version, cache::format_storage_key_prefix,
1616 };
1717 use crate::{Client, Result};
1818
@@ -295,13 +295,17 @@ impl SlidingSyncBuilder {
295295
296296 lists,
297297
298- position: Arc::new(AsyncMutex::new(SlidingSyncPositionMarkers { pos })),
298+ position: Arc::new(AsyncMutex::new(SlidingSyncPositionMarkers { pos: pos.clone() })),
299+ held_positions: AsyncMutex::new(HeldPositions {
300+ pending: Default::default(),
301+ stored: pos,
302+ }),
303+ position_saver_started: AtomicBool::new(false),
299304
300305 room_subscriptions: StdRwLock::new(self.room_subscriptions),
301306 extensions: self.extensions.unwrap_or_default(),
302307
303308 internal_channel: internal_channel_sender,
304- event_cache_behind: AtomicBool::new(false),
305309
306310 poll_timeout: self.poll_timeout,
307311 network_timeout: self.network_timeout,
Modified file: crates/matrix-sdk/src/sliding_sync/mod.rs+139 additions−30 deletions
@@ -23,7 +23,7 @@ mod error;
2323 mod list;
2424
2525 use std::{
26- collections::{BTreeMap, btree_map::Entry},
26+ collections::{BTreeMap, VecDeque, btree_map::Entry},
2727 fmt::Debug,
2828 future::Future,
2929 sync::{
@@ -48,7 +48,7 @@ use ruma::{
4848 };
4949 use tokio::{
5050 select,
51- sync::{Mutex as AsyncMutex, OwnedMutexGuard, RwLock as AsyncRwLock, broadcast::Sender},
51+ sync::{Mutex as AsyncMutex, OwnedMutexGuard, RwLock as AsyncRwLock, broadcast::Sender, watch},
5252 };
5353 use tracing::{Instrument, Span, debug, error, info, instrument, trace, warn};
5454
@@ -56,14 +56,6 @@ pub use self::{builder::*, client::VersionBuilderError, error::*, list::*};
5656 use self::{cache::restore_sliding_sync_state, client::SlidingSyncResponseProcessor};
5757 use crate::{Client, Result, config::RequestConfig};
5858
59-/// How long a rooms response waits for the event cache before it gives up on
60-/// saving its `pos`.
61-const ROOM_UPDATES_WAIT: Duration = Duration::from_secs(10);
62-
63-/// The same wait once the event cache is known to be behind, long enough for a
64-/// recovered one to handle the latest response.
65-const ROOM_UPDATES_RECHECK: Duration = Duration::from_secs(1);
66-
6759 /// The Sliding Sync instance.
6860 ///
6961 /// It is OK to clone this type as much as you need: cloning it is cheap.
@@ -116,6 +108,13 @@ pub(super) struct SlidingSyncInner {
116108 /// `position` being updated, before sending a new request.
117109 position: Arc<AsyncMutex<SlidingSyncPositionMarkers>>,
118110
111+ /// Positions waiting for the event cache before they're saved.
112+ held_positions: AsyncMutex<HeldPositions>,
113+
114+ /// Whether the task saving held positions as the event cache catches up
115+ /// has been spawned.
116+ position_saver_started: AtomicBool,
117+
119118 /// The lists of this Sliding Sync instance.
120119 lists: AsyncRwLock<BTreeMap<String, SlidingSyncList>>,
121120
@@ -130,10 +129,32 @@ pub(super) struct SlidingSyncInner {
130129 /// Internal channel used to pass messages between Sliding Sync and other
131130 /// types.
132131 internal_channel: Sender<SlidingSyncInternalMessage>,
132+}
133+
134+/// A `pos` saved before the event cache stores its response's events resumes
135+/// past them after a kill, with no gap left to fetch them, so each `pos` waits
136+/// here until the event cache has handled its room updates.
137+#[derive(Debug)]
138+pub(super) struct HeldPositions {
139+ /// Oldest first, each with the client's `room_updates_sent` count the
140+ /// event cache has to reach before it can be saved.
141+ pub(super) pending: VecDeque<(u64, SlidingSyncPositionMarkers)>,
133142
134- /// Whether the event cache had not handled every response when the last
135- /// one was handled, in which case `pos` was not saved.
136- event_cache_behind: AtomicBool,
143+ /// The `pos` this instance last restored or saved.
144+ pub(super) stored: Option<String>,
145+}
146+
147+/// How many positions wait at most, as a stalled event cache would otherwise
148+/// add one per response. Dropping the oldest only saves a later one later.
149+const HELD_POSITIONS: usize = 256;
150+
151+impl HeldPositions {
152+ fn hold(&mut self, target: u64, position: SlidingSyncPositionMarkers) {
153+ if self.pending.len() == HELD_POSITIONS {
154+ self.pending.pop_front();
155+ }
156+ self.pending.push_back((target, position));
157+ }
137158 }
138159
139160 impl SlidingSync {
@@ -145,6 +166,46 @@ impl SlidingSync {
145166 cache::store_sliding_sync_state(self, position).await
146167 }
147168
169+ /// Save the newest held position whose room updates are within the
170+ /// `handled` count.
171+ async fn save_handled_positions(&self, handled: u64) -> Result<()> {
172+ let mut held = self.inner.held_positions.lock().await;
173+ if self.inner.client.event_cache().dropped_room_updates() {
174+ let before = held.pending.len();
175+ held.pending.retain(|(target, _)| *target == 0);
176+ if held.pending.len() < before {
177+ warn!("Not saving the position, the event cache dropped a room update");
178+ }
179+ }
180+ let mut newest = None;
181+ while held.pending.front().is_some_and(|(target, _)| *target <= handled) {
182+ newest = held.pending.pop_front().map(|(_, position)| position);
183+ }
184+ if let Some(position) = newest {
185+ self.cache_to_storage(&position).await?;
186+ held.stored = position.pos;
187+ }
188+ Ok(())
189+ }
190+
191+ fn start_position_saver(&self, mut handled: watch::Receiver<u64>) {
192+ if self.inner.position_saver_started.swap(true, Ordering::SeqCst) {
193+ return;
194+ }
195+ let inner = Arc::downgrade(&self.inner);
196+ spawn(async move {
197+ while handled.changed().await.is_ok() {
198+ let Some(inner) = inner.upgrade() else {
199+ return;
200+ };
201+ let count = *handled.borrow_and_update();
202+ if let Err(error) = (SlidingSync { inner }).save_handled_positions(count).await {
203+ warn!(%error, "Failed to save a held position");
204+ }
205+ }
206+ });
207+ }
208+
148209 /// Create a new [`SlidingSyncBuilder`].
149210 pub fn builder(id: String, client: Client) -> Result<SlidingSyncBuilder, Error> {
150211 SlidingSyncBuilder::new(id, client)
@@ -473,6 +534,7 @@ impl SlidingSync {
473534
474535 let to_device_enabled = self.inner.extensions.to_device.enabled == Some(true);
475536
537+ let mut held = self.inner.held_positions.lock().await;
476538 let restored_fields = if self.inner.share_pos || to_device_enabled {
477539 restore_sliding_sync_state(&self.inner.client, &self.inner.storage_key).await?
478540 } else {
@@ -484,21 +546,27 @@ impl SlidingSync {
484546 let pos = if self.inner.share_pos {
485547 if let Some(fields) = &restored_fields {
486548 // Override the memory one with the database one, for consistency.
487- if fields.pos != position_guard.pos {
549+ // Only another process's `pos` though, since the one this instance saved
550+ // lags while the event cache catches up, and going back to it refetches
551+ // everything since.
552+ if fields.pos != held.stored {
488553 info!(
489554 "Pos from previous request ('{:?}') was different from \
490555 pos in database ('{:?}').",
491556 position_guard.pos, fields.pos
492557 );
493558 position_guard.pos = fields.pos.clone();
559+ held.stored = fields.pos.clone();
560+ held.pending.clear();
494561 }
495- fields.pos.clone()
562+ position_guard.pos.clone()
496563 } else {
497564 position_guard.pos.clone()
498565 }
499566 } else {
500567 position_guard.pos.clone()
501568 };
569+ drop(held);
502570
503571 // When the client sends a request with no `pos`, MSC4186 returns no device
504572 // lists updates, as it only returns changes since the provided `pos`
@@ -664,26 +732,27 @@ impl SlidingSync {
664732 .handle_response(response, &mut position_guard, requested_required_states)
665733 .await?;
666734
667- // A `pos` saved before the event cache stores this response's events
668- // resumes past them after a kill, and no gap is left to fetch them. The
669- // event cache can stall for good, so a lagging one leaves the last saved
670- // `pos` in place, and a restart replays from there instead.
671- let stored = !this.must_process_rooms_response().await || {
672- let behind = this.inner.event_cache_behind.load(Ordering::SeqCst);
673- let wait = if behind { ROOM_UPDATES_RECHECK } else { ROOM_UPDATES_WAIT };
674- this.inner.client.event_cache().wait_for_room_updates(wait).await
675- };
676- this.inner.event_cache_behind.store(!stored, Ordering::SeqCst);
677- if stored {
678- this.cache_to_storage(&position_guard).await?;
735+ let handled = if this.must_process_rooms_response().await {
736+ this.inner.client.event_cache().room_updates_handled()
679737 } else {
680- warn!("Not saving the position, the event cache hasn't handled every response");
681- }
738+ None
739+ };
740+ let target = handled.as_ref().map_or(0, |_| this.inner.client.room_updates_sent());
741+ this.inner.held_positions.lock().await.hold(target, position_guard.clone());
682742
683743 // Release the position guard lock.
684744 // It means that other responses can be generated and then handled later.
685745 drop(position_guard);
686746
747+ match handled {
748+ Some(handled) => {
749+ let count = *handled.borrow();
750+ this.start_position_saver(handled);
751+ this.save_handled_positions(count).await?;
752+ }
753+ None => this.save_handled_positions(u64::MAX).await?,
754+ }
755+
687756 debug!("Done handling response");
688757
689758 Ok(updates)
@@ -843,12 +912,16 @@ impl SlidingSync {
843912
844913 // Invalidate in memory.
845914 position.pos = None;
915+ let mut held = self.inner.held_positions.lock().await;
916+ held.pending.clear();
846917
847918 // Propagate to disk.
848919 // Note: this propagates both the sliding sync state and the cached lists'
849920 // state to disk.
850921 if let Err(err) = self.cache_to_storage(&position).await {
851922 warn!("Failed to invalidate cached sliding sync state: {err}");
923+ } else {
924+ held.stored = None;
852925 }
853926 }
854927
@@ -1022,7 +1095,7 @@ mod tests {
10221095
10231096 use super::{
10241097 SlidingSync, SlidingSyncBuilder, SlidingSyncList, SlidingSyncListBuilder, SlidingSyncMode,
1025- cache::restore_sliding_sync_state, http,
1098+ SlidingSyncPositionMarkers, cache::restore_sliding_sync_state, http,
10261099 };
10271100 use crate::{
10281101 Client, Result,
@@ -1736,6 +1809,42 @@ mod tests {
17361809 Ok(())
17371810 }
17381811
1812+ #[cfg(feature = "e2e-encryption")]
1813+ #[async_test]
1814+ async fn test_sliding_sync_keeps_its_own_pos_while_the_saved_one_waits() -> Result<()> {
1815+ let server = MockServer::start().await;
1816+ let client = logged_in_client(Some(server.uri())).await;
1817+ let sliding_sync = client.sliding_sync("elephant-sync")?.share_pos().build().await?;
1818+ let saved = || async {
1819+ restore_sliding_sync_state(&client, &sliding_sync.inner.storage_key)
1820+ .await
1821+ .unwrap()
1822+ .and_then(|fields| fields.pos)
1823+ };
1824+
1825+ {
1826+ let mut held = sliding_sync.inner.held_positions.lock().await;
1827+ held.hold(1, SlidingSyncPositionMarkers { pos: Some("1".to_owned()) });
1828+ held.hold(2, SlidingSyncPositionMarkers { pos: Some("2".to_owned()) });
1829+ }
1830+ sliding_sync.inner.position.lock().await.pos = Some("2".to_owned());
1831+
1832+ sliding_sync.save_handled_positions(0).await?;
1833+ assert_eq!(saved().await, None);
1834+ let (request, _, _) = sliding_sync.generate_sync_request().await?;
1835+ assert_eq!(request.pos.as_deref(), Some("2"));
1836+
1837+ sliding_sync.save_handled_positions(1).await?;
1838+ assert_eq!(saved().await.as_deref(), Some("1"));
1839+ let (request, _, _) = sliding_sync.generate_sync_request().await?;
1840+ assert_eq!(request.pos.as_deref(), Some("2"));
1841+
1842+ sliding_sync.save_handled_positions(5).await?;
1843+ assert_eq!(saved().await.as_deref(), Some("2"));
1844+
1845+ Ok(())
1846+ }
1847+
17391848 #[async_test]
17401849 async fn test_stop_sync_loop() -> Result<()> {
17411850 let (_server, sliding_sync) = new_sliding_sync(vec![