Commit ef3c294b31cc
tarball9 files changed+578-29
@@ -209,5 +209,9 @@ required-features = ["testing"]
209209
name = "response_commit"210210
required-features = ["testing", "e2e-encryption"]211211
212
+[[test]]213
+name = "history_reseed"214
+required-features = ["testing"]215
+212216
[lints]213217
workspace = true@@ -144,24 +144,31 @@ where
144144
}145145
}146146
147
+ let generation = self.cache.generation();147148
let reset_status_on_drop_guard = ResetStatusOnDrop {148149
prev_status: Some(status_guard.clone()),149
- pagination_status: status_observable.clone(),150
+ cache: self.cache.clone(),151
+ generation,150152
};151153
152154
let this = self.clone();153155
154156
let fut: Pin<Box<dyn SharedPaginationFuture>> = Box::pin(async move {155
- match this.paginate_backwards_impl(batch_size).await? {157
+ match this.paginate_backwards_impl(batch_size, generation).await? {156158
Some(outcome) => {159
+ let mut status = this.cache.status().write();160
+ if this.cache.generation() != generation {161
+ return Ok(None);162
+ }157163
// Back-pagination's over and successful, don't reset the status to the previous158164
// value.159165
reset_status_on_drop_guard.disarm();160166
161167
// Notify subscribers that pagination ended.162
- this.cache.status().set(SharedPaginationStatus::Idle {163
- hit_timeline_start: outcome.reached_start,164
- });168
+ ObservableWriteGuard::set(169
+ &mut status,170
+ SharedPaginationStatus::Idle { hit_timeline_start: outcome.reached_start },171
+ );165172
166173
Ok(Some(outcome))167174
}@@ -207,6 +214,7 @@ where
207214
async fn paginate_backwards_impl(208215
&self,209216
batch_size: u16,217
+ generation: u64,210218
) -> Result<Option<BackPaginationOutcome>> {211219
// A linked chunk might not be entirely loaded (if it's been lazy-loaded). Try212220
// to load from disk/storage first, then from network if disk/storage indicated@@ -250,7 +258,9 @@ where
250258
}251259
252260
// We have a gap, so resolve it with a network back-pagination.253
- return self.paginate_backwards_with_network(batch_size, prev_token).await;261
+ return self262
+ .paginate_backwards_with_network(batch_size, prev_token, generation)263
+ .await;254264
}255265
256266
LoadMoreEventsBackwardsOutcome::StartOfTimeline => {@@ -262,15 +272,15 @@ where
262272
timeline_event_diffs,263273
reached_start,264274
} => {265
- return Ok(Some(266
- self.cache267
- .conclude_backwards_pagination_from_disk(268
- events,269
- timeline_event_diffs,270
- reached_start,271
- )272
- .await,273
- ));275
+ return self276
+ .cache277
+ .conclude_backwards_pagination_from_disk(278
+ events,279
+ timeline_event_diffs,280
+ reached_start,281
+ generation,282
+ )283
+ .await;274284
}275285
}276286
}@@ -285,6 +295,7 @@ where
285295
&self,286296
batch_size: u16,287297
prev_token: Option<String>,298
+ generation: u64,288299
) -> Result<Option<BackPaginationOutcome>> {289300
let Some((events, new_token)) =290301
self.cache.paginate_backwards_with_network(batch_size, &prev_token).await?@@ -296,7 +307,9 @@ where
296307
}));297308
};298309
299
- self.cache.conclude_backwards_pagination_from_network(events, prev_token, new_token).await310
+ self.cache311
+ .conclude_backwards_pagination_from_network(events, prev_token, new_token, generation)312
+ .await300313
}301314
}302315
@@ -340,6 +353,10 @@ pub(in super::super) enum SharedPaginationStatus {
340353
}341354
342355
pub(in super::super) trait PaginatedCache {356
+ fn generation(&self) -> u64 {357
+ 0358
+ }359
+343360
fn status(&self) -> &SharedObservable<SharedPaginationStatus>;344361
345362
fn load_more_events_backwards(@@ -363,13 +380,15 @@ pub(in super::super) trait PaginatedCache {
363380
events: Vec<Event>,364381
timeline_event_diffs: Vec<VectorDiff<Event>>,365382
reached_start: bool,366
- ) -> impl Future<Output = BackPaginationOutcome> + SendOutsideWasm;383
+ generation: u64,384
+ ) -> impl Future<Output = Result<Option<BackPaginationOutcome>>> + SendOutsideWasm;367385
368386
fn conclude_backwards_pagination_from_network(369387
&self,370388
events: Vec<Event>,371389
prev_token: Option<String>,372390
new_token: Option<String>,391
+ generation: u64,373392
) -> impl Future<Output = Result<Option<BackPaginationOutcome>>> + SendOutsideWasm;374393
}375394
@@ -390,22 +409,26 @@ pub enum PaginationStatus {
390409
391410
/// Small RAII guard to reset the pagination status on drop, if not disarmed in392411
/// the meanwhile.393
-struct ResetStatusOnDrop {412
+struct ResetStatusOnDrop<C: PaginatedCache> {394413
prev_status: Option<SharedPaginationStatus>,395
- pagination_status: SharedObservable<SharedPaginationStatus>,414
+ cache: C,415
+ generation: u64,396416
}397417
398
-impl ResetStatusOnDrop {418
+impl<C: PaginatedCache> ResetStatusOnDrop<C> {399419
/// Make the RAII guard have no effect.400420
fn disarm(mut self) {401421
self.prev_status = None;402422
}403423
}404424
405
-impl Drop for ResetStatusOnDrop {425
+impl<C: PaginatedCache> Drop for ResetStatusOnDrop<C> {406426
fn drop(&mut self) {407427
if let Some(status) = self.prev_status.take() {408
- let _ = self.pagination_status.set(status);428
+ let mut guard = self.cache.status().write();429
+ if self.cache.generation() == self.generation {430
+ ObservableWriteGuard::set(&mut guard, status);431
+ }409432
}410433
}411434
}@@ -20,10 +20,13 @@ mod updates;
2020
use std::{2121
collections::BTreeMap,2222
fmt,23
- sync::{Arc, atomic::Ordering},23
+ sync::{24
+ Arc,25
+ atomic::{AtomicU64, Ordering},26
+ },2427
};2528
26
-use eyeball::SharedObservable;29
+use eyeball::{ObservableWriteGuard, SharedObservable};2730
use matrix_sdk_base::{2831
deserialized_responses::AmbiguityChange,2932
event_cache::Event,@@ -336,7 +339,16 @@ impl RoomEventCache {
336339
/// storage.337340
pub async fn clear(&self) -> Result<()> {338341
// Clear the linked chunk and persisted storage.339
- let updates_as_vector_diffs = self.inner.state.write().await?.reset().await?;342
+ let mut state = self.inner.state.write().await?;343
+ let updates_as_vector_diffs = state.reset().await?;344
+ {345
+ let mut status = self.inner.shared_pagination_status.write();346
+ self.inner.generation.fetch_add(1, Ordering::SeqCst);347
+ ObservableWriteGuard::set(348
+ &mut status,349
+ SharedPaginationStatus::Idle { hit_timeline_start: false },350
+ );351
+ }340352
341353
// Notify observers about the update.342354
self.inner.update_sender.send(@@ -350,6 +362,47 @@ impl RoomEventCache {
350362
Ok(())351363
}352364
365
+ #[doc = include_str!("reseed.md")]366
+ pub async fn reseed(&self, events: Vec<Event>, prev_batch: Option<String>) -> Result<()> {367
+ let cache = self.clone();368
+ matrix_sdk_common::executor::spawn(async move {369
+ let mut state = cache.inner.state.write().await?;370
+ state.reseed(events, prev_batch.clone()).await?;371
+ {372
+ let mut status = cache.inner.shared_pagination_status.write();373
+ cache.inner.generation.fetch_add(1, Ordering::SeqCst);374
+ ObservableWriteGuard::set(375
+ &mut status,376
+ SharedPaginationStatus::Idle { hit_timeline_start: prev_batch.is_none() },377
+ );378
+ }379
+ let events =380
+ state.room_linked_chunk().events().map(|(_, event)| event.clone()).collect();381
+ let result = async {382
+ state.clear_loaded_threads().await?;383
+ state384
+ .post_process_new_events(events, PostProcessingOrigin::Backpagination, None)385
+ .await386
+ }387
+ .await;388
+ let items =389
+ state.room_linked_chunk().events().map(|(_, event)| event.clone()).collect();390
+ cache.inner.update_sender.send(391
+ RoomEventCacheUpdate::UpdateTimelineEvents(TimelineVectorDiffs {392
+ diffs: vec![eyeball_im::VectorDiff::Reset { values: items }],393
+ origin: EventsOrigin::Cache,394
+ }),395
+ Some(RoomEventCacheGenericUpdate { room_id: cache.inner.room_id.clone() }),396
+ );397
+ if prev_batch.is_some() {398
+ cache.inner.pagination_batch_token_notifier.notify_waiters();399
+ }400
+ result401
+ })402
+ .await403
+ .expect("joining history reseed failed")404
+ }405
+353406
/// Return a reference to the state.354407
pub(in super::super) fn state(&self) -> &LockedRoomEventCacheState {355408
&self.inner.state@@ -423,6 +476,7 @@ impl RoomEventCache {
423476
424477
/// The (non-cloneable) details of the `RoomEventCache`.425478
pub(super) struct RoomEventCacheInner {479
+ pub generation: AtomicU64,426480
/// The room id for this room.427481
room_id: OwnedRoomId,428482
@@ -458,6 +512,7 @@ impl RoomEventCacheInner {
458512
update_sender: RoomEventCacheUpdateSender,459513
) -> Self {460514
Self {515
+ generation: AtomicU64::new(0),461516
room_id,462517
weak_room,463518
state,@@ -154,6 +154,10 @@ impl RoomPagination {
154154
}155155
156156
impl PaginatedCache for Arc<RoomEventCacheInner> {157
+ fn generation(&self) -> u64 {158
+ self.generation.load(std::sync::atomic::Ordering::SeqCst)159
+ }160
+157161
fn status(&self) -> &SharedObservable<SharedPaginationStatus> {158162
&self.shared_pagination_status159163
}@@ -308,7 +312,12 @@ impl PaginatedCache for Arc<RoomEventCacheInner> {
308312
events: Vec<Event>,309313
timeline_event_diffs: Vec<VectorDiff<Event>>,310314
reached_start: bool,311
- ) -> BackPaginationOutcome {315
+ generation: u64,316
+ ) -> Result<Option<BackPaginationOutcome>> {317
+ let _state = self.state.read().await?;318
+ if self.generation.load(std::sync::atomic::Ordering::SeqCst) != generation {319
+ return Ok(None);320
+ }312321
if !timeline_event_diffs.is_empty() {313322
self.update_sender.send(314323
RoomEventCacheUpdate::UpdateTimelineEvents(TimelineVectorDiffs {@@ -319,12 +328,12 @@ impl PaginatedCache for Arc<RoomEventCacheInner> {
319328
);320329
}321330
322
- BackPaginationOutcome {331
+ Ok(Some(BackPaginationOutcome {323332
reached_start,324333
// This is a backwards pagination. `BackPaginationOutcome` expects events to325334
// be in “reverse order”.326335
events: events.into_iter().rev().collect(),327
- }336
+ }))328337
}329338
330339
async fn conclude_backwards_pagination_from_network(@@ -332,8 +341,12 @@ impl PaginatedCache for Arc<RoomEventCacheInner> {
332341
events: Vec<Event>,333342
prev_token: Option<String>,334343
mut new_token: Option<String>,344
+ generation: u64,335345
) -> Result<Option<BackPaginationOutcome>> {336346
let mut state = self.state.write().await?;347
+ if self.generation.load(std::sync::atomic::Ordering::SeqCst) != generation {348
+ return Ok(None);349
+ }337350
338351
// Check that the previous token still exists; otherwise it's a sign that the339352
// room's timeline has been cleared.@@ -0,0 +1,24 @@
1
+Replace this room's timeline with independently fetched history and its older2
+pagination cursor. The replacement is persisted in one linked chunk update3
+transaction before a timeline reset is published.4
+5
+Pass events in topological order, oldest first. For a backwards6
+[`Room::messages`](crate::Room::messages) response, reverse its `chunk` and pass7
+its `end` as `prev_batch`. Decryption should happen through the normal room8
+messages API before reseeding. Duplicate event IDs within the supplied window9
+are retained once. `None` means the start of accessible history has been reached,10
+including for an empty window. An empty window with a cursor remains pageable.11
+12
+This replaces cached history and invalidates loaded thread timelines. It does13
+not establish continuity with discarded history. Account credentials, encryption14
+state, and pending outgoing messages are not cleared.15
+16
+Once started, the replacement finishes even if the caller drops its future.17
+Failure to persist the replacement leaves the prior room timeline intact.18
+Post-processing can fail after the replacement is installed, in which case the19
+new timeline is still published and the error is returned. Completion means the20
+cache update has been emitted, not that an application mirror has consumed it.21
+22
+Callers must coordinate live sync and requests that fetched this window. Sync23
+updates are serialized against replacement but may append normally afterward.24
+In-flight older pagination is fenced against timeline replacement.@@ -772,6 +772,31 @@ impl<'a> RoomEventCacheStateLockWriteGuard<'a> {
772772
Ok(diff_updates)773773
}774774
775
+ pub async fn reseed(776
+ &mut self,777
+ mut events: Vec<Event>,778
+ prev_batch: Option<String>,779
+ ) -> Result<(), EventCacheError> {780
+ let mut event_ids = HashSet::new();781
+ events.retain(|event| event.event_id().is_none_or(|id| event_ids.insert(id)));782
+ let mut replacement = EventLinkedChunk::new();783
+ replacement.reset();784
+ replacement.push_live_events(prev_batch.clone().map(|token| Gap { token }), &events);785
+ let updates = replacement.store_updates().take();786
+ self.send_updates_to_store(updates).await?;787
+ let _ = replacement.updates_as_vector_diffs();788
+ self.state.room_linked_chunk = replacement;789
+ self.state.waited_for_initial_prev_token = true;790
+ Ok(())791
+ }792
+793
+ pub async fn clear_loaded_threads(&mut self) -> Result<(), EventCacheError> {794
+ for thread in self.state.threads.values_mut() {795
+ thread.clear().await?;796
+ }797
+ Ok(())798
+ }799
+775800
async fn reset_internal(&mut self) -> Result<(), EventCacheError> {776801
self.state.room_linked_chunk.reset();777802
@@ -170,7 +170,8 @@ impl PaginatedCache for ThreadEventCacheWrapper {
170170
_events: Vec<Event>,171171
_timeline_event_diffs: Vec<VectorDiff<Event>>,172172
_reached_start: bool,173
- ) -> BackPaginationOutcome {173
+ _generation: u64,174
+ ) -> Result<Option<BackPaginationOutcome>> {174175
unimplemented!("loading from disk for threads is not implemented yet");175176
}176177
@@ -179,6 +180,7 @@ impl PaginatedCache for ThreadEventCacheWrapper {
179180
mut events: Vec<Event>,180181
prev_token: Option<String>,181182
new_token: Option<String>,183
+ _generation: u64,182184
) -> Result<Option<BackPaginationOutcome>> {183185
let Some(room) = self.cache.weak_room.get() else {184186
// The client is shutting down.@@ -0,0 +1,262 @@
1
+use std::{2
+ sync::{Arc, atomic::Ordering},3
+ time::Duration,4
+};5
+6
+use eyeball_im::VectorDiff;7
+use matrix_sdk::{8
+ config::StoreConfig,9
+ event_cache::{PaginationStatus, RoomEventCacheUpdate},10
+ room::MessagesOptions,11
+ ruma::{event_id, room_id},12
+ test_utils::mocks::{MatrixMockServer, RoomMessagesResponseTemplate},13
+};14
+use matrix_sdk_base::{event_cache::store::EventCacheStore, linked_chunk::LinkedChunkId};15
+use matrix_sdk_common::cross_process_lock::CrossProcessLockConfig;16
+use matrix_sdk_test::{ALICE, event_factory::EventFactory};17
+18
+#[path = "history_reseed/failing_store.rs"]19
+mod failing_store;20
+21
+#[tokio::test]22
+async fn reseed_publishes_one_reset_and_preserves_the_fetched_cursor() {23
+ let server = MatrixMockServer::new().await;24
+ let client = server.client_builder().build().await;25
+ let room_id = room_id!("!reseed:localhost");26
+ client.event_cache().subscribe().unwrap();27
+ server.sync_joined_room(&client, room_id).await;28
+ let room = client.get_room(room_id).unwrap();29
+ let (cache, _handles) = room.event_cache().await.unwrap();30
+ let (_, mut updates) = cache.subscribe().await.unwrap();31
+ let factory = EventFactory::new().room(room_id).sender(&ALICE);32
+ cache33
+ .reseed(vec![factory.text_msg("old").into_event()], Some("old-cursor".into()))34
+ .await35
+ .unwrap();36
+ updates.recv().await.unwrap();37
+ let recent_id = event_id!("$recent:localhost");38
+ let older_id = event_id!("$older:localhost");39
+ server40
+ .mock_room_messages()41
+ .ok(RoomMessagesResponseTemplate::default()42
+ .events(vec![43
+ factory.text_msg("recent").event_id(recent_id),44
+ factory.text_msg("older").event_id(older_id),45
+ factory.text_msg("older").event_id(older_id),46
+ ])47
+ .end_token("fetched-older-cursor"))48
+ .mock_once()49
+ .mount()50
+ .await;51
+ let mut history = room.messages(MessagesOptions::backward()).await.unwrap();52
+ history.chunk.reverse();53
+ cache.reseed(history.chunk, history.end).await.unwrap();54
+ let update =55
+ tokio::time::timeout(Duration::from_secs(2), updates.recv()).await.unwrap().unwrap();56
+ let RoomEventCacheUpdate::UpdateTimelineEvents(update) = update else {57
+ panic!("expected reset")58
+ };59
+ assert_eq!(update.diffs.len(), 1);60
+ let VectorDiff::Reset { values } = &update.diffs[0] else { panic!("expected reset") };61
+ assert_eq!(values.len(), 2);62
+ assert_eq!(values[0].event_id().as_deref(), Some(older_id));63
+ assert_eq!(values[1].event_id().as_deref(), Some(recent_id));64
+ assert!(updates.is_empty());65
+ assert_eq!(cache.events().await.unwrap().len(), 2);66
+ server67
+ .mock_room_messages()68
+ .match_from("fetched-older-cursor")69
+ .ok(RoomMessagesResponseTemplate::default())70
+ .mock_once()71
+ .mount()72
+ .await;73
+ assert!(cache.pagination().run_backwards_once(10).await.unwrap().reached_start);74
+}75
+76
+#[tokio::test]77
+async fn reseed_empty_history_distinguishes_a_cursor_from_the_start_of_history() {78
+ let server = MatrixMockServer::new().await;79
+ let client = server.client_builder().build().await;80
+ let room_id = room_id!("!reseed:localhost");81
+ client.event_cache().subscribe().unwrap();82
+ server.sync_joined_room(&client, room_id).await;83
+ let (cache, _handles) = client.get_room(room_id).unwrap().event_cache().await.unwrap();84
+ cache.reseed(Vec::new(), None).await.unwrap();85
+ assert_eq!(86
+ cache.pagination().status().get(),87
+ PaginationStatus::Idle { hit_timeline_start: true }88
+ );89
+ assert!(cache.pagination().run_backwards_once(10).await.unwrap().reached_start);90
+ cache.reseed(Vec::new(), Some("empty-page-cursor".into())).await.unwrap();91
+ assert_eq!(92
+ cache.pagination().status().get(),93
+ PaginationStatus::Idle { hit_timeline_start: false }94
+ );95
+ server96
+ .mock_room_messages()97
+ .match_from("empty-page-cursor")98
+ .ok(RoomMessagesResponseTemplate::default())99
+ .mock_once()100
+ .mount()101
+ .await;102
+ assert!(cache.pagination().run_backwards_once(10).await.unwrap().reached_start);103
+}104
+105
+#[tokio::test]106
+async fn reseed_fences_inflight_tokenless_pagination() {107
+ let server = MatrixMockServer::new().await;108
+ let client = server.client_builder().build().await;109
+ let room_id = room_id!("!reseed:localhost");110
+ client.event_cache().subscribe().unwrap();111
+ server.sync_joined_room(&client, room_id).await;112
+ let (cache, _handles) = client.get_room(room_id).unwrap().event_cache().await.unwrap();113
+ let factory = EventFactory::new().room(room_id).sender(&ALICE);114
+ let stale = factory.text_msg("stale request");115
+ server116
+ .mock_room_messages()117
+ .ok(RoomMessagesResponseTemplate::default()118
+ .events(vec![stale])119
+ .with_delay(Duration::from_millis(300)))120
+ .mock_once()121
+ .mount()122
+ .await;123
+ let pagination = tokio::spawn({124
+ let cache = cache.clone();125
+ async move { cache.pagination().run_backwards_once(10).await }126
+ });127
+ tokio::time::timeout(Duration::from_secs(8), async {128
+ loop {129
+ if server130
+ .received_requests()131
+ .await132
+ .unwrap()133
+ .iter()134
+ .any(|r| r.url.path().ends_with("/messages"))135
+ {136
+ break;137
+ }138
+ tokio::task::yield_now().await;139
+ }140
+ })141
+ .await142
+ .unwrap();143
+ let recent_id = event_id!("$fresh:localhost");144
+ cache145
+ .reseed(vec![factory.text_msg("fresh").event_id(recent_id).into_event()], None)146
+ .await147
+ .unwrap();148
+ tokio::time::timeout(Duration::from_secs(3), pagination).await.unwrap().unwrap().unwrap();149
+ let events = cache.events().await.unwrap();150
+ assert_eq!(events.len(), 1);151
+ assert_eq!(events[0].event_id().as_deref(), Some(recent_id));152
+ assert_eq!(153
+ cache.pagination().status().get(),154
+ PaginationStatus::Idle { hit_timeline_start: true }155
+ );156
+}157
+158
+#[tokio::test]159
+async fn reseed_preserves_timeline_when_store_write_fails() {160
+ let server = MatrixMockServer::new().await;161
+ let store = Arc::new(failing_store::FailingStore::default());162
+ let client = server163
+ .client_builder()164
+ .on_builder(|builder| {165
+ builder.store_config(166
+ StoreConfig::new(CrossProcessLockConfig::SingleProcess)167
+ .event_cache_store(store.clone()),168
+ )169
+ })170
+ .build()171
+ .await;172
+ let room_id = room_id!("!reseed:localhost");173
+ client.event_cache().subscribe().unwrap();174
+ server.sync_joined_room(&client, room_id).await;175
+ let (cache, _handles) = client.get_room(room_id).unwrap().event_cache().await.unwrap();176
+ let factory = EventFactory::new().room(room_id).sender(&ALICE);177
+ let original_id = event_id!("$original:localhost");178
+ cache179
+ .reseed(180
+ vec![factory.text_msg("original").event_id(original_id).into_event()],181
+ Some("original-cursor".into()),182
+ )183
+ .await184
+ .unwrap();185
+ let (original_events, updates) = cache.subscribe().await.unwrap();186
+ assert_eq!(original_events.len(), 1);187
+ assert_eq!(original_events[0].event_id().as_deref(), Some(original_id));188
+ let original_chunks = store.inner.load_all_chunks(LinkedChunkId::Room(room_id)).await.unwrap();189
+190
+ store.fail_writes.store(true, Ordering::SeqCst);191
+ let result = cache192
+ .reseed(193
+ vec![194
+ factory195
+ .text_msg("replacement")196
+ .event_id(event_id!("$replacement:localhost"))197
+ .into_event(),198
+ ],199
+ Some("replacement-cursor".into()),200
+ )201
+ .await;202
+ store.fail_writes.store(false, Ordering::SeqCst);203
+204
+ assert!(result.is_err());205
+ assert!(updates.is_empty());206
+ let current_events = cache.events().await.unwrap();207
+ assert_eq!(current_events.len(), 1);208
+ assert_eq!(current_events[0].event_id().as_deref(), Some(original_id));209
+ let current_chunks = store.inner.load_all_chunks(LinkedChunkId::Room(room_id)).await.unwrap();210
+ assert_eq!(format!("{current_chunks:?}"), format!("{original_chunks:?}"));211
+}212
+213
+#[cfg(feature = "sqlite")]214
+#[tokio::test]215
+async fn reseed_persists_the_window_and_cursor_across_client_recreation() {216
+ let server = MatrixMockServer::new().await;217
+ let directory = tempfile::tempdir().unwrap();218
+ let room_id = room_id!("!reseed:localhost");219
+ let recent_id = event_id!("$recent:localhost");220
+ {221
+ let client = server222
+ .client_builder()223
+ .on_builder(|b| b.sqlite_store(directory.path(), None))224
+ .build()225
+ .await;226
+ client.event_cache().subscribe().unwrap();227
+ server.sync_joined_room(&client, room_id).await;228
+ let (cache, _handles) = client.get_room(room_id).unwrap().event_cache().await.unwrap();229
+ cache230
+ .reseed(231
+ vec![232
+ EventFactory::new()233
+ .room(room_id)234
+ .sender(&ALICE)235
+ .text_msg("recent")236
+ .event_id(recent_id)237
+ .into_event(),238
+ ],239
+ Some("persisted-cursor".into()),240
+ )241
+ .await242
+ .unwrap();243
+ }244
+ let client = server245
+ .client_builder()246
+ .on_builder(|b| b.sqlite_store(directory.path(), None))247
+ .build()248
+ .await;249
+ client.event_cache().subscribe().unwrap();250
+ let (cache, _handles) = client.get_room(room_id).unwrap().event_cache().await.unwrap();251
+ let (events, _subscriber) = cache.subscribe().await.unwrap();252
+ assert_eq!(events.len(), 1);253
+ assert_eq!(events[0].event_id().as_deref(), Some(recent_id));254
+ server255
+ .mock_room_messages()256
+ .match_from("persisted-cursor")257
+ .ok(RoomMessagesResponseTemplate::default())258
+ .mock_once()259
+ .mount()260
+ .await;261
+ assert!(cache.pagination().run_backwards_once(10).await.unwrap().reached_start);262
+}@@ -0,0 +1,141 @@
1
+use std::sync::atomic::{AtomicBool, Ordering};2
+3
+use async_trait::async_trait;4
+use matrix_sdk_base::{5
+ event_cache::{6
+ Event, Gap,7
+ store::{EventCacheStore, EventCacheStoreError, MemoryStore},8
+ },9
+ linked_chunk::{10
+ ChunkIdentifier, ChunkIdentifierGenerator, ChunkMetadata, LinkedChunkId, Position,11
+ RawChunk, Update,12
+ },13
+};14
+use matrix_sdk_common::cross_process_lock::CrossProcessLockGeneration;15
+use ruma::{EventId, OwnedEventId, RoomId, events::relation::RelationType};16
+17
+#[derive(Debug, Default)]18
+pub struct FailingStore {19
+ pub inner: MemoryStore,20
+ pub fail_writes: AtomicBool,21
+}22
+23
+#[cfg_attr(target_family = "wasm", async_trait(?Send))]24
+#[cfg_attr(not(target_family = "wasm"), async_trait)]25
+impl EventCacheStore for FailingStore {26
+ type Error = EventCacheStoreError;27
+28
+ async fn try_take_leased_lock(29
+ &self,30
+ lease_duration_ms: u32,31
+ key: &str,32
+ holder: &str,33
+ ) -> Result<Option<CrossProcessLockGeneration>, Self::Error> {34
+ self.inner.try_take_leased_lock(lease_duration_ms, key, holder).await35
+ }36
+37
+ async fn handle_linked_chunk_updates(38
+ &self,39
+ linked_chunk_id: LinkedChunkId<'_>,40
+ updates: Vec<Update<Event, Gap>>,41
+ ) -> Result<(), Self::Error> {42
+ if self.fail_writes.load(Ordering::SeqCst) {43
+ return Err(EventCacheStoreError::InvalidData {44
+ details: "injected write failure".into(),45
+ });46
+ }47
+48
+ self.inner.handle_linked_chunk_updates(linked_chunk_id, updates).await49
+ }50
+51
+ async fn remove_room(&self, room_id: &RoomId) -> Result<(), Self::Error> {52
+ self.inner.remove_room(room_id).await53
+ }54
+55
+ async fn load_all_chunks(56
+ &self,57
+ linked_chunk_id: LinkedChunkId<'_>,58
+ ) -> Result<Vec<RawChunk<Event, Gap>>, Self::Error> {59
+ self.inner.load_all_chunks(linked_chunk_id).await60
+ }61
+62
+ async fn load_all_chunks_metadata(63
+ &self,64
+ linked_chunk_id: LinkedChunkId<'_>,65
+ ) -> Result<Vec<ChunkMetadata>, Self::Error> {66
+ self.inner.load_all_chunks_metadata(linked_chunk_id).await67
+ }68
+69
+ async fn load_last_chunk(70
+ &self,71
+ linked_chunk_id: LinkedChunkId<'_>,72
+ ) -> Result<(Option<RawChunk<Event, Gap>>, ChunkIdentifierGenerator), Self::Error> {73
+ self.inner.load_last_chunk(linked_chunk_id).await74
+ }75
+76
+ async fn load_previous_chunk(77
+ &self,78
+ linked_chunk_id: LinkedChunkId<'_>,79
+ before_chunk_identifier: ChunkIdentifier,80
+ ) -> Result<Option<RawChunk<Event, Gap>>, Self::Error> {81
+ self.inner.load_previous_chunk(linked_chunk_id, before_chunk_identifier).await82
+ }83
+84
+ async fn clear_all_linked_chunks(&self) -> Result<(), Self::Error> {85
+ self.inner.clear_all_linked_chunks().await86
+ }87
+88
+ async fn filter_duplicated_events(89
+ &self,90
+ linked_chunk_id: LinkedChunkId<'_>,91
+ events: Vec<OwnedEventId>,92
+ ) -> Result<Vec<(OwnedEventId, Position)>, Self::Error> {93
+ self.inner.filter_duplicated_events(linked_chunk_id, events).await94
+ }95
+96
+ async fn find_event(97
+ &self,98
+ room_id: &RoomId,99
+ event_id: &EventId,100
+ ) -> Result<Option<Event>, Self::Error> {101
+ self.inner.find_event(room_id, event_id).await102
+ }103
+104
+ async fn find_event_relations(105
+ &self,106
+ room_id: &RoomId,107
+ event_id: &EventId,108
+ filter: Option<&[RelationType]>,109
+ ) -> Result<Vec<(Event, Option<Position>)>, Self::Error> {110
+ self.inner.find_event_relations(room_id, event_id, filter).await111
+ }112
+113
+ async fn get_room_events(114
+ &self,115
+ room_id: &RoomId,116
+ event_type: Option<&str>,117
+ session_id: Option<&str>,118
+ ) -> Result<Vec<Event>, Self::Error> {119
+ self.inner.get_room_events(room_id, event_type, session_id).await120
+ }121
+122
+ async fn save_event(&self, room_id: &RoomId, event: Event) -> Result<(), Self::Error> {123
+ self.inner.save_event(room_id, event).await124
+ }125
+126
+ async fn close(&self) -> Result<(), Self::Error> {127
+ self.inner.close().await128
+ }129
+130
+ async fn reopen(&self) -> Result<(), Self::Error> {131
+ self.inner.reopen().await132
+ }133
+134
+ async fn optimize(&self) -> Result<(), Self::Error> {135
+ self.inner.optimize().await136
+ }137
+138
+ async fn get_size(&self) -> Result<Option<usize>, Self::Error> {139
+ self.inner.get_size().await140
+ }141
+}