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        //
720        // We don't handle `updates.invite` because they contain stripped-state events,
721        // which is not handled by the Event Cache for the moment.
722
723        Ok(())
724    }
725
726    /// Return all the event caches associated to a specific room.
727    async fn all_caches_for_room(
728        &self,
729        room_id: &RoomId,
730    ) -> Result<OwnedRwLockReadGuard<CachesByRoom, Caches>> {
731        // Fast path: the entry exists; let's acquire a read lock, it's cheaper than a
732        // write lock.
733        match OwnedRwLockReadGuard::try_map(self.by_room.clone().read_owned().await, |by_room| {
734            by_room.get(room_id)
735        }) {
736            Ok(caches) => Ok(caches),
737
738            Err(by_room_guard) => {
739                // Slow-path: the entry doesn't exist; let's acquire a write 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 access and done
744                // 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 code, otherwise
759                    // 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.automatic_pagination.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 event
821        // 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 event
826        // 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 will
887        // 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}