1use std::{
40 cmp::Ordering,
41 collections::{BinaryHeap, HashMap},
42 num::NonZeroUsize,
43 ops::ControlFlow,
44 sync::{Arc, Weak},
45};
46
47use matrix_sdk_base::{locks::Mutex, task_monitor::TaskMonitor};
48use matrix_sdk_common::executor::{AbortOnDrop, JoinHandleExt as _, spawn};
49use ruma::OwnedRoomId;
50use tokio::sync::{mpsc, oneshot};
51use tokio_util::sync::{CancellationToken, DropGuard};
52use tracing::{debug, info, instrument, trace, warn};
53
54use super::{EventCacheInner, caches::pagination::BackPaginationOutcome};
55
56pub(crate) const BATCH_SIZE: u16 = 30;
58
59#[allow(dead_code)]
62#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
63pub(crate) enum Priority {
64 Low,
67 Normal,
70 High,
72}
73
74pub(crate) type StopCondition = Box<dyn FnMut(&BackPaginationOutcome) -> ControlFlow<()> + Send>;
82
83pub(crate) struct BackPaginationRequest {
86 pub room_id: OwnedRoomId,
88 pub priority: Priority,
90 pub stop: StopCondition,
92 pub batch_size: u16,
94 pub max_batches: Option<usize>,
96}
97
98#[cfg(not(tarpaulin_include))]
99impl std::fmt::Debug for BackPaginationRequest {
100 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
101 f.debug_struct("BackPaginationRequest")
102 .field("room_id", &self.room_id)
103 .field("priority", &self.priority)
104 .field("batch_size", &self.batch_size)
105 .field("max_batches", &self.max_batches)
106 .finish_non_exhaustive()
107 }
108}
109
110#[derive(Clone, Copy, Debug, PartialEq, Eq)]
112pub(crate) enum BackPaginationStopReason {
113 ReachedTimelineStart,
115 StopConditionMet,
117 BatchLimitReached,
120 NoDataAvailable,
123 Failed,
125 Cancelled,
127}
128
129#[allow(dead_code)]
131#[derive(Clone, Copy, Debug)]
132pub(crate) struct BackPaginationRunResult {
133 pub reason: BackPaginationStopReason,
135}
136
137type RequestCoalescingKey = (OwnedRoomId, Priority);
139
140pub(crate) struct BackPaginationHandle {
143 guard: Arc<DropGuard>,
146 #[allow(dead_code)]
148 completion: Option<oneshot::Receiver<BackPaginationRunResult>>,
149}
150
151#[cfg(not(tarpaulin_include))]
152impl std::fmt::Debug for BackPaginationHandle {
153 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
154 f.debug_struct("BackPaginationHandle").finish_non_exhaustive()
155 }
156}
157
158impl BackPaginationHandle {
159 pub(crate) fn detach(self) {
165 if let Some(guard) = Arc::into_inner(self.guard) {
166 guard.disarm();
167 }
168 }
169
170 #[allow(dead_code)]
172 pub(crate) async fn join(mut self) -> BackPaginationRunResult {
173 let cancelled = BackPaginationRunResult { reason: BackPaginationStopReason::Cancelled };
174 match self.completion.take() {
175 Some(completion) => completion.await.unwrap_or(cancelled),
176 None => cancelled,
177 }
178 }
179}
180
181#[derive(Clone)]
183pub struct BackPaginationQueue {
184 inner: Arc<BackPaginationQueueInner>,
185}
186
187struct BackPaginationQueueInner {
188 sender: mpsc::UnboundedSender<SchedulerEvent>,
189 cancellations: Mutex<HashMap<RequestCoalescingKey, SharedCancellation>>,
196 _task: matrix_sdk_base::task_monitor::BackgroundTaskHandle,
197}
198
199struct SharedCancellation {
201 token: CancellationToken,
203 guard: Weak<DropGuard>,
206}
207
208#[cfg(not(tarpaulin_include))]
209impl std::fmt::Debug for BackPaginationQueue {
210 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
211 f.debug_struct("BackPaginationQueue").finish_non_exhaustive()
212 }
213}
214
215impl BackPaginationQueue {
216 pub(super) fn new(
218 event_cache: Weak<EventCacheInner>,
219 max_concurrent: NonZeroUsize,
220 task_monitor: &TaskMonitor,
221 ) -> Self {
222 let (sender, receiver) = mpsc::unbounded_channel();
223
224 let task = task_monitor
228 .spawn_infinite_task(
229 "event_cache::back_pagination_queue",
230 scheduler(event_cache, receiver, sender.clone(), max_concurrent.get()),
231 )
232 .abort_on_drop();
233
234 Self {
235 inner: Arc::new(BackPaginationQueueInner {
236 sender,
237 cancellations: Mutex::new(HashMap::new()),
238 _task: task,
239 }),
240 }
241 }
242
243 pub(crate) fn enqueue(
247 &self,
248 request: BackPaginationRequest,
249 ) -> Result<BackPaginationHandle, BackPaginationQueueError> {
250 let key = (request.room_id.clone(), request.priority);
251 let (token, guard) = cancellation_for(&self.inner.cancellations, key);
252
253 let (completion_tx, completion_rx) = oneshot::channel();
254 let submitted = SubmittedRequest { request, token, completion: completion_tx };
255
256 self.inner
257 .sender
258 .send(SchedulerEvent::Submitted(submitted))
259 .map_err(|_| BackPaginationQueueError::ShutDown)?;
260
261 Ok(BackPaginationHandle { guard, completion: Some(completion_rx) })
262 }
263}
264
265fn cancellation_for(
272 cancellations: &Mutex<HashMap<RequestCoalescingKey, SharedCancellation>>,
273 key: RequestCoalescingKey,
274) -> (CancellationToken, Arc<DropGuard>) {
275 let mut cancellations = cancellations.lock();
276
277 if let Some(existing) = cancellations.get(&key)
278 && let Some(guard) = existing.guard.upgrade()
279 {
280 return (existing.token.clone(), guard);
281 }
282
283 cancellations.retain(|_, cancellation| cancellation.guard.strong_count() > 0);
285
286 let token = CancellationToken::new();
287 let guard = Arc::new(token.clone().drop_guard());
288 cancellations
289 .insert(key, SharedCancellation { token: token.clone(), guard: Arc::downgrade(&guard) });
290
291 (token, guard)
292}
293
294#[derive(Debug, thiserror::Error)]
296pub(crate) enum BackPaginationQueueError {
297 #[error("the back-pagination queue executor is not running")]
300 ShutDown,
301}
302
303enum SchedulerEvent {
306 Submitted(SubmittedRequest),
308 Finished(RequestCoalescingKey, BackPaginationRunResult),
310}
311
312struct SubmittedRequest {
315 request: BackPaginationRequest,
316 token: CancellationToken,
317 completion: oneshot::Sender<BackPaginationRunResult>,
318}
319
320struct PendingRequest {
323 request: BackPaginationRequest,
324 seq: u64,
326 token: CancellationToken,
327}
328
329impl PartialEq for PendingRequest {
330 fn eq(&self, other: &Self) -> bool {
331 self.request.priority == other.request.priority && self.seq == other.seq
332 }
333}
334
335impl Eq for PendingRequest {}
336
337impl PartialOrd for PendingRequest {
338 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
339 Some(self.cmp(other))
340 }
341}
342
343impl Ord for PendingRequest {
344 fn cmp(&self, other: &Self) -> Ordering {
345 self.request.priority.cmp(&other.request.priority).then_with(|| other.seq.cmp(&self.seq))
347 }
348}
349
350#[instrument(skip_all)]
353async fn scheduler(
354 event_cache: Weak<EventCacheInner>,
355 mut receiver: mpsc::UnboundedReceiver<SchedulerEvent>,
356 sender: mpsc::UnboundedSender<SchedulerEvent>,
357 max_concurrent: usize,
358) {
359 trace!("Spawning the back-pagination queue executor");
360
361 let mut pending_requests: BinaryHeap<PendingRequest> = BinaryHeap::new();
362 let mut active_requests: HashMap<OwnedRoomId, AbortOnDrop<()>> = HashMap::new();
366 let mut next_seq: u64 = 0;
367
368 let mut waiters: HashMap<RequestCoalescingKey, Vec<oneshot::Sender<BackPaginationRunResult>>> =
374 HashMap::new();
375
376 let mut events = Vec::with_capacity(max_concurrent);
379
380 loop {
381 schedule(
384 &event_cache,
385 &mut pending_requests,
386 &mut active_requests,
387 max_concurrent,
388 &sender,
389 );
390
391 if receiver.recv_many(&mut events, max_concurrent).await == 0 {
394 info!("Back-pagination queue channel closed, exiting");
395 break;
396 }
397
398 for event in events.drain(..) {
399 match event {
400 SchedulerEvent::Submitted(submitted) => {
401 let key = (submitted.request.room_id.clone(), submitted.request.priority);
402
403 if try_coalesce(&mut waiters, &key, submitted.completion) {
404 trace!(
405 room_id = %key.0,
406 priority = ?key.1,
407 "coalesced back-pagination request onto an existing run"
408 );
409 continue;
410 }
411
412 pending_requests.push(PendingRequest {
413 request: submitted.request,
414 seq: next_seq,
415 token: submitted.token,
416 });
417 next_seq += 1;
418 }
419
420 SchedulerEvent::Finished(key, result) => {
421 active_requests.remove(&key.0);
422 if let Some(senders) = waiters.remove(&key) {
424 for waiter in senders {
425 let _ = waiter.send(result);
426 }
427 }
428 }
429 }
430 }
431 }
432}
433
434fn try_coalesce(
443 waiters: &mut HashMap<RequestCoalescingKey, Vec<oneshot::Sender<BackPaginationRunResult>>>,
444 key: &RequestCoalescingKey,
445 completion: oneshot::Sender<BackPaginationRunResult>,
446) -> bool {
447 match waiters.get_mut(key) {
448 Some(existing) => {
449 existing.push(completion);
450 true
451 }
452 None => {
453 waiters.insert(key.to_owned(), vec![completion]);
454 false
455 }
456 }
457}
458
459fn schedule(
461 event_cache: &Weak<EventCacheInner>,
462 pending_requests: &mut BinaryHeap<PendingRequest>,
463 active_requests: &mut HashMap<OwnedRoomId, AbortOnDrop<()>>,
464 max_concurrent: usize,
465 sender: &mpsc::UnboundedSender<SchedulerEvent>,
466) {
467 for request in next_runnable(pending_requests, active_requests, max_concurrent) {
468 let key = (request.request.room_id.clone(), request.request.priority);
469
470 trace!(
471 room_id = %key.0,
472 priority = ?key.1,
473 active = active_requests.len(),
474 queued = pending_requests.len(),
475 "back-pagination scheduled"
476 );
477
478 let room_id = key.0.clone();
479 let event_cache = event_cache.clone();
480 let sender = sender.clone();
481 let task = spawn(async move {
482 let result = run_request(&event_cache, request.request, &request.token).await;
483 let _ = sender.send(SchedulerEvent::Finished(key, result));
486 });
487
488 active_requests.insert(room_id, task.abort_on_drop());
489 }
490}
491
492fn next_runnable<T>(
500 pending_requests: &mut BinaryHeap<PendingRequest>,
501 active_requests: &HashMap<OwnedRoomId, T>,
502 max_concurrent: usize,
503) -> Vec<PendingRequest> {
504 let mut picked: Vec<PendingRequest> = Vec::new();
505 let mut skipped = Vec::new();
506
507 while active_requests.len() + picked.len() < max_concurrent {
508 let Some(request) = pending_requests.pop() else {
509 break;
510 };
511
512 let room_id = &request.request.room_id;
513 if active_requests.contains_key(room_id)
514 || picked.iter().any(|other| other.request.room_id == *room_id)
515 {
516 skipped.push(request);
518 continue;
519 }
520
521 picked.push(request);
522 }
523
524 pending_requests.extend(skipped);
525
526 picked
527}
528
529#[instrument(skip_all, fields(room_id = %request.room_id, priority = ?request.priority))]
532async fn run_request(
533 event_cache: &Weak<EventCacheInner>,
534 mut request: BackPaginationRequest,
535 token: &CancellationToken,
536) -> BackPaginationRunResult {
537 if token.is_cancelled() {
539 return BackPaginationRunResult { reason: BackPaginationStopReason::Cancelled };
540 }
541
542 let pagination = {
545 let Some(inner) = event_cache.upgrade() else {
546 return BackPaginationRunResult { reason: BackPaginationStopReason::Cancelled };
547 };
548 match inner.all_caches_for_room(&request.room_id).await {
549 Ok(caches) => caches.room().pagination(),
550 Err(err) => {
551 warn!("no caches for room while back-paginating: {err}");
552 return BackPaginationRunResult { reason: BackPaginationStopReason::Failed };
553 }
554 }
555 };
556
557 let mut batches = 0usize;
558
559 let reason = loop {
560 if token.is_cancelled() {
561 break BackPaginationStopReason::Cancelled;
562 }
563
564 let outcome = match pagination.run_backwards_once(request.batch_size).await {
565 Ok(outcome) => outcome,
566 Err(err) => {
567 warn!("back-pagination failed: {err}");
568 break BackPaginationStopReason::Failed;
569 }
570 };
571
572 if (request.stop)(&outcome).is_break() {
575 break BackPaginationStopReason::StopConditionMet;
576 }
577
578 if outcome.reached_start {
579 break BackPaginationStopReason::ReachedTimelineStart;
580 }
581
582 if outcome.events.is_empty() {
583 break BackPaginationStopReason::NoDataAvailable;
584 }
585
586 batches += 1;
587 if let Some(max) = request.max_batches
588 && batches >= max
589 {
590 break BackPaginationStopReason::BatchLimitReached;
591 }
592 };
593
594 debug!(?reason, "back-pagination run finished");
595
596 BackPaginationRunResult { reason }
597}
598
599#[cfg(all(test, not(target_arch = "wasm32")))]
600mod tests {
601 use std::{collections::HashMap, ops::ControlFlow};
602
603 use matrix_sdk_base::locks::Mutex;
604 use ruma::room_id;
605
606 use super::{
607 BackPaginationRequest, PendingRequest, Priority, cancellation_for, next_runnable,
608 try_coalesce,
609 };
610
611 fn queued(room_id: ruma::OwnedRoomId, priority: Priority, seq: u64) -> PendingRequest {
613 PendingRequest {
614 request: BackPaginationRequest {
615 room_id,
616 priority,
617 stop: Box::new(|_| ControlFlow::Continue(())),
619 batch_size: 10,
620 max_batches: None,
621 },
622 seq,
623 token: tokio_util::sync::CancellationToken::new(),
624 }
625 }
626
627 #[test]
630 fn test_scheduling_priority_and_fifo() {
631 use std::collections::BinaryHeap;
632
633 let (a, b, c, d) = (room_id!("!a:e"), room_id!("!b:e"), room_id!("!c:e"), room_id!("!d:e"));
634
635 let mut pending_requests = BinaryHeap::new();
636 pending_requests.push(queued(a.to_owned(), Priority::Low, 0));
638 pending_requests.push(queued(b.to_owned(), Priority::High, 1));
639 pending_requests.push(queued(c.to_owned(), Priority::Normal, 2));
640 pending_requests.push(queued(d.to_owned(), Priority::High, 3));
641
642 let active_requests: HashMap<_, ()> = HashMap::new();
643 let picked: Vec<_> = next_runnable(&mut pending_requests, &active_requests, 10)
644 .into_iter()
645 .map(|r| r.request.room_id)
646 .collect();
647
648 assert_eq!(picked, vec![b.to_owned(), d.to_owned(), c.to_owned(), a.to_owned()]);
650 }
651
652 #[test]
654 fn test_scheduling_respects_concurrency_cap() {
655 use std::collections::BinaryHeap;
656
657 let mut pending_requests = BinaryHeap::new();
658 for (i, room) in [room_id!("!a:e"), room_id!("!b:e"), room_id!("!c:e")].iter().enumerate() {
659 pending_requests.push(queued((*room).to_owned(), Priority::Normal, i as u64));
660 }
661
662 let active_requests: HashMap<_, ()> = HashMap::new();
663 let picked = next_runnable(&mut pending_requests, &active_requests, 2);
664
665 assert_eq!(picked.len(), 2);
666 assert_eq!(pending_requests.len(), 1);
668 }
669
670 #[test]
673 fn test_scheduling_per_room_single_flight() {
674 use std::collections::BinaryHeap;
675
676 let (a, b) = (room_id!("!a:e"), room_id!("!b:e"));
677
678 let active_requests = HashMap::from([(a.to_owned(), ())]);
680
681 let mut pending_requests = BinaryHeap::new();
682 pending_requests.push(queued(a.to_owned(), Priority::High, 0)); pending_requests.push(queued(a.to_owned(), Priority::High, 1)); pending_requests.push(queued(b.to_owned(), Priority::Low, 2)); let picked: Vec<_> = next_runnable(&mut pending_requests, &active_requests, 10)
687 .into_iter()
688 .map(|r| r.request.room_id)
689 .collect();
690
691 assert_eq!(picked, vec![b.to_owned()]);
693 assert_eq!(pending_requests.len(), 2);
694 }
695
696 #[test]
699 fn test_cancellation_is_shared_per_key() {
700 let cancellations = Mutex::new(HashMap::new());
701 let key = (room_id!("!a:e").to_owned(), Priority::Normal);
702
703 let (first_token, first_guard) = cancellation_for(&cancellations, key.clone());
704 let (second_token, second_guard) = cancellation_for(&cancellations, key.clone());
705
706 assert!(!first_token.is_cancelled());
708 drop(first_guard);
709 assert!(!first_token.is_cancelled());
710 assert!(!second_token.is_cancelled());
711
712 drop(second_guard);
714 assert!(first_token.is_cancelled());
715 assert!(second_token.is_cancelled());
716
717 let (third_token, _third_guard) = cancellation_for(&cancellations, key);
720 assert!(!third_token.is_cancelled());
721
722 let other = (room_id!("!a:e").to_owned(), Priority::High);
724 let (other_token, other_guard) = cancellation_for(&cancellations, other);
725 drop(other_guard);
726 assert!(other_token.is_cancelled());
727 assert!(!third_token.is_cancelled());
728 }
729
730 #[test]
734 fn test_coalescing() {
735 let a = room_id!("!a:e");
736 let mut waiters = HashMap::new();
737
738 let completion = || tokio::sync::oneshot::channel().0;
739 let normal = (a.to_owned(), Priority::Normal);
740 let high = (a.to_owned(), Priority::High);
741
742 assert!(!try_coalesce(&mut waiters, &normal, completion()));
744 assert_eq!(waiters[&normal].len(), 1);
745
746 assert!(try_coalesce(&mut waiters, &normal, completion()));
748 assert_eq!(waiters[&normal].len(), 2);
749
750 assert!(!try_coalesce(&mut waiters, &high, completion()));
752 assert_eq!(waiters.len(), 2);
753 assert_eq!(waiters[&high].len(), 1);
754 }
755}