Skip to main content

matrix_sdk/event_cache/states/
mod.rs

1// Copyright 2026 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//! This module handles the state of the [`EventCache`].
16
17use std::{
18    collections::HashMap,
19    fmt,
20    ops::{Deref, DerefMut},
21    sync::Arc,
22};
23
24use matrix_sdk_base::{
25    event_cache::store::{EventCacheStoreLock, EventCacheStoreLockGuard, EventCacheStoreLockState},
26    timer,
27    tracing_timer::TracingTimer,
28};
29use ruma::{OwnedEventId, OwnedRoomId, RoomId};
30use tokio::sync::{Mutex, RwLock, RwLockMappedWriteGuard, RwLockReadGuard, RwLockWriteGuard};
31use tracing::{instrument, trace};
32
33use super::{
34    CachesByRoom, EventCacheError, EventsOrigin, Result,
35    caches::{
36        TimelineVectorDiffs,
37        event_focused::{EventFocusedCacheKey, EventFocusedCacheState},
38        pinned_events::PinnedEventsCacheState,
39        room::{self, RoomEventCacheState},
40        thread::{self, ThreadEventCacheState},
41    },
42};
43
44pub(in super::super) mod selectors;
45
46/// The type containing all the states, for real.
47pub struct State {
48    store: EventCacheStoreLock,
49    by_room: HashMap<OwnedRoomId, StateForRoom>,
50}
51
52#[derive(Default)]
53pub(super) struct StateForRoom {
54    room: Option<RoomEventCacheState>,
55    threads: HashMap<OwnedEventId, ThreadEventCacheState>,
56    pinned_events: Option<PinnedEventsCacheState>,
57    event_focused: HashMap<EventFocusedCacheKey, EventFocusedCacheState>,
58}
59
60/// State for the entire Event Cache.
61///
62/// This aims at containing all the inner mutable states that ought to be
63/// updated, behind a per-process lock and a cross-process lock.
64///
65/// This type can be cloned at low-cost. It will do a shallow clone.
66#[derive(Clone)]
67pub struct StateLock {
68    inner: Arc<StateLockInner>,
69}
70
71struct StateLockInner {
72    /// The per-process lock around the real state.
73    locked_state: RwLock<State>,
74
75    /// A lock taken to avoid multiple attempts to upgrade from a read lock
76    /// to a write lock.
77    ///
78    /// Please see inline comment of [`Self::read`] to understand why it
79    /// exists.
80    state_lock_upgrade_mutex: Mutex<()>,
81}
82
83impl StateLock {
84    /// Construct a new [`EventCacheStateLock`].
85    pub fn new(store: EventCacheStoreLock) -> Self {
86        Self {
87            inner: Arc::new(StateLockInner {
88                locked_state: RwLock::new(State { store, by_room: HashMap::new() }),
89                state_lock_upgrade_mutex: Mutex::new(()),
90            }),
91        }
92    }
93
94    /// Lock this [`StateLock`] with per-thread shared access.
95    ///
96    /// This method locks the per-thread lock over the state, and then locks
97    /// the cross-process lock over the store. It returns an RAII guard
98    /// which will drop the read access to the state and to the store when
99    /// dropped.
100    ///
101    /// If the cross-process lock over the store is dirty (see
102    /// [`EventCacheStoreLockState`]), the state is reloaded.
103    #[instrument(skip_all)]
104    pub(super) async fn read<'state>(&'state self) -> Result<StateLockReadGuard<'state, State>> {
105        trace!("Acquiring the lock");
106        let tracing_timer = timer!("`read` lock");
107
108        // Only one call at a time to `read` is allowed.
109        //
110        // Why? Because in case the cross-process lock over the store is dirty, we need
111        // to upgrade the read lock over the state to a write lock.
112        //
113        // ## Upgradable read lock
114        //
115        // One may argue that this upgrades can be done with an _upgradable read lock_
116        // [^1] [^2]. We don't want to use this solution: an upgradable read lock is
117        // basically a mutex because we are losing the shared access property, i.e.
118        // having multiple read locks at the same time. This is an important property to
119        // hold for performance concerns.
120        //
121        // ## Downgradable write lock
122        //
123        // One may also argue we could first obtain a write lock over the state from the
124        // beginning, thus removing the need to upgrade the read lock to a write lock.
125        // The write lock is then downgraded to a read lock once the dirty is cleaned
126        // up. It can potentially create a deadlock in the following situation:
127        //
128        // - `read` is called once, it takes a write lock, then downgrades it to a read
129        //   lock: the guard is kept alive somewhere,
130        // - `read` is called again, and waits to obtain the write lock, which is
131        //   impossible as long as the guard from the previous call is not dropped.
132        //
133        // ## “Atomic” read and write
134        //
135        // One may finally argue to first obtain a read lock over the state, then drop
136        // it if the cross-process lock over the store is dirty, and immediately obtain
137        // a write lock (which can later be downgraded to a read lock). The problem is
138        // that this write lock is async: anything can happen between the drop and the
139        // new lock acquisition, and it's not possible to pause the runtime in the
140        // meantime.
141        //
142        // ## Semaphore with 1 permit, aka a Mutex
143        //
144        // The chosen idea is to allow only one execution at a time of this method: it
145        // becomes a critical section. That way we are free to “upgrade” the read lock
146        // by dropping it and obtaining a new write lock. All callers to this method are
147        // waiting, so nothing can happen in the meantime.
148        //
149        // Note that it doesn't conflict with the `write` method because this latter
150        // immediately obtains a write lock, which avoids any conflict with this method.
151        //
152        // [^1]: https://docs.rs/lock_api/0.4.14/lock_api/struct.RwLock.html#method.upgradable_read
153        // [^2]: https://docs.rs/async-lock/3.4.1/async_lock/struct.RwLock.html#method.upgradable_read
154        let _state_lock_upgrade_guard = self.inner.state_lock_upgrade_mutex.lock().await;
155
156        // Obtain a read lock.
157        let state_guard = self.inner.locked_state.read().await;
158
159        Ok(match state_guard.store.lock().await? {
160            EventCacheStoreLockState::Clean(store_guard) => {
161                trace!("Lock acquired (from clean)");
162
163                StateLockReadGuard {
164                    state: StateLockReadGuardKind::Owned(state_guard),
165                    store: store_guard,
166                    tracing_timer: Some(tracing_timer),
167                }
168            }
169            EventCacheStoreLockState::Dirty(store_guard) => {
170                // Drop the read lock, and take a write lock to modify the state.
171                // This is safe because only one reader at a time (see
172                // `Self::state_lock_upgrade_mutex`) is allowed.
173                drop(state_guard);
174
175                let mut guard = ReloadableStateLockWriteGuard {
176                    state: self.inner.locked_state.write().await,
177                    store: store_guard,
178                    tracing_timer,
179                };
180
181                // Reload the state.
182                guard.reload(ReloadPreprocessing::None).await?;
183
184                // All good now, mark the cross-process lock as non-dirty.
185                EventCacheStoreLockGuard::clear_dirty(&guard.store);
186
187                trace!("Lock acquired (from dirty)");
188
189                // Downgrade the write guard to a read guard, and map it into a cache state.
190                guard.downgrade()
191            }
192        })
193    }
194
195    /// Lock this [`StateLock`] with exclusive per-thread write access.
196    ///
197    /// This method locks the per-thread lock over the state, and then locks
198    /// the cross-process lock over the store. It returns an RAII guard
199    /// which will drop the write access to the state and to the store when
200    /// dropped.
201    ///
202    /// If the cross-process lock over the store is dirty (see
203    /// [`EventCacheStoreLockState`]), the state is reloaded automatically.
204    #[instrument(skip_all)]
205    async fn write<'state>(&'state self) -> Result<ReloadableStateLockWriteGuard<'state>> {
206        trace!("Acquiring lock");
207        let tracing_timer = timer!("`write` lock");
208
209        let state_guard = self.inner.locked_state.write().await;
210
211        Ok(match state_guard.store.lock().await? {
212            EventCacheStoreLockState::Clean(store_guard) => {
213                trace!("Lock acquired (from clean)");
214
215                ReloadableStateLockWriteGuard {
216                    state: state_guard,
217                    store: store_guard,
218                    tracing_timer,
219                }
220            }
221            EventCacheStoreLockState::Dirty(store_guard) => {
222                let mut guard = ReloadableStateLockWriteGuard {
223                    state: state_guard,
224                    store: store_guard,
225                    tracing_timer,
226                };
227
228                // Reload the state.
229                guard.reload(ReloadPreprocessing::None).await?;
230
231                // All good now, mark the cross-process lock as non-dirty.
232                EventCacheStoreLockGuard::clear_dirty(&guard.store);
233
234                trace!("Lock acquired (from dirty)");
235
236                guard
237            }
238        })
239    }
240
241    /// Clear and reload all states —in-memory and in-store— for all rooms if
242    /// `room` is `None`, otherwise for a single room.
243    ///
244    /// The `caches_for_all_rooms_exclusive_lock_guard` argument ensures an
245    /// exclusive lock over all the caches has been acquired. This is required
246    /// to ensure safety for this method.
247    #[instrument(skip_all)]
248    pub(super) async fn clear_and_reload(
249        &self,
250        _caches_for_all_rooms_exclusive_lock_guard: &RwLockWriteGuard<'_, CachesByRoom>,
251        room_id: Option<&RoomId>,
252    ) -> Result<()> {
253        let tracing_timer = timer!("`clear_and_reload` lock");
254
255        let state_guard = self.inner.locked_state.write().await;
256
257        let mut guard = match state_guard.store.lock().await? {
258            EventCacheStoreLockState::Clean(store_guard)
259            | EventCacheStoreLockState::Dirty(store_guard) => ReloadableStateLockWriteGuard {
260                state: state_guard,
261                store: store_guard,
262                tracing_timer,
263            },
264        };
265
266        // Clear all the events.
267        guard.store.clear_all_events(room_id).await?;
268
269        // At this point, all the in-memory `LinkedChunk`s are desynchronised
270        // from the storage. Resynchronise them manually by reloading them.
271        guard.reload(ReloadPreprocessing::ForgetAll).await?;
272
273        if EventCacheStoreLockGuard::is_dirty(&guard.store) {
274            // All good because the state has been reloaded, mark the
275            // cross-process lock as non-dirty.
276            EventCacheStoreLockGuard::clear_dirty(&guard.store);
277        }
278
279        Ok(())
280    }
281
282    /// Insert a new cache state at location `cache_state_selector` if none
283    /// exists.
284    ///
285    /// This method calls [`Self::write`] to acquire an exclusive access to the
286    /// [`State`] in order to insert the cache.
287    #[instrument(skip_all)]
288    pub(super) async fn try_insert_once_with<Selector, Constructor>(
289        &self,
290        cache_state_selector: Selector,
291        cache_constructor: Constructor,
292    ) -> Result<CacheStateLock<Selector>>
293    where
294        Selector: selectors::CacheState,
295        Constructor: AsyncFnOnce(EventCacheStoreLockGuard) -> Result<Selector::Item>,
296    {
297        let mut state = self.write().await?;
298        let cache_state = cache_constructor(state.store).await?;
299
300        cache_state_selector
301            .insert_once(&mut state.state, cache_state)
302            .then(|| CacheStateLock::new(cache_state_selector, self.clone()))
303            .ok_or_else(|| EventCacheError::CacheStateAlreadyExists)
304    }
305}
306
307impl fmt::Debug for StateLock {
308    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
309        formatter.debug_struct("StateLock").finish_non_exhaustive()
310    }
311}
312
313/// The read lock guard returned by [`StateLock::read`].
314pub struct StateLockReadGuard<'state, S> {
315    /// The per-thread read lock guard over the state `S`.
316    pub state: StateLockReadGuardKind<'state, S>,
317
318    /// The cross-process lock guard over the store.
319    pub store: EventCacheStoreLockGuard,
320
321    /// The [`timer!`] value, used to compute the time the lock is live.
322    tracing_timer: Option<TracingTimer>,
323}
324
325impl<'state> StateLockReadGuard<'state, State> {
326    /// Try to map this read lock guard over a [`State`] to over a
327    /// [`selectors::CacheState::Item`].
328    ///
329    /// In other words, it returns a subset of the state, selected by
330    /// `cache_state_selector`.
331    fn try_map_into_cache_state<'selector, Selector>(
332        self,
333        cache_state_selector: &'selector Selector,
334    ) -> Result<StateLockReadGuard<'state, Selector::Item>>
335    where
336        Selector: selectors::CacheState,
337        EventCacheError: From<&'selector Selector>,
338    {
339        Ok(StateLockReadGuard {
340            state: match self.state {
341                StateLockReadGuardKind::Reference(state) => StateLockReadGuardKind::Reference(
342                    cache_state_selector
343                        .select(state)
344                        .ok_or_else(|| EventCacheError::from(cache_state_selector))?,
345                ),
346
347                StateLockReadGuardKind::Owned(state) => StateLockReadGuardKind::Owned(
348                    RwLockReadGuard::try_map(state, |state| cache_state_selector.select(state))
349                        .map_err(|_| EventCacheError::from(cache_state_selector))?,
350                ),
351            },
352            store: self.store,
353            tracing_timer: self.tracing_timer,
354        })
355    }
356}
357
358impl<'state> StateLockReadGuard<'state, StateForRoom> {
359    /// Project the current read lock guard onto the room cache state.
360    pub(super) fn room(&'state self) -> Option<StateLockReadGuard<'state, RoomEventCacheState>> {
361        self.state.room.as_ref().map(|room| StateLockReadGuard {
362            state: StateLockReadGuardKind::Reference(room),
363            store: self.store.clone(),
364            tracing_timer: None,
365        })
366    }
367
368    /// Project the current read lock guard onto all thread cache states.
369    pub(super) fn threads(
370        &'state self,
371    ) -> StateLockReadGuard<'state, HashMap<OwnedEventId, ThreadEventCacheState>> {
372        StateLockReadGuard {
373            state: StateLockReadGuardKind::Reference(&self.state.threads),
374            store: self.store.clone(),
375            tracing_timer: None,
376        }
377    }
378}
379
380impl<'state> StateLockReadGuard<'state, HashMap<OwnedEventId, ThreadEventCacheState>> {
381    /// Project the current read lock guard onto all thread cache states via an
382    /// iterator.
383    pub(super) fn values(
384        &'state self,
385    ) -> impl Iterator<Item = StateLockReadGuard<'state, ThreadEventCacheState>> {
386        self.state.values().map(|item| StateLockReadGuard {
387            state: StateLockReadGuardKind::Reference(item),
388            store: self.store.clone(),
389            tracing_timer: None,
390        })
391    }
392}
393
394impl<'state, S> Deref for StateLockReadGuard<'state, S> {
395    type Target = S;
396
397    fn deref(&self) -> &Self::Target {
398        &self.state
399    }
400}
401
402/// The kind of guard [`StateLockReadGuard`] owns.
403pub enum StateLockReadGuardKind<'state, S> {
404    /// A read lock over the state is acquired, and this is a reference to a
405    /// cache (sub-)state.
406    ///
407    /// This is useful if one needs to run operations over multiple cache
408    /// (sub-)states without mapping the read lock guard over the state
409    /// (because it would consume it).
410    Reference(&'state S),
411
412    /// The read lock over the state `S` is acquired, and this is a mapped
413    /// guard to a cache (sub-)state.
414    Owned(RwLockReadGuard<'state, S>),
415}
416
417impl<'state, S> Deref for StateLockReadGuardKind<'state, S> {
418    type Target = S;
419
420    fn deref(&self) -> &Self::Target {
421        match self {
422            Self::Reference(state) => state,
423            Self::Owned(state) => state.deref(),
424        }
425    }
426}
427
428/// Private type to hold a “reloadable” write lock guard around the state and
429/// the store.
430///
431/// This type aims at being transient: either it maps to a
432/// [`StateLockReadGuard`] with [`Self::downgrade`], or it maps to a
433/// [`StateLockWriteGuard`] with [`Self::try_map_into_cache_state`]. Its main
434/// goal remains to provide the [`Self::reload`] method to reload all the state
435/// of the Event Cache.
436struct ReloadableStateLockWriteGuard<'state> {
437    /// The per-thread read lock guard over the state `S`.
438    state: RwLockWriteGuard<'state, State>,
439
440    /// The cross-process lock guard over the store.
441    store: EventCacheStoreLockGuard,
442
443    /// The [`timer!`] value, used to compute the time the lock is live.
444    tracing_timer: TracingTimer,
445}
446
447impl<'state> ReloadableStateLockWriteGuard<'state> {
448    /// Try to map this write lock guard over a [`State`] to over a
449    /// [`selectors::CacheState::Item`].
450    ///
451    /// In other words, it returns a subset of the state, selected by
452    /// `cache_state_selector`.
453    fn try_map_into_cache_state<'selector, Selector>(
454        self,
455        cache_state_selector: &'selector Selector,
456    ) -> Result<StateLockWriteGuard<'state, Selector::Item>>
457    where
458        Selector: selectors::CacheState,
459        EventCacheError: From<&'selector Selector>,
460    {
461        Ok(StateLockWriteGuard {
462            state: StateLockWriteGuardKind::Owned(
463                RwLockWriteGuard::try_map(self.state, |state| {
464                    cache_state_selector.select_mut(state)
465                })
466                .map_err(|_| EventCacheError::from(cache_state_selector))?,
467            ),
468            store: self.store,
469            _tracing_timer: Some(self.tracing_timer),
470        })
471    }
472
473    /// Synchronously downgrades a write lock into a read lock.
474    ///
475    /// The per-thread/state lock is downgraded atomically, without allowing
476    /// any writers to take exclusive access of the lock in the meantime.
477    ///
478    /// It returns an RAII guard which will drop the read access to the
479    /// state and to the store when dropped.
480    fn downgrade(self) -> StateLockReadGuard<'state, State> {
481        StateLockReadGuard {
482            state: StateLockReadGuardKind::Owned(self.state.downgrade()),
483            store: self.store,
484            tracing_timer: Some(self.tracing_timer),
485        }
486    }
487
488    async fn reload(&mut self, preprocessing: ReloadPreprocessing) -> Result<()> {
489        trace!("Reloading the state");
490
491        // Iterate over all states and reload them.
492        for (room_id, StateForRoom { room, threads, pinned_events, event_focused }) in
493            self.state.by_room.iter_mut()
494        {
495            // Room.
496            if let Some(room_state) = room {
497                let mut room_state = StateLockWriteGuard {
498                    state: StateLockWriteGuardKind::Reference(room_state),
499                    store: self.store.clone(),
500                    _tracing_timer: None,
501                };
502
503                let updates_as_vector_diffs = room_state.reload(preprocessing).await?;
504                room_state.update_sender.send(
505                    room::RoomEventCacheUpdate::UpdateTimelineEvents(TimelineVectorDiffs {
506                        diffs: updates_as_vector_diffs,
507                        origin: EventsOrigin::Cache,
508                    }),
509                    Some(room::RoomEventCacheGenericUpdate { room_id: room_id.clone() }),
510                );
511            }
512
513            // Threads.
514            for thread_state in threads.values_mut() {
515                let mut thread_state = StateLockWriteGuard {
516                    state: StateLockWriteGuardKind::Reference(thread_state),
517                    store: self.store.clone(),
518                    _tracing_timer: None,
519                };
520
521                let updates_as_vector_diffs = thread_state.reload(preprocessing).await?;
522                thread_state.update_sender.send(
523                    thread::ThreadEventCacheUpdate::UpdateTimelineEvents(TimelineVectorDiffs {
524                        diffs: updates_as_vector_diffs,
525                        origin: EventsOrigin::Cache,
526                    }),
527                    Some(room::RoomEventCacheGenericUpdate { room_id: room_id.clone() }),
528                );
529            }
530
531            // Pinned events.
532            if let Some(pinned_events_state) = pinned_events {
533                let mut pinned_events_state = StateLockWriteGuard {
534                    state: StateLockWriteGuardKind::Reference(pinned_events_state),
535                    store: self.store.clone(),
536                    _tracing_timer: None,
537                };
538
539                let updates_as_vector_diffs = pinned_events_state.reload(preprocessing).await?;
540                pinned_events_state.update_sender.send(TimelineVectorDiffs {
541                    diffs: updates_as_vector_diffs,
542                    origin: EventsOrigin::Cache,
543                });
544            }
545
546            // Event-focused.
547            for event_focused_state in event_focused.values_mut() {
548                let mut event_focused_state = StateLockWriteGuard {
549                    state: StateLockWriteGuardKind::Reference(event_focused_state),
550                    store: self.store.clone(),
551                    _tracing_timer: None,
552                };
553
554                let updates_as_vector_diffs = event_focused_state.reload(preprocessing).await?;
555                let _ = event_focused_state.update_sender.send(TimelineVectorDiffs {
556                    diffs: updates_as_vector_diffs,
557                    origin: EventsOrigin::Cache,
558                });
559            }
560        }
561
562        Ok(())
563    }
564}
565
566/// The write lock guard returned by [`StateLock::write`].
567pub struct StateLockWriteGuard<'state, S> {
568    /// The per-thread write lock guard over the state `S`.
569    pub state: StateLockWriteGuardKind<'state, S>,
570
571    /// The cross-process lock guard over the store.
572    pub store: EventCacheStoreLockGuard,
573
574    /// The [`timer!`] value, used to compute the time the lock is live.
575    _tracing_timer: Option<TracingTimer>,
576}
577
578impl<'state, S> Deref for StateLockWriteGuard<'state, S> {
579    type Target = S;
580
581    fn deref(&self) -> &Self::Target {
582        &self.state
583    }
584}
585
586impl<'state, S> DerefMut for StateLockWriteGuard<'state, S> {
587    fn deref_mut(&mut self) -> &mut Self::Target {
588        &mut self.state
589    }
590}
591
592/// The kind of guard [`StateLockWriteGuard`] owns.
593pub enum StateLockWriteGuardKind<'state, S> {
594    /// A write lock over the state is acquired, and this is a reference to a
595    /// cache (sub-)state.
596    ///
597    /// This is useful if one needs to run operations over multiple cache
598    /// (sub-)states without mapping the write lock guard over the state
599    /// (because it would consume it).
600    Reference(&'state mut S),
601
602    /// The write lock over the state `S` is acquired, and this is a mapped
603    /// guard to a cache (sub-)state.
604    Owned(RwLockMappedWriteGuard<'state, S>),
605}
606
607impl<'state, S> Deref for StateLockWriteGuardKind<'state, S> {
608    type Target = S;
609
610    fn deref(&self) -> &Self::Target {
611        match self {
612            Self::Reference(state) => state,
613            Self::Owned(state) => state.deref(),
614        }
615    }
616}
617
618impl<'state, S> DerefMut for StateLockWriteGuardKind<'state, S> {
619    fn deref_mut(&mut self) -> &mut Self::Target {
620        match self {
621            Self::Reference(state) => state,
622            Self::Owned(state) => state.deref_mut(),
623        }
624    }
625}
626
627/// A wrapper around [`State`] with a [`CacheStateSelector`], facilitating the
628/// embedding of these API in a single type.
629pub struct CacheStateLock<Selector> {
630    cache_state_selector: Selector,
631    state_lock: StateLock,
632}
633
634impl<Selector> CacheStateLock<Selector>
635where
636    Selector: selectors::CacheState,
637{
638    pub(super) fn new(cache_state_selector: Selector, state_lock: StateLock) -> Self {
639        Self { cache_state_selector, state_lock }
640    }
641}
642
643// Fallible methods.
644impl<Selector> CacheStateLock<Selector>
645where
646    Selector: selectors::CacheState,
647    EventCacheError: for<'a> From<&'a Selector>,
648{
649    /// Lock this [`CacheStateLock`] by locking the full [`State`] with
650    /// per-thread shared access.
651    ///
652    /// This method locks the per-thread lock over the state, and then locks
653    /// the cross-process lock over the store. It returns an RAII guard
654    /// which will drop the read access to the state and to the store when
655    /// dropped.
656    ///
657    /// If the cross-process lock over the store is dirty (see
658    /// [`EventCacheStoreLockState`]), the state is reloaded.
659    pub async fn read(&self) -> Result<StateLockReadGuard<'_, Selector::Item>> {
660        self.state_lock.read().await?.try_map_into_cache_state(&self.cache_state_selector)
661    }
662
663    /// Lock this [`CacheStateLock`] by locking the full [`State`] with
664    /// exclusive per-thread write access.
665    ///
666    /// This method locks the per-thread lock over the state, and then locks
667    /// the cross-process lock over the store. It returns an RAII guard
668    /// which will drop the write access to the state and to the store when
669    /// dropped.
670    ///
671    /// If the cross-process lock over the store is dirty (see
672    /// [`EventCacheStoreLockState`]), the state is reloaded.
673    pub async fn write(&self) -> Result<StateLockWriteGuard<'_, Selector::Item>> {
674        self.state_lock.write().await?.try_map_into_cache_state(&self.cache_state_selector)
675    }
676
677    /// Shortcut to reload (with no preprocessing) the state cache just for
678    /// test.
679    #[cfg(test)]
680    pub async fn reload_no_preprocessing(&self) -> Result<()> {
681        self.state_lock.write().await?.reload(ReloadPreprocessing::None).await
682    }
683}
684
685/// Kind of pre-processing to do when reloading a cache.
686#[derive(Clone, Copy)]
687pub enum ReloadPreprocessing {
688    /// Erase all events before reloading.
689    ForgetAll,
690
691    /// Do nothing before reloading.
692    None,
693}