Commit 19e9256c1d96
tarball3 files changed+158-52
@@ -382,24 +382,17 @@ impl EventCache {
382382
self.inner.handle_room_updates(updates).await383383
}384384
385
- /// Wait until every [`RoomUpdates`] the client had sent when called has386
- /// 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, in386
+ /// the client's `room_updates_sent` terms, or `None` if this cache never387
+ /// 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 as393
+ /// 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.@@ -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,@@ -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 on60
-/// 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 a64
-/// 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 up115
+ /// 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 other131130
/// types.132131
internal_channel: Sender<SlidingSyncInternalMessage>,132
+}133
+134
+/// A `pos` saved before the event cache stores its response's events resumes135
+/// past them after a kill, with no gap left to fetch them, so each `pos` waits136
+/// 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 the140
+ /// 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 last135
- /// 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 otherwise148
+/// 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).await146167
}147168
169
+ /// Save the newest held position whose room updates are within the170
+ /// `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 saved550
+ // lags while the event cache catches up, and going back to it refetches551
+ // 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.pos492557
);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 device504572
// 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 events668
- // resumes past them after a kill, and no gap is left to fetch them. The669
- // event cache can stall for good, so a lagging one leaves the last saved670
- // `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).await675
- };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
+ None739
+ };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
+ .await1821
+ .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![