atagen

matrix-rust-sdk

public
Star0
Watch0

Commit ba8ba5be174f

tarball

atagen <boss@atagen.co>2026-10-27 05:11 UTC

4 files changed+215-18
event-cache: never trust our own sends to mark where history joins

`insert_sent_event_from_send_queue` appends a sent event at the live end before sync delivers what others sent ahead of it, so its retained position is a guess. As the newest retained event it made a limited sync that contained it drop its `prev_batch`, and gap fills, recent loads and the hole probe joined on it, so the events before it were lost with no gap left. Our events now anchor only in rooms where nobody else's event is retained, and a server page re-places them otherwise.
Modified file: crates/matrix-sdk/src/event_cache/caches/room/history.rs+66 additions−10 deletions
@@ -8,7 +8,7 @@ use matrix_sdk_base::{
88 event_cache::{Event, Gap},
99 linked_chunk::ChunkContent,
1010 };
11-use ruma::{OwnedEventId, OwnedTransactionId, TransactionId};
11+use ruma::{OwnedEventId, OwnedTransactionId, TransactionId, UserId};
1212 use tokio::sync::{mpsc, oneshot, watch};
1313
1414 use super::{HistoryCommitScope, RoomEventCache, RoomEventCacheInner};
@@ -258,16 +258,21 @@ impl RoomEventCache {
258258 &self,
259259 mut cancellation: watch::Receiver<bool>,
260260 ) -> Result<RecentHistoryOutcome> {
261- let (anchor, retained) = {
261+ let room = self.inner.weak_room.get().ok_or(EventCacheError::ClientDropped)?;
262+ let (anchor, retained, anchors) = {
262263 let state = self.inner.state.write().await?;
264+ let anchors = Anchors::new(
265+ room.own_user_id(),
266+ state.room_linked_chunk().events().map(|(_, event)| event),
267+ );
263268 let retained: HashSet<_> = state
264269 .room_linked_chunk()
265270 .events()
271+ .filter(|(_, event)| anchors.holds(event))
266272 .filter_map(|(_, event)| event.event_id())
267273 .collect();
268- (state.newest_retained_event_id().await?, retained)
274+ (state.newest_retained_event_id().await?, retained, anchors)
269275 };
270- let room = self.inner.weak_room.get().ok_or(EventCacheError::ClientDropped)?;
271276 let mut cursor = None;
272277 let mut visited = Vec::new();
273278 let mut events = Vec::new();
@@ -293,7 +298,10 @@ impl RoomEventCache {
293298 let mut page = response.chunk;
294299 if let Some(index) = anchor.as_ref().and_then(|anchor| {
295300 page.iter().position(|event| {
296- event.event_id().is_some_and(|id| &id == anchor || retained.contains(&id))
301+ anchors.holds(event)
302+ && event
303+ .event_id()
304+ .is_some_and(|id| &id == anchor || retained.contains(&id))
297305 })
298306 }) {
299307 page.truncate(index);
@@ -472,22 +480,27 @@ impl RoomEventCache {
472480 let mut candidate = state.history_candidate().await?;
473481 let known: HashSet<_> =
474482 candidate.events().filter_map(|(_, event)| event.event_id()).collect();
483+ let anchors = Anchors::new(room.own_user_id(), candidate.events().map(|(_, event)| event));
475484 let mut joined = false;
476485 let mut missing = Vec::new();
477486 for event in response.chunk {
478487 match event.event_id() {
479- Some(id) if known.contains(&id) => {
488+ Some(id) if known.contains(&id) && anchors.holds(&event) => {
480489 joined = true;
481490 break;
482491 }
483492 _ => missing.push(event),
484493 }
485494 }
486- let count = missing.len();
495+ let count = missing
496+ .iter()
497+ .filter(|event| event.event_id().is_none_or(|id| !known.contains(&id)))
498+ .count();
487499 tracing::debug!(room_id = %self.room_id(), new = count, joined, "history gap probe");
488- if missing.is_empty() {
500+ if count == 0 {
489501 return Ok(Some(0));
490502 }
503+ anchors.unplace(&mut candidate, &missing);
491504 missing.reverse();
492505 let gap = (!joined).then(|| response.end).flatten().map(|token| Gap {
493506 token,
@@ -676,6 +689,7 @@ impl RoomEventCache {
676689 };
677690 let (gap, events) = apply_page(
678691 &mut candidate,
692+ room.own_user_id(),
679693 &gap_id,
680694 events,
681695 next,
@@ -691,6 +705,40 @@ impl RoomEventCache {
691705 }
692706 }
693707
708+pub(super) fn from_others(own_user_id: &UserId, event: &Event) -> bool {
709+ event.sender().is_some_and(|sender| sender != own_user_id)
710+}
711+
712+/// Which retained events show where a server page meets retained history. The send queue
713+/// appends our own sends at the live end, ahead of whatever sync has yet to deliver, so ours
714+/// only count while nobody else's event is retained, and a page re-places them otherwise.
715+#[derive(Clone, Copy)]
716+struct Anchors<'a> {
717+ own_user_id: &'a UserId,
718+ ours: bool,
719+}
720+
721+impl<'a> Anchors<'a> {
722+ fn new<'e>(own_user_id: &'a UserId, retained: impl IntoIterator<Item = &'e Event>) -> Self {
723+ let ours = !retained.into_iter().any(|event| from_others(own_user_id, event));
724+ Self { own_user_id, ours }
725+ }
726+
727+ fn holds(&self, event: &Event) -> bool {
728+ self.ours || from_others(self.own_user_id, event)
729+ }
730+
731+ fn unplace(&self, candidate: &mut EventLinkedChunk, page: &[Event]) {
732+ let positions = page
733+ .iter()
734+ .filter(|event| !self.holds(event))
735+ .filter_map(|event| candidate.find_event(&event.event_id()?))
736+ .map(|(position, _)| position)
737+ .collect();
738+ candidate.remove_events_by_position(positions).expect("positions were just found");
739+ }
740+}
741+
694742 fn insert_run(
695743 candidate: &mut EventLinkedChunk,
696744 before: Option<&ruma::EventId>,
@@ -743,6 +791,7 @@ fn find_gap(candidate: &EventLinkedChunk, gap_id: &TransactionId) -> Option<Gap>
743791
744792 fn apply_page(
745793 candidate: &mut EventLinkedChunk,
794+ own_user_id: &UserId,
746795 gap_id: &TransactionId,
747796 mut events: Vec<Event>,
748797 next: Option<String>,
@@ -757,14 +806,20 @@ fn apply_page(
757806 let history = gap.history.as_mut().expect("the gap was checked under the state lock");
758807 // `/messages` may never return the boundary itself (filtered, redacted, or ordered apart
759808 // from sync arrival), so any event already below the gap proves the page reached it
760- let below: HashSet<_> = candidate
809+ let below_events: Vec<_> = candidate
761810 .chunks()
762811 .take_while(|chunk| chunk.identifier() != chunk_id)
763812 .filter_map(|chunk| match chunk.content() {
764- ChunkContent::Items(items) => Some(items.iter().filter_map(Event::event_id)),
813+ ChunkContent::Items(items) => Some(items.iter()),
765814 ChunkContent::Gap(_) => None,
766815 })
767816 .flatten()
817+ .collect();
818+ let anchors = Anchors::new(own_user_id, below_events.iter().copied());
819+ let below: HashSet<_> = below_events
820+ .into_iter()
821+ .filter(|event| anchors.holds(event))
822+ .filter_map(Event::event_id)
768823 .chain(history.older_boundary.clone())
769824 .collect();
770825 let overlap =
@@ -791,6 +846,7 @@ fn apply_page(
791846 if let Some(next) = next {
792847 gap.token = next;
793848 }
849+ anchors.unplace(candidate, &events);
794850 let mut known: HashSet<_> =
795851 candidate.events().filter_map(|(_, event)| event.event_id()).collect();
796852 events.retain(|event| event.event_id().is_none_or(|id| known.insert(id)));
Modified file: crates/matrix-sdk/src/event_cache/caches/room/state.rs+26 additions−7 deletions
@@ -859,8 +859,25 @@ impl<'a> RoomEventCacheStateLockWriteGuard<'a> {
859859 pub(super) async fn newest_retained_event_id(
860860 &self,
861861 ) -> Result<Option<OwnedEventId>, EventCacheError> {
862+ self.newest_retained_event_id_where(|_| true).await
863+ }
864+
865+ async fn newest_retained_event_id_from_others(
866+ &self,
867+ ) -> Result<Option<OwnedEventId>, EventCacheError> {
868+ let own_user_id = &self.state.own_user_id;
869+ self.newest_retained_event_id_where(|event| super::history::from_others(own_user_id, event))
870+ .await
871+ }
872+
873+ async fn newest_retained_event_id_where(
874+ &self,
875+ keep: impl Fn(&Event) -> bool,
876+ ) -> Result<Option<OwnedEventId>, EventCacheError> {
862877 let chunk = &self.state.room_linked_chunk;
863- if let Some(id) = chunk.revents().find_map(|(_, event)| event.event_id()) {
878+ if let Some(id) =
879+ chunk.revents().filter(|(_, event)| keep(event)).find_map(|(_, event)| event.event_id())
880+ {
864881 return Ok(Some(id));
865882 }
866883 let linked_chunk_id = LinkedChunkId::Room(&self.state.room_id);
@@ -870,7 +887,8 @@ impl<'a> RoomEventCacheStateLockWriteGuard<'a> {
870887 let mut seen = HashSet::from([first]);
871888 while let Some(previous) = self.store.load_previous_chunk(linked_chunk_id, first).await? {
872889 if let ChunkContent::Items(events) = &previous.content
873- && let Some(id) = events.iter().rev().find_map(Event::event_id)
890+ && let Some(id) =
891+ events.iter().rev().filter(|event| keep(event)).find_map(Event::event_id)
874892 {
875893 return Ok(Some(id));
876894 }
@@ -878,10 +896,7 @@ impl<'a> RoomEventCacheStateLockWriteGuard<'a> {
878896 if !seen.insert(first) {
879897 error!(room_id = %self.state.room_id, "stored linked chunk loops back on itself");
880898 return Err(EventCacheError::LinkedChunkLoader(
881- lazy_loader::LazyLoaderError::Cycle {
882- new_chunk: first,
883- with_chunk: first,
884- },
899+ lazy_loader::LazyLoaderError::Cycle { new_chunk: first, with_chunk: first },
885900 ));
886901 }
887902 }
@@ -1071,7 +1086,11 @@ impl<'a> RoomEventCacheStateLockWriteGuard<'a> {
10711086 }
10721087
10731088 let retained = newest_retained.is_some();
1074- let older_boundary = timeline.limited.then_some(newest_retained).flatten();
1089+ let older_boundary = if timeline.limited {
1090+ self.newest_retained_event_id_from_others().await?
1091+ } else {
1092+ None
1093+ };
10751094 let newer_boundary =
10761095 timeline.limited.then(|| events.iter().find_map(Event::event_id)).flatten();
10771096 if older_boundary.is_some() && events.iter().any(|event| event.event_id() == older_boundary)
Modified file: crates/matrix-sdk/tests/recent_history.rs+49 additions
@@ -773,3 +773,52 @@ async fn test_a_probe_behind_a_non_limited_sync_brings_back_what_it_skipped() {
773773 .await;
774774 assert_eq!(cache.probe_sync_hole().await.unwrap(), Some(0));
775775 }
776+
777+#[async_test]
778+async fn test_recent_history_does_not_join_on_our_own_send() {
779+ let server = MatrixMockServer::new().await;
780+ let client = server.client_builder().build().await;
781+ client.event_cache().subscribe().unwrap();
782+ let room_id = room_id!("!sent:example.org");
783+ let room = server.sync_joined_room(&client, room_id).await;
784+ let (cache, _handles) = room.event_cache().await.unwrap();
785+ let f = event_factory(room_id);
786+ let own_user_id = client.user_id().unwrap();
787+ server
788+ .sync_room(
789+ &client,
790+ JoinedRoomBuilder::new(room_id)
791+ .add_timeline_event(f.text_msg("first").event_id(event_id!("$first"))),
792+ )
793+ .await;
794+ server.mock_room_state_encryption().plain().mount().await;
795+ server.mock_room_send().ok(event_id!("$mine")).mock_once().mount().await;
796+ room.send_queue()
797+ .send(ruma::events::room::message::RoomMessageEventContent::text_plain("mine").into())
798+ .await
799+ .unwrap();
800+ tokio::time::timeout(Duration::from_secs(5), async {
801+ while cache.events().await.unwrap().len() < 2 {
802+ tokio::time::sleep(Duration::from_millis(10)).await;
803+ }
804+ })
805+ .await
806+ .unwrap();
807+ server
808+ .mock_room_messages()
809+ .match_limit(50)
810+ .ok(RoomMessagesResponseTemplate::default().end_token("unused").events(vec![
811+ f.text_msg("mine").sender(own_user_id).event_id(event_id!("$mine")),
812+ f.text_msg("theirs").event_id(event_id!("$theirs")),
813+ f.text_msg("first").event_id(event_id!("$first")),
814+ ]))
815+ .mock_once()
816+ .mount()
817+ .await;
818+
819+ let outcome = cache.load_recent_history().await.unwrap();
820+ assert_eq!(outcome.continuity, RecentHistoryContinuity::Joined);
821+ let ids: Vec<_> =
822+ cache.events().await.unwrap().iter().filter_map(|event| event.event_id()).collect();
823+ assert_eq!(ids, [event_id!("$first"), event_id!("$theirs"), event_id!("$mine")]);
824+}
Modified file: crates/matrix-sdk/tests/segmented_history.rs+74 additions−1 deletions
@@ -836,8 +836,81 @@ async fn test_cold_open_pagination_keeps_the_order_tracker_in_link_order() {
836836 cache.pagination().run_backwards_once(20).await.unwrap();
837837 let ids: Vec<_> =
838838 cache.events().await.unwrap().iter().filter_map(|event| event.event_id()).collect();
839+ assert_eq!(ids, vec![event_id!("$y"), event_id!("$z"), event_id!("$a"), event_id!("$live")]);
840+}
841+
842+#[async_test]
843+async fn test_a_gap_after_our_send_fills_what_others_sent_before_it() {
844+ let server = MatrixMockServer::new().await;
845+ let client = server.client_builder().build().await;
846+ client.event_cache().subscribe().unwrap();
847+ let room_id = room_id!("!sent:example.org");
848+ let room = server.sync_joined_room(&client, room_id).await;
849+ let (cache, _handles) = room.event_cache().await.unwrap();
850+ let f = EventFactory::new().room(room_id).sender(user_id!("@alice:example.org"));
851+ let own_user_id = client.user_id().unwrap();
852+ server
853+ .sync_room(
854+ &client,
855+ JoinedRoomBuilder::new(room_id)
856+ .add_timeline_event(f.text_msg("first").event_id(event_id!("$first")))
857+ .set_timeline_prev_batch("old-edge"),
858+ )
859+ .await;
860+ server.mock_room_state_encryption().plain().mount().await;
861+ server.mock_room_send().ok(event_id!("$mine")).mock_once().mount().await;
862+ room.send_queue()
863+ .send(ruma::events::room::message::RoomMessageEventContent::text_plain("mine").into())
864+ .await
865+ .unwrap();
866+ tokio::time::timeout(Duration::from_secs(5), async {
867+ while !cache
868+ .events()
869+ .await
870+ .unwrap()
871+ .iter()
872+ .any(|event| event.event_id().as_deref() == Some(event_id!("$mine")))
873+ {
874+ tokio::time::sleep(Duration::from_millis(10)).await;
875+ }
876+ })
877+ .await
878+ .unwrap();
879+
880+ server
881+ .sync_room(
882+ &client,
883+ JoinedRoomBuilder::new(room_id)
884+ .add_timeline_event(f.text_msg("later").event_id(event_id!("$later")))
885+ .set_timeline_limited()
886+ .set_timeline_prev_batch("before-later"),
887+ )
888+ .await;
889+ let gaps = cache.history_gaps().await.unwrap();
890+ assert_eq!(gaps.len(), 1);
891+ assert_eq!(gaps[0].older_boundary.as_deref(), Some(event_id!("$first")));
892+
893+ server
894+ .mock_room_messages()
895+ .match_from("before-later")
896+ .ok(RoomMessagesResponseTemplate::default().end_token("further").events(vec![
897+ f.text_msg("mine").sender(own_user_id).event_id(event_id!("$mine")),
898+ f.text_msg("theirs").event_id(event_id!("$theirs")),
899+ f.text_msg("first").event_id(event_id!("$first")),
900+ ]))
901+ .mock_once()
902+ .mount()
903+ .await;
904+ let HistoryGapFillOutcome::Updated { gap, .. } =
905+ cache.fill_history_gap_once(gaps[0].id.clone()).await.unwrap()
906+ else {
907+ panic!()
908+ };
909+ assert_eq!(gap.status, HistoryGapStatus::Joined);
910+ let ids: Vec<_> =
911+ cache.events().await.unwrap().iter().filter_map(|event| event.event_id()).collect();
839912 assert_eq!(
840913 ids,
841- vec![event_id!("$y"), event_id!("$z"), event_id!("$a"), event_id!("$live")]
914+ vec![event_id!("$first"), event_id!("$theirs"), event_id!("$mine"), event_id!("$later")]
842915 );
843916 }