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