Skip to main content

matrix_sdk/event_cache/
mod.rs

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