1use std::{collections::HashSet, ops::ControlFlow, time::Duration};
22
23use matrix_sdk_base::sleep::sleep;
24use ruma::{MilliSecondsSinceUnixEpoch, OwnedRoomId, time::Instant};
25use tracing::{debug, info, trace, warn};
26
27use super::{
28 EventCache,
29 back_pagination_queue::{
30 BATCH_SIZE, BackPaginationQueue, BackPaginationRequest, BackPaginationStopReason, Priority,
31 },
32 caches::pagination::BackPaginationOutcome,
33};
34use crate::Client;
35
36const WEEK: Duration = Duration::from_secs(7 * 24 * 60 * 60);
38
39const MAX_BACKFILL_WEEKS: u32 = 13;
41
42const ROOM_BATCH: usize = 100;
45
46const SEARCH_MAX_BATCHES_PER_ROOM: usize = 10;
53
54#[derive(Clone, Copy, Debug)]
56#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
57pub enum SearchBackfillStrategy {
58 Foreground,
61 Background,
64}
65
66impl SearchBackfillStrategy {
67 fn enqueue_delay(self) -> Option<Duration> {
70 match self {
71 Self::Foreground => Some(Duration::from_secs(1)),
72 Self::Background => None,
73 }
74 }
75}
76
77impl EventCache {
78 pub async fn run_search_backfill(&self, strategy: SearchBackfillStrategy) {
92 let Some(queue) = self.back_pagination_queue() else {
93 return;
94 };
95 let Ok(client) = self.inner.client() else {
96 return;
97 };
98
99 let enqueue_delay = strategy.enqueue_delay();
100 let started = Instant::now();
101
102 let mut rooms = rooms_by_relevancy(&client);
103 let total_rooms = rooms.len();
104 let target_age = MAX_BACKFILL_WEEKS * WEEK;
105 info!(
106 ?strategy,
107 total_rooms,
108 weeks = MAX_BACKFILL_WEEKS,
109 ?target_age,
110 "search backfill started"
111 );
112
113 let mut drained: HashSet<OwnedRoomId> = HashSet::new();
115
116 for week in 1..=MAX_BACKFILL_WEEKS {
117 let max_age = week * WEEK;
118
119 rooms.retain(|room_id| !drained.contains(room_id));
120 debug!(
121 week,
122 of = MAX_BACKFILL_WEEKS,
123 ?max_age,
124 number_of_rooms_to_process = rooms.len(),
125 "search backfill week"
126 );
127
128 for chunk in rooms.chunks(ROOM_BATCH) {
129 let mut handles = Vec::new();
133 for room_id in chunk {
134 if !handles.is_empty()
135 && let Some(delay) = enqueue_delay
136 {
137 sleep(delay).await;
138 }
139 debug!(%room_id, "started search backfill");
140 match enqueue(&queue, room_id.clone(), max_age) {
141 Ok(handle) => handles.push((room_id.clone(), handle)),
142 Err(err) => {
143 warn!(%room_id, "couldn't enqueue a search backfill request: {err}")
144 }
145 }
146 }
147
148 for (room_id, handle) in handles {
149 let reason = handle.join().await.reason;
150 trace!(
151 %room_id,
152 week,
153 of = MAX_BACKFILL_WEEKS,
154 ?reason,
155 "finished search backpagination request"
156 );
157 if reason == BackPaginationStopReason::ReachedTimelineStart {
158 drained.insert(room_id);
159 }
160 }
161 }
162 }
163
164 info!(
165 total_rooms,
166 rooms_drained = drained.len(),
167 reached_age = ?target_age,
168 elapsed = ?started.elapsed(),
169 "search backfill finished"
170 );
171 }
172}
173
174fn rooms_by_relevancy(client: &Client) -> Vec<OwnedRoomId> {
176 let mut rooms = client.joined_rooms();
177 rooms.sort_by_key(|room| std::cmp::Reverse(room.recency_stamp()));
178 rooms.into_iter().map(|room| room.room_id().to_owned()).collect()
179}
180
181fn enqueue(
185 queue: &BackPaginationQueue,
186 room_id: OwnedRoomId,
187 max_age: Duration,
188) -> Result<
189 super::back_pagination_queue::BackPaginationHandle,
190 super::back_pagination_queue::BackPaginationQueueError,
191> {
192 queue.enqueue(BackPaginationRequest {
193 room_id,
194 priority: Priority::Low,
195 stop: Box::new(stop_when_older_than(max_age)),
196 batch_size: BATCH_SIZE,
197 max_batches: Some(SEARCH_MAX_BATCHES_PER_ROOM),
198 })
199}
200
201fn stop_when_older_than(
204 max_age: Duration,
205) -> impl FnMut(&BackPaginationOutcome) -> ControlFlow<()> + Send + 'static {
206 move |outcome| {
207 let old_enough =
208 oldest_event_timestamp(outcome).and_then(age_of).is_some_and(|age| age >= max_age);
209
210 if old_enough { ControlFlow::Break(()) } else { ControlFlow::Continue(()) }
211 }
212}
213
214fn oldest_event_timestamp(outcome: &BackPaginationOutcome) -> Option<MilliSecondsSinceUnixEpoch> {
218 outcome.events.iter().rev().find_map(|event| event.timestamp())
219}
220
221fn age_of(ts: MilliSecondsSinceUnixEpoch) -> Option<Duration> {
225 ts.to_system_time()?.elapsed().ok()
226}
227
228#[cfg(all(test, not(target_arch = "wasm32")))]
229mod tests {
230 use std::time::Duration;
231
232 use assert_matches::assert_matches;
233 use eyeball_im::VectorDiff;
234 use matrix_sdk_test::{BOB, JoinedRoomBuilder, async_test, event_factory::EventFactory};
235 use ruma::{MilliSecondsSinceUnixEpoch, event_id, room_id, time::SystemTime};
236
237 use super::{SearchBackfillStrategy, WEEK, stop_when_older_than};
238 use crate::{
239 assert_let_timeout,
240 event_cache::{BackPaginationOutcome, EventsOrigin, RoomEventCacheUpdate},
241 test_utils::mocks::{MatrixMockServer, RoomMessagesResponseTemplate},
242 };
243
244 fn ts_ago(age: Duration) -> MilliSecondsSinceUnixEpoch {
246 MilliSecondsSinceUnixEpoch::from_system_time(SystemTime::now() - age).unwrap()
247 }
248
249 #[test]
252 fn test_stop_when_older_than() {
253 let f = EventFactory::new().room(room_id!("!omelette:fromage.fr")).sender(*BOB);
254 let outcome = BackPaginationOutcome {
255 reached_start: false,
256 events: vec![
257 f.text_msg("recent").server_ts(ts_ago(Duration::from_secs(60))).into_event(),
258 f.text_msg("older").server_ts(ts_ago(WEEK)).into_event(),
259 ],
260 };
261
262 assert!(stop_when_older_than(2 * WEEK)(&outcome).is_continue());
264 assert!(stop_when_older_than(WEEK)(&outcome).is_break());
266 assert!(stop_when_older_than(Duration::from_secs(3600))(&outcome).is_break());
267 }
268
269 #[async_test]
272 async fn test_search_backfill_drains_room() {
273 let server = MatrixMockServer::new().await;
274 let client = server
275 .client_builder()
276 .on_builder(|builder| builder.with_enable_automatic_back_pagination(true))
277 .build()
278 .await;
279
280 let event_cache = client.event_cache();
281 event_cache.subscribe().unwrap();
282
283 let room_id = room_id!("!omelette:fromage.fr");
284 let f = EventFactory::new().room(room_id).sender(*BOB);
285
286 let room = server.sync_joined_room(&client, room_id).await;
287 let (room_event_cache, _drop_handles) = room.event_cache().await.unwrap();
288 let (room_events, mut room_cache_updates) = room_event_cache.subscribe().await.unwrap();
289 assert!(room_events.is_empty());
290
291 server
292 .sync_room(
293 &client,
294 JoinedRoomBuilder::new(room_id)
295 .set_timeline_limited()
296 .set_timeline_prev_batch("prev_batch"),
297 )
298 .await;
299
300 assert_let_timeout!(
301 Ok(RoomEventCacheUpdate::UpdateTimelineEvents(update)) = room_cache_updates.recv()
302 );
303 assert_matches!(update.diffs[0], VectorDiff::Clear);
304
305 server
308 .mock_room_messages()
309 .match_from("prev_batch")
310 .ok(RoomMessagesResponseTemplate::default().events(vec![
311 f.text_msg("comté").event_id(event_id!("$2")),
312 f.text_msg("beaufort").event_id(event_id!("$1")),
313 ]))
314 .mock_once()
315 .mount()
316 .await;
317
318 event_cache.run_search_backfill(SearchBackfillStrategy::Foreground).await;
321
322 assert_let_timeout!(
323 Ok(RoomEventCacheUpdate::UpdateTimelineEvents(update)) = room_cache_updates.recv()
324 );
325 assert_matches!(update.origin, EventsOrigin::Pagination);
326
327 let mut room_events = room_events.into();
328 for diff in update.diffs {
329 diff.apply(&mut room_events);
330 }
331 assert_eq!(room_events.len(), 2);
332 assert_eq!(room_events[0].event_id().unwrap(), event_id!("$1"));
333 assert_eq!(room_events[1].event_id().unwrap(), event_id!("$2"));
334 }
335}