Commit 7c26fad34822
tarball5 files changed+215-99
@@ -152,6 +152,13 @@ missed when that channel overflowed at the wrong moment, since then there's no
152152
knowing which. a failure isn't an ending, a wedged upload can still be retried.153153
uploads don't belong to a room's actor, so closing the room leaves them be154154
155
+the forwarders hand their progress to that watcher rather than the stream, so156
+it's the only thing speaking about uploads. anything that ever showed progress157
+or a failure is followed until it gets exactly one `MediaUploadFinished`, and158
+nothing about it goes out after. it remembers what has ended for as long as it159
+runs, since a forwarder can still be sitting on progress for an upload that's160
+already sent and nothing says when that backlog is gone161
+155162
## sessions and secrets156163
157164
`Session::login_password`, `begin_oauth` and `restore` share one client builder@@ -219,6 +219,7 @@ struct Queues {
219219
notifications: mpsc::Receiver<Job<NotificationCommand>>,220220
encryption: mpsc::Receiver<Job<EncryptionCommand>>,221221
session_changes: broadcast::Receiver<SessionChange>,222
+ upload_progress: mpsc::Receiver<watchers::UploadProgress>,222223
}223224
224225
/// requests other devices send us, handed from the sdk's event handler to the encryption worker@@ -244,6 +245,7 @@ pub struct Engine {
244245
queues: Option<Queues>,245246
/// one progress forwarder per room that has uploaded, and whether it's subscribed yet246247
uploads: HashMap<OwnedRoomId, watch::Receiver<bool>>,248
+ upload_progress: mpsc::Sender<watchers::UploadProgress>,247249
/// the client can outlive us, so the handler has to come off it when we stop248250
verification_handler: Option<matrix_sdk::event_handler::EventHandlerHandle>,249251
stop: mpsc::UnboundedSender<Termination>,@@ -288,6 +290,7 @@ impl Engine {
288290
client.send_queue().enable_upload_progress(true);289291
let (note_sender, notes) = mpsc::unbounded_channel();290292
let (stop, stops) = mpsc::unbounded_channel();293
+ let (upload_progress, upload_progress_queue) = mpsc::channel(config.event_capacity);291294
let (mutations, mutation_queue) = mpsc::channel(config.command_capacity);292295
let (notifications, notification_queue) = mpsc::channel(config.command_capacity);293296
let (encryption, encryption_queue) = mpsc::channel(config.command_capacity);@@ -313,7 +316,9 @@ impl Engine {
313316
notifications: notification_queue,314317
encryption: encryption_queue,315318
session_changes,319
+ upload_progress: upload_progress_queue,316320
}),321
+ upload_progress,317322
uploads: HashMap::new(),318323
verification_handler: None,319324
stop,@@ -401,10 +406,11 @@ impl Engine {
401406
let events = || self.port.event_sender();402407
let watchers: [(&'static str, EngineBoxFuture<'static, ()>); 11] = [403408
(404
- "upload watcher",409
+ "upload tracker",405410
Box::pin(watchers::uploads(406411
client.clone(),407412
client.send_queue().subscribe(),413
+ queues.upload_progress,408414
events(),409415
)),410416
),@@ -648,7 +654,7 @@ impl Engine {
648654
let (subscribed, ready) = watch::channel(false);649655
self.tasks.spawn(650656
TaskContext::Command("upload watcher"),651
- watchers::room_uploads(room.clone(), self.port.event_sender(), subscribed),657
+ watchers::room_uploads(room.clone(), self.upload_progress.clone(), subscribed),652658
);653659
self.uploads654660
.insert(room.room_id().to_owned(), ready.clone());@@ -42,15 +42,15 @@ pub(super) async fn session(
4242
let event = match changes.recv().await {4343
Ok(SessionChange::TokensRefreshed) => Event::SessionTokensChanged,4444
Ok(SessionChange::UnknownToken(data)) => {45
+ let soft_logout = data.soft_logout.then_some(device_id);4546
let _ = events4647
.send(Event::SessionAccessRejected {47
- soft_logout: data.soft_logout,48
+ soft_logout: soft_logout.clone(),4849
})4950
.await;50
- let _ = stop.send(if data.soft_logout {51
- Termination::SoftLoggedOut { device_id }52
- } else {53
- Termination::AccessRevoked51
+ let _ = stop.send(match soft_logout {52
+ Some(device_id) => Termination::SoftLoggedOut { device_id },53
+ None => Termination::AccessRevoked,5454
});5555
return;5656
}@@ -146,7 +146,11 @@ pub(super) async fn send_queue_errors(
146146
}147147
148148
/// upload progress only goes out on the room's own channel, never the client-wide one149
-pub(super) async fn room_uploads(room: Room, events: EventSender, ready: watch::Sender<bool>) {149
+pub(super) async fn room_uploads(150
+ room: Room,151
+ progress: mpsc::Sender<UploadProgress>,152
+ ready: watch::Sender<bool>,153
+) {150154
let room_id = room.room_id().to_owned();151155
let mut updates = match room.send_queue().subscribe().await {152156
Ok((_, updates)) => updates,@@ -166,66 +170,68 @@ pub(super) async fn room_uploads(room: Room, events: EventSender, ready: watch::
166170
let RoomSendQueueUpdate::MediaUpload {167171
related_to,168172
file,169
- progress,173
+ progress: done,170174
..171175
} = update172176
else {173177
continue;174178
};175
- let event = Event::MediaUploadProgress {176
- room_id: room_id.clone(),177
- transaction_id: related_to,178
- uploaded_bytes: progress.current as u64,179
- total_bytes: progress.total as u64,180
- complete: file.is_some() && progress.current == progress.total,179
+ let tick = UploadProgress {180
+ upload: (room_id.clone(), related_to),181
+ uploaded_bytes: done.current as u64,182
+ total_bytes: done.total as u64,183
+ complete: file.is_some() && done.current == done.total,181184
};182
- if events.send(event).await.is_err() {185
+ if progress.send(tick).await.is_err() {183186
return;184187
}185188
}186189
}187190
188
-/// progress only goes out on each room's own channel, everything else reaches this189
-/// one, which exists before any upload does191
+pub(super) struct UploadProgress {192
+ upload: Upload,193
+ uploaded_bytes: u64,194
+ total_bytes: u64,195
+ complete: bool,196
+}197
+198
+/// the one place upload events leave from, so an upload is never reported after it199
+/// ended. progress comes in from the room forwarders, the rest from the client-wide200
+/// channel, which exists before any upload does190201
pub(super) async fn uploads(191202
client: Client,192203
mut updates: tokio::sync::broadcast::Receiver<SendQueueUpdate>,204
+ mut progress: mpsc::Receiver<UploadProgress>,193205
events: EventSender,194206
) {195207
let (_, mut rooms) = client.rooms_stream();196
- let mut uploads = attachment_echoes(&client).await;208
+ let mut tracker = UploadTracker {209
+ live: attachment_echoes(&client).await,210
+ ended: HashSet::new(),211
+ };197212
loop {198
- let finished = tokio::select! {213
+ let reports = tokio::select! {199214
update = updates.recv() => match update {200
- Ok(update) => match upload_update(&mut uploads, update) {201
- Some(event) => vec![event],202
- None => continue,203
- },215
+ Ok(update) => tracker.update(update).into_iter().collect(),204216
Err(RecvError::Lagged(skipped)) => {205217
tracing::warn!(skipped, "upload watcher lagged");206218
let current = attachment_echoes(&client).await;207
- let missed = uploads.difference(¤t).cloned().collect::<Vec<_>>();208
- uploads = current;209
- finished_uploads(missed, UploadOutcome::Missed)219
+ tracker.resnapshot(current)210220
}211221
Err(RecvError::Closed) => return,212222
},223
+ Some(tick) = progress.recv() => tracker.progress(tick).into_iter().collect(),213224
update = rooms.next() => {214225
if update.is_none() {215226
return;216227
}217
- let forgotten = uploads218
- .iter()219
- .filter(|(room_id, _)| client.get_room(room_id).is_none())220
- .cloned()221
- .collect::<Vec<_>>();222
- for upload in &forgotten {223
- uploads.remove(upload);224
- }225
- finished_uploads(forgotten, UploadOutcome::RoomForgotten)228
+ tracker.finish_where(229
+ |(room_id, _)| client.get_room(room_id).is_none(),230
+ &UploadOutcome::RoomForgotten,231
+ )226232
}227233
};228
- for event in finished {234
+ for event in reports {229235
if events.send(event).await.is_err() {230236
return;231237
}@@ -235,75 +241,119 @@ pub(super) async fn uploads(
235241
236242
type Upload = (OwnedRoomId, OwnedTransactionId);237243
238
-async fn attachment_echoes(client: &Client) -> HashSet<Upload> {239
- let Ok(echoes) = client.send_queue().local_echoes().await else {240
- return HashSet::new();241
- };242
- echoes243
- .into_iter()244
- .flat_map(|(room_id, echoes)| {245
- echoes246
- .into_iter()247
- .filter(|echo| is_attachment_echo(&echo.content))248
- .map(move |echo| (room_id.clone(), echo.transaction_id))249
- })250
- .collect()244
+/// everything reported gets exactly one `MediaUploadFinished`. `ended` is kept for good,245
+/// since a room forwarder can still be holding progress for an upload the client-wide246
+/// channel has already seen sent247
+struct UploadTracker {248
+ live: HashSet<Upload>,249
+ ended: HashSet<Upload>,251250
}252251
253
-fn upload_update(uploads: &mut HashSet<Upload>, update: SendQueueUpdate) -> Option<Event> {254
- let room_id = update.room_id;255
- let (transaction_id, outcome) = match update.update {256
- RoomSendQueueUpdate::NewLocalEvent(echo) => {257
- if is_attachment_echo(&echo.content) {258
- uploads.insert((room_id, echo.transaction_id));259
- }260
- return None;261
- }262
- RoomSendQueueUpdate::SendError {263
- transaction_id,264
- error,265
- is_recoverable,266
- } => {267
- let upload = (room_id, transaction_id);268
- if !uploads.contains(&upload) {252
+impl UploadTracker {253
+ fn update(&mut self, update: SendQueueUpdate) -> Option<Event> {254
+ let room_id = update.room_id;255
+ let (transaction_id, outcome) = match update.update {256
+ RoomSendQueueUpdate::NewLocalEvent(echo) => {257
+ let upload = (room_id, echo.transaction_id);258
+ if is_attachment_echo(&echo.content) && !self.ended.contains(&upload) {259
+ self.live.insert(upload);260
+ }269261
return None;270262
}271
- let (room_id, transaction_id) = upload;272
- return Some(Event::MediaUploadFailed {273
- room_id,263
+ RoomSendQueueUpdate::SendError {274264
transaction_id,275
- kind: classify_sdk_error(&error),276
- will_retry: is_recoverable,277
- });265
+ error,266
+ is_recoverable,267
+ } => {268
+ let upload = (room_id, transaction_id);269
+ if !self.live.contains(&upload) {270
+ return None;271
+ }272
+ let (room_id, transaction_id) = upload;273
+ return Some(Event::MediaUploadFailed {274
+ room_id,275
+ transaction_id,276
+ kind: classify_sdk_error(&error),277
+ will_retry: is_recoverable,278
+ });279
+ }280
+ RoomSendQueueUpdate::SentEvent {281
+ transaction_id,282
+ event_id,283
+ } => (transaction_id, UploadOutcome::Sent { event_id }),284
+ RoomSendQueueUpdate::CancelledLocalEvent { transaction_id } => {285
+ (transaction_id, UploadOutcome::Cancelled)286
+ }287
+ _ => return None,288
+ };289
+ self.finish((room_id, transaction_id), outcome)290
+ }291
+292
+ fn progress(&mut self, tick: UploadProgress) -> Option<Event> {293
+ if self.ended.contains(&tick.upload) {294
+ return None;278295
}279
- RoomSendQueueUpdate::SentEvent {296
+ self.live.insert(tick.upload.clone());297
+ let (room_id, transaction_id) = tick.upload;298
+ Some(Event::MediaUploadProgress {299
+ room_id,280300
transaction_id,281
- event_id,282
- } => (transaction_id, UploadOutcome::Sent { event_id }),283
- RoomSendQueueUpdate::CancelledLocalEvent { transaction_id } => {284
- (transaction_id, UploadOutcome::Cancelled)301
+ uploaded_bytes: tick.uploaded_bytes,302
+ total_bytes: tick.total_bytes,303
+ complete: tick.complete,304
+ })305
+ }306
+307
+ fn resnapshot(&mut self, current: HashSet<Upload>) -> Vec<Event> {308
+ let reports = self.finish_where(|upload| !current.contains(upload), &UploadOutcome::Missed);309
+ let ended = &self.ended;310
+ self.live311
+ .extend(current.into_iter().filter(|upload| !ended.contains(upload)));312
+ reports313
+ }314
+315
+ fn finish_where(316
+ &mut self,317
+ ends: impl Fn(&Upload) -> bool,318
+ outcome: &UploadOutcome,319
+ ) -> Vec<Event> {320
+ let ending = self321
+ .live322
+ .iter()323
+ .filter(|upload| ends(upload))324
+ .cloned()325
+ .collect::<Vec<_>>();326
+ ending327
+ .into_iter()328
+ .filter_map(|upload| self.finish(upload, outcome.clone()))329
+ .collect()330
+ }331
+332
+ fn finish(&mut self, upload: Upload, outcome: UploadOutcome) -> Option<Event> {333
+ if !self.live.remove(&upload) {334
+ return None;285335
}286
- _ => return None,287
- };288
- let upload = (room_id, transaction_id);289
- if !uploads.remove(&upload) {290
- return None;336
+ self.ended.insert(upload.clone());337
+ let (room_id, transaction_id) = upload;338
+ Some(Event::MediaUploadFinished {339
+ room_id,340
+ transaction_id,341
+ outcome,342
+ })291343
}292
- let (room_id, transaction_id) = upload;293
- Some(Event::MediaUploadFinished {294
- room_id,295
- transaction_id,296
- outcome,297
- })298344
}299345
300
-fn finished_uploads(uploads: Vec<Upload>, outcome: UploadOutcome) -> Vec<Event> {301
- uploads346
+async fn attachment_echoes(client: &Client) -> HashSet<Upload> {347
+ let Ok(echoes) = client.send_queue().local_echoes().await else {348
+ return HashSet::new();349
+ };350
+ echoes302351
.into_iter()303
- .map(|(room_id, transaction_id)| Event::MediaUploadFinished {304
- room_id,305
- transaction_id,306
- outcome: outcome.clone(),352
+ .flat_map(|(room_id, echoes)| {353
+ echoes354
+ .into_iter()355
+ .filter(|echo| is_attachment_echo(&echo.content))356
+ .map(move |echo| (room_id.clone(), echo.transaction_id))307357
})308358
.collect()309359
}@@ -389,3 +439,51 @@ pub(super) async fn notification_settings(client: Client, events: EventSender) {
389439
}390440
}391441
}442
+443
+#[cfg(test)]444
+mod tests {445
+ use matrix_sdk::send_queue::{RoomSendQueueUpdate, SendQueueUpdate};446
+ use ruma::{OwnedTransactionId, event_id, owned_room_id};447
+448
+ use super::{HashSet, UploadProgress, UploadTracker};449
+ use crate::{Event, UploadOutcome};450
+451
+ #[test]452
+ fn an_upload_only_progress_saw_still_ends_once() {453
+ let upload = (454
+ owned_room_id!("!room:localhost"),455
+ OwnedTransactionId::from("upload"),456
+ );457
+ let tick = || UploadProgress {458
+ upload: upload.clone(),459
+ uploaded_bytes: 1,460
+ total_bytes: 2,461
+ complete: false,462
+ };463
+ let mut tracker = UploadTracker {464
+ live: HashSet::new(),465
+ ended: HashSet::new(),466
+ };467
+ assert!(tracker.progress(tick()).is_some());468
+469
+ let missed = tracker.resnapshot(HashSet::new());470
+ assert!(matches!(471
+ missed.as_slice(),472
+ [Event::MediaUploadFinished {473
+ outcome: UploadOutcome::Missed,474
+ ..475
+ }]476
+ ));477
+478
+ assert!(tracker.progress(tick()).is_none());479
+ let sent = SendQueueUpdate {480
+ room_id: upload.0.clone(),481
+ update: RoomSendQueueUpdate::SentEvent {482
+ transaction_id: upload.1.clone(),483
+ event_id: event_id!("$sent:localhost").to_owned(),484
+ },485
+ };486
+ assert!(tracker.update(sent).is_none());487
+ assert!(tracker.resnapshot(HashSet::from([upload])).is_empty());488
+ }489
+}@@ -637,11 +637,11 @@ pub enum Event {
637637
/// Refresh tokens changed. Persist a fresh `ClientHandle::session_record()`638638
/// atomically; a record captured before this event may no longer restore.639639
SessionTokensChanged,640
- /// the server stopped honouring our token and the engine stops. on a soft641
- /// logout keep the store and log in again with the same device, otherwise642
- /// the record and the store are both dead640
+ /// the server stopped honouring our token and the engine stops. a soft logout641
+ /// names the device to log in again as, keeping the store. otherwise the record642
+ /// and the store are both dead643643
SessionAccessRejected {644
- soft_logout: bool,644
+ soft_logout: Option<OwnedDeviceId>,645645
},646646
NotificationSettingsChanged,647647
VerificationTargetChanged(VerificationTarget),@@ -90,7 +90,12 @@ async fn soft_logout_says_so_and_stops_the_engine() {
9090
.unwrap();9191
let running = tokio::spawn(engine.run());9292
support::receive_until(&mut events, |event| {93
- matches!(event, Event::SessionAccessRejected { soft_logout: true })93
+ matches!(94
+ event,95
+ Event::SessionAccessRejected {96
+ soft_logout: Some(_)97
+ }98
+ )9499
})95100
.await;96101
support::receive_until(&mut events, |event| matches!(event, Event::Stopped)).await;