matrix_sdk/event_cache/pagination.rs
1// Copyright 2024 The Matrix.org Foundation C.I.C.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! A sub-object for running pagination tasks on a given room.
16
17use std::{sync::Arc, time::Duration};
18
19use eyeball::{SharedObservable, Subscriber};
20use matrix_sdk_base::timeout::timeout;
21use ruma::api::Direction;
22use tracing::{debug, instrument, trace};
23
24use super::{
25 room::{LoadMoreEventsBackwardsOutcome, RoomEventCacheInner},
26 BackPaginationOutcome, EventsOrigin, Result, RoomEventCacheUpdate,
27};
28use crate::{
29 event_cache::{EventCacheError, RoomEventCacheGenericUpdate},
30 room::MessagesOptions,
31};
32
33/// Status for the back-pagination on a room event cache.
34#[derive(Debug, PartialEq, Clone, Copy)]
35#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
36pub enum RoomPaginationStatus {
37 /// No back-pagination is happening right now.
38 Idle {
39 /// Have we hit the start of the timeline, i.e. back-paginating wouldn't
40 /// have any effect?
41 hit_timeline_start: bool,
42 },
43
44 /// Back-pagination is already running in the background.
45 Paginating,
46}
47
48/// Small RAII guard to reset the pagination status on drop, if not disarmed in
49/// the meanwhile.
50struct ResetStatusOnDrop {
51 prev_status: Option<RoomPaginationStatus>,
52 pagination_status: SharedObservable<RoomPaginationStatus>,
53}
54
55impl ResetStatusOnDrop {
56 /// Make the RAII guard have no effect.
57 fn disarm(mut self) {
58 self.prev_status = None;
59 }
60}
61
62impl Drop for ResetStatusOnDrop {
63 fn drop(&mut self) {
64 if let Some(status) = self.prev_status.take() {
65 let _ = self.pagination_status.set(status);
66 }
67 }
68}
69
70/// An API object to run pagination queries on a [`super::RoomEventCache`].
71///
72/// Can be created with [`super::RoomEventCache::pagination()`].
73#[allow(missing_debug_implementations)]
74#[derive(Clone)]
75pub struct RoomPagination {
76 pub(super) inner: Arc<RoomEventCacheInner>,
77}
78
79impl RoomPagination {
80 /// Starts a back-pagination for the requested number of events.
81 ///
82 /// This automatically takes care of waiting for a pagination token from
83 /// sync, if we haven't done that before.
84 ///
85 /// It will run multiple back-paginations until one of these two conditions
86 /// is met:
87 /// - either we've reached the start of the timeline,
88 /// - or we've obtained enough events to fulfill the requested number of
89 /// events.
90 #[instrument(skip(self))]
91 pub async fn run_backwards_until(
92 &self,
93 num_requested_events: u16,
94 ) -> Result<BackPaginationOutcome> {
95 let mut events = Vec::new();
96
97 loop {
98 if let Some(outcome) = self.run_backwards_impl(num_requested_events).await? {
99 events.extend(outcome.events);
100 if outcome.reached_start || events.len() >= num_requested_events as usize {
101 return Ok(BackPaginationOutcome {
102 reached_start: outcome.reached_start,
103 events,
104 });
105 }
106 trace!(
107 "restarting back-pagination, because we haven't reached \
108 the start or obtained enough events yet"
109 );
110 }
111
112 debug!("restarting back-pagination because of a timeline reset.");
113 }
114 }
115
116 /// Run a single back-pagination for the requested number of events.
117 ///
118 /// This automatically takes care of waiting for a pagination token from
119 /// sync, if we haven't done that before.
120 #[instrument(skip(self))]
121 pub async fn run_backwards_once(&self, batch_size: u16) -> Result<BackPaginationOutcome> {
122 loop {
123 if let Some(outcome) = self.run_backwards_impl(batch_size).await? {
124 return Ok(outcome);
125 }
126 debug!("restarting back-pagination because of a timeline reset.");
127 }
128 }
129
130 /// Paginate from either the storage or the network, and let pagination
131 /// status observers know about updates.
132 async fn run_backwards_impl(&self, batch_size: u16) -> Result<Option<BackPaginationOutcome>> {
133 // There is at least one gap that must be resolved; reach the network.
134 // First, ensure there's no other ongoing back-pagination.
135 let status_observable = &self.inner.pagination_status;
136
137 let prev_status = status_observable.set(RoomPaginationStatus::Paginating);
138 if !matches!(prev_status, RoomPaginationStatus::Idle { .. }) {
139 return Err(EventCacheError::AlreadyBackpaginating);
140 }
141
142 let reset_status_on_drop_guard = ResetStatusOnDrop {
143 prev_status: Some(prev_status),
144 pagination_status: status_observable.clone(),
145 };
146
147 match self.paginate_backwards_impl(batch_size).await? {
148 Some(outcome) => {
149 // Back-pagination's over and successful, don't reset the status to the previous
150 // value.
151 reset_status_on_drop_guard.disarm();
152
153 // Notify subscribers that pagination ended.
154 status_observable
155 .set(RoomPaginationStatus::Idle { hit_timeline_start: outcome.reached_start });
156
157 // Send a room event cache generic update.
158 if !outcome.events.is_empty() {
159 let _ = self.inner.generic_update_sender.send(
160 RoomEventCacheGenericUpdate::UpdateTimeline {
161 room_id: self.inner.room_id.clone(),
162 },
163 );
164 }
165
166 Ok(Some(outcome))
167 }
168
169 None => {
170 // We keep the previous status value, because we haven't obtained more
171 // information about the pagination.
172 Ok(None)
173 }
174 }
175 }
176
177 /// Paginate from either the storage or the network.
178 ///
179 /// This method isn't concerned with setting the pagination status; only the
180 /// caller is.
181 async fn paginate_backwards_impl(
182 &self,
183 batch_size: u16,
184 ) -> Result<Option<BackPaginationOutcome>> {
185 // A linked chunk might not be entirely loaded (if it's been lazy-loaded). Try
186 // to load from storage first, then from network if storage indicated
187 // there's no previous events chunk to load.
188
189 loop {
190 let mut state_guard = self.inner.state.write().await;
191
192 match state_guard.load_more_events_backwards().await? {
193 LoadMoreEventsBackwardsOutcome::WaitForInitialPrevToken => {
194 const DEFAULT_WAIT_FOR_TOKEN_DURATION: Duration = Duration::from_secs(3);
195
196 // Release the state guard while waiting, to not deadlock the sync task.
197 drop(state_guard);
198
199 // Otherwise, wait for a notification that we received a previous-batch token.
200 trace!("waiting for a pagination token…");
201 let _ = timeout(
202 self.inner.pagination_batch_token_notifier.notified(),
203 DEFAULT_WAIT_FOR_TOKEN_DURATION,
204 )
205 .await;
206 trace!("done waiting");
207
208 self.inner.state.write().await.waited_for_initial_prev_token = true;
209
210 // Retry!
211 //
212 // Note: the next call to `load_more_events_backwards` can't return
213 // `WaitForInitialPrevToken` because we've just set to
214 // `waited_for_initial_prev_token`, so this is not an infinite loop.
215 //
216 // Note 2: not a recursive call, because recursive and async have a bad time
217 // together.
218 continue;
219 }
220
221 LoadMoreEventsBackwardsOutcome::Gap { prev_token } => {
222 // We have a gap, so resolve it with a network back-pagination.
223 drop(state_guard);
224 return self.paginate_backwards_with_network(batch_size, prev_token).await;
225 }
226
227 LoadMoreEventsBackwardsOutcome::StartOfTimeline => {
228 return Ok(Some(BackPaginationOutcome { reached_start: true, events: vec![] }));
229 }
230
231 LoadMoreEventsBackwardsOutcome::Events {
232 events,
233 timeline_event_diffs,
234 reached_start,
235 } => {
236 if !timeline_event_diffs.is_empty() {
237 let _ =
238 self.inner.sender.send(RoomEventCacheUpdate::UpdateTimelineEvents {
239 diffs: timeline_event_diffs,
240 origin: EventsOrigin::Cache,
241 });
242 }
243
244 return Ok(Some(BackPaginationOutcome {
245 reached_start,
246 // This is a backwards pagination. `BackPaginationOutcome` expects events to
247 // be in “reverse order”.
248 events: events.into_iter().rev().collect(),
249 }));
250 }
251 }
252 }
253 }
254
255 /// Run a single pagination request (/messages) to the server.
256 ///
257 /// If there are no previous-batch tokens, it will wait for one for a short
258 /// while to get one, or if it's already done so or if it's seen a
259 /// previous-batch token before, it will immediately indicate it's
260 /// reached the end of the timeline.
261 async fn paginate_backwards_with_network(
262 &self,
263 batch_size: u16,
264 prev_token: Option<String>,
265 ) -> Result<Option<BackPaginationOutcome>> {
266 let (events, new_token) = {
267 let Some(room) = self.inner.weak_room.get() else {
268 // The client is shutting down, return an empty default response.
269 return Ok(Some(BackPaginationOutcome {
270 reached_start: false,
271 events: Default::default(),
272 }));
273 };
274
275 let mut options = MessagesOptions::new(Direction::Backward).from(prev_token.as_deref());
276 options.limit = batch_size.into();
277
278 let response = room
279 .messages(options)
280 .await
281 .map_err(|err| EventCacheError::BackpaginationError(Box::new(err)))?;
282
283 (response.chunk, response.end)
284 };
285
286 if let Some((outcome, timeline_event_diffs)) = self
287 .inner
288 .state
289 .write()
290 .await
291 .handle_backpagination(events, new_token, prev_token)
292 .await?
293 {
294 if !timeline_event_diffs.is_empty() {
295 let _ = self.inner.sender.send(RoomEventCacheUpdate::UpdateTimelineEvents {
296 diffs: timeline_event_diffs,
297 origin: EventsOrigin::Pagination,
298 });
299 }
300
301 Ok(Some(outcome))
302 } else {
303 // The previous token has gone missing, so the timeline has been reset in the
304 // meanwhile, but it's fine per this function's contract.
305 Ok(None)
306 }
307 }
308
309 /// Returns a subscriber to the pagination status used for the
310 /// back-pagination integrated to the event cache.
311 pub fn status(&self) -> Subscriber<RoomPaginationStatus> {
312 self.inner.pagination_status.subscribe()
313 }
314}