Commit 0bfa3f46f0be
tarball9 files changed+195-56
@@ -58,6 +58,10 @@ kicked or banned from, closes itself. it tells the engine first, and anything
5858
already queued to it comes back to be answered or replayed, so an open racing a5959
leave still gets a fresh actor6060
61
+however an actor ends, short of the engine stopping, its typing goes out empty62
+and each of its timelines sends `TimelineClosed`. so does a closed thread. a63
+timeline that never sent a reset never sends a close either64
+6165
opening sends `TimelineReset` on the stream before it answers. opening an6266
already open room sends a fresh reset, so a second frontend gets a snapshot6367
@@ -138,8 +142,12 @@ closes, least recently seen first once the cap is hit
138142
uploads are commands that answer once the send queue has them. progress only139143
goes out on a room's own send queue channel, so each room that uploads gets a140144
forwarder, subscribed before the upload is queued and restarted for uploads the141
-sdk resumes from last time. failures do reach the client-wide channel, so one142
-watcher there reports them145
+sdk resumes from last time. everything but progress reaches the client-wide146
+channel, so one watcher there follows every attachment in the queue and ends147
+each with one `MediaUploadFinished`. sent, cancelled, its room forgotten, or148
+missed when that channel overflowed at the wrong moment, since then there's no149
+knowing which. a failure isn't an ending, a wedged upload can still be retried.150
+uploads don't belong to a room's actor, so closing the room leaves them be143151
144152
## sessions and secrets145153
@@ -175,7 +183,10 @@ sdk ui types stay behind the boundary
175183
176184
`rooms` normalises on demand with bounded concurrency and sorts by latest event,177185
then name, then id. `RoomsChanged` is an invalidation, coalesced so one sync178
-touching fifty rooms is one event186
+touching fifty rooms is one event. a left room stays in the list with its187
+membership until it's forgotten. push rule changes never touch room info, so188
+`NotificationSettingsChanged` brings a `RoomsChanged` along for the modes in189
+the summaries179190
180191
## verification181192
@@ -401,8 +401,8 @@ impl Engine {
401401
let events = || self.port.event_sender();402402
let watchers: [(&'static str, EngineBoxFuture<'static, ()>); 11] = [403403
(404
- "upload failure watcher",405
- Box::pin(watchers::upload_failures(404
+ "upload watcher",405
+ Box::pin(watchers::uploads(406406
client.clone(),407407
client.send_queue().subscribe(),408408
events(),@@ -91,7 +91,7 @@ impl RoomActor {
9191
reply.done(Ok(()));9292
}9393
let (typing_guard, typing) = self.room.subscribe_to_typing_notifications();94
- let typing_watcher = TaskGuard(self.context.supervisor.spawn(94
+ let mut typing_watcher = TaskGuard(self.context.supervisor.spawn(9595
"typing watcher",9696
watch_typing(self.room_id.clone(), typing, self.context.events.clone()),9797
));@@ -112,7 +112,16 @@ impl RoomActor {
112112
// let redactions and receipts that already left finish rather than lie about them113113
while timelines.work.join_next().await.is_some() {}114114
drop(typing_guard);115
- drop(typing_watcher);115
+ typing_watcher.0.abort();116
+ let _ = (&mut typing_watcher.0).await;117
+ let _ = self118
+ .context119
+ .events120
+ .send(Event::TypingChanged(TypingState {121
+ room_id: self.room_id.clone(),122
+ users: Vec::new(),123
+ }))124
+ .await;116125
timelines.threads.close().await;117126
timelines.room.close().await;118127
if let Some(reply) = close {@@ -159,6 +159,13 @@ impl TimelineSession {
159159
let _ = (&mut self.watcher).await;160160
// after the watcher, or a last diff could register media for a timeline that's gone161161
self.context.media.release(&self.scope).await;162
+ let _ = self163
+ .context164
+ .events165
+ .send(Event::TimelineClosed {166
+ timeline_id: self.id.clone(),167
+ })168
+ .await;162169
}163170
164171
pub(super) async fn execute(@@ -1,19 +1,19 @@
1
-use std::{sync::Arc, time::Duration};1
+use std::{collections::HashSet, sync::Arc, time::Duration};22
33
use futures_util::StreamExt;44
use matrix_sdk::{55
Client, Room, SessionChange,66
ruma::events::{AnyMessageLikeEventContent, room::message::MessageType},7
- send_queue::{LocalEchoContent, RoomSendQueueUpdate},7
+ send_queue::{LocalEchoContent, RoomSendQueueUpdate, SendQueueUpdate},88
};99
use matrix_sdk_ui::sync_service::{State as SyncState, SyncService};10
-use ruma::{OwnedDeviceId, time::Instant};10
+use ruma::{OwnedDeviceId, OwnedRoomId, OwnedTransactionId, time::Instant};1111
use tokio::sync::{broadcast::error::RecvError, mpsc, watch};1212
1313
use super::executor::TaskSet;1414
use crate::{15
- ConnectionState, Event, Termination, encryption::encryption_state, error::classify_sdk_error,16
- transport::EventSender,15
+ ConnectionState, Event, Termination, UploadOutcome, encryption::encryption_state,16
+ error::classify_sdk_error, transport::EventSender,1717
};1818
1919
const SYNC_RESTART_MIN: Duration = Duration::from_secs(1);@@ -185,54 +185,127 @@ pub(super) async fn room_uploads(room: Room, events: EventSender, ready: watch::
185185
}186186
}187187
188
-/// failures do reach the client-wide channel, and this one exists before any upload does189
-pub(super) async fn upload_failures(188
+/// progress only goes out on each room's own channel, everything else reaches this189
+/// one, which exists before any upload does190
+pub(super) async fn uploads(190191
client: Client,191
- mut updates: tokio::sync::broadcast::Receiver<matrix_sdk::send_queue::SendQueueUpdate>,192
+ mut updates: tokio::sync::broadcast::Receiver<SendQueueUpdate>,192193
events: EventSender,193194
) {195
+ let (_, mut rooms) = client.rooms_stream();196
+ let mut uploads = attachment_echoes(&client).await;194197
loop {195
- let update = match updates.recv().await {196
- Ok(update) => update,197
- Err(RecvError::Lagged(skipped)) => {198
- tracing::warn!(skipped, "upload failure watcher lagged");199
- continue;198
+ let finished = tokio::select! {199
+ update = updates.recv() => match update {200
+ Ok(update) => match upload_update(&mut uploads, update) {201
+ Some(event) => vec![event],202
+ None => continue,203
+ },204
+ Err(RecvError::Lagged(skipped)) => {205
+ tracing::warn!(skipped, "upload watcher lagged");206
+ let current = attachment_echoes(&client).await;207
+ let missed = uploads.difference(¤t).cloned().collect::<Vec<_>>();208
+ uploads = current;209
+ finished_uploads(missed, UploadOutcome::Missed)210
+ }211
+ Err(RecvError::Closed) => return,212
+ },213
+ update = rooms.next() => {214
+ if update.is_none() {215
+ return;216
+ }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)200226
}201
- Err(RecvError::Closed) => return,202227
};203
- let RoomSendQueueUpdate::SendError {228
+ for event in finished {229
+ if events.send(event).await.is_err() {230
+ return;231
+ }232
+ }233
+ }234
+}235
+236
+type Upload = (OwnedRoomId, OwnedTransactionId);237
+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()251
+}252
+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 {204263
transaction_id,205264
error,206265
is_recoverable,207
- } = update.update208
- else {209
- continue;210
- };211
- let Some(room) = client.get_room(&update.room_id) else {212
- continue;213
- };214
- let is_attachment = room215
- .send_queue()216
- .subscribe()217
- .await218
- .is_ok_and(|(echoes, _)| {219
- echoes.iter().any(|echo| {220
- echo.transaction_id == transaction_id && is_attachment_echo(&echo.content)221
- })266
+ } => {267
+ let upload = (room_id, transaction_id);268
+ if !uploads.contains(&upload) {269
+ return None;270
+ }271
+ let (room_id, transaction_id) = upload;272
+ return Some(Event::MediaUploadFailed {273
+ room_id,274
+ transaction_id,275
+ kind: classify_sdk_error(&error),276
+ will_retry: is_recoverable,222277
});223
- if !is_attachment {224
- continue;225278
}226
- let event = Event::MediaUploadFailed {227
- room_id: update.room_id,279
+ RoomSendQueueUpdate::SentEvent {228280
transaction_id,229
- kind: classify_sdk_error(&error),230
- recoverable: is_recoverable,231
- };232
- if events.send(event).await.is_err() {233
- return;281
+ event_id,282
+ } => (transaction_id, UploadOutcome::Sent { event_id }),283
+ RoomSendQueueUpdate::CancelledLocalEvent { transaction_id } => {284
+ (transaction_id, UploadOutcome::Cancelled)234285
}286
+ _ => return None,287
+ };288
+ let upload = (room_id, transaction_id);289
+ if !uploads.remove(&upload) {290
+ return None;235291
}292
+ let (room_id, transaction_id) = upload;293
+ Some(Event::MediaUploadFinished {294
+ room_id,295
+ transaction_id,296
+ outcome,297
+ })298
+}299
+300
+fn finished_uploads(uploads: Vec<Upload>, outcome: UploadOutcome) -> Vec<Event> {301
+ uploads302
+ .into_iter()303
+ .map(|(room_id, transaction_id)| Event::MediaUploadFinished {304
+ room_id,305
+ transaction_id,306
+ outcome: outcome.clone(),307
+ })308
+ .collect()236309
}237310
238311
fn is_attachment_echo(content: &LocalEchoContent) -> bool {@@ -308,12 +381,11 @@ pub(super) async fn notification_settings(client: Client, events: EventSender) {
308381
let settings = client.notification_settings().await;309382
let mut changes = settings.subscribe_to_changes();310383
while let Ok(()) | Err(RecvError::Lagged(_)) = changes.recv().await {311
- if events312
- .send(Event::NotificationSettingsChanged)313
- .await314
- .is_err()315
- {316
- return;384
+ // room summaries carry the effective mode, and push rule changes never touch room info385
+ for event in [Event::NotificationSettingsChanged, Event::RoomsChanged] {386
+ if events.send(event).await.is_err() {387
+ return;388
+ }317389
}318390
}319391
}@@ -38,8 +38,8 @@ pub use model::{
3838
RoomNotificationSettings, RoomPowerLevels, RoomPreset, RoomSummary, RoomVisibility, SendState,3939
Sender, SpaceChild, Termination, ThreadSummary, TimelineCommand, TimelineContent, TimelineDiff,4040
TimelineDiffError, TimelineEvent, TimelineId, TimelineItem, TimelineItemId, TypingState,41
- UploadDetails, UploadThumbnail, UtdCause, VerificationAction, VerificationFailure,42
- VerificationState, VerificationTarget,41
+ UploadDetails, UploadOutcome, UploadThumbnail, UtdCause, VerificationAction,42
+ VerificationFailure, VerificationState, VerificationTarget,4343
};4444
pub use transport::{ClientHandle, EventStream, Pending, SpaceHierarchy};4545
pub use uiaa::{@@ -658,6 +658,12 @@ pub enum Event {
658658
timeline_id: TimelineId,659659
state: HistoryState,660660
},661
+ /// the timeline was closed, or its room was left, and nothing more comes for it662
+ /// until another `TimelineReset`663
+ TimelineClosed {664
+ timeline_id: TimelineId,665
+ },666
+ /// a room that stops being watched says so with an empty `users`661667
TypingChanged(TypingState),662668
MediaUploadProgress {663669
room_id: OwnedRoomId,@@ -666,11 +672,19 @@ pub enum Event {
666672
total_bytes: u64,667673
complete: bool,668674
},675
+ /// not the end of it. `will_retry` means the send queue has another go by itself,676
+ /// otherwise it waits for `retry_send` or `cancel_send`669677
MediaUploadFailed {670678
room_id: OwnedRoomId,671679
transaction_id: OwnedTransactionId,672680
kind: crate::ErrorKind,673
- recoverable: bool,681
+ will_retry: bool,682
+ },683
+ /// every upload ends with exactly one of these684
+ MediaUploadFinished {685
+ room_id: OwnedRoomId,686
+ transaction_id: OwnedTransactionId,687
+ outcome: UploadOutcome,674688
},675689
EncryptionChanged(EncryptionState),676690
VerificationChanged(VerificationState),@@ -682,6 +696,20 @@ pub enum Event {
682696
Stopped,683697
}684698
699
+#[derive(Clone, Debug, Eq, PartialEq)]700
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]701
+#[non_exhaustive]702
+pub enum UploadOutcome {703
+ Sent {704
+ event_id: OwnedEventId,705
+ },706
+ Cancelled,707
+ /// its room was forgotten, and the queue with it708
+ RoomForgotten,709
+ /// the sdk's update channel overflowed while it ended, so whether it was sent is unknown710
+ Missed,711
+}712
+685713
/// why `Engine::run` came back, when nothing went wrong686714
#[derive(Clone, Debug, Eq, PartialEq)]687715
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]@@ -180,6 +180,10 @@ async fn keeps_open_room_timelines_independent() {
180180
.expect("both retained rooms did not update");181181
182182
handle.close_room(first_id.to_owned()).await.unwrap();183
+ receive_until(&mut events, |event| {184
+ matches!(event, Event::TimelineClosed { timeline_id } if timeline_id.room_id() == first_id)185
+ })186
+ .await;183187
server184188
.sync_room(185189
&sync_client,@@ -4,7 +4,7 @@ use std::{sync::Arc, time::Duration};
44
55
use macltok::{66
AttachmentUpload, ClientHandle, Engine, EngineConfig, EngineError, ErrorKind, Event,7
- EventStream, MediaId, MediaSize, UploadDetails, UploadThumbnail,7
+ EventStream, MediaId, MediaSize, UploadDetails, UploadOutcome, UploadThumbnail,88
};99
use matrix_sdk::{1010
media::{MediaFormat, MediaRequestParameters},@@ -600,6 +600,14 @@ async fn voice_upload_emits_voice_details_and_correlated_progress() {
600600
total_bytes,601601
complete: true,602602
} if room_id == room_id!("!media:localhost") && seen == transaction_id && uploaded_bytes == data.len() as u64 && total_bytes == data.len() as u64));603
+ support::receive_until(&mut harness.events, |event| {604
+ matches!(event, Event::MediaUploadFinished {605
+ transaction_id: seen,606
+ outcome: UploadOutcome::Sent { event_id },607
+ ..608
+ } if seen == &transaction_id && event_id == event_id!("$voice-upload:localhost"))609
+ })610
+ .await;603611
604612
let content = uploaded_content(&harness.server).await;605613
assert_eq!(content["msgtype"], "m.audio");