Skip to main content

matrix_sdk/event_cache/
mod.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//! The event cache is an abstraction layer, sitting between the Rust SDK and a
16//! final client, that acts as a global observer of all the rooms, gathering and
17//! inferring some extra useful information about each room. In particular, this
18//! doesn't require subscribing to a specific room to get access to this
19//! information.
20//!
21//! It's intended to be fast, robust and easy to maintain, having learned from
22//! previous endeavours at implementing middle to high level features elsewhere
23//! in the SDK, notably in the UI's Timeline object.
24//!
25//! See the [github issue](https://github.com/matrix-org/matrix-rust-sdk/issues/3058) for more
26//! details about the historical reasons that led us to start writing this.
27
28#![forbid(missing_docs)]
29
30use std::{
31    collections::HashMap,
32    fmt,
33    num::NonZeroUsize,
34    ops::Deref,
35    sync::{Arc, OnceLock, RwLock as StdRwLock, RwLockReadGuard, RwLockWriteGuard},
36};
37
38use matrix_sdk_base::{
39    cross_process_lock::CrossProcessLockError,
40    event_cache::store::{EventCacheStoreError, EventCacheStoreLock},
41    linked_chunk::lazy_loader::LazyLoaderError,
42    sync::RoomUpdates,
43    task_monitor::BackgroundTaskHandle,
44};
45use ruma::{EventId, OwnedEventId, OwnedRoomId, RoomId};
46use tokio::sync::{
47    OwnedRwLockReadGuard, OwnedRwLockWriteGuard, RwLock,
48    broadcast::{Receiver, Sender, channel},
49    mpsc,
50};
51use tracing::{error, instrument, trace};
52
53use crate::{
54    Client,
55    client::{ClientInner, WeakClient},
56    paginators::PaginatorError,
57};
58
59pub(crate) mod back_pagination_queue;
60mod caches;
61mod deduplicator;
62mod persistence;
63#[cfg(feature = "e2e-encryption")]
64mod redecryptor;
65mod states;
66mod tasks;
67
68#[cfg(feature = "e2e-encryption")]
69pub use redecryptor::{DecryptionRetryRequest, RedecryptorReport};
70
71pub use self::{
72    back_pagination_queue::BackPaginationQueue,
73    caches::{
74        TimelineVectorDiffs,
75        event_focused::{EventFocusThreadMode, EventFocusedCache, EventFocusedCacheKey},
76        pagination::{BackPaginationOutcome, PaginationStatus},
77        pinned_events::PinnedEventsCache,
78        room::{
79            RoomEventCache, RoomEventCacheGenericUpdate, RoomEventCacheUpdate,
80            pagination::RoomPagination,
81        },
82        subscriber::Subscriber,
83        thread::{ThreadEventCache, pagination::ThreadPagination},
84    },
85};
86use self::{
87    caches::{Caches, room::RoomEventCacheLinkedChunkUpdate, subscriber::AutoShrinkMessage},
88    states::StateLock,
89};
90
91/// An error observed in the [`EventCache`].
92#[derive(thiserror::Error, Clone, Debug)]
93pub enum EventCacheError {
94    /// The [`EventCache`] instance hasn't been initialized with
95    /// [`EventCache::subscribe`]
96    #[error(
97        "The EventCache hasn't subscribed to sync responses yet, call `EventCache::subscribe()`"
98    )]
99    NotSubscribedYet,
100
101    /// Room cache is not found.
102    #[error("Room cache `{room_id}` is not found.")]
103    RoomNotFound {
104        /// The room ID.
105        room_id: OwnedRoomId,
106    },
107
108    /// Thread cache is not found.
109    #[error("Thread cache `{thread_id}` of room `{room_id}` is not found.")]
110    ThreadNotFound {
111        /// The room ID of the thread.
112        room_id: OwnedRoomId,
113
114        /// The thread root event ID.
115        thread_id: OwnedEventId,
116    },
117
118    /// Pinned-events cache are not found.
119    #[error("Pinned-events cache for room `{room_id}` are not found.")]
120    PinnedEventsNotFound {
121        /// The room ID of the pinned-events.
122        room_id: OwnedRoomId,
123    },
124
125    /// Event-focused cache is not found.
126    #[error("Event-focused cache `{event_focused_id:?}` of room `{room_id}` is not found.")]
127    EventFocusedNotFound {
128        /// The room ID of the thread.
129        room_id: OwnedRoomId,
130
131        /// The thread root event ID.
132        event_focused_id: EventFocusedCacheKey,
133    },
134
135    /// A new cache was inserted at an occupied place, i.e. where an existing
136    /// cache state was present.
137    #[error("The state of a cache is not found")]
138    CacheStateAlreadyExists,
139
140    /// An error has been observed while back- or forward- paginating.
141    #[error(transparent)]
142    PaginationError(Arc<crate::Error>),
143
144    /// An error has been observed while initiating an event-focused timeline.
145    #[error(transparent)]
146    InitialPaginationError(#[from] PaginatorError),
147
148    /// An error happening when interacting with storage.
149    #[error(transparent)]
150    Storage(#[from] EventCacheStoreError),
151
152    /// An error happening when attempting to (cross-process) lock storage.
153    #[error(transparent)]
154    LockingStorage(#[from] CrossProcessLockError),
155
156    /// The [`EventCache`] owns a weak reference to the [`Client`] it pertains
157    /// to. It's possible this weak reference points to nothing anymore, at
158    /// times where we try to use the client.
159    #[error("The owning client of the event cache has been dropped.")]
160    ClientDropped,
161
162    /// An error happening when interacting with the [`LinkedChunk`]'s lazy
163    /// loader.
164    ///
165    /// [`LinkedChunk`]: matrix_sdk_common::linked_chunk::LinkedChunk
166    #[error(transparent)]
167    LinkedChunkLoader(#[from] LazyLoaderError),
168
169    /// An error happened when trying to load pinned events; none of them could
170    /// be loaded, which would otherwise result in an empty pinned events
171    /// list, incorrectly.
172    #[error("Unable to load any of the pinned events.")]
173    UnableToLoadPinnedEvents,
174
175    /// An error happened when reading the metadata of a linked chunk, upon
176    /// reload.
177    #[error("the linked chunk metadata is invalid: {details}")]
178    InvalidLinkedChunkMetadata {
179        /// A string containing details about the error.
180        details: String,
181    },
182}
183
184/// A result using the [`EventCacheError`].
185pub type Result<T> = std::result::Result<T, EventCacheError>;
186
187/// Hold handles to the tasks spawn by a [`EventCache`].
188pub struct EventCacheDropHandles {
189    /// Task that listens to room updates.
190    _listen_updates_task: BackgroundTaskHandle,
191
192    /// Task that listens to updates to the user's ignored list.
193    _ignore_user_list_update_task: BackgroundTaskHandle,
194
195    /// The task used to automatically shrink the linked chunks.
196    _auto_shrink_linked_chunk_task: BackgroundTaskHandle,
197
198    /// A background task listening to room and send queue updates, and
199    /// automatically subscribing the user to threads when needed, based on
200    /// the semantics of MSC4306.
201    ///
202    /// One important constraint is that there is only one such task per
203    /// [`EventCache`], so it does listen to *all* rooms at the same time.
204    _thread_subscriber_task: BackgroundTaskHandle,
205
206    /// A background task listening to room updates, and
207    /// automatically handling search index operations add/remove/edit
208    /// depending on the event type.
209    ///
210    /// One important constraint is that there is only one such task per
211    /// [`EventCache`], so it does listen to *all* rooms at the same time.
212    #[cfg(feature = "experimental-search")]
213    _search_indexing_task: BackgroundTaskHandle,
214
215    /// The task used to automatically redecrypt UTDs.
216    #[cfg(feature = "e2e-encryption")]
217    _redecryptor: redecryptor::Redecryptor,
218}
219
220impl fmt::Debug for EventCacheDropHandles {
221    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
222        f.debug_struct("EventCacheDropHandles").finish_non_exhaustive()
223    }
224}
225
226/// An event cache, providing lots of useful functionality for clients.
227///
228/// Cloning is shallow, and thus is cheap to do.
229///
230/// See also the module-level comment.
231#[derive(Clone)]
232pub struct EventCache {
233    /// Reference to the inner cache.
234    inner: Arc<EventCacheInner>,
235}
236
237impl fmt::Debug for EventCache {
238    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
239        f.debug_struct("EventCache").finish_non_exhaustive()
240    }
241}
242
243impl EventCache {
244    /// Create a new [`EventCache`] for the given client.
245    pub(crate) fn new(
246        client: &Arc<ClientInner>,
247        event_cache_store: EventCacheStoreLock,
248        enable_automatic_back_pagination: bool,
249    ) -> Self {
250        let (generic_update_sender, _) = channel(128);
251        let (linked_chunk_update_sender, _) = channel(128);
252
253        let weak_client = WeakClient::from_inner(client);
254
255        let (thread_subscriber_sender, _thread_subscriber_receiver) = channel(128);
256
257        #[cfg(feature = "e2e-encryption")]
258        let redecryption_channels = redecryptor::RedecryptorChannels::new();
259
260        Self {
261            inner: Arc::new(EventCacheInner {
262                client: weak_client,
263                config: StdRwLock::new(EventCacheConfig::default()),
264                state: StateLock::new(event_cache_store),
265                by_room: Default::default(),
266                drop_handles: Default::default(),
267                auto_shrink_sender: Default::default(),
268                generic_update_sender,
269                linked_chunk_update_sender,
270                #[cfg(feature = "e2e-encryption")]
271                redecryption_channels,
272                enable_automatic_back_pagination,
273                back_pagination_queue: OnceLock::new(),
274                thread_subscriber_sender,
275            }),
276        }
277    }
278
279    /// Get a read-only handle to the global configuration of the
280    /// [`EventCache`].
281    pub fn config(&self) -> RwLockReadGuard<'_, EventCacheConfig> {
282        self.inner.config.read().unwrap()
283    }
284
285    /// Get a writable handle to the global configuration of the [`EventCache`].
286    pub fn config_mut(&self) -> RwLockWriteGuard<'_, EventCacheConfig> {
287        self.inner.config.write().unwrap()
288    }
289
290    /// Subscribes to updates that a thread subscription has been sent.
291    ///
292    /// For testing purposes only.
293    #[cfg(feature = "testing")]
294    pub fn subscribe_thread_subscriber_updates(&self) -> Receiver<()> {
295        self.inner.thread_subscriber_sender.subscribe()
296    }
297
298    /// Starts subscribing the [`EventCache`] to sync responses, if not done
299    /// before.
300    ///
301    /// Re-running this has no effect if we already subscribed before, and is
302    /// cheap.
303    pub fn subscribe(&self) -> Result<()> {
304        let client = self.inner.client()?;
305
306        // Initialize the drop handles.
307        let _ = self.inner.drop_handles.get_or_init(|| {
308            let task_monitor = client.task_monitor();
309
310            // Spawn the task that will listen to all the room updates at once.
311            let listen_updates_task = task_monitor.spawn_infinite_task("event_cache::room_updates_task", tasks::room_updates_task(
312                self.inner.clone(),
313                client.subscribe_to_all_room_updates(),
314            )).abort_on_drop();
315
316            let ignore_user_list_update_task = task_monitor.spawn_infinite_task("event_cache::ignore_user_list_update_task", tasks::ignore_user_list_update_task(
317                self.inner.clone(),
318                client.subscribe_to_ignore_user_list_changes(),
319            )).abort_on_drop();
320
321            let (auto_shrink_sender, auto_shrink_receiver) = mpsc::channel(32);
322
323            // Force-initialize the sender in the [`RoomEventCacheInner`].
324            self.inner.auto_shrink_sender.get_or_init(|| auto_shrink_sender);
325
326            let auto_shrink_linked_chunk_task = task_monitor.spawn_infinite_task("event_cache::auto_shrink_linked_chunk_task", tasks::auto_shrink_linked_chunk_task(
327                Arc::downgrade(&self.inner),
328                auto_shrink_receiver,
329            )).abort_on_drop();
330
331            #[cfg(feature = "e2e-encryption")]
332            let redecryptor = {
333                let receiver = self
334                    .inner
335                    .redecryption_channels
336                    .decryption_request_receiver
337                    .lock()
338                    .take()
339                    .expect("We should have initialized the channel an subscribing should happen only once");
340
341                redecryptor::Redecryptor::new(&client, Arc::downgrade(&self.inner), receiver, &self.inner.linked_chunk_update_sender)
342            };
343
344        let thread_subscriber_task = client
345            .task_monitor()
346            .spawn_infinite_task(
347                "event_cache::thread_subscriber",
348                tasks::thread_subscriber_task(
349                    self.inner.client.clone(),
350                    self.inner.linked_chunk_update_sender.clone(),
351                    self.inner.thread_subscriber_sender.clone(),
352                ),
353            )
354            .abort_on_drop();
355
356        #[cfg(feature = "experimental-search")]
357        let search_indexing_task = client
358            .task_monitor()
359            .spawn_infinite_task(
360                "event_cache::search_indexing",
361                tasks::search_indexing_task(
362                    self.inner.client.clone(),
363                    self.inner.linked_chunk_update_sender.clone(),
364                ),
365            )
366            .abort_on_drop();
367
368            if self.inner.enable_automatic_back_pagination {
369                // Deferred initialization of the shared back-pagination queue.
370                trace!("spawning the back-pagination queue");
371                let max_concurrent = self.config().max_concurrent_back_paginations;
372                self.inner.back_pagination_queue.get_or_init(|| {
373                    BackPaginationQueue::new(
374                        Arc::downgrade(&self.inner),
375                        max_concurrent,
376                        task_monitor,
377                    )
378                });
379            } else {
380                trace!("back-pagination queue is disabled");
381            }
382
383            Arc::new(EventCacheDropHandles {
384                _listen_updates_task: listen_updates_task,
385                _ignore_user_list_update_task: ignore_user_list_update_task,
386                _auto_shrink_linked_chunk_task: auto_shrink_linked_chunk_task,
387                #[cfg(feature = "e2e-encryption")]
388                _redecryptor: redecryptor,
389                _thread_subscriber_task: thread_subscriber_task,
390                #[cfg(feature = "experimental-search")]
391                _search_indexing_task: search_indexing_task,
392            })
393        });
394
395        Ok(())
396    }
397
398    /// For benchmarking purposes only.
399    #[doc(hidden)]
400    pub async fn handle_room_updates(&self, updates: RoomUpdates) -> Result<()> {
401        self.inner.handle_room_updates(updates).await
402    }
403
404    /// Check whether [`EventCache::subscribe`] has been called.
405    pub fn has_subscribed(&self) -> bool {
406        self.inner.drop_handles.get().is_some()
407    }
408
409    /// Return a room-specific view over the [`EventCache`].
410    pub async fn room(
411        &self,
412        room_id: &RoomId,
413    ) -> Result<(RoomEventCache, Arc<EventCacheDropHandles>)> {
414        let Some(drop_handles) = self.inner.drop_handles.get().cloned() else {
415            return Err(EventCacheError::NotSubscribedYet);
416        };
417
418        let caches_for_room = self.inner.all_caches_for_room(room_id).await?;
419
420        Ok((caches_for_room.room().clone(), drop_handles))
421    }
422
423    /// Return a thread-specific view over the [`EventCache`].
424    pub async fn thread(
425        &self,
426        room_id: &RoomId,
427        thread_id: &EventId,
428    ) -> Result<(ThreadEventCache, Arc<EventCacheDropHandles>)> {
429        let Some(drop_handles) = self.inner.drop_handles.get().cloned() else {
430            return Err(EventCacheError::NotSubscribedYet);
431        };
432
433        let caches_for_room = self.inner.all_caches_for_room(room_id).await?;
434
435        Ok((caches_for_room.thread(thread_id.to_owned()).await?.deref().clone(), drop_handles))
436    }
437
438    /// Return a pinned-events-specific view over the [`EventCache`].
439    pub async fn pinned_events(
440        &self,
441        room_id: &RoomId,
442    ) -> Result<(PinnedEventsCache, Arc<EventCacheDropHandles>)> {
443        let Some(drop_handles) = self.inner.drop_handles.get().cloned() else {
444            return Err(EventCacheError::NotSubscribedYet);
445        };
446
447        let caches_for_room = self.inner.all_caches_for_room(room_id).await?;
448
449        Ok((caches_for_room.pinned_events().await?.clone(), drop_handles))
450    }
451
452    /// Return an event-focused view over the [`EventCache`].
453    pub async fn event_focused(
454        &self,
455        room_id: &RoomId,
456        event_id: &EventId,
457        thread_mode: EventFocusThreadMode,
458        number_of_initial_events: u16,
459    ) -> Result<(EventFocusedCache, Arc<EventCacheDropHandles>)> {
460        let Some(drop_handles) = self.inner.drop_handles.get().cloned() else {
461            return Err(EventCacheError::NotSubscribedYet);
462        };
463
464        let caches_for_room = self.inner.all_caches_for_room(room_id).await?;
465
466        Ok((
467            caches_for_room
468                .event_focused(event_id.to_owned(), thread_mode, number_of_initial_events)
469                .await?
470                .deref()
471                .clone(),
472            drop_handles,
473        ))
474    }
475
476    /// Forget all caches related to a single room.
477    ///
478    /// This will notify any live observers that the room has been cleared.
479    pub async fn forget_room(&self, room_id: &RoomId) -> Result<()> {
480        self.inner.forget_room(room_id).await
481    }
482
483    /// Cleanly clear all the rooms' event caches.
484    ///
485    /// This will notify any live observers that the room has been cleared.
486    pub async fn clear_all_rooms(&self) -> Result<()> {
487        self.inner.clear_all_rooms().await
488    }
489
490    /// Subscribe to room _generic_ updates.
491    ///
492    /// If one wants to listen what has changed in a specific room for example,
493    /// the [`RoomEventCache::subscribe`] is recommended. However, the
494    /// [`Subscriber`] type triggers side-effects.
495    ///
496    /// If one wants to get a high-overview, generic, updates for rooms, and
497    /// without side-effects, this method is recommended. Also, dropping the
498    /// receiver of this channel will not trigger any side-effect.
499    pub fn subscribe_to_room_generic_updates(&self) -> Receiver<RoomEventCacheGenericUpdate> {
500        self.inner.generic_update_sender.subscribe()
501    }
502
503    /// Returns the shared [`BackPaginationQueue`], if enabled at construction
504    /// with [`ClientBuilder::with_enable_automatic_back_pagination`].
505    ///
506    /// [`ClientBuilder::with_enable_automatic_back_pagination`]: crate::ClientBuilder::with_enable_automatic_back_pagination
507    pub fn back_pagination_queue(&self) -> Option<BackPaginationQueue> {
508        self.inner.back_pagination_queue.get().cloned()
509    }
510}
511
512/// Global configuration for the [`EventCache`], applied to every single room.
513#[derive(Clone, Copy, Debug)]
514pub struct EventCacheConfig {
515    /// Maximum number of concurrent /event requests when loading pinned events.
516    pub max_pinned_events_concurrent_requests: usize,
517
518    /// Maximum number of pinned events to load, for any room.
519    pub max_pinned_events_to_load: usize,
520
521    /// The maximum number of back-paginations the background queue runs at
522    /// once, across all rooms and use cases. Bounds server load.
523    ///
524    /// Defaults to
525    /// [`EventCacheConfig::DEFAULT_MAX_CONCURRENT_BACK_PAGINATIONS`].
526    pub max_concurrent_back_paginations: NonZeroUsize,
527}
528
529impl EventCacheConfig {
530    /// The default maximum number of pinned events to load.
531    pub const DEFAULT_MAX_EVENTS_TO_LOAD: usize = 128;
532
533    /// The default maximum number of concurrent requests to perform when
534    /// loading the pinned events.
535    pub const DEFAULT_MAX_CONCURRENT_REQUESTS: usize = 8;
536
537    /// The default maximum number of concurrent background back-paginations
538    /// (see also [`EventCacheConfig::max_concurrent_back_paginations`]).
539    pub const DEFAULT_MAX_CONCURRENT_BACK_PAGINATIONS: NonZeroUsize = NonZeroUsize::new(3).unwrap();
540}
541
542impl Default for EventCacheConfig {
543    fn default() -> Self {
544        Self {
545            max_pinned_events_concurrent_requests: Self::DEFAULT_MAX_CONCURRENT_REQUESTS,
546            max_pinned_events_to_load: Self::DEFAULT_MAX_EVENTS_TO_LOAD,
547            max_concurrent_back_paginations: Self::DEFAULT_MAX_CONCURRENT_BACK_PAGINATIONS,
548        }
549    }
550}
551
552type CachesByRoom = HashMap<OwnedRoomId, Caches>;
553
554struct EventCacheInner {
555    /// A weak reference to the inner client, useful when trying to get a handle
556    /// on the owning client.
557    client: WeakClient,
558
559    /// Global configuration for the event cache.
560    config: StdRwLock<EventCacheConfig>,
561
562    /// Lock around the state of the Event Cache, containing all the cache
563    /// states.
564    state: StateLock,
565
566    /// Lazily-filled cache of live [`RoomEventCache`], once per room.
567    //
568    // It's behind an `Arc` to get owned locks.
569    by_room: Arc<RwLock<CachesByRoom>>,
570
571    /// Handles to keep alive the task listening to updates.
572    drop_handles: OnceLock<Arc<EventCacheDropHandles>>,
573
574    /// A sender for notifications that a room *may* need to be auto-shrunk.
575    ///
576    /// Needs to live here, so it may be passed to each [`RoomEventCache`]
577    /// instance.
578    ///
579    /// It's a `OnceLock` because its initialization is deferred to
580    /// [`EventCache::subscribe`].
581    ///
582    /// See doc comment of [`tasks::auto_shrink_linked_chunk_task`].
583    auto_shrink_sender: OnceLock<mpsc::Sender<AutoShrinkMessage>>,
584
585    /// A sender for room generic update.
586    ///
587    /// See doc comment of [`RoomEventCacheGenericUpdate`] and
588    /// [`EventCache::subscribe_to_room_generic_updates`].
589    generic_update_sender: Sender<RoomEventCacheGenericUpdate>,
590
591    /// A sender for a persisted linked chunk update.
592    ///
593    /// This is used to notify that some linked chunk has persisted some updates
594    /// to a store, during sync or a back-pagination of *any* linked chunk.
595    /// This can be used by observers to look for new events.
596    ///
597    /// See doc comment of [`RoomEventCacheLinkedChunkUpdate`].
598    linked_chunk_update_sender: Sender<RoomEventCacheLinkedChunkUpdate>,
599
600    /// A test helper receiver that will be emitted every time the thread
601    /// subscriber task subscribed to a new thread.
602    ///
603    /// This is helpful for tests to coordinate that a new thread subscription
604    /// has been sent or not.
605    thread_subscriber_sender: Sender<()>,
606
607    #[cfg(feature = "e2e-encryption")]
608    redecryption_channels: redecryptor::RedecryptorChannels,
609
610    /// Whether to spawn the [`BackPaginationQueue`] at subscription time; set
611    /// once, at construction, via
612    /// [`ClientBuilder::with_enable_automatic_back_pagination`].
613    ///
614    /// [`ClientBuilder::with_enable_automatic_back_pagination`]: crate::ClientBuilder::with_enable_automatic_back_pagination
615    enable_automatic_back_pagination: bool,
616
617    /// State for the automatic pagination mechanism.
618    ///
619    /// Deferred initialization: spawned at subscription time, if
620    /// `enable_automatic_back_pagination` is set.
621    back_pagination_queue: OnceLock<BackPaginationQueue>,
622}
623
624impl EventCacheInner {
625    fn client(&self) -> Result<Client> {
626        self.client.get().ok_or(EventCacheError::ClientDropped)
627    }
628
629    /// Clear a single room's data.
630    async fn forget_room(&self, room_id: &RoomId) -> Result<()> {
631        // The constraints are very similar to what we do in `clear_all_rooms`. See this
632        // information to understand them.
633
634        let mut caches_for_all_rooms = self.by_room.write().await;
635        self.state.clear_and_reload(&caches_for_all_rooms, Some(room_id)).await?;
636
637        // Finally, we forget all the caches if any exists in memory.
638        caches_for_all_rooms.remove(room_id);
639
640        Ok(())
641    }
642
643    /// Clears all the rooms' data.
644    async fn clear_all_rooms(&self) -> Result<()> {
645        // Okay, here's where things get delicate.
646        //
647        // On the one hand, `by_room` may include storage for *some* caches
648        // that we know about, but not *all* of them. Any cache that hasn't been
649        // loaded in the client, or touched by a sync, will remain unloaded in
650        // memory, so it will be missing from `self.by_room`. As a result, we
651        // need to make sure that we're hitting the storage backend to *really*
652        // clear all the caches, including those that haven't been loaded yet.
653        //
654        // On the other hand, one must NOT clear the `by_room` map, because if
655        // someone subscribed to a cache update, they would never get any new
656        // update for that cache, since re-creating the cache would create a
657        // new, unrelated sender.
658        //
659        // So we need to *keep* the caches in `by_room` alive, while clearing
660        // them in the store backend.
661        //
662        // As a result, for a short while, the in-memory linked chunks will be
663        // desynchronised from the storage. We need to be careful then. During
664        // that short while, we don't want *anyone* to touch the linked chunks
665        // (be it in memory or in the storage).
666        //
667        // And since that requirement applies to *any* cache in `by_room` at the
668        // same time, we'll have to take the lock for *all* the live caches and
669        // for the states, so as to properly clear the underlying storage.
670
671        // We acquire an exclusive access to `by_room`.
672        let caches_for_all_rooms = self.by_room.write().await;
673
674        // Then, we can clear and reload the states for all the rooms.
675        self.state.clear_and_reload(&caches_for_all_rooms, None).await?;
676
677        Ok(())
678    }
679
680    /// Handles a single set of room updates at once.
681    #[instrument(skip(self, updates))]
682    async fn handle_room_updates(&self, updates: RoomUpdates) -> Result<()> {
683        // NOTE: We tried to make this concurrent at some point, but it turned out to be
684        // a performance regression, even for large sync updates. Lacking time
685        // to investigate, this code remains sequential for now. See also
686        // https://github.com/matrix-org/matrix-rust-sdk/pull/5426.
687
688        // Left rooms.
689        for (room_id, left_room_update) in updates.left {
690            let Ok(caches) = self.all_caches_for_room(&room_id).await else {
691                error!(?room_id, "Room must exist");
692                continue;
693            };
694
695            if let Err(err) = caches.handle_left_room_update(left_room_update).await {
696                // Non-fatal error, try to continue to the next room.
697                error!("handling left room update: {err}");
698            }
699        }
700
701        // Joined rooms.
702        for (room_id, joined_room_update) in updates.joined {
703            trace!(?room_id, "Handling a `JoinedRoomUpdate`");
704
705            let Ok(caches) = self.all_caches_for_room(&room_id).await else {
706                error!(?room_id, "Room must exist");
707                continue;
708            };
709
710            if let Err(err) = caches.handle_joined_room_update(joined_room_update).await {
711                // Non-fatal error, try to continue to the next room.
712                error!(%room_id, "handling joined room update: {err}");
713            }
714        }
715
716        // Invited rooms.
717        //
718        // We don't handle `updates.invite` because they contain stripped-state events,
719        // which is not handled by the Event Cache for the moment.
720
721        Ok(())
722    }
723
724    /// Return all the event caches associated to a specific room.
725    async fn all_caches_for_room(
726        &self,
727        room_id: &RoomId,
728    ) -> Result<OwnedRwLockReadGuard<CachesByRoom, Caches>> {
729        // Fast path: the entry exists; let's acquire a read lock, it's cheaper than a
730        // write lock.
731        match OwnedRwLockReadGuard::try_map(self.by_room.clone().read_owned().await, |by_room| {
732            by_room.get(room_id)
733        }) {
734            Ok(caches) => Ok(caches),
735
736            Err(by_room_guard) => {
737                // Slow-path: the entry doesn't exist; let's acquire a write lock.
738                drop(by_room_guard);
739                let by_room_guard = self.by_room.clone().write_owned().await;
740
741                // In the meanwhile, some other caller might have obtained write access and done
742                // the same, so check for existence again.
743                let mut by_room_guard =
744                    match OwnedRwLockWriteGuard::try_downgrade_map(by_room_guard, |by_room| {
745                        by_room.get(room_id)
746                    }) {
747                        Ok(caches) => return Ok(caches),
748                        Err(by_room_guard) => by_room_guard,
749                    };
750
751                let caches = Caches::new(
752                    &self.client,
753                    room_id,
754                    self.generic_update_sender.clone(),
755                    self.linked_chunk_update_sender.clone(),
756                    // SAFETY: we must have subscribed before reaching this code, otherwise
757                    // something is very wrong.
758                    self.auto_shrink_sender.get().cloned().expect(
759                        "we must have called `EventCache::subscribe()` before calling here.",
760                    ),
761                    &self.state,
762                    self.back_pagination_queue.get().cloned(),
763                )
764                .await?;
765
766                by_room_guard.insert(room_id.to_owned(), caches);
767
768                Ok(OwnedRwLockWriteGuard::try_downgrade_map(by_room_guard, |by_room| {
769                    by_room.get(room_id)
770                })
771                .expect("`Caches` has just been inserted"))
772            }
773        }
774    }
775}
776
777/// Indicate where events are coming from.
778#[derive(Debug, Clone)]
779pub enum EventsOrigin {
780    /// Events are coming from a sync.
781    Sync,
782
783    /// Events are coming from pagination.
784    Pagination,
785
786    /// The cause of the change is purely internal to the cache.
787    Cache,
788}
789
790#[cfg(test)]
791mod tests {
792    use std::{ops::Not, sync::Arc, time::Duration};
793
794    use assert_matches::assert_matches;
795    use futures_util::FutureExt as _;
796    use matrix_sdk_base::{
797        RoomState,
798        linked_chunk::{ChunkIdentifier, LinkedChunkId, Position, Update},
799        sync::{JoinedRoomUpdate, RoomUpdates, Timeline},
800    };
801    use matrix_sdk_test::{
802        JoinedRoomBuilder, SyncResponseBuilder, async_test, event_factory::EventFactory,
803    };
804    use ruma::{event_id, room_id, user_id};
805    use tokio::time::sleep;
806
807    use super::{EventCacheError, RoomEventCacheGenericUpdate};
808    use crate::test_utils::{
809        assert_event_matches_msg, client::MockClientBuilder, logged_in_client,
810    };
811
812    #[async_test]
813    async fn test_must_explicitly_subscribe() {
814        let client = logged_in_client(None).await;
815
816        let event_cache = client.event_cache();
817
818        // If I create a room event subscriber for a room before subscribing the event
819        // cache,
820        let room_id = room_id!("!omelette:fromage.fr");
821        let result = event_cache.room(room_id).await;
822
823        // Then it fails, because one must explicitly call `.subscribe()` on the event
824        // cache.
825        assert_matches!(result, Err(EventCacheError::NotSubscribedYet));
826    }
827
828    #[async_test]
829    async fn test_get_event_by_id() {
830        let client = logged_in_client(None).await;
831        let room_id1 = room_id!("!galette:saucisse.bzh");
832        let room_id2 = room_id!("!crepe:saucisse.bzh");
833
834        client.base_client().get_or_create_room(room_id1, RoomState::Joined);
835        client.base_client().get_or_create_room(room_id2, RoomState::Joined);
836
837        let event_cache = client.event_cache();
838        event_cache.subscribe().unwrap();
839
840        // Insert two rooms with a few events.
841        let f = EventFactory::new().room(room_id1).sender(user_id!("@ben:saucisse.bzh"));
842
843        let eid1 = event_id!("$1");
844        let eid2 = event_id!("$2");
845        let eid3 = event_id!("$3");
846
847        let joined_room_update1 = JoinedRoomUpdate {
848            timeline: Timeline {
849                events: vec![
850                    f.text_msg("hey").event_id(eid1).into(),
851                    f.text_msg("you").event_id(eid2).into(),
852                ],
853                ..Default::default()
854            },
855            ..Default::default()
856        };
857
858        let joined_room_update2 = JoinedRoomUpdate {
859            timeline: Timeline {
860                events: vec![f.text_msg("bjr").event_id(eid3).into()],
861                ..Default::default()
862            },
863            ..Default::default()
864        };
865
866        let mut updates = RoomUpdates::default();
867        updates.joined.insert(room_id1.to_owned(), joined_room_update1);
868        updates.joined.insert(room_id2.to_owned(), joined_room_update2);
869
870        // Have the event cache handle them.
871        event_cache.inner.handle_room_updates(updates).await.unwrap();
872
873        // We can find the events in a single room.
874        let room1 = client.get_room(room_id1).unwrap();
875
876        let (room_event_cache, _drop_handles) = room1.event_cache().await.unwrap();
877
878        let found1 = room_event_cache.find_event(eid1).await.unwrap().unwrap();
879        assert_event_matches_msg(&found1, "hey");
880
881        let found2 = room_event_cache.find_event(eid2).await.unwrap().unwrap();
882        assert_event_matches_msg(&found2, "you");
883
884        // Retrieving the event with id3 from the room which doesn't contain it will
885        // fail…
886        assert!(room_event_cache.find_event(eid3).await.unwrap().is_none());
887    }
888
889    #[async_test]
890    async fn test_generic_update_when_loading_rooms() {
891        // Create 2 rooms. One of them has data in the event cache storage.
892        let user = user_id!("@mnt_io:matrix.org");
893        let client = logged_in_client(None).await;
894        let room_id_0 = room_id!("!raclette:patate.ch");
895        let room_id_1 = room_id!("!fondue:patate.ch");
896
897        let event_factory = EventFactory::new().room(room_id_0).sender(user);
898
899        let event_cache = client.event_cache();
900        event_cache.subscribe().unwrap();
901
902        client.base_client().get_or_create_room(room_id_0, RoomState::Joined);
903        client.base_client().get_or_create_room(room_id_1, RoomState::Joined);
904
905        client
906            .event_cache_store()
907            .lock()
908            .await
909            .expect("Could not acquire the event cache lock")
910            .as_clean()
911            .expect("Could not acquire a clean event cache lock")
912            .handle_linked_chunk_updates(
913                LinkedChunkId::Room(room_id_0),
914                vec![
915                    // Non-empty items chunk.
916                    Update::NewItemsChunk {
917                        previous: None,
918                        new: ChunkIdentifier::new(0),
919                        next: None,
920                    },
921                    Update::PushItems {
922                        at: Position::new(ChunkIdentifier::new(0), 0),
923                        items: vec![
924                            event_factory
925                                .text_msg("hello")
926                                .sender(user)
927                                .event_id(event_id!("$ev0"))
928                                .into_event(),
929                        ],
930                    },
931                ],
932            )
933            .await
934            .unwrap();
935
936        let mut generic_stream = event_cache.subscribe_to_room_generic_updates();
937
938        // Room 0 has initial data, so it must trigger a generic update.
939        {
940            let _room_event_cache = event_cache.room(room_id_0).await.unwrap();
941
942            assert_matches!(
943                generic_stream.recv().await,
944                Ok(RoomEventCacheGenericUpdate { room_id }) => {
945                    assert_eq!(room_id, room_id_0);
946                }
947            );
948        }
949
950        // Room 1 has NO initial data, so nothing should happen.
951        {
952            let _room_event_cache = event_cache.room(room_id_1).await.unwrap();
953
954            assert!(generic_stream.recv().now_or_never().is_none());
955        }
956    }
957
958    #[async_test]
959    async fn test_generic_update_when_paginating_room() {
960        // Create 1 room, with 4 chunks in the event cache storage.
961        let user = user_id!("@mnt_io:matrix.org");
962        let client = logged_in_client(None).await;
963        let room_id = room_id!("!raclette:patate.ch");
964
965        let event_factory = EventFactory::new().room(room_id).sender(user);
966
967        let event_cache = client.event_cache();
968        event_cache.subscribe().unwrap();
969
970        client.base_client().get_or_create_room(room_id, RoomState::Joined);
971
972        client
973            .event_cache_store()
974            .lock()
975            .await
976            .expect("Could not acquire the event cache lock")
977            .as_clean()
978            .expect("Could not acquire a clean event cache lock")
979            .handle_linked_chunk_updates(
980                LinkedChunkId::Room(room_id),
981                vec![
982                    // Empty chunk.
983                    Update::NewItemsChunk {
984                        previous: None,
985                        new: ChunkIdentifier::new(0),
986                        next: None,
987                    },
988                    // Empty chunk.
989                    Update::NewItemsChunk {
990                        previous: Some(ChunkIdentifier::new(0)),
991                        new: ChunkIdentifier::new(1),
992                        next: None,
993                    },
994                    // Non-empty items chunk.
995                    Update::NewItemsChunk {
996                        previous: Some(ChunkIdentifier::new(1)),
997                        new: ChunkIdentifier::new(2),
998                        next: None,
999                    },
1000                    Update::PushItems {
1001                        at: Position::new(ChunkIdentifier::new(2), 0),
1002                        items: vec![
1003                            event_factory
1004                                .text_msg("hello")
1005                                .sender(user)
1006                                .event_id(event_id!("$ev0"))
1007                                .into_event(),
1008                        ],
1009                    },
1010                    // Non-empty items chunk.
1011                    Update::NewItemsChunk {
1012                        previous: Some(ChunkIdentifier::new(2)),
1013                        new: ChunkIdentifier::new(3),
1014                        next: None,
1015                    },
1016                    Update::PushItems {
1017                        at: Position::new(ChunkIdentifier::new(3), 0),
1018                        items: vec![
1019                            event_factory
1020                                .text_msg("world")
1021                                .sender(user)
1022                                .event_id(event_id!("$ev1"))
1023                                .into_event(),
1024                        ],
1025                    },
1026                ],
1027            )
1028            .await
1029            .unwrap();
1030
1031        let mut generic_stream = event_cache.subscribe_to_room_generic_updates();
1032
1033        // Room is initialised, it gets one event in the timeline.
1034        let (room_event_cache, _) = event_cache.room(room_id).await.unwrap();
1035
1036        assert_matches!(
1037            generic_stream.recv().await,
1038            Ok(RoomEventCacheGenericUpdate { room_id: expected_room_id }) => {
1039                assert_eq!(room_id, expected_room_id);
1040            }
1041        );
1042
1043        let pagination = room_event_cache.pagination();
1044
1045        // Paginate, it gets one new event in the timeline.
1046        let pagination_outcome = pagination.run_backwards_once(1).await.unwrap();
1047
1048        assert_eq!(pagination_outcome.events.len(), 1);
1049        assert!(pagination_outcome.reached_start.not());
1050        assert_matches!(
1051            generic_stream.recv().await,
1052            Ok(RoomEventCacheGenericUpdate { room_id: expected_room_id }) => {
1053                assert_eq!(room_id, expected_room_id);
1054            }
1055        );
1056
1057        // Paginate, it gets zero new event in the timeline.
1058        let pagination_outcome = pagination.run_backwards_once(1).await.unwrap();
1059
1060        assert!(pagination_outcome.events.is_empty());
1061        assert!(pagination_outcome.reached_start.not());
1062        assert!(generic_stream.recv().now_or_never().is_none());
1063
1064        // Paginate once more. Just checking our scenario is correct.
1065        let pagination_outcome = pagination.run_backwards_once(1).await.unwrap();
1066
1067        assert!(pagination_outcome.reached_start);
1068        assert!(generic_stream.recv().now_or_never().is_none());
1069    }
1070
1071    #[async_test]
1072    async fn test_for_room_when_room_is_not_found() {
1073        let client = logged_in_client(None).await;
1074        let room_id = room_id!("!raclette:patate.ch");
1075
1076        let event_cache = client.event_cache();
1077        event_cache.subscribe().unwrap();
1078
1079        // Room doesn't exist. It returns an error.
1080        assert_matches!(
1081            event_cache.room(room_id).await,
1082            Err(EventCacheError::RoomNotFound { room_id: not_found_room_id }) => {
1083                assert_eq!(room_id, not_found_room_id);
1084            }
1085        );
1086
1087        // Now create the room.
1088        client.base_client().get_or_create_room(room_id, RoomState::Joined);
1089
1090        // Room exists. Everything fine.
1091        assert!(event_cache.room(room_id).await.is_ok());
1092    }
1093
1094    /// Test that the event cache does not create reference cycles or tasks that
1095    /// retain its reference indefinitely, preventing it from being deallocated.
1096    #[cfg(not(target_family = "wasm"))]
1097    #[async_test]
1098    async fn test_no_refcycle_event_cache_tasks() {
1099        let client = MockClientBuilder::new(None).build().await;
1100
1101        // Wait for the init tasks to die.
1102        sleep(Duration::from_secs(1)).await;
1103
1104        let event_cache_weak = Arc::downgrade(&client.event_cache().inner);
1105        assert_eq!(event_cache_weak.strong_count(), 1);
1106
1107        {
1108            let room_id = room_id!("!room:example.org");
1109
1110            // Have the client know the room.
1111            let response = SyncResponseBuilder::default()
1112                .add_joined_room(JoinedRoomBuilder::new(room_id))
1113                .build_sync_response();
1114            client.inner.base_client.receive_sync_response(response).await.unwrap();
1115
1116            client.event_cache().subscribe().unwrap();
1117
1118            let (_room_event_cache, _drop_handles) =
1119                client.get_room(room_id).unwrap().event_cache().await.unwrap();
1120        }
1121
1122        drop(client);
1123
1124        // Give a bit of time for background tasks to die.
1125        sleep(Duration::from_secs(1)).await;
1126
1127        // No strong counts should exist now that the Client has been dropped.
1128        assert_eq!(
1129            event_cache_weak.strong_count(),
1130            0,
1131            "Too many strong references to the event cache {}",
1132            event_cache_weak.strong_count()
1133        );
1134    }
1135}