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