You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
I would like to revisit the Event Cache design now it has grown a bit more and has more responsibilities. See the Problems Section to understand the motivation.
Current architecture
Architecture
The current architecture of the Event Cache is the following:
EventCache
contains one RoomEventCache per room
contains all events for the main timeline in an EventLinkedChunk
contains one ThreadEventCache per thread
contains all events for the thread timeline in an EventLinkedChunk
contains one PinnedEventCache for the pinned events in an EventLinkedChunk
Sync
The sync flow is the following (super simplified):
For each updated room, fetch the appropriate RoomEventCache
Push all events in it
Post-process the events
Copy and dispatch the threaded events
Copy and dispatch the pinned events
Pagination
The pagination flow is the following (super simplified):
Use a dedicated method on a RoomEventCache to paginate:
One method for the main timeline
One method for threads
None for pinned events
Push the events in their own buckets
Problems
The major problems with this architecture are the following:
Threaded events and pinned events are first and foremost pushed in the RoomEventCache, before being copied over ThreadEventCache and PinnedEventCache.
It forces us to filter the threaded events from RoomEventCache, and possibly other events in the future
This is a waste of memory and disk usage
There is a non-negligible amount of duplicated code between RoomEventCache, ThreadEventCache and PinnedEventCache. Some are using a very similar read-write lock around their state (RoomEventCache and PinnedEventCache), some are sharing some parts of the pagination mechanism (RoomEventCache and ThreadEventCache).
The code is really confusing
Bugs can be duplicated
Fixing a bug can create a new bug somewhere else
Proposal
Pipelines
I propose to improve the sync flow by introducing pipelines. It basically reverses the post process to a pre process:
Each event is dispatched in its own pipeline
An event can be present in multiple pipelines, e.g. a pinned event must be part of PinnedEventCache, but also either RoomEventCache or ThreadEventCache
It brings nice benefits:
Clear separation of events
Clear knowledge of what we are manipulating where
No more undesired duplicated events
All events at the correct place: until now, edits of threaded events live in the RoomEventCache as far as I understand the code
And… we can remove the dependency between RoomEventCache, and ThreadEventCache and PinnedEventCache. Let me explain.
where RoomEventCaches (plural… well, we might want a better name, maybe RoomEventCacheDispatcher or something? it's a bit long, anyway) would play the role of the dispatcher/pipeliner: it will distribute/dispatch the events in their associated buckets/caches.
This new design would:
Prevent to abuse RoomEventCacheState to contain code related to the main timeline, but also threads, and pinned events. In order terms, it forces to have a clear separation of concerns, making the code robuster and easier to understand.
This isn't mandatory but it helps to introduce new refactorings regarding the read-write lock and pagination. See next sections.
Common read-write lock implementation
RoomEventCacheState is embedded in an RoomEventCacheStateLock, which is nice (introduced in #5856). But now, PinnedEventCacheState also has its own PinnedEventCacheStateLock (introduced in #6085). It's duplicated code for a sensible, delicate logic.
I propose to have a common generic type for the lock.
structStateLock<S>{locked_state:RwLock<S>,state_lock_upgrade_mutex:Mutex<()>,}traitStore{fnstore(&self) -> &EventCacheStoreLock;}impl<S>StateLock<S>whereS:Store{pubasyncfnread(&self) -> Result<StateLockReadGuard<'_>,EventCacheError>{// the usual `read` implementation}pubasyncfnwrite(&self) -> Result<StateLockWriteGuard<'_>,EventCacheError>{// the usual `write` implementation}}pubstructStateLockReadGuard<'a,S>{state:RwLockReadGuard<'a,S>,store:EventCacheStoreLockGuard,}pubstructStateLockWriteGuard<'a,S>{state:RwLockWriteGuard<'a,S>,store:EventCacheStoreLockGuard,}impl<'a,S>StateLockWriteGuard<'a,S>{fndowngrade(self) -> StateLockReadGuard<'a,S>{// the usual `downgrade` implementation}}
If we need more methods on these read and write guards (as it's the case today), we can implement them like so:
room_event_cache.pagination().run_backwards_once() to paginate the main timeline/RoomEventCache over the database and the network,
room_event_cache.paginate_thread_backwards() to paginate over the network only (because there is no storage yet),
nothing for pinned-events (which makes sense for the moment)
The pagination is cut in multiple steps: most of them makes sense but they are combined manually to produce the desired behaviour. For RoomEvenCache, almost everything happens in Pagination, whilst for ThreadEventCache, it all lives in the RoomEventCache::paginate_thread_backwards method (not in Pagination, not in ThreadEventCache, but in RoomEventCache).
The major difference between a pagination for RoomEventCache and ThreadEventCache is the HTTP endpoint: /messages vs. /relations. It's too bad we don't use Pagination for threads because this API is really well-designed.
With the pipelines and the new design, each cache can have its own Pagination type. It will also help to provide a PaginationStatus to all kind of caches (which is missing for ThreadEventCache as we speak). The Pagination type could be improved to take a generic parameter:
The matrix_sdk_ui::timeline::Timeline type will also be happier with a uniform Pagination type, it will simplify the code a little bit more.
Execution
The goods news is that it doesn't require too much work. The tasks can be done in parallel without too much troubles, and they seem to be rather small and self-contained for what I foresee. For example, the work on the read-write lock can be done now without blocking other projects happening on the Event Cache right now.
Blinking Sam (Samuel Johnson)
I would like to revisit the Event Cache design now it has grown a bit more and has more responsibilities. See the Problems Section to understand the motivation.
Current architecture
Architecture
The current architecture of the Event Cache is the following:
EventCacheRoomEventCacheper roomEventLinkedChunkThreadEventCacheper threadEventLinkedChunkPinnedEventCachefor the pinned events in anEventLinkedChunkSync
The sync flow is the following (super simplified):
RoomEventCachePagination
The pagination flow is the following (super simplified):
RoomEventCacheto paginate:Problems
The major problems with this architecture are the following:
RoomEventCache, before being copied overThreadEventCacheandPinnedEventCache.RoomEventCache, and possibly other events in the futureRoomEventCache,ThreadEventCacheandPinnedEventCache. Some are using a very similar read-write lock around their state (RoomEventCacheandPinnedEventCache), some are sharing some parts of the pagination mechanism (RoomEventCacheandThreadEventCache).Proposal
Pipelines
I propose to improve the sync flow by introducing pipelines. It basically reverses the post process to a pre process:
PinnedEventCache, but also eitherRoomEventCacheorThreadEventCacheIt brings nice benefits:
RoomEventCacheas far as I understand the codeRoomEventCache, andThreadEventCacheandPinnedEventCache. Let me explain.I woud like to go from:
graph TD; EventCache-->RoomEventCache; RoomEventCache-->RoomEventCacheState; RoomEventCacheState-->ThreadEventCache; RoomEventCacheState-->PinnedEventCache; RoomEventCacheState-->EventFocusedCache; ThreadEventCache-->ThredEventCacheState; PinnedEventCache-->PinnedEventCacheState; EventFocusedCache-->EventFocusedCacheState;to:
graph TD; EventCache-->RoomEventCaches; RoomEventCaches-->RoomEventCache; RoomEventCaches-->ThreadEventCache; RoomEventCaches-->PinnedEventCache; RoomEventCaches-->EventFocusedCache; RoomEventCache-->RoomEventCacheState; ThreadEventCache-->ThredEventCacheState; PinnedEventCache-->PinnedEventCacheState; EventFocusedCache-->EventFocusedCacheState;where
RoomEventCaches(plural… well, we might want a better name, maybeRoomEventCacheDispatcheror something? it's a bit long, anyway) would play the role of the dispatcher/pipeliner: it will distribute/dispatch the events in their associated buckets/caches.This new design would:
RoomEventCacheStateto contain code related to the main timeline, but also threads, and pinned events. In order terms, it forces to have a clear separation of concerns, making the code robuster and easier to understand.Common read-write lock implementation
RoomEventCacheStateis embedded in anRoomEventCacheStateLock, which is nice (introduced in #5856). But now,PinnedEventCacheStatealso has its ownPinnedEventCacheStateLock(introduced in #6085). It's duplicated code for a sensible, delicate logic.I propose to have a common generic type for the lock.
If we need more methods on these read and write guards (as it's the case today), we can implement them like so:
Common pagination implementation
For the moment, here is what we have:
room_event_cache.pagination().run_backwards_once()to paginate the main timeline/RoomEventCacheover the database and the network,room_event_cache.paginate_thread_backwards()to paginate over the network only (because there is no storage yet),The pagination is cut in multiple steps: most of them makes sense but they are combined manually to produce the desired behaviour. For
RoomEvenCache, almost everything happens inPagination, whilst forThreadEventCache, it all lives in theRoomEventCache::paginate_thread_backwardsmethod (not inPagination, not inThreadEventCache, but inRoomEventCache).The major difference between a pagination for
RoomEventCacheandThreadEventCacheis the HTTP endpoint:/messagesvs./relations. It's too bad we don't usePaginationfor threads because this API is really well-designed.With the pipelines and the new design, each cache can have its own
Paginationtype. It will also help to provide aPaginationStatusto all kind of caches (which is missing forThreadEventCacheas we speak). ThePaginationtype could be improved to take a generic parameter:This way, we can implement the following methods:
RoomEventCache::pagination(&self) -> Pagination<Self>ThreadEventCache::pagination(&self) -> Pagination<Self>The
matrix_sdk_ui::timeline::Timelinetype will also be happier with a uniformPaginationtype, it will simplify the code a little bit more.Execution
The goods news is that it doesn't require too much work. The tasks can be done in parallel without too much troubles, and they seem to be rather small and self-contained for what I foresee. For example, the work on the read-write lock can be done now without blocking other projects happening on the Event Cache right now.
Tasks
event_cache#6216event_cache/caches/room/#6219EventCache::*_taskto newtasksmodule #6222Cachestype #6223ThreadEventCacheState#6280RoomEventCacheStateLockWriteGuard::load_more_events_backwardsinRoomPagination#6288ThreadSummarywhen an in-thread event is decrypted #6619StateLocktype inEventCache#6154SubscribersHandleandSubscriberHandle#6684Paginationtype andPaginatedCachetrait #6174To do after the main tasks:
test_latest_thread_event_is_redecrypted_and_updatedThreadSummarywhen an in-thread event is decrypted #6619