Skip to content

feat(event cache): rework the SQL schema #4849

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 17 commits into from
Apr 2, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
398c84e
refactor(event cache): store the events' content independently of the…
bnjbvr Mar 26, 2025
ca258ad
feat(event cache): extract an event's relationship before inserting i…
bnjbvr Mar 26, 2025
d2e3101
refactor(linked chunk): rejigger the relational linked chunk so it ca…
bnjbvr Mar 27, 2025
c88be28
feat(event cache): allow to persist an out-of-band event into storage
bnjbvr Mar 27, 2025
c2f8f20
refactor(event cache): don't return a position in find_event
bnjbvr Mar 27, 2025
389460c
refactor(event cache): move relation extraction into common store hel…
bnjbvr Mar 27, 2025
5bd2af7
feat(event cache): allow retrieving an event and all its relations
bnjbvr Mar 27, 2025
c508595
refactor(event cache): use the store methods to retrieve an event by …
bnjbvr Mar 27, 2025
942fe22
refactor(event cache): get rid of the `AllEventsCache`
bnjbvr Mar 27, 2025
f380f00
feat(event cache): get the transitive closure of related events when …
bnjbvr Mar 31, 2025
5d92f42
refactor(event cache): don't have find_event_with_relations return re…
bnjbvr Mar 31, 2025
4b0a55a
refactor(event cache): don't have find_event_with_relations return re…
bnjbvr Mar 31, 2025
7a84f2f
refactor(event cache): don't return the event itself, in `find_event_…
bnjbvr Apr 1, 2025
5ca0a4f
refactor(event cache): regroup code to compute the filter strings
bnjbvr Apr 1, 2025
ee5fa77
refactor(event cache): have `EventCacheStore::clear_all_rooms_chunks`…
bnjbvr Apr 1, 2025
f19be33
test(event cache): add tests for save_event() and find_event_relations()
bnjbvr Apr 1, 2025
c1cb324
refactor(sqlite): rename gaps to gap_chunks / events_chunks to event_…
bnjbvr Apr 2, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion benchmarks/benches/room_bench.rs
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,14 @@ pub fn load_pinned_events_benchmark(c: &mut Criterion) {
assert_eq!(pinned_event_ids.len(), PINNED_EVENTS_COUNT);

// Reset cache so it always loads the events from the mocked endpoint
client.event_cache().empty_immutable_cache().await;
client
.event_cache_store()
.lock()
.await
.unwrap()
.clear_all_rooms_chunks()
.await
.unwrap();

let timeline = Timeline::builder(&room)
.with_focus(TimelineFocus::PinnedEvents {
Expand Down
179 changes: 168 additions & 11 deletions crates/matrix-sdk-base/src/event_cache/store/integration_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,15 @@ use matrix_sdk_common::{
};
use matrix_sdk_test::{event_factory::EventFactory, ALICE, DEFAULT_TEST_ROOM_ID};
use ruma::{
api::client::media::get_content_thumbnail::v3::Method, events::room::MediaSource, mxc_uri,
push::Action, room_id, uint, RoomId,
api::client::media::get_content_thumbnail::v3::Method,
event_id,
events::{
relation::RelationType,
room::{message::RoomMessageEventContentWithoutRelation, MediaSource},
},
mxc_uri,
push::Action,
room_id, uint, EventId, RoomId,
};

use super::{media::IgnoreMediaRetentionPolicy, DynEventCacheStore};
Expand All @@ -40,6 +47,15 @@ use crate::{
///
/// Keep in sync with [`check_test_event`].
pub fn make_test_event(room_id: &RoomId, content: &str) -> TimelineEvent {
make_test_event_with_event_id(room_id, content, None)
}

/// Same as [`make_test_event`], with an extra event id.
pub fn make_test_event_with_event_id(
room_id: &RoomId,
content: &str,
event_id: Option<&EventId>,
) -> TimelineEvent {
let encryption_info = EncryptionInfo {
sender: (*ALICE).into(),
sender_device: None,
Expand All @@ -51,12 +67,11 @@ pub fn make_test_event(room_id: &RoomId, content: &str) -> TimelineEvent {
session_id: Some("mysessionid9".to_owned()),
};

let event = EventFactory::new()
.text_msg(content)
.room(room_id)
.sender(*ALICE)
.into_raw_timeline()
.cast();
let mut builder = EventFactory::new().text_msg(content).room(room_id).sender(*ALICE);
if let Some(event_id) = event_id {
builder = builder.event_id(event_id);
}
let event = builder.into_raw_timeline().cast();

TimelineEvent {
kind: TimelineEventKind::Decrypted(DecryptedRoomEvent {
Expand Down Expand Up @@ -131,6 +146,12 @@ pub trait EventCacheStoreIntegrationTests {

/// Test that an event can be found or not.
async fn test_find_event(&self);

/// Test that finding event relations works as expected.
async fn test_find_event_relations(&self);

/// Test that saving an event works as expected.
async fn test_save_event(&self);
}

#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
Expand Down Expand Up @@ -854,14 +875,12 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
.unwrap();

// Now let's find the event.
let (position, event) = self
let event = self
.find_event(room_id, event_comte.event_id().unwrap().as_ref())
.await
.expect("failed to query for finding an event")
.expect("failed to find an event");

assert_eq!(position.chunk_identifier(), 0);
assert_eq!(position.index(), 0);
assert_eq!(event.event_id(), event_comte.event_id());

// Now let's try to find an event that exists, but not in the expected room.
Expand All @@ -870,6 +889,130 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
.await
.expect("failed to query for finding an event")
.is_none());

// Clearing the rooms also clears the event's storage.
self.clear_all_rooms_chunks().await.expect("failed to clear all rooms chunks");
assert!(self
.find_event(room_id, event_comte.event_id().unwrap().as_ref())
.await
.expect("failed to query for finding an event")
.is_none());
}

async fn test_find_event_relations(&self) {
let room_id = room_id!("!r0:matrix.org");
let another_room_id = room_id!("!r1:matrix.org");

let f = EventFactory::new().room(room_id).sender(*ALICE);

// Create event and related events for the first room.
let eid1 = event_id!("$event1:matrix.org");
let e1 = f.text_msg("comter").event_id(eid1).into_event();

let edit_eid1 = event_id!("$edit_event1:matrix.org");
let edit_e1 = f
.text_msg("* comté")
.event_id(edit_eid1)
.edit(eid1, RoomMessageEventContentWithoutRelation::text_plain("comté"))
.into_event();

let reaction_eid1 = event_id!("$reaction_event1:matrix.org");
let reaction_e1 = f.reaction(eid1, "👍").event_id(reaction_eid1).into_event();

let eid2 = event_id!("$event2:matrix.org");
let e2 = f.text_msg("galette saucisse").event_id(eid2).into_event();

// Create events for the second room.
let f = f.room(another_room_id);

let eid3 = event_id!("$event3:matrix.org");
let e3 = f.text_msg("gruyère").event_id(eid3).into_event();

let reaction_eid3 = event_id!("$reaction_event3:matrix.org");
let reaction_e3 = f.reaction(eid3, "👍").event_id(reaction_eid3).into_event();

// Save All The Things!
self.save_event(room_id, e1).await.unwrap();
self.save_event(room_id, edit_e1).await.unwrap();
self.save_event(room_id, reaction_e1).await.unwrap();
self.save_event(room_id, e2).await.unwrap();
self.save_event(another_room_id, e3).await.unwrap();
self.save_event(another_room_id, reaction_e3).await.unwrap();

// Finding relations without a filter returns all of them.
let relations = self.find_event_relations(room_id, eid1, None).await.unwrap();
assert_eq!(relations.len(), 2);
assert!(relations.iter().any(|r| r.event_id().as_deref() == Some(edit_eid1)));
assert!(relations.iter().any(|r| r.event_id().as_deref() == Some(reaction_eid1)));

// Finding relations with a filter only returns a subset.
let relations = self
.find_event_relations(room_id, eid1, Some(&[RelationType::Replacement]))
.await
.unwrap();
assert_eq!(relations.len(), 1);
assert_eq!(relations[0].event_id().as_deref(), Some(edit_eid1));

let relations = self
.find_event_relations(
room_id,
eid1,
Some(&[RelationType::Replacement, RelationType::Annotation]),
)
.await
.unwrap();
assert_eq!(relations.len(), 2);
assert!(relations.iter().any(|r| r.event_id().as_deref() == Some(edit_eid1)));
assert!(relations.iter().any(|r| r.event_id().as_deref() == Some(reaction_eid1)));

// We can't find relations using the wrong room.
let relations = self
.find_event_relations(another_room_id, eid1, Some(&[RelationType::Replacement]))
.await
.unwrap();
assert!(relations.is_empty());
}

async fn test_save_event(&self) {
let room_id = room_id!("!r0:matrix.org");
let another_room_id = room_id!("!r1:matrix.org");

let event = |msg: &str| make_test_event(room_id, msg);
let event_comte = event("comté");
let event_gruyere = event("gruyère");

// Add one event in one room.
self.save_event(room_id, event_comte.clone()).await.unwrap();

// Add another event in another room.
self.save_event(another_room_id, event_gruyere.clone()).await.unwrap();

// Events can be found, when searched in their own rooms.
let event = self
.find_event(room_id, event_comte.event_id().unwrap().as_ref())
.await
.expect("failed to query for finding an event")
.expect("failed to find an event");
assert_eq!(event.event_id(), event_comte.event_id());

let event = self
.find_event(another_room_id, event_gruyere.event_id().unwrap().as_ref())
.await
.expect("failed to query for finding an event")
.expect("failed to find an event");
assert_eq!(event.event_id(), event_gruyere.event_id());

// But they won't be returned when searching in the wrong room.
assert!(self
.find_event(another_room_id, event_comte.event_id().unwrap().as_ref())
.await
.expect("failed to query for finding an event")
.is_none());
assert!(self
.find_event(room_id, event_gruyere.event_id().unwrap().as_ref())
.await
.expect("failed to query for finding an event")
.is_none());
}
}

Expand Down Expand Up @@ -974,6 +1117,20 @@ macro_rules! event_cache_store_integration_tests {
get_event_cache_store().await.unwrap().into_event_cache_store();
event_cache_store.test_find_event().await;
}

#[async_test]
async fn test_find_event_relations() {
let event_cache_store =
get_event_cache_store().await.unwrap().into_event_cache_store();
event_cache_store.test_find_event_relations().await;
}

#[async_test]
async fn test_save_event() {
let event_cache_store =
get_event_cache_store().await.unwrap().into_event_cache_store();
event_cache_store.test_save_event().await;
}
}
};
}
Expand Down
62 changes: 56 additions & 6 deletions crates/matrix-sdk-base/src/event_cache/store/memory_store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,14 @@ use matrix_sdk_common::{
store_locks::memory_store_helper::try_take_leased_lock,
};
use ruma::{
events::relation::RelationType,
time::{Instant, SystemTime},
EventId, MxcUri, OwnedEventId, OwnedMxcUri, RoomId,
};
use tracing::error;

use super::{
compute_filters_string, extract_event_relation,
media::{EventCacheStoreMedia, IgnoreMediaRetentionPolicy, MediaRetentionPolicy, MediaService},
EventCacheStore, EventCacheStoreError, Result,
};
Expand All @@ -54,7 +57,7 @@ pub struct MemoryStore {
struct MemoryStoreInner {
media: RingBuffer<MediaContent>,
leases: HashMap<String, (String, Instant)>,
events: RelationalLinkedChunk<Event, Gap>,
events: RelationalLinkedChunk<OwnedEventId, Event, Gap>,
media_retention_policy: Option<MediaRetentionPolicy>,
last_media_cleanup_time: SystemTime,
}
Expand Down Expand Up @@ -206,15 +209,62 @@ impl EventCacheStore for MemoryStore {
&self,
room_id: &RoomId,
event_id: &EventId,
) -> Result<Option<(Position, Event)>, Self::Error> {
) -> Result<Option<Event>, Self::Error> {
let inner = self.inner.read().unwrap();

let event_and_room = inner.events.items().find_map(|(position, event, this_room_id)| {
(room_id == this_room_id && event.event_id()? == event_id)
.then_some((position, event.clone()))
let event = inner.events.items().find_map(|(event, this_room_id)| {
(room_id == this_room_id && event.event_id()? == event_id).then_some(event.clone())
});

Ok(event_and_room)
Ok(event)
}

async fn find_event_relations(
&self,
room_id: &RoomId,
event_id: &EventId,
filters: Option<&[RelationType]>,
) -> Result<Vec<Event>, Self::Error> {
let inner = self.inner.read().unwrap();

let filters = compute_filters_string(filters);

let related_events = inner
.events
.items()
.filter_map(|(event, this_room_id)| {
// Must be in the same room.
if room_id != this_room_id {
return None;
}

// Must have a relation.
let (related_to, rel_type) = extract_event_relation(event.raw())?;

// Must relate to the target item.
if related_to != event_id {
return None;
}

// Must not be filtered out.
if let Some(filters) = &filters {
filters.iter().any(|f| *f == rel_type).then_some(event.clone())
} else {
Some(event.clone())
}
})
.collect();

Ok(related_events)
}

async fn save_event(&self, room_id: &RoomId, event: Event) -> Result<(), Self::Error> {
if event.event_id().is_none() {
error!(%room_id, "Trying to save an event with no ID");
return Ok(());
}
self.inner.write().unwrap().events.save_item(room_id.to_owned(), event);
Ok(())
}

async fn add_media_content(
Expand Down
Loading
Loading