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    /// Cleanly clear all the rooms' event caches.
465    ///
466    /// This will notify any live observers that the room has been cleared.
467    pub async fn clear_all_rooms(&self) -> Result<()> {
468        self.inner.clear_all_rooms().await
469    }
470
471    /// Subscribe to room _generic_ updates.
472    ///
473    /// If one wants to listen what has changed in a specific room for example,
474    /// the [`RoomEventCache::subscribe`] is recommended. However, the
475    /// [`Subscriber`] type triggers side-effects.
476    ///
477    /// If one wants to get a high-overview, generic, updates for rooms, and
478    /// without side-effects, this method is recommended. Also, dropping the
479    /// receiver of this channel will not trigger any side-effect.
480    pub fn subscribe_to_room_generic_updates(&self) -> Receiver<RoomEventCacheGenericUpdate> {
481        self.inner.generic_update_sender.subscribe()
482    }
483
484    /// Returns a reference to the [`AutomaticPagination`] API, if enabled at
485    /// construction with the
486    /// [`EventCacheConfig::experimental_auto_backpagination`] flag.
487    pub fn automatic_pagination(&self) -> Option<AutomaticPagination> {
488        self.inner.automatic_pagination.get().cloned()
489    }
490}
491
492/// Global configuration for the [`EventCache`], applied to every single room.
493#[derive(Clone, Copy, Debug)]
494pub struct EventCacheConfig {
495    /// Maximum number of concurrent /event requests when loading pinned events.
496    pub max_pinned_events_concurrent_requests: usize,
497
498    /// Maximum number of pinned events to load, for any room.
499    pub max_pinned_events_to_load: usize,
500
501    /// Whether to automatically backpaginate a room under certain conditions.
502    ///
503    /// Off by default.
504    pub experimental_auto_backpagination: bool,
505
506    /// The maximum number of allowed room paginations, for a given room, that
507    /// can be executed in the automatic paginations task.
508    ///
509    /// After that number of paginations, the task will stop executing
510    /// paginations for that room *in the background* (user-requested
511    /// paginations will still be executed, of course).
512    ///
513    /// Defaults to [`EventCacheConfig::DEFAULT_ROOM_PAGINATION_CREDITS`].
514    pub room_pagination_per_room_credit: usize,
515
516    /// The number of messages to paginate in a single batch, when executing an
517    /// automatic pagination request.
518    ///
519    /// Defaults to [`EventCacheConfig::DEFAULT_ROOM_PAGINATION_BATCH_SIZE`].
520    pub room_pagination_batch_size: u16,
521}
522
523impl EventCacheConfig {
524    /// The default maximum number of pinned events to load.
525    pub const DEFAULT_MAX_EVENTS_TO_LOAD: usize = 128;
526
527    /// The default maximum number of concurrent requests to perform when
528    /// loading the pinned events.
529    pub const DEFAULT_MAX_CONCURRENT_REQUESTS: usize = 8;
530
531    /// The default number of credits to give to a room for automatic
532    /// paginations (see also
533    /// [`EventCacheConfig::room_pagination_per_room_credit`]).
534    pub const DEFAULT_ROOM_PAGINATION_CREDITS: usize = 20;
535
536    /// The default number of messages to paginate in a single batch, when
537    /// executing an automatic pagination request (see also
538    /// [`EventCacheConfig::room_pagination_batch_size`]).
539    pub const DEFAULT_ROOM_PAGINATION_BATCH_SIZE: u16 = 30;
540}
541
542impl Default for EventCacheConfig {
543    fn default() -> Self {
544        Self {
545            max_pinned_events_concurrent_requests: Self::DEFAULT_MAX_CONCURRENT_REQUESTS,
546            max_pinned_events_to_load: Self::DEFAULT_MAX_EVENTS_TO_LOAD,
547            room_pagination_per_room_credit: Self::DEFAULT_ROOM_PAGINATION_CREDITS,
548            room_pagination_batch_size: Self::DEFAULT_ROOM_PAGINATION_BATCH_SIZE,
549            experimental_auto_backpagination: false,
550        }
551    }
552}
553
554type CachesByRoom = HashMap<OwnedRoomId, Caches>;
555
556struct EventCacheInner {
557    /// A weak reference to the inner client, useful when trying to get a handle
558    /// on the owning client.
559    client: WeakClient,
560
561    /// Global configuration for the event cache.
562    config: StdRwLock<EventCacheConfig>,
563
564    /// Lock around the state of the Event Cache, containing all the cache
565    /// states.
566    state: StateLock,
567
568    /// Lazily-filled cache of live [`RoomEventCache`], once per room.
569    //
570    // It's behind an `Arc` to get owned locks.
571    by_room: Arc<RwLock<CachesByRoom>>,
572
573    /// Handles to keep alive the task listening to updates.
574    drop_handles: OnceLock<Arc<EventCacheDropHandles>>,
575
576    /// A sender for notifications that a room *may* need to be auto-shrunk.
577    ///
578    /// Needs to live here, so it may be passed to each [`RoomEventCache`]
579    /// instance.
580    ///
581    /// It's a `OnceLock` because its initialization is deferred to
582    /// [`EventCache::subscribe`].
583    ///
584    /// See doc comment of [`tasks::auto_shrink_linked_chunk_task`].
585    auto_shrink_sender: OnceLock<mpsc::Sender<AutoShrinkMessage>>,
586
587    /// A sender for room generic update.
588    ///
589    /// See doc comment of [`RoomEventCacheGenericUpdate`] and
590    /// [`EventCache::subscribe_to_room_generic_updates`].
591    generic_update_sender: Sender<RoomEventCacheGenericUpdate>,
592
593    /// A sender for a persisted linked chunk update.
594    ///
595    /// This is used to notify that some linked chunk has persisted some updates
596    /// to a store, during sync or a back-pagination of *any* linked chunk.
597    /// This can be used by observers to look for new events.
598    ///
599    /// See doc comment of [`RoomEventCacheLinkedChunkUpdate`].
600    linked_chunk_update_sender: Sender<RoomEventCacheLinkedChunkUpdate>,
601
602    /// A test helper receiver that will be emitted every time the thread
603    /// subscriber task subscribed to a new thread.
604    ///
605    /// This is helpful for tests to coordinate that a new thread subscription
606    /// has been sent or not.
607    thread_subscriber_sender: Sender<()>,
608
609    #[cfg(feature = "e2e-encryption")]
610    redecryption_channels: redecryptor::RedecryptorChannels,
611
612    /// State for the automatic pagination mechanism.
613    ///
614    /// Depends on the [`EventCacheConfig::experimental_auto_backpagination`]
615    /// flag to be set at subscription time.
616    automatic_pagination: OnceLock<AutomaticPagination>,
617}
618
619impl EventCacheInner {
620    fn client(&self) -> Result<Client> {
621        self.client.get().ok_or(EventCacheError::ClientDropped)
622    }
623
624    /// Clears all the room's data.
625    async fn clear_all_rooms(&self) -> Result<()> {
626        // Okay, here's where things get delicate.
627        //
628        // On the one hand, `by_room` may include storage for *some* caches
629        // that we know about, but not *all* of them. Any cache that hasn't been
630        // loaded in the client, or touched by a sync, will remain unloaded in
631        // memory, so it will be missing from `self.by_room`. As a result, we
632        // need to make sure that we're hitting the storage backend to *really*
633        // clear all the caches, including those that haven't been loaded yet.
634        //
635        // On the other hand, one must NOT clear the `by_room` map, because if
636        // someone subscribed to a cache update, they would never get any new
637        // update for that cache, since re-creating the cache would create a
638        // new, unrelated sender.
639        //
640        // So we need to *keep* the caches in `by_room` alive, while clearing
641        // them in the store backend.
642        //
643        // As a result, for a short while, the in-memory linked chunks will be
644        // desynchronised from the storage. We need to be careful then. During
645        // that short while, we don't want *anyone* to touch the linked chunks
646        // (be it in memory or in the storage).
647        //
648        // And since that requirement applies to *any* cache in `by_room` at the
649        // same time, we'll have to take the lock for *all* the live caches and
650        // for the states, so as to properly clear the underlying storage.
651
652        self.state.clear_and_reload(self.by_room.write().await).await?;
653
654        Ok(())
655    }
656
657    /// Handles a single set of room updates at once.
658    #[instrument(skip(self, updates))]
659    async fn handle_room_updates(&self, updates: RoomUpdates) -> Result<()> {
660        // NOTE: We tried to make this concurrent at some point, but it turned out to be
661        // a performance regression, even for large sync updates. Lacking time
662        // to investigate, this code remains sequential for now. See also
663        // https://github.com/matrix-org/matrix-rust-sdk/pull/5426.
664
665        // Left rooms.
666        for (room_id, left_room_update) in updates.left {
667            let Ok(caches) = self.all_caches_for_room(&room_id).await else {
668                error!(?room_id, "Room must exist");
669                continue;
670            };
671
672            if let Err(err) = caches.handle_left_room_update(left_room_update).await {
673                // Non-fatal error, try to continue to the next room.
674                error!("handling left room update: {err}");
675            }
676        }
677
678        // Joined rooms.
679        for (room_id, joined_room_update) in updates.joined {
680            trace!(?room_id, "Handling a `JoinedRoomUpdate`");
681
682            let Ok(caches) = self.all_caches_for_room(&room_id).await else {
683                error!(?room_id, "Room must exist");
684                continue;
685            };
686
687            if let Err(err) = caches.handle_joined_room_update(joined_room_update).await {
688                // Non-fatal error, try to continue to the next room.
689                error!(%room_id, "handling joined room update: {err}");
690            }
691        }
692
693        // Invited rooms.
694        // TODO: we don't anything with `updates.invite` at this point.
695
696        Ok(())
697    }
698
699    /// Return all the event caches associated to a specific room.
700    async fn all_caches_for_room(
701        &self,
702        room_id: &RoomId,
703    ) -> Result<OwnedRwLockReadGuard<CachesByRoom, Caches>> {
704        // Fast path: the entry exists; let's acquire a read lock, it's cheaper than a
705        // write lock.
706        match OwnedRwLockReadGuard::try_map(self.by_room.clone().read_owned().await, |by_room| {
707            by_room.get(room_id)
708        }) {
709            Ok(caches) => Ok(caches),
710
711            Err(by_room_guard) => {
712                // Slow-path: the entry doesn't exist; let's acquire a write lock.
713                drop(by_room_guard);
714                let by_room_guard = self.by_room.clone().write_owned().await;
715
716                // In the meanwhile, some other caller might have obtained write access and done
717                // the same, so check for existence again.
718                let mut by_room_guard =
719                    match OwnedRwLockWriteGuard::try_downgrade_map(by_room_guard, |by_room| {
720                        by_room.get(room_id)
721                    }) {
722                        Ok(caches) => return Ok(caches),
723                        Err(by_room_guard) => by_room_guard,
724                    };
725
726                let caches = Caches::new(
727                    &self.client,
728                    room_id,
729                    self.generic_update_sender.clone(),
730                    self.linked_chunk_update_sender.clone(),
731                    // SAFETY: we must have subscribed before reaching this code, otherwise
732                    // something is very wrong.
733                    self.auto_shrink_sender.get().cloned().expect(
734                        "we must have called `EventCache::subscribe()` before calling here.",
735                    ),
736                    &self.state,
737                    self.automatic_pagination.get().cloned(),
738                )
739                .await?;
740
741                by_room_guard.insert(room_id.to_owned(), caches);
742
743                Ok(OwnedRwLockWriteGuard::try_downgrade_map(by_room_guard, |by_room| {
744                    by_room.get(room_id)
745                })
746                .expect("`Caches` has just been inserted"))
747            }
748        }
749    }
750}
751
752/// Indicate where events are coming from.
753#[derive(Debug, Clone)]
754pub enum EventsOrigin {
755    /// Events are coming from a sync.
756    Sync,
757
758    /// Events are coming from pagination.
759    Pagination,
760
761    /// The cause of the change is purely internal to the cache.
762    Cache,
763}
764
765#[cfg(test)]
766mod tests {
767    use std::{ops::Not, sync::Arc, time::Duration};
768
769    use assert_matches::assert_matches;
770    use futures_util::FutureExt as _;
771    use matrix_sdk_base::{
772        RoomState,
773        linked_chunk::{ChunkIdentifier, LinkedChunkId, Position, Update},
774        sync::{JoinedRoomUpdate, RoomUpdates, Timeline},
775    };
776    use matrix_sdk_test::{
777        JoinedRoomBuilder, SyncResponseBuilder, async_test, event_factory::EventFactory,
778    };
779    use ruma::{event_id, room_id, user_id};
780    use tokio::time::sleep;
781
782    use super::{EventCacheError, RoomEventCacheGenericUpdate};
783    use crate::test_utils::{
784        assert_event_matches_msg, client::MockClientBuilder, logged_in_client,
785    };
786
787    #[async_test]
788    async fn test_must_explicitly_subscribe() {
789        let client = logged_in_client(None).await;
790
791        let event_cache = client.event_cache();
792
793        // If I create a room event subscriber for a room before subscribing the event
794        // cache,
795        let room_id = room_id!("!omelette:fromage.fr");
796        let result = event_cache.room(room_id).await;
797
798        // Then it fails, because one must explicitly call `.subscribe()` on the event
799        // cache.
800        assert_matches!(result, Err(EventCacheError::NotSubscribedYet));
801    }
802
803    #[async_test]
804    async fn test_get_event_by_id() {
805        let client = logged_in_client(None).await;
806        let room_id1 = room_id!("!galette:saucisse.bzh");
807        let room_id2 = room_id!("!crepe:saucisse.bzh");
808
809        client.base_client().get_or_create_room(room_id1, RoomState::Joined);
810        client.base_client().get_or_create_room(room_id2, RoomState::Joined);
811
812        let event_cache = client.event_cache();
813        event_cache.subscribe().unwrap();
814
815        // Insert two rooms with a few events.
816        let f = EventFactory::new().room(room_id1).sender(user_id!("@ben:saucisse.bzh"));
817
818        let eid1 = event_id!("$1");
819        let eid2 = event_id!("$2");
820        let eid3 = event_id!("$3");
821
822        let joined_room_update1 = JoinedRoomUpdate {
823            timeline: Timeline {
824                events: vec![
825                    f.text_msg("hey").event_id(eid1).into(),
826                    f.text_msg("you").event_id(eid2).into(),
827                ],
828                ..Default::default()
829            },
830            ..Default::default()
831        };
832
833        let joined_room_update2 = JoinedRoomUpdate {
834            timeline: Timeline {
835                events: vec![f.text_msg("bjr").event_id(eid3).into()],
836                ..Default::default()
837            },
838            ..Default::default()
839        };
840
841        let mut updates = RoomUpdates::default();
842        updates.joined.insert(room_id1.to_owned(), joined_room_update1);
843        updates.joined.insert(room_id2.to_owned(), joined_room_update2);
844
845        // Have the event cache handle them.
846        event_cache.inner.handle_room_updates(updates).await.unwrap();
847
848        // We can find the events in a single room.
849        let room1 = client.get_room(room_id1).unwrap();
850
851        let (room_event_cache, _drop_handles) = room1.event_cache().await.unwrap();
852
853        let found1 = room_event_cache.find_event(eid1).await.unwrap().unwrap();
854        assert_event_matches_msg(&found1, "hey");
855
856        let found2 = room_event_cache.find_event(eid2).await.unwrap().unwrap();
857        assert_event_matches_msg(&found2, "you");
858
859        // Retrieving the event with id3 from the room which doesn't contain it will
860        // fail…
861        assert!(room_event_cache.find_event(eid3).await.unwrap().is_none());
862    }
863
864    #[async_test]
865    async fn test_generic_update_when_loading_rooms() {
866        // Create 2 rooms. One of them has data in the event cache storage.
867        let user = user_id!("@mnt_io:matrix.org");
868        let client = logged_in_client(None).await;
869        let room_id_0 = room_id!("!raclette:patate.ch");
870        let room_id_1 = room_id!("!fondue:patate.ch");
871
872        let event_factory = EventFactory::new().room(room_id_0).sender(user);
873
874        let event_cache = client.event_cache();
875        event_cache.subscribe().unwrap();
876
877        client.base_client().get_or_create_room(room_id_0, RoomState::Joined);
878        client.base_client().get_or_create_room(room_id_1, RoomState::Joined);
879
880        client
881            .event_cache_store()
882            .lock()
883            .await
884            .expect("Could not acquire the event cache lock")
885            .as_clean()
886            .expect("Could not acquire a clean event cache lock")
887            .handle_linked_chunk_updates(
888                LinkedChunkId::Room(room_id_0),
889                vec![
890                    // Non-empty items chunk.
891                    Update::NewItemsChunk {
892                        previous: None,
893                        new: ChunkIdentifier::new(0),
894                        next: None,
895                    },
896                    Update::PushItems {
897                        at: Position::new(ChunkIdentifier::new(0), 0),
898                        items: vec![
899                            event_factory
900                                .text_msg("hello")
901                                .sender(user)
902                                .event_id(event_id!("$ev0"))
903                                .into_event(),
904                        ],
905                    },
906                ],
907            )
908            .await
909            .unwrap();
910
911        let mut generic_stream = event_cache.subscribe_to_room_generic_updates();
912
913        // Room 0 has initial data, so it must trigger a generic update.
914        {
915            let _room_event_cache = event_cache.room(room_id_0).await.unwrap();
916
917            assert_matches!(
918                generic_stream.recv().await,
919                Ok(RoomEventCacheGenericUpdate { room_id }) => {
920                    assert_eq!(room_id, room_id_0);
921                }
922            );
923        }
924
925        // Room 1 has NO initial data, so nothing should happen.
926        {
927            let _room_event_cache = event_cache.room(room_id_1).await.unwrap();
928
929            assert!(generic_stream.recv().now_or_never().is_none());
930        }
931    }
932
933    #[async_test]
934    async fn test_generic_update_when_paginating_room() {
935        // Create 1 room, with 4 chunks in the event cache storage.
936        let user = user_id!("@mnt_io:matrix.org");
937        let client = logged_in_client(None).await;
938        let room_id = room_id!("!raclette:patate.ch");
939
940        let event_factory = EventFactory::new().room(room_id).sender(user);
941
942        let event_cache = client.event_cache();
943        event_cache.subscribe().unwrap();
944
945        client.base_client().get_or_create_room(room_id, RoomState::Joined);
946
947        client
948            .event_cache_store()
949            .lock()
950            .await
951            .expect("Could not acquire the event cache lock")
952            .as_clean()
953            .expect("Could not acquire a clean event cache lock")
954            .handle_linked_chunk_updates(
955                LinkedChunkId::Room(room_id),
956                vec![
957                    // Empty chunk.
958                    Update::NewItemsChunk {
959                        previous: None,
960                        new: ChunkIdentifier::new(0),
961                        next: None,
962                    },
963                    // Empty chunk.
964                    Update::NewItemsChunk {
965                        previous: Some(ChunkIdentifier::new(0)),
966                        new: ChunkIdentifier::new(1),
967                        next: None,
968                    },
969                    // Non-empty items chunk.
970                    Update::NewItemsChunk {
971                        previous: Some(ChunkIdentifier::new(1)),
972                        new: ChunkIdentifier::new(2),
973                        next: None,
974                    },
975                    Update::PushItems {
976                        at: Position::new(ChunkIdentifier::new(2), 0),
977                        items: vec![
978                            event_factory
979                                .text_msg("hello")
980                                .sender(user)
981                                .event_id(event_id!("$ev0"))
982                                .into_event(),
983                        ],
984                    },
985                    // Non-empty items chunk.
986                    Update::NewItemsChunk {
987                        previous: Some(ChunkIdentifier::new(2)),
988                        new: ChunkIdentifier::new(3),
989                        next: None,
990                    },
991                    Update::PushItems {
992                        at: Position::new(ChunkIdentifier::new(3), 0),
993                        items: vec![
994                            event_factory
995                                .text_msg("world")
996                                .sender(user)
997                                .event_id(event_id!("$ev1"))
998                                .into_event(),
999                        ],
1000                    },
1001                ],
1002            )
1003            .await
1004            .unwrap();
1005
1006        let mut generic_stream = event_cache.subscribe_to_room_generic_updates();
1007
1008        // Room is initialised, it gets one event in the timeline.
1009        let (room_event_cache, _) = event_cache.room(room_id).await.unwrap();
1010
1011        assert_matches!(
1012            generic_stream.recv().await,
1013            Ok(RoomEventCacheGenericUpdate { room_id: expected_room_id }) => {
1014                assert_eq!(room_id, expected_room_id);
1015            }
1016        );
1017
1018        let pagination = room_event_cache.pagination();
1019
1020        // Paginate, it gets one new event in the timeline.
1021        let pagination_outcome = pagination.run_backwards_once(1).await.unwrap();
1022
1023        assert_eq!(pagination_outcome.events.len(), 1);
1024        assert!(pagination_outcome.reached_start.not());
1025        assert_matches!(
1026            generic_stream.recv().await,
1027            Ok(RoomEventCacheGenericUpdate { room_id: expected_room_id }) => {
1028                assert_eq!(room_id, expected_room_id);
1029            }
1030        );
1031
1032        // Paginate, it gets zero new event in the timeline.
1033        let pagination_outcome = pagination.run_backwards_once(1).await.unwrap();
1034
1035        assert!(pagination_outcome.events.is_empty());
1036        assert!(pagination_outcome.reached_start.not());
1037        assert!(generic_stream.recv().now_or_never().is_none());
1038
1039        // Paginate once more. Just checking our scenario is correct.
1040        let pagination_outcome = pagination.run_backwards_once(1).await.unwrap();
1041
1042        assert!(pagination_outcome.reached_start);
1043        assert!(generic_stream.recv().now_or_never().is_none());
1044    }
1045
1046    #[async_test]
1047    async fn test_for_room_when_room_is_not_found() {
1048        let client = logged_in_client(None).await;
1049        let room_id = room_id!("!raclette:patate.ch");
1050
1051        let event_cache = client.event_cache();
1052        event_cache.subscribe().unwrap();
1053
1054        // Room doesn't exist. It returns an error.
1055        assert_matches!(
1056            event_cache.room(room_id).await,
1057            Err(EventCacheError::RoomNotFound { room_id: not_found_room_id }) => {
1058                assert_eq!(room_id, not_found_room_id);
1059            }
1060        );
1061
1062        // Now create the room.
1063        client.base_client().get_or_create_room(room_id, RoomState::Joined);
1064
1065        // Room exists. Everything fine.
1066        assert!(event_cache.room(room_id).await.is_ok());
1067    }
1068
1069    /// Test that the event cache does not create reference cycles or tasks that
1070    /// retain its reference indefinitely, preventing it from being deallocated.
1071    #[cfg(not(target_family = "wasm"))]
1072    #[async_test]
1073    async fn test_no_refcycle_event_cache_tasks() {
1074        let client = MockClientBuilder::new(None).build().await;
1075
1076        // Wait for the init tasks to die.
1077        sleep(Duration::from_secs(1)).await;
1078
1079        let event_cache_weak = Arc::downgrade(&client.event_cache().inner);
1080        assert_eq!(event_cache_weak.strong_count(), 1);
1081
1082        {
1083            let room_id = room_id!("!room:example.org");
1084
1085            // Have the client know the room.
1086            let response = SyncResponseBuilder::default()
1087                .add_joined_room(JoinedRoomBuilder::new(room_id))
1088                .build_sync_response();
1089            client.inner.base_client.receive_sync_response(response).await.unwrap();
1090
1091            client.event_cache().subscribe().unwrap();
1092
1093            let (_room_event_cache, _drop_handles) =
1094                client.get_room(room_id).unwrap().event_cache().await.unwrap();
1095        }
1096
1097        drop(client);
1098
1099        // Give a bit of time for background tasks to die.
1100        sleep(Duration::from_secs(1)).await;
1101
1102        // No strong counts should exist now that the Client has been dropped.
1103        assert_eq!(
1104            event_cache_weak.strong_count(),
1105            0,
1106            "Too many strong references to the event cache {}",
1107            event_cache_weak.strong_count()
1108        );
1109    }
1110}