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    ops::Deref,
34    sync::{Arc, OnceLock, RwLock as StdRwLock, RwLockReadGuard, RwLockWriteGuard},
35};
36
37use matrix_sdk_base::{
38    cross_process_lock::CrossProcessLockError,
39    event_cache::store::{EventCacheStoreError, EventCacheStoreLock},
40    linked_chunk::lazy_loader::LazyLoaderError,
41    sync::RoomUpdates,
42    task_monitor::BackgroundTaskHandle,
43};
44use ruma::{EventId, OwnedEventId, OwnedRoomId, RoomId};
45use tokio::sync::{
46    OwnedRwLockReadGuard, OwnedRwLockWriteGuard, RwLock,
47    broadcast::{Receiver, Sender, channel},
48    mpsc,
49};
50use tracing::{error, instrument, trace};
51
52use crate::{
53    Client,
54    client::{ClientInner, WeakClient},
55    paginators::PaginatorError,
56};
57
58mod automatic_pagination;
59mod caches;
60mod deduplicator;
61mod persistence;
62#[cfg(feature = "e2e-encryption")]
63mod redecryptor;
64mod states;
65mod tasks;
66
67#[cfg(feature = "e2e-encryption")]
68pub use redecryptor::{DecryptionRetryRequest, RedecryptorReport};
69
70pub use self::{
71    automatic_pagination::AutomaticPagination,
72    caches::{
73        TimelineVectorDiffs,
74        event_focused::{EventFocusThreadMode, EventFocusedCache, EventFocusedCacheKey},
75        pagination::{BackPaginationOutcome, PaginationStatus},
76        pinned_events::PinnedEventsCache,
77        room::{
78            RoomEventCache, RoomEventCacheGenericUpdate, RoomEventCacheUpdate,
79            pagination::RoomPagination,
80        },
81        subscriber::Subscriber,
82        thread::{ThreadEventCache, pagination::ThreadPagination},
83    },
84};
85use self::{
86    caches::{Caches, room::RoomEventCacheLinkedChunkUpdate, subscriber::AutoShrinkMessage},
87    states::StateLock,
88};
89
90/// An error observed in the [`EventCache`].
91#[derive(thiserror::Error, Clone, Debug)]
92pub enum EventCacheError {
93    /// The [`EventCache`] instance hasn't been initialized with
94    /// [`EventCache::subscribe`]
95    #[error(
96        "The EventCache hasn't subscribed to sync responses yet, call `EventCache::subscribe()`"
97    )]
98    NotSubscribedYet,
99
100    /// Room cache is not found.
101    #[error("Room cache `{room_id}` is not found.")]
102    RoomNotFound {
103        /// The room ID.
104        room_id: OwnedRoomId,
105    },
106
107    /// Thread cache is not found.
108    #[error("Thread cache `{thread_id}` of room `{room_id}` is not found.")]
109    ThreadNotFound {
110        /// The room ID of the thread.
111        room_id: OwnedRoomId,
112
113        /// The thread root event ID.
114        thread_id: OwnedEventId,
115    },
116
117    /// Pinned-events cache are not found.
118    #[error("Pinned-events cache for room `{room_id}` are not found.")]
119    PinnedEventsNotFound {
120        /// The room ID of the pinned-events.
121        room_id: OwnedRoomId,
122    },
123
124    /// Event-focused cache is not found.
125    #[error("Event-focused cache `{event_focused_id:?}` of room `{room_id}` is not found.")]
126    EventFocusedNotFound {
127        /// The room ID of the thread.
128        room_id: OwnedRoomId,
129
130        /// The thread root event ID.
131        event_focused_id: EventFocusedCacheKey,
132    },
133
134    /// A new cache was inserted at an occupied place, i.e. where an existing
135    /// cache state was present.
136    #[error("The state of a cache is not found")]
137    CacheStateAlreadyExists,
138
139    /// An error has been observed while back- or forward- paginating.
140    #[error(transparent)]
141    PaginationError(Arc<crate::Error>),
142
143    /// An error has been observed while initiating an event-focused timeline.
144    #[error(transparent)]
145    InitialPaginationError(#[from] PaginatorError),
146
147    /// An error happening when interacting with storage.
148    #[error(transparent)]
149    Storage(#[from] EventCacheStoreError),
150
151    /// An error happening when attempting to (cross-process) lock storage.
152    #[error(transparent)]
153    LockingStorage(#[from] CrossProcessLockError),
154
155    /// The [`EventCache`] owns a weak reference to the [`Client`] it pertains
156    /// to. It's possible this weak reference points to nothing anymore, at
157    /// times where we try to use the client.
158    #[error("The owning client of the event cache has been dropped.")]
159    ClientDropped,
160
161    /// An error happening when interacting with the [`LinkedChunk`]'s lazy
162    /// loader.
163    ///
164    /// [`LinkedChunk`]: matrix_sdk_common::linked_chunk::LinkedChunk
165    #[error(transparent)]
166    LinkedChunkLoader(#[from] LazyLoaderError),
167
168    /// An error happened when trying to load pinned events; none of them could
169    /// be loaded, which would otherwise result in an empty pinned events
170    /// list, incorrectly.
171    #[error("Unable to load any of the pinned events.")]
172    UnableToLoadPinnedEvents,
173
174    /// An error happened when reading the metadata of a linked chunk, upon
175    /// reload.
176    #[error("the linked chunk metadata is invalid: {details}")]
177    InvalidLinkedChunkMetadata {
178        /// A string containing details about the error.
179        details: String,
180    },
181}
182
183/// A result using the [`EventCacheError`].
184pub type Result<T> = std::result::Result<T, EventCacheError>;
185
186/// Hold handles to the tasks spawn by a [`EventCache`].
187pub struct EventCacheDropHandles {
188    /// Task that listens to room updates.
189    _listen_updates_task: BackgroundTaskHandle,
190
191    /// Task that listens to updates to the user's ignored list.
192    _ignore_user_list_update_task: BackgroundTaskHandle,
193
194    /// The task used to automatically shrink the linked chunks.
195    _auto_shrink_linked_chunk_task: BackgroundTaskHandle,
196
197    /// A background task listening to room and send queue updates, and
198    /// automatically subscribing the user to threads when needed, based on
199    /// the semantics of MSC4306.
200    ///
201    /// One important constraint is that there is only one such task per
202    /// [`EventCache`], so it does listen to *all* rooms at the same time.
203    _thread_subscriber_task: BackgroundTaskHandle,
204
205    /// A background task listening to room updates, and
206    /// automatically handling search index operations add/remove/edit
207    /// depending on the event type.
208    ///
209    /// One important constraint is that there is only one such task per
210    /// [`EventCache`], so it does listen to *all* rooms at the same time.
211    #[cfg(feature = "experimental-search")]
212    _search_indexing_task: BackgroundTaskHandle,
213
214    /// The task used to automatically redecrypt UTDs.
215    #[cfg(feature = "e2e-encryption")]
216    _redecryptor: redecryptor::Redecryptor,
217}
218
219impl fmt::Debug for EventCacheDropHandles {
220    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
221        f.debug_struct("EventCacheDropHandles").finish_non_exhaustive()
222    }
223}
224
225/// An event cache, providing lots of useful functionality for clients.
226///
227/// Cloning is shallow, and thus is cheap to do.
228///
229/// See also the module-level comment.
230#[derive(Clone)]
231pub struct EventCache {
232    /// Reference to the inner cache.
233    inner: Arc<EventCacheInner>,
234}
235
236impl fmt::Debug for EventCache {
237    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
238        f.debug_struct("EventCache").finish_non_exhaustive()
239    }
240}
241
242impl EventCache {
243    /// Create a new [`EventCache`] for the given client.
244    pub(crate) fn new(client: &Arc<ClientInner>, event_cache_store: EventCacheStoreLock) -> Self {
245        let (generic_update_sender, _) = channel(128);
246        let (linked_chunk_update_sender, _) = channel(128);
247
248        let weak_client = WeakClient::from_inner(client);
249
250        let (thread_subscriber_sender, _thread_subscriber_receiver) = channel(128);
251
252        #[cfg(feature = "e2e-encryption")]
253        let redecryption_channels = redecryptor::RedecryptorChannels::new();
254
255        Self {
256            inner: Arc::new(EventCacheInner {
257                client: weak_client,
258                config: StdRwLock::new(EventCacheConfig::default()),
259                state: StateLock::new(event_cache_store),
260                by_room: Default::default(),
261                drop_handles: Default::default(),
262                auto_shrink_sender: Default::default(),
263                generic_update_sender,
264                linked_chunk_update_sender,
265                #[cfg(feature = "e2e-encryption")]
266                redecryption_channels,
267                automatic_pagination: OnceLock::new(),
268                thread_subscriber_sender,
269            }),
270        }
271    }
272
273    /// Get a read-only handle to the global configuration of the
274    /// [`EventCache`].
275    pub fn config(&self) -> RwLockReadGuard<'_, EventCacheConfig> {
276        self.inner.config.read().unwrap()
277    }
278
279    /// Get a writable handle to the global configuration of the [`EventCache`].
280    pub fn config_mut(&self) -> RwLockWriteGuard<'_, EventCacheConfig> {
281        self.inner.config.write().unwrap()
282    }
283
284    /// Subscribes to updates that a thread subscription has been sent.
285    ///
286    /// For testing purposes only.
287    #[cfg(feature = "testing")]
288    pub fn subscribe_thread_subscriber_updates(&self) -> Receiver<()> {
289        self.inner.thread_subscriber_sender.subscribe()
290    }
291
292    /// Starts subscribing the [`EventCache`] to sync responses, if not done
293    /// before.
294    ///
295    /// Re-running this has no effect if we already subscribed before, and is
296    /// cheap.
297    pub fn subscribe(&self) -> Result<()> {
298        let client = self.inner.client()?;
299
300        // Initialize the drop handles.
301        let _ = self.inner.drop_handles.get_or_init(|| {
302            let task_monitor = client.task_monitor();
303
304            // Spawn the task that will listen to all the room updates at once.
305            let listen_updates_task = task_monitor.spawn_infinite_task("event_cache::room_updates_task", tasks::room_updates_task(
306                self.inner.clone(),
307                client.subscribe_to_all_room_updates(),
308            )).abort_on_drop();
309
310            let ignore_user_list_update_task = task_monitor.spawn_infinite_task("event_cache::ignore_user_list_update_task", tasks::ignore_user_list_update_task(
311                self.inner.clone(),
312                client.subscribe_to_ignore_user_list_changes(),
313            )).abort_on_drop();
314
315            let (auto_shrink_sender, auto_shrink_receiver) = mpsc::channel(32);
316
317            // Force-initialize the sender in the [`RoomEventCacheInner`].
318            self.inner.auto_shrink_sender.get_or_init(|| auto_shrink_sender);
319
320            let auto_shrink_linked_chunk_task = task_monitor.spawn_infinite_task("event_cache::auto_shrink_linked_chunk_task", tasks::auto_shrink_linked_chunk_task(
321                Arc::downgrade(&self.inner),
322                auto_shrink_receiver,
323            )).abort_on_drop();
324
325            #[cfg(feature = "e2e-encryption")]
326            let redecryptor = {
327                let receiver = self
328                    .inner
329                    .redecryption_channels
330                    .decryption_request_receiver
331                    .lock()
332                    .take()
333                    .expect("We should have initialized the channel an subscribing should happen only once");
334
335                redecryptor::Redecryptor::new(&client, Arc::downgrade(&self.inner), receiver, &self.inner.linked_chunk_update_sender)
336            };
337
338        let thread_subscriber_task = client
339            .task_monitor()
340            .spawn_infinite_task(
341                "event_cache::thread_subscriber",
342                tasks::thread_subscriber_task(
343                    self.inner.client.clone(),
344                    self.inner.linked_chunk_update_sender.clone(),
345                    self.inner.thread_subscriber_sender.clone(),
346                ),
347            )
348            .abort_on_drop();
349
350        #[cfg(feature = "experimental-search")]
351        let search_indexing_task = client
352            .task_monitor()
353            .spawn_infinite_task(
354                "event_cache::search_indexing",
355                tasks::search_indexing_task(
356                    self.inner.client.clone(),
357                    self.inner.linked_chunk_update_sender.clone(),
358                ),
359            )
360            .abort_on_drop();
361
362            if self.config().experimental_auto_backpagination {
363                // Run the deferred initialization of the automatic pagination request sender, that
364                // is shared with every room.
365                trace!("spawning the automatic paginations API");
366                self.inner.automatic_pagination.get_or_init(|| AutomaticPagination::new(Arc::downgrade(&self.inner), task_monitor));
367            } else {
368                trace!("automatic paginations API is disabled");
369            }
370
371            Arc::new(EventCacheDropHandles {
372                _listen_updates_task: listen_updates_task,
373                _ignore_user_list_update_task: ignore_user_list_update_task,
374                _auto_shrink_linked_chunk_task: auto_shrink_linked_chunk_task,
375                #[cfg(feature = "e2e-encryption")]
376                _redecryptor: redecryptor,
377                _thread_subscriber_task: thread_subscriber_task,
378                #[cfg(feature = "experimental-search")]
379                _search_indexing_task: search_indexing_task,
380            })
381        });
382
383        Ok(())
384    }
385
386    /// For benchmarking purposes only.
387    #[doc(hidden)]
388    pub async fn handle_room_updates(&self, updates: RoomUpdates) -> Result<()> {
389        self.inner.handle_room_updates(updates).await
390    }
391
392    /// Check whether [`EventCache::subscribe`] has been called.
393    pub fn has_subscribed(&self) -> bool {
394        self.inner.drop_handles.get().is_some()
395    }
396
397    /// Return a room-specific view over the [`EventCache`].
398    pub async fn room(
399        &self,
400        room_id: &RoomId,
401    ) -> Result<(RoomEventCache, Arc<EventCacheDropHandles>)> {
402        let Some(drop_handles) = self.inner.drop_handles.get().cloned() else {
403            return Err(EventCacheError::NotSubscribedYet);
404        };
405
406        let caches_for_room = self.inner.all_caches_for_room(room_id).await?;
407
408        Ok((caches_for_room.room().clone(), drop_handles))
409    }
410
411    /// Return a thread-specific view over the [`EventCache`].
412    pub async fn thread(
413        &self,
414        room_id: &RoomId,
415        thread_id: &EventId,
416    ) -> Result<(ThreadEventCache, Arc<EventCacheDropHandles>)> {
417        let Some(drop_handles) = self.inner.drop_handles.get().cloned() else {
418            return Err(EventCacheError::NotSubscribedYet);
419        };
420
421        let caches_for_room = self.inner.all_caches_for_room(room_id).await?;
422
423        Ok((caches_for_room.thread(thread_id.to_owned()).await?.deref().clone(), drop_handles))
424    }
425
426    /// Return a pinned-events-specific view over the [`EventCache`].
427    pub async fn pinned_events(
428        &self,
429        room_id: &RoomId,
430    ) -> Result<(PinnedEventsCache, Arc<EventCacheDropHandles>)> {
431        let Some(drop_handles) = self.inner.drop_handles.get().cloned() else {
432            return Err(EventCacheError::NotSubscribedYet);
433        };
434
435        let caches_for_room = self.inner.all_caches_for_room(room_id).await?;
436
437        Ok((caches_for_room.pinned_events().await?.clone(), drop_handles))
438    }
439
440    /// Return an event-focused view over the [`EventCache`].
441    pub async fn event_focused(
442        &self,
443        room_id: &RoomId,
444        event_id: &EventId,
445        thread_mode: EventFocusThreadMode,
446        number_of_initial_events: u16,
447    ) -> Result<(EventFocusedCache, Arc<EventCacheDropHandles>)> {
448        let Some(drop_handles) = self.inner.drop_handles.get().cloned() else {
449            return Err(EventCacheError::NotSubscribedYet);
450        };
451
452        let caches_for_room = self.inner.all_caches_for_room(room_id).await?;
453
454        Ok((
455            caches_for_room
456                .event_focused(event_id.to_owned(), thread_mode, number_of_initial_events)
457                .await?
458                .deref()
459                .clone(),
460            drop_handles,
461        ))
462    }
463
464    /// Forget all caches related to a single room.
465    ///
466    /// This will notify any live observers that the room has been cleared.
467    pub async fn forget_room(&self, room_id: &RoomId) -> Result<()> {
468        self.inner.forget_room(room_id).await
469    }
470
471    /// Cleanly clear all the rooms' event caches.
472    ///
473    /// This will notify any live observers that the room has been cleared.
474    pub async fn clear_all_rooms(&self) -> Result<()> {
475        self.inner.clear_all_rooms().await
476    }
477
478    /// Subscribe to room _generic_ updates.
479    ///
480    /// If one wants to listen what has changed in a specific room for example,
481    /// the [`RoomEventCache::subscribe`] is recommended. However, the
482    /// [`Subscriber`] type triggers side-effects.
483    ///
484    /// If one wants to get a high-overview, generic, updates for rooms, and
485    /// without side-effects, this method is recommended. Also, dropping the
486    /// receiver of this channel will not trigger any side-effect.
487    pub fn subscribe_to_room_generic_updates(&self) -> Receiver<RoomEventCacheGenericUpdate> {
488        self.inner.generic_update_sender.subscribe()
489    }
490
491    /// Returns a reference to the [`AutomaticPagination`] API, if enabled at
492    /// construction with the
493    /// [`EventCacheConfig::experimental_auto_backpagination`] flag.
494    pub fn automatic_pagination(&self) -> Option<AutomaticPagination> {
495        self.inner.automatic_pagination.get().cloned()
496    }
497}
498
499/// Global configuration for the [`EventCache`], applied to every single room.
500#[derive(Clone, Copy, Debug)]
501pub struct EventCacheConfig {
502    /// Maximum number of concurrent /event requests when loading pinned events.
503    pub max_pinned_events_concurrent_requests: usize,
504
505    /// Maximum number of pinned events to load, for any room.
506    pub max_pinned_events_to_load: usize,
507
508    /// Whether to automatically backpaginate a room under certain conditions.
509    ///
510    /// Off by default.
511    pub experimental_auto_backpagination: bool,
512
513    /// The maximum number of allowed room paginations, for a given room, that
514    /// can be executed in the automatic paginations task.
515    ///
516    /// After that number of paginations, the task will stop executing
517    /// paginations for that room *in the background* (user-requested
518    /// paginations will still be executed, of course).
519    ///
520    /// Defaults to [`EventCacheConfig::DEFAULT_ROOM_PAGINATION_CREDITS`].
521    pub room_pagination_per_room_credit: usize,
522
523    /// The number of messages to paginate in a single batch, when executing an
524    /// automatic pagination request.
525    ///
526    /// Defaults to [`EventCacheConfig::DEFAULT_ROOM_PAGINATION_BATCH_SIZE`].
527    pub room_pagination_batch_size: u16,
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 number of credits to give to a room for automatic
539    /// paginations (see also
540    /// [`EventCacheConfig::room_pagination_per_room_credit`]).
541    pub const DEFAULT_ROOM_PAGINATION_CREDITS: usize = 20;
542
543    /// The default number of messages to paginate in a single batch, when
544    /// executing an automatic pagination request (see also
545    /// [`EventCacheConfig::room_pagination_batch_size`]).
546    pub const DEFAULT_ROOM_PAGINATION_BATCH_SIZE: u16 = 30;
547}
548
549impl Default for EventCacheConfig {
550    fn default() -> Self {
551        Self {
552            max_pinned_events_concurrent_requests: Self::DEFAULT_MAX_CONCURRENT_REQUESTS,
553            max_pinned_events_to_load: Self::DEFAULT_MAX_EVENTS_TO_LOAD,
554            room_pagination_per_room_credit: Self::DEFAULT_ROOM_PAGINATION_CREDITS,
555            room_pagination_batch_size: Self::DEFAULT_ROOM_PAGINATION_BATCH_SIZE,
556            experimental_auto_backpagination: false,
557        }
558    }
559}
560
561type CachesByRoom = HashMap<OwnedRoomId, Caches>;
562
563struct EventCacheInner {
564    /// A weak reference to the inner client, useful when trying to get a handle
565    /// on the owning client.
566    client: WeakClient,
567
568    /// Global configuration for the event cache.
569    config: StdRwLock<EventCacheConfig>,
570
571    /// Lock around the state of the Event Cache, containing all the cache
572    /// states.
573    state: StateLock,
574
575    /// Lazily-filled cache of live [`RoomEventCache`], once per room.
576    //
577    // It's behind an `Arc` to get owned locks.
578    by_room: Arc<RwLock<CachesByRoom>>,
579
580    /// Handles to keep alive the task listening to updates.
581    drop_handles: OnceLock<Arc<EventCacheDropHandles>>,
582
583    /// A sender for notifications that a room *may* need to be auto-shrunk.
584    ///
585    /// Needs to live here, so it may be passed to each [`RoomEventCache`]
586    /// instance.
587    ///
588    /// It's a `OnceLock` because its initialization is deferred to
589    /// [`EventCache::subscribe`].
590    ///
591    /// See doc comment of [`tasks::auto_shrink_linked_chunk_task`].
592    auto_shrink_sender: OnceLock<mpsc::Sender<AutoShrinkMessage>>,
593
594    /// A sender for room generic update.
595    ///
596    /// See doc comment of [`RoomEventCacheGenericUpdate`] and
597    /// [`EventCache::subscribe_to_room_generic_updates`].
598    generic_update_sender: Sender<RoomEventCacheGenericUpdate>,
599
600    /// A sender for a persisted linked chunk update.
601    ///
602    /// This is used to notify that some linked chunk has persisted some updates
603    /// to a store, during sync or a back-pagination of *any* linked chunk.
604    /// This can be used by observers to look for new events.
605    ///
606    /// See doc comment of [`RoomEventCacheLinkedChunkUpdate`].
607    linked_chunk_update_sender: Sender<RoomEventCacheLinkedChunkUpdate>,
608
609    /// A test helper receiver that will be emitted every time the thread
610    /// subscriber task subscribed to a new thread.
611    ///
612    /// This is helpful for tests to coordinate that a new thread subscription
613    /// has been sent or not.
614    thread_subscriber_sender: Sender<()>,
615
616    #[cfg(feature = "e2e-encryption")]
617    redecryption_channels: redecryptor::RedecryptorChannels,
618
619    /// State for the automatic pagination mechanism.
620    ///
621    /// Depends on the [`EventCacheConfig::experimental_auto_backpagination`]
622    /// flag to be set at subscription time.
623    automatic_pagination: OnceLock<AutomaticPagination>,
624}
625
626impl EventCacheInner {
627    fn client(&self) -> Result<Client> {
628        self.client.get().ok_or(EventCacheError::ClientDropped)
629    }
630
631    /// Clear a single room's data.
632    async fn forget_room(&self, room_id: &RoomId) -> Result<()> {
633        // The constraints are very similar to what we do in `clear_all_rooms`. See this
634        // information to understand them.
635
636        let mut caches_for_all_rooms = self.by_room.write().await;
637        self.state.clear_and_reload(&caches_for_all_rooms, Some(room_id)).await?;
638
639        // Finally, we forget all the caches if any exists in memory.
640        caches_for_all_rooms.remove(room_id);
641
642        Ok(())
643    }
644
645    /// Clears all the rooms' data.
646    async fn clear_all_rooms(&self) -> Result<()> {
647        // Okay, here's where things get delicate.
648        //
649        // On the one hand, `by_room` may include storage for *some* caches
650        // that we know about, but not *all* of them. Any cache that hasn't been
651        // loaded in the client, or touched by a sync, will remain unloaded in
652        // memory, so it will be missing from `self.by_room`. As a result, we
653        // need to make sure that we're hitting the storage backend to *really*
654        // clear all the caches, including those that haven't been loaded yet.
655        //
656        // On the other hand, one must NOT clear the `by_room` map, because if
657        // someone subscribed to a cache update, they would never get any new
658        // update for that cache, since re-creating the cache would create a
659        // new, unrelated sender.
660        //
661        // So we need to *keep* the caches in `by_room` alive, while clearing
662        // them in the store backend.
663        //
664        // As a result, for a short while, the in-memory linked chunks will be
665        // desynchronised from the storage. We need to be careful then. During
666        // that short while, we don't want *anyone* to touch the linked chunks
667        // (be it in memory or in the storage).
668        //
669        // And since that requirement applies to *any* cache in `by_room` at the
670        // same time, we'll have to take the lock for *all* the live caches and
671        // for the states, so as to properly clear the underlying storage.
672
673        // We acquire an exclusive access to `by_room`.
674        let caches_for_all_rooms = self.by_room.write().await;
675
676        // Then, we can clear and reload the states for all the rooms.
677        self.state.clear_and_reload(&caches_for_all_rooms, None).await?;
678
679        Ok(())
680    }
681
682    /// Handles a single set of room updates at once.
683    #[instrument(skip(self, updates))]
684    async fn handle_room_updates(&self, updates: RoomUpdates) -> Result<()> {
685        // NOTE: We tried to make this concurrent at some point, but it turned out to be
686        // a performance regression, even for large sync updates. Lacking time
687        // to investigate, this code remains sequential for now. See also
688        // https://github.com/matrix-org/matrix-rust-sdk/pull/5426.
689
690        // Left rooms.
691        for (room_id, left_room_update) in updates.left {
692            let Ok(caches) = self.all_caches_for_room(&room_id).await else {
693                error!(?room_id, "Room must exist");
694                continue;
695            };
696
697            if let Err(err) = caches.handle_left_room_update(left_room_update).await {
698                // Non-fatal error, try to continue to the next room.
699                error!("handling left room update: {err}");
700            }
701        }
702
703        // Joined rooms.
704        for (room_id, joined_room_update) in updates.joined {
705            trace!(?room_id, "Handling a `JoinedRoomUpdate`");
706
707            let Ok(caches) = self.all_caches_for_room(&room_id).await else {
708                error!(?room_id, "Room must exist");
709                continue;
710            };
711
712            if let Err(err) = caches.handle_joined_room_update(joined_room_update).await {
713                // Non-fatal error, try to continue to the next room.
714                error!(%room_id, "handling joined room update: {err}");
715            }
716        }
717
718        // Invited rooms.
719        // TODO: we don't anything with `updates.invite` at this point.
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.automatic_pagination.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}