Commit dc40ad30111d
tarball3 files changed+149-0
@@ -99,6 +99,7 @@ enum HistoryOperation {
9999
Recent,100100
Fill(OwnedTransactionId),101101
SpendBudget(OwnedTransactionId),102
+ ProbeSyncHole,102103
Append(Vec<Event>, Option<String>),103104
Restore,104105
Barrier,@@ -110,6 +111,7 @@ impl HistoryOperation {
110111
Self::Recent => "recent",111112
Self::Fill(_) => "fill",112113
Self::SpendBudget(_) => "spend budget",114
+ Self::ProbeSyncHole => "probe sync hole",113115
Self::Append(..) => "append",114116
Self::Restore => "restore",115117
Self::Barrier => "barrier",@@ -121,6 +123,7 @@ impl HistoryOperation {
121123
enum HistoryResponse {122124
Recent(RecentHistoryOutcome),123125
Fill(HistoryGapFillOutcome),126
+ Probe(Option<usize>),124127
Append(Option<HistoryGap>),125128
Restored,126129
}@@ -162,6 +165,9 @@ async fn coordinate_history(
162165
HistoryOperation::SpendBudget(id) => {163166
cache.spend_history_budget_inner(id).await.map(HistoryResponse::Fill)164167
}168
+ HistoryOperation::ProbeSyncHole => {169
+ cache.probe_sync_hole_inner().await.map(HistoryResponse::Probe)170
+ }165171
HistoryOperation::Append(events, cursor) => cache166172
.append_history_section_inner(events, cursor)167173
.await@@ -440,6 +446,61 @@ impl RoomEventCache {
440446
Ok((chunk.events().map(|(_, event)| event.clone()).collect(), gap_positions(chunk)))441447
}442448
449
+ #[doc = "Looks once behind the newest previous-batch token a non-limited sync handed out. Answers None when there was none, else how many unknown events it inserted."]450
+ pub async fn probe_sync_hole(&self) -> Result<Option<usize>> {451
+ match self.request_history(HistoryOperation::ProbeSyncHole).await? {452
+ HistoryResponse::Probe(outcome) => Ok(outcome),453
+ _ => unreachable!("coordinator response matches request"),454
+ }455
+ }456
+457
+ async fn probe_sync_hole_inner(&self) -> Result<Option<usize>> {458
+ let Some(hole) = self.inner.state.write().await?.take_sync_hole() else {459
+ return Ok(None);460
+ };461
+ let room = self.inner.weak_room.get().ok_or(EventCacheError::ClientDropped)?;462
+ let mut options = MessagesOptions::backward().from(hole.token.as_str());463
+ options.limit = 50u16.into();464
+ let response = room465
+ .messages(options)466
+ .await467
+ .map_err(|error| EventCacheError::PaginationError(std::sync::Arc::new(error)))?;468
+ let mut state = self.inner.state.write().await?;469
+ let mut candidate = state.history_candidate().await?;470
+ let known: HashSet<_> =471
+ candidate.events().filter_map(|(_, event)| event.event_id()).collect();472
+ let mut joined = false;473
+ let mut missing = Vec::new();474
+ for event in response.chunk {475
+ match event.event_id() {476
+ Some(id) if known.contains(&id) => {477
+ joined = true;478
+ break;479
+ }480
+ _ => missing.push(event),481
+ }482
+ }483
+ let count = missing.len();484
+ tracing::debug!(room_id = %self.room_id(), new = count, joined, "history gap probe");485
+ if missing.is_empty() {486
+ return Ok(Some(0));487
+ }488
+ missing.reverse();489
+ let gap = (!joined).then(|| response.end).flatten().map(|token| Gap {490
+ token,491
+ history: Some(HistoryGap {492
+ id: TransactionId::new(),493
+ older_boundary: None,494
+ newer_boundary: missing.iter().find_map(Event::event_id),495
+ visited_cursors: Vec::new(),496
+ status: HistoryGapStatus::Idle,497
+ }),498
+ });499
+ candidate.insert_history_before(Some(&hole.before), missing.clone(), gap);500
+ state.commit_history_candidate(candidate, missing, HistoryCommitScope::Loaded).await?;501
+ Ok(Some(count))502
+ }503
+443504
#[doc = "Whether any event is retained, in memory or in the store."]444505
pub async fn has_retained_events(&self) -> Result<bool> {445506
Ok(self.inner.state.write().await?.newest_retained_event_id().await?.is_some())@@ -151,6 +151,17 @@ pub struct RoomEventCacheState {
151151
152152
/// A copy of the automatic pagination API object.153153
automatic_pagination: Option<AutomaticPagination>,154
+155
+ /// The newest previous-batch token a non-limited sync handed out and we dropped, with the156
+ /// first event of that batch. A server that under-reports `limited` hides skipped events157
+ /// behind it, so [`super::RoomEventCache::probe_sync_hole`] looks once.158
+ sync_hole: Option<SyncHole>,159
+}160
+161
+#[derive(Clone, Debug)]162
+pub(super) struct SyncHole {163
+ pub token: String,164
+ pub before: OwnedEventId,154165
}155166
156167
impl RoomEventCacheState {@@ -407,6 +418,7 @@ impl LockedRoomEventCacheState {
407418
linked_chunk_update_sender,408419
room_version_rules,409420
waited_for_initial_prev_token: false,421
+ sync_hole: None,410422
subscriber_count: Default::default(),411423
pinned_event_cache: OnceLock::new(),412424
automatic_pagination,@@ -588,6 +600,10 @@ impl<'a> RoomEventCacheStateLockWriteGuard<'a> {
588600
self.state.waited_for_initial_prev_token589601
}590602
603
+ pub(super) fn take_sync_hole(&mut self) -> Option<SyncHole> {604
+ self.state.sync_hole.take()605
+ }606
+591607
/// Get a mutable reference to the `waited_for_initial_prev_token` value.592608
pub fn waited_for_initial_prev_token_mut(&mut self) -> &mut bool {593609
&mut self.state.waited_for_initial_prev_token@@ -1025,6 +1041,13 @@ impl<'a> RoomEventCacheStateLockWriteGuard<'a> {
10251041
// knew about. This is slightly incorrect in the presence of10261042
// network splits, but this has shown to be Good Enough™.10271043
let newest_retained = self.newest_retained_event_id().await?;1044
+ if !timeline.limited && newest_retained.is_some() && !all_duplicates {1045
+ if let (Some(token), Some(before)) =1046
+ (prev_batch.clone(), events.iter().find_map(Event::event_id))1047
+ {1048
+ self.state.sync_hole = Some(SyncHole { token, before });1049
+ }1050
+ }10281051
if !timeline.limited && newest_retained.is_some() || all_duplicates {10291052
prev_batch = None;10301053
}@@ -708,3 +708,68 @@ async fn test_a_gap_whose_page_is_all_known_does_not_claim_the_start() {
708708
let outcome = cache.pagination().run_backwards_once(20).await.unwrap();709709
assert!(outcome.reached_start);710710
}711
+712
+#[async_test]713
+async fn test_a_probe_behind_a_non_limited_sync_brings_back_what_it_skipped() {714
+ let server = MatrixMockServer::new().await;715
+ let client = server.client_builder().build().await;716
+ client.event_cache().subscribe().unwrap();717
+ let room_id = room_id!("!hole:example.org");718
+ let room = server.sync_joined_room(&client, room_id).await;719
+ let (cache, _handles) = room.event_cache().await.unwrap();720
+ let f = event_factory(room_id);721
+ server722
+ .sync_room(723
+ &client,724
+ JoinedRoomBuilder::new(room_id)725
+ .add_timeline_event(f.text_msg("held").event_id(event_id!("$held"))),726
+ )727
+ .await;728
+ assert_eq!(cache.probe_sync_hole().await.unwrap(), None);729
+730
+ // the server skipped $skipped but still said the timeline was not limited731
+ server732
+ .sync_room(733
+ &client,734
+ JoinedRoomBuilder::new(room_id)735
+ .add_timeline_event(f.text_msg("new").event_id(event_id!("$new")))736
+ .set_timeline_prev_batch("before-new"),737
+ )738
+ .await;739
+ server740
+ .mock_room_messages()741
+ .match_from("before-new")742
+ .ok(RoomMessagesResponseTemplate::default().end_token("further").events(vec![743
+ f.text_msg("skipped").event_id(event_id!("$skipped")),744
+ f.text_msg("held").event_id(event_id!("$held")),745
+ ]))746
+ .mock_once()747
+ .mount()748
+ .await;749
+ assert_eq!(cache.probe_sync_hole().await.unwrap(), Some(1));750
+ assert_eq!(cache.probe_sync_hole().await.unwrap(), None);751
+ let ids: Vec<_> =752
+ cache.events().await.unwrap().iter().filter_map(|event| event.event_id()).collect();753
+ assert_eq!(ids, [event_id!("$held"), event_id!("$skipped"), event_id!("$new")]);754
+ assert!(cache.history_gaps().await.unwrap().is_empty());755
+756
+ // an honest server's page starts at what we hold, and nothing changes757
+ server758
+ .sync_room(759
+ &client,760
+ JoinedRoomBuilder::new(room_id)761
+ .add_timeline_event(f.text_msg("newer").event_id(event_id!("$newer")))762
+ .set_timeline_prev_batch("before-newer"),763
+ )764
+ .await;765
+ server766
+ .mock_room_messages()767
+ .match_from("before-newer")768
+ .ok(RoomMessagesResponseTemplate::default()769
+ .end_token("further")770
+ .events(vec![f.text_msg("new").event_id(event_id!("$new"))]))771
+ .mock_once()772
+ .mount()773
+ .await;774
+ assert_eq!(cache.probe_sync_hole().await.unwrap(), Some(0));775
+}