Description
matrix_sdk::EventCache
is a new module that has been introduced with #3058. The idea is to manage all events inside the Rust SDK in a single place. An important user of EventCache
is the matrix_sdk_ui::Timeline
.
EventCache
uses a new data structure to organize all events, LinkedChunk
, since:
- feat(sdk): Event cache experimental store:
LinkedChunk
#3166 - feat(sdk):
EventCache
fully usesRoomEvents
/LinkedChunk
#3230
This issue describes the plan to get a persistent storage for EventCache
, along with the ability to make it reactive.
Persistent storage
In order to create a persistent storage for EventCache
, we need a mechanism that listens to LinkedChunk
updates, and map these updates to database operations (like INSERT
, DELETE
and so on).
We initially went to using a reactive mechanism (like ObservableLinkedChunk
), but like any reactive mechanisms, we need to handle the lag.
Note
What is a lag? When new updates are generated, they are usually accumulated in a buffer. This buffer is drained by subscribers of the observed value. When the buffer is full, which can happen because subscribers lag to consume updates, the buffer is reset.
The lag, in this case, is pretty problematic. In case of a lag, the database will be out of sync —data will be missing—, and there is no easy way to get them again. In case of a lag, we might imagine to reset the database and to rewrite everything again, but not all events are loaded in memory. Alternatively, we may want to recompute all the differences between what is in memory and what is inside the database, but again, it's not an easy problem. Anyway, it implies more guards and more complexity.
Instead, we've decided to define a LinkedChunkListener
trait, used by LinkedChunk
, onto which methods will be called on some particular operations, like insert_chunk
, remove_chunk
, insert_events
and remove_events
, that's probably all we need.
The cons:
- It's a new API to manage,
- It cannot be re-used by something else. It exists for one usage only.
The pros:
- It's immediate: it removes the problem of the lag introduced by the initial reactive approach,
- It's easily unit-testable!
- The API is quite small and clearly not complex at all, it's straighforward.
Tasks
- Create
LinkedChunkListener
: feat(sdk): IntroduceLinkedChunkUpdate
#3281 - Create a persistent storage for
EventCache
; write mode: from memory to persistent storage:- feat: Implement cross-process lock for the
EventCache
#4192 - feat: Implement
EventCacheStoreLock::lock()
with poison error, and::lock_unchecked
#4285 - feat(base): Add
EventCacheStore::handle_linked_chunk_updates
#4299 - feat(base) Implement
RelationalLinkedChunk
#4298 - feat(event cache): handle linked chunk updates and reload in the sqlite backend #4340
- feat: Implement cross-process lock for the
- Create a
LinkedChunk
from the storage; read mode: from persistent storage to memory: - Disable the persistent storage for
EventCache
.
Reactive EventCache
Right now, there is no satisfying way to get a stream of EventCache
updates. The only mechanism that exists so far is RoomEventCache::subscribe
. It returns a tokio::sync::mpsc::Receiver<RoomEventCacheUpdate>
. RoomEventCacheUpdate
is defined like so:
matrix-rust-sdk/crates/matrix-sdk/src/event_cache/mod.rs
Lines 816 to 838 in ac0bc95
Constraint: Prepare for reconciliation
This is OK-ish for the moment (at the time of writing), but it will quickly show limitations, in particular with the reconciliation.
Note
When events are received in different orders —e.g. between /messages
, /context
or /sync
—, it's important to re-order them. We call that reconciliation. It's far, far, faaar from trivial. Actually, there is no solution to this problem, but we will try to make the best heuristics as possible.
Why reconciliation is going to create a problem here? Because it's impossible to represent an update like: “remove item at position
The assiduous reader (oh, hi1) will think: “How does it work with back- or front-pagination?”. Thanks for asking. Let's make a detour.
Constraint: Pagination
Frontpagination isn't implemented yet (not something hard, just not here yet). Backpagination is done with RoomEventCache::backpaginate
. It returns a BackPaginationOutcome
, defined like so:
matrix-rust-sdk/crates/matrix-sdk/src/event_cache/mod.rs
Lines 792 to 814 in ac0bc95
Ah. Isn't it using RoomEventCacheUpdate
? Well, no, because of the Timeline
! The API from EventCache
has been extracted from the Timeline
. The Timeline
had and still has 2 sources of updates: /sync
and /messages
for pagination.
Would it be hard to switch to a single Stream<Item = SyncTimelineEvent>
? Well. Yes and no.
- For
/sync
, theTimeline
listens toRoomEventCache::subscribe
:
matrix-rust-sdk/crates/matrix-sdk-ui/src/timeline/builder.rs
Lines 133 to 137 in ac0bc95
- For
/messages
, theTimeline
has a dedicated mechanism that calls/messages
in a loop until$n$ TimelineItem
s are received. This is different of$n$ SyncTimelineItem
, because some events are state events and cannot be displayed to the user:
matrix-rust-sdk/crates/matrix-sdk-ui/src/timeline/pagination.rs
Lines 49 to 51 in ac0bc95
What do we want? A single source of data for Timeline
.
What does it require? Change the backpagination mechanism. Not a big deal, it's doable. The biggest difficulty is that the Timeline
will ask for data that will be injected to another place (Timeline::paginate_backwards
will see the results of RoomEventCache::backpaginate
via RoomEventCache::subscribe
). It's not easy to connect both.
How to do it? Glad you ask.
The solution: Step 1
LinkedChunk
must expose a Stream<Item = Vec<VectorDiff<SyncTimelineEvent>>>
-like API, something like:
impl LinkedChunk {
fn subscribe_as_vector(&self) -> (Vec<SyncTimelineEvent>, impl Stream<Item = Vec<VectorDiff<SyncTimelineEvent>>>) {
todo!()
}
Easy right? Well. No. LinkedChunk
is not a Vector
. The algorithm is going to be fun here. LinkedChunk::subscribe_as_vector
should fake it's a Vector
and should emit VectorDiff
, à la eyeball_im::ObservableVector
.
Of course, RoomEventCache::subscribe
must be rewritten to use LinkedChunk::subscribe_as_vector
.
The solution: Step 2
Timeline
must listen to RoomEventCache::subscribe
but for all updates. Then, Timeline
will map Stream<Item = Vec<VectorDiff<SyncTimelineItem>>>
into TimelineItem
s that will be inserted/deleted/moved in the correct places inside its own ObservableVector<TimelineItem>
inside TimelineInnerState
:
matrix-rust-sdk/crates/matrix-sdk-ui/src/timeline/inner/state.rs
Lines 71 to 75 in ac0bc95
This is going to be delicate.
Tasks
The 2 following lists can be done in parallel:
Tasks on EventCache
:
- Create
LinkedChunk::subscribe_as_vector
:
Tasks on EventCache
and Timeline
:
- Improve/refactor the pagination mechanism of
Timeline
toEventCache
so thatEventCache
is the only place to have pagination logics.Timeline
must entirely depend onEventCache
.
The following list must be done after the 2 previous lists:
- Update
RoomEventCache::subscribe
to useLinkedChunk::subscribe_as_vector
:- What must actually be done is: we update
RoomEventCacheUpdate
to include theVectorDiff
, and theTimeline
must handle theseVectorDiff
itself (there is already aTimelineItemPosition
type, it must be expanded a little bit) - feat(sdk): Improve
RoomEventCacheUpdate
#3471 - feat(sdk): Add
RoomPagination::run_backwards(…, until)
#3472 - feat(common): Implement
LinkedChunk::clear
#4317 - refactor:
RoomEvents::reset
really clears the linked chunk #4321
- What must actually be done is: we update
- Update
Timeline
to handle aStream<Item = Vec<VectorDiff<SyncTimelineItem>>>
.- feat(ui): Propagate
RoomEventCache
'sVectorDiff
toTimeline
#3512 - chore(ui): Remove a useless clone #3525
- chore(ui): Make
TimelineInnerMetadata::next_internal_id
private #3526 - chore(ui): Rewrite a bit
TimelineEventHandler::add
#3527 - fix(ui): Change the behaviour when a duplicated event is received by the
Timeline
#3550 - chore(ui): Rename
TimelineEnd
toTimelineNewItemPosition
#4328 - chore(ui): Clarifies what
TimelineItemPosition::UpdateDecrypted
holds #4330 - chore(ui): Unify the logic for timeline item insertions #4331
- refactor(ui): Introduce the
AllRemoteEvents
type #4370 - refactor(ui): Create a mapping between remote events to timeline items #4377
- feat(ui):
Timeline
consumes updates asVectorDiff
s from theEventCache
#4408
- feat(ui): Propagate
Lazy EventCache
: Combine pagination and persistent storage
Before being able to enable persistent storage in EventCache
, one last problem must be addressed.
RoomEventCache
will use RoomEvents
(which uses LinkedChunk
) to load events from the persistent storage. That's fine. However, we don't want to load all events from the persistent storage. Imagine rooms with 1'000'000 events: do we want to load all the events in memory? Absolutely no! RoomEventCache
must be lazy: it must load only the
OK, but what happens when we backpaginate?
- We must load events from the persistent storage first,
- We must run a proper backpagination (with
/messages
) to see if events aren't missing from the federation that could have missed by/sync
, - We must reconciliate both.
Note
A more detailed plan is being drafted by @bnjbvr and @Hywan to define when running a network request (/messages
) is necessary or not necessary. The heuristic is not trivial, all edge-cases must be well-defined and considered carefully.
Once we have this mechanism in-place, we can enable the persistent storage for EventCache
.
Tasks
-
RoomEventCache
is lazy: load chunks on-demand, -
RoomEventCache
is able to backpaginate from its persistent storage and from/messages
at the same time, - Finally, enable the persistent storage for
EventCache
🎉.
Naive reconciliation
To start with, we can implement a super native reconciliation algorithm that will simply remove duplicated events. For a first shot, it's totally fine. A better reconciliation algorithm can be defined later. For the record, what is present now is the remove duplicated events approach.
What we want to ensure is that the reconciliation algorithm will modify events in LinkedChunk
, which will propagate to the persistent storage via LinkedChunkListener
and to the Timeline
via RoomEventCache::subscribe
. That's one of the main goal of this proposed designed.
Tasks
Conclusion
This plan provides a solution to support a reconciliation mechanism in a single place. It will benefit to other users, like Timeline
. The Timeline
needs to be refactored due to having 2 source of updates, one for /sync
(live events), and one for /messages
(front- and back-paginations of old events).
Because EventCache
will be reactive, it will simplify a lot all the testing aspects:
EventCache
will be easy to test: assert a stream,Timeline
will be super easy to test without having to mock a server will all its possible requests: simply generate a stream that will emit what we want to test exactly, it's a matter of writing the following code for the inputs of theTimeline
:
let inputs = stream! {
yield vec![VectorDiff::Append { values: events![…, …, …] }];
yield vec![VectorDiff::Remove { index: … }];
yield vec![VectorDiff::Insert { index: …, value: … }];
};
- Components will be fully decoupled. Each part will be unit-tested. It will increase the robustness of the
Timeline
.
Bonus
LinkedChunk
uses mutation-based testing and property-based testing:
- Address [meta] Event Cache API 🕸 #3058
- Address [Story] 🦀 Event cache storage element-hq/element-meta#2405
Footnotes
-
Mark? ↩