Commit 2c13c8db6174
tarball4 files changed+17-280
@@ -98,7 +98,7 @@ The checkpoint keeps the position the committed response started from as well
9898
as the one it ended at. A crash after the commit but before the SDK saves `pos`9999
replays from that earlier position, and the replay is a superset of what was100100
committed, so it is not treated as a discontinuity. Any other starting position101
-records a `SyncDiscontinuity` gap and marks every observed room unresolved.101
+records a `SyncDiscontinuity` gap.102102
103103
The hook never fails sync. A failed write keeps the inbox in memory, records a104104
`StoreFailed` gap, acks, and retries the write a minute later, so a restart@@ -281,8 +281,8 @@ session resume.
281281
282282
The store holds two records. `inbox` carries entries, saved events, context283283
windows, candidates, edits, redactions, coverage and recovery cursors. `sync`284
-carries the connection checkpoints, observed rooms, per-room repair coverage285
-and the inbox revision it belongs to. Every response writes `sync`, and284
+carries the connection checkpoints, observed rooms and the inbox285
+revision it belongs to. Every response writes `sync`, and286286
`inbox` only when the response changed it, both in one commit. A quiet287287
response therefore costs one small record, while a changed inbox is still288288
reserialised and re-encrypted whole. Background completions are applied in@@ -14,7 +14,7 @@ use matrix_sdk::{
1414
sliding_sync::{ResponseCommitBatch, ResponseCommitHook},1515
};1616
use ruma::{17
- OwnedEventId, OwnedRoomId, OwnedUserId, RoomId,17
+ OwnedRoomId, OwnedUserId, RoomId,1818
events::{1919
AnySyncTimelineEvent, ignored_user_list::IgnoredUserListEventContent,2020
room::encrypted::OriginalSyncRoomEncryptedEvent,@@ -24,13 +24,11 @@ use ruma::{
2424
use serde_json::Value;2525
use tokio::sync::{mpsc, oneshot, watch};2626
27
-use super::state::{28
- Candidate, Checkpoint, Data, RepairCoverage, SavedEvent, SyncState, Window, id, invalid, now,29
-};27
+use super::state::{Candidate, Checkpoint, Data, SavedEvent, SyncState, Window, id, invalid, now};3028
use crate::{31
- CommandOutput, EngineConfig, Error, ErrorKind, Event, HistoryRepairPolicy, MentionCaptureState,32
- MentionEntry, MentionGapReason, MentionKey, MentionReason, MentionsCommand, MentionsPage,33
- MentionsUpdate, TimelineContent, TimelineItem,29
+ CommandOutput, EngineConfig, Error, ErrorKind, Event, MentionCaptureState, MentionEntry,30
+ MentionGapReason, MentionKey, MentionReason, MentionsCommand, MentionsPage, MentionsUpdate,31
+ TimelineContent, TimelineItem,3432
engine::{MediaService, MentionsStore, context, executor::EngineBoxFuture},3533
normalise::{MediaRegistration, normalise_observed},3634
transport::EventSender,@@ -55,10 +53,6 @@ enum Request {
5553
MentionsCommand,5654
oneshot::Sender<Result<CommandOutput, Error>>,5755
),58
- GetRepairPolicy(oneshot::Sender<Result<HistoryRepairPolicy, Error>>),59
- SetRepairPolicy(HistoryRepairPolicy, oneshot::Sender<Result<(), Error>>),60
- GetRepairCoverage(OwnedRoomId, oneshot::Sender<Result<RepairCoverage, Error>>),61
- RepairResolved(OwnedRoomId, oneshot::Sender<Result<(), Error>>),6256
Stop(oneshot::Sender<()>),6357
}6458
@@ -134,45 +128,6 @@ impl Handle {
134128
response.await.map_err(|_| Error::interrupted())?135129
}136130
137
- pub(crate) async fn repair_policy(&self) -> Result<HistoryRepairPolicy, Error> {138
- let (reply, response) = oneshot::channel();139
- self.requests140
- .send(Request::GetRepairPolicy(reply))141
- .await142
- .map_err(|_| Error::interrupted())?;143
- response.await.map_err(|_| Error::interrupted())?144
- }145
-146
- pub(crate) async fn set_repair_policy(&self, policy: HistoryRepairPolicy) -> Result<(), Error> {147
- let (reply, response) = oneshot::channel();148
- self.requests149
- .send(Request::SetRepairPolicy(policy, reply))150
- .await151
- .map_err(|_| Error::interrupted())?;152
- response.await.map_err(|_| Error::interrupted())?153
- }154
-155
- pub(crate) async fn repair_coverage(156
- &self,157
- room_id: OwnedRoomId,158
- ) -> Result<RepairCoverage, Error> {159
- let (reply, response) = oneshot::channel();160
- self.requests161
- .send(Request::GetRepairCoverage(room_id, reply))162
- .await163
- .map_err(|_| Error::interrupted())?;164
- response.await.map_err(|_| Error::interrupted())?165
- }166
-167
- pub(crate) async fn repair_resolved(&self, room_id: OwnedRoomId) -> Result<(), Error> {168
- let (reply, response) = oneshot::channel();169
- self.requests170
- .send(Request::RepairResolved(room_id, reply))171
- .await172
- .map_err(|_| Error::interrupted())?;173
- response.await.map_err(|_| Error::interrupted())?174
- }175
-176131
pub(crate) async fn stop(&self) {177132
let (reply, response) = oneshot::channel();178133
if self.requests.send(Request::Stop(reply)).await.is_ok() {@@ -407,19 +362,12 @@ impl Service {
407362
Ok(())408363
}409364
410
- async fn apply(&mut self, data: Option<Data>, sync: Option<SyncState>) -> Result<(), Error> {411
- let dirty = self.dirty;412
- let previous_data = data.map(|data| std::mem::replace(&mut self.data, data));413
- let previous_sync = sync.map(|sync| std::mem::replace(&mut self.sync, sync));414
- self.dirty |= previous_data.is_some();365
+ async fn apply(&mut self, data: Data) -> Result<(), Error> {366
+ let dirty = std::mem::replace(&mut self.dirty, true);367
+ let previous = std::mem::replace(&mut self.data, data);415368
let result = self.flush().await;416369
if result.is_err() {417
- if let Some(data) = previous_data {418
- self.data = data;419
- }420
- if let Some(sync) = previous_sync {421
- self.sync = sync;422
- }370
+ self.data = previous;423371
self.dirty = dirty;424372
}425373
result@@ -433,23 +381,6 @@ impl Service {
433381
self.publish().await;434382
}435383
436
- async fn set_repair_policy(&mut self, policy: HistoryRepairPolicy) -> Result<(), Error> {437
- let mut next = self.data.clone();438
- next.repair_policy = policy;439
- self.apply(Some(next), None).await440
- }441
-442
- async fn repair_resolved(&mut self, room_id: OwnedRoomId) -> Result<(), Error> {443
- let mut sync = self.sync.clone();444
- let coverage = sync.repair_coverage.entry(room_id.clone()).or_default();445
- coverage.boundary_ms = None;446
- coverage.last_confirmed_ms = Some(now());447
- coverage.reliable = false;448
- coverage.unresolved = false;449
- sync.observed_rooms.insert(room_id);450
- self.apply(None, Some(sync)).await451
- }452
-453384
async fn command(&mut self, command: MentionsCommand) -> Result<CommandOutput, Error> {454385
match command {455386
MentionsCommand::Page { cursor, limit } => {@@ -496,7 +427,7 @@ impl Service {
496427
start: None,497428
end: None,498429
});499
- self.apply(Some(next), None).await?;430
+ self.apply(next).await?;500431
let (context, media) = self.data.context(&key)?;501432
self.media.register(media).await;502433
Ok(CommandOutput::MentionContext(context))@@ -509,7 +440,7 @@ impl Service {
509440
entry.revision = self.data.revision + 1;510441
}511442
}512
- self.apply(Some(next), None).await?;443
+ self.apply(next).await?;513444
Ok(CommandOutput::Done)514445
}515446
MentionsCommand::ReadThrough(boundary) => {@@ -525,7 +456,7 @@ impl Service {
525456
entry.read = true;526457
entry.revision = self.data.revision + 1;527458
}528
- self.apply(Some(next), None).await?;459
+ self.apply(next).await?;529460
Ok(CommandOutput::Done)530461
}531462
MentionsCommand::Recover => {@@ -535,7 +466,7 @@ impl Service {
535466
let mut next = self.data.clone();536467
next.coverage.recovering = true;537468
next.coverage.historical_supported = None;538
- self.apply(Some(next), None).await?;469
+ self.apply(next).await?;539470
Ok(CommandOutput::Done)540471
}541472
}@@ -562,7 +493,6 @@ impl Service {
562493
if self.timed_out.swap(false, Ordering::AcqRel) {563494
next.gap(None, MentionGapReason::HookTimedOut, None);564495
}565
- let observed_at_ms = now();566496
// a crash after the commit but before the sdk saved `pos` replays from the567497
// previous position, and that response is a superset of the one committed568498
let continuous = match &checkpoint {@@ -574,10 +504,6 @@ impl Service {
574504
};575505
if !continuous {576506
next.gap(None, MentionGapReason::SyncDiscontinuity, None);577
- let observed_rooms: Vec<_> = sync.observed_rooms.iter().cloned().collect();578
- for room_id in observed_rooms {579
- Self::mark_unresolved(&mut sync, &room_id, observed_at_ms);580
- }581507
}582508
for (room_id, raw) in &batch.raw_rooms {583509
if raw.limited && sync.observed_rooms.contains(room_id) {@@ -599,37 +525,18 @@ impl Service {
599525
if self.observable(room_id).is_none() {600526
continue;601527
}602
- let previously_observed = sync.observed_rooms.contains(room_id);603
- let limited = batch.raw_rooms.get(room_id).is_some_and(|raw| raw.limited);604
- if !continuous || limited || !previously_observed {605
- Self::mark_unresolved(&mut sync, room_id, observed_at_ms);606
- } else {607
- let coverage = sync.repair_coverage.entry(room_id.clone()).or_default();608
- if !coverage.unresolved {609
- coverage.last_confirmed_ms = Some(observed_at_ms);610
- }611
- coverage.reliable = false;612
- }613528
sync.observed_rooms.insert(room_id.clone());614529
let mut observed = BTreeMap::new();615
- let mut latest_anchor = None;616530
if let Some(raw) = batch.raw_rooms.get(room_id) {617531
for event in &raw.timeline {618532
let Ok(value) = serde_json::from_str::<Value>(event.json().get()) else {619533
continue;620534
};621535
if let Some(event_id) = value["event_id"].as_str() {622
- latest_anchor = event_id.parse::<OwnedEventId>().ok();623536
observed.insert(event_id.to_owned(), value);624537
}625538
}626539
}627
- if let Some(latest_anchor) = latest_anchor {628
- sync.repair_coverage629
- .entry(room_id.clone())630
- .or_default()631
- .latest_anchor = Some(latest_anchor);632
- }633540
let processed = batch634541
.response635542
.rooms@@ -710,15 +617,6 @@ impl Service {
710617
}711618
}712619
713
- fn mark_unresolved(sync: &mut SyncState, room_id: &OwnedRoomId, observed_at_ms: u64) {714
- let coverage = sync.repair_coverage.entry(room_id.clone()).or_default();715
- if !coverage.unresolved {716
- coverage.boundary_ms = Some(observed_at_ms);717
- coverage.unresolved = true;718
- }719
- coverage.reliable = false;720
- }721
-722620
fn filter_ignored(&self, next: &mut Data) -> bool {723621
let removed: Vec<_> = next724622
.entries@@ -1114,10 +1012,6 @@ impl Service {
11141012
}11151013
Some(Request::Batch(batch, reply)) => { self.batch(batch).await; let _ = reply.send(()); }11161014
Some(Request::Command(command, reply)) => { let result = self.command(command).await; let _ = reply.send(result); }1117
- Some(Request::GetRepairPolicy(reply)) => { let _ = reply.send(Ok(self.data.repair_policy.clone())); }1118
- Some(Request::SetRepairPolicy(policy, reply)) => { let result = self.set_repair_policy(policy).await; let _ = reply.send(result); }1119
- Some(Request::GetRepairCoverage(room_id, reply)) => { let coverage = self.sync.repair_coverage.get(&room_id).cloned().unwrap_or_default(); let _ = reply.send(Ok(coverage)); }1120
- Some(Request::RepairResolved(room_id, reply)) => { let result = self.repair_resolved(room_id).await; let _ = reply.send(result); }11211015
None => return,11221016
},11231017
Some(result) = work.next(), if !work.is_empty() => {@@ -353,142 +353,6 @@ async fn an_unreadable_store_resets_behind_a_gap() {
353353
}354354
355355
#[tokio::test]356
-async fn repair_policy_and_coverage_roundtrip_through_the_inbox() {357
- let server = MatrixMockServer::new().await;358
- let room_id = room_id!("!repair-coverage:localhost");359
- let (client, _) = joined_client(&server, room_id).await;360
- let config = config();361
- let store = MentionsStore::open(&config, account(&client), MAX_BYTES)362
- .await363
- .unwrap();364
- let media = Arc::new(MediaService::new(365
- client.clone(),366
- EngineConfig::default().max_media_bytes,367
- EngineConfig::default().max_media_sources,368
- ));369
- let (handle, mut service, _) = Service::new(370
- client.clone(),371
- media,372
- Some(Ok(store)),373
- EngineConfig::default(),374
- )375
- .await376
- .unwrap();377
- service.recovery_budget = 0;378
- service.latest_scan = false;379
- let service_task = tokio::spawn(service.run());380
-381
- let default_policy = HistoryRepairPolicy::default();382
- assert_eq!(handle.repair_policy().await.unwrap(), default_policy);383
- let stored_policy = HistoryRepairPolicy::Bridge;384
- handle385
- .set_repair_policy(stored_policy.clone())386
- .await387
- .unwrap();388
- handle.repair_resolved(room_id.to_owned()).await.unwrap();389
- let coverage = handle.repair_coverage(room_id.to_owned()).await.unwrap();390
- assert!(coverage.last_confirmed_ms.is_some());391
- assert!(!coverage.reliable);392
- assert_eq!(coverage.duration_ms(), None);393
-394
- handle.stop().await;395
- service_task.await.unwrap();396
- drop(handle);397
-398
- let reopened = MentionsStore::open(&config, account(&client), MAX_BYTES)399
- .await400
- .unwrap();401
- let persisted: Data = reopened.read("inbox").unwrap().unwrap();402
- assert_eq!(persisted.repair_policy, stored_policy);403
- let sync: SyncState = reopened.read("sync").unwrap().unwrap();404
- let coverage = sync.repair_coverage.get(room_id).unwrap();405
- assert!(coverage.last_confirmed_ms.is_some());406
- assert!(coverage.boundary_ms.is_none());407
- assert!(!coverage.unresolved);408
- assert!(!coverage.reliable);409
- drop(reopened);410
- config.delete().await.unwrap();411
-}412
-413
-#[tokio::test]414
-async fn repair_limited_sync_freezes_the_last_room_checkpoint() {415
- let server = MatrixMockServer::new().await;416
- let room_id = room_id!("!repair-boundary:localhost");417
- let (client, _) = joined_client(&server, room_id).await;418
- let media = Arc::new(MediaService::new(419
- client.clone(),420
- EngineConfig::default().max_media_bytes,421
- EngineConfig::default().max_media_sources,422
- ));423
- let (_handle, mut service, _) = Service::new(client, media, None, EngineConfig::default())424
- .await425
- .unwrap();426
- service.sync.checkpoints.insert(427
- "mentions-connection".to_owned(),428
- Checkpoint {429
- pos: "position-before".to_owned(),430
- previous: None,431
- },432
- );433
- service.sync.observed_rooms.insert(room_id.to_owned());434
- service435
- .sync436
- .repair_coverage437
- .entry(room_id.to_owned())438
- .or_default()439
- .last_confirmed_ms = Some(1_000);440
-441
- let events = [442
- EventFactory::new()443
- .room(room_id)444
- .text_msg("older event")445
- .sender(user_id!("@sender:localhost"))446
- .event_id(event_id!("$repair-older:localhost"))447
- .into_raw_sync(),448
- EventFactory::new()449
- .room(room_id)450
- .text_msg("latest event")451
- .sender(user_id!("@sender:localhost"))452
- .event_id(event_id!("$repair-latest:localhost"))453
- .into_raw_sync(),454
- ];455
- let timeline: Vec<_> = events456
- .iter()457
- .map(|event| serde_json::to_value(event).unwrap())458
- .collect();459
- let raw_room = serde_json::from_value(json!({460
- "limited": true,461
- "prev_batch": "repair-gap-token",462
- "timeline": timeline,463
- }))464
- .unwrap();465
- let batch = ResponseCommitBatch {466
- connection_id: "mentions-connection".to_owned(),467
- previous_pos: Some("position-before".to_owned()),468
- next_pos: "position-after".to_owned(),469
- response: Default::default(),470
- raw_rooms: [(room_id.to_owned(), raw_room)].into(),471
- };472
-473
- service.batch(batch).await;474
-475
- let coverage = service.sync.repair_coverage.get(room_id).unwrap();476
- assert_eq!(coverage.last_confirmed_ms, Some(1_000));477
- assert!(478
- coverage479
- .boundary_ms480
- .is_some_and(|boundary| boundary > 1_000)481
- );482
- assert!(coverage.unresolved);483
- assert!(!coverage.reliable);484
- assert_eq!(coverage.duration_ms(), None);485
- assert_eq!(486
- coverage.latest_anchor.as_deref(),487
- Some(event_id!("$repair-latest:localhost"))488
- );489
-}490
-491
-#[tokio::test]492356
async fn service_recovers_notification_history_into_durable_unread_inbox() {493357
let server = MatrixMockServer::new().await;494358
let room_id = room_id!("!mentions:localhost");@@ -1,6 +1,6 @@
11
use std::collections::{BTreeMap, BTreeSet};22
3
-use ruma::{OwnedEventId, OwnedUserId};3
+use ruma::OwnedUserId;44
use serde_json::Value;55
66
use crate::{@@ -67,25 +67,6 @@ pub(super) struct Window {
6767
pub end: Option<String>,6868
}6969
70
-#[derive(Clone, Debug, Default, PartialEq)]71
-#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]72
-pub(crate) struct RepairCoverage {73
- pub(crate) last_confirmed_ms: Option<u64>,74
- pub(crate) boundary_ms: Option<u64>,75
- pub(crate) reliable: bool,76
- pub(crate) unresolved: bool,77
- pub(crate) latest_anchor: Option<OwnedEventId>,78
-}79
-80
-impl RepairCoverage {81
- pub(crate) fn duration_ms(&self) -> Option<u64> {82
- if !self.reliable {83
- return None;84
- }85
- self.boundary_ms?.checked_sub(self.last_confirmed_ms?)86
- }87
-}88
-8970
#[derive(Clone, Default, PartialEq)]9071
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]9172
pub(super) struct Data {@@ -103,7 +84,6 @@ pub(super) struct Data {
10384
pub latest_cursor: Option<String>,10485
pub latest_anchor: Option<String>,10586
pub latest_head: Option<String>,106
- pub repair_policy: crate::HistoryRepairPolicy,10787
}10888
10989
#[derive(Clone, Default, PartialEq)]@@ -112,7 +92,6 @@ pub(super) struct SyncState {
11292
pub inbox_revision: u64,11393
pub checkpoints: BTreeMap<String, Checkpoint>,11494
pub observed_rooms: BTreeSet<ruma::OwnedRoomId>,115
- pub repair_coverage: BTreeMap<ruma::OwnedRoomId, RepairCoverage>,11695
}11796
11897
#[derive(Clone, Debug, PartialEq)]