atagen

macltok

public
Star0
Watch0

Commit 7cf348521204

tarball

atagen <boss@atagen.co>2026-09-27 01:42 UTC

4 files changed+158-79
engine: never let an upload outlive its end

An end seen only on the client-wide channel for an upload the tracker hadn't met yet was dropped without a trace, so progress still queued in the room forwarder brought the upload back with nothing left to end it. Each forwarder now relays its room's sends and cancels behind its progress, which orders an upload's end after its ticks, and an end from anywhere else is remembered only until that relayed end arrives rather than for the whole run.
Modified file: docs/design-decisions.md+5 additions−3 deletions
@@ -155,9 +155,11 @@ uploads don't belong to a room's actor, so closing the room leaves them be
155155 the forwarders hand their progress to that watcher rather than the stream, so
156156 it's the only thing speaking about uploads. anything that ever showed progress
157157 or a failure is followed until it gets exactly one `MediaUploadFinished`, and
158-nothing about it goes out after. it remembers what has ended for as long as it
159-runs, since a forwarder can still be sitting on progress for an upload that's
160-already sent and nothing says when that backlog is gone
158+nothing about it goes out after. each forwarder relays its room's sends and
159+cancels behind its progress, so an end never overtakes the ticks before it. an
160+upload ended from anywhere else is remembered until its forwarder's own end
161+turns up, since until then there may be progress still queued for it. the
162+engine stopping ends nothing, `Stopped` covers it
161163
162164 ## sessions and secrets
163165
Modified file: src/engine/mod.rs+7 additions−7 deletions
@@ -219,7 +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>,
222+ upload_reports: mpsc::Receiver<watchers::RoomUpload>,
223223 }
224224
225225 /// requests other devices send us, handed from the sdk's event handler to the encryption worker
@@ -245,7 +245,7 @@ pub struct Engine {
245245 queues: Option<Queues>,
246246 /// one progress forwarder per room that has uploaded, and whether it's subscribed yet
247247 uploads: HashMap<OwnedRoomId, watch::Receiver<bool>>,
248- upload_progress: mpsc::Sender<watchers::UploadProgress>,
248+ upload_reports: mpsc::Sender<watchers::RoomUpload>,
249249 /// the client can outlive us, so the handler has to come off it when we stop
250250 verification_handler: Option<matrix_sdk::event_handler::EventHandlerHandle>,
251251 stop: mpsc::UnboundedSender<Termination>,
@@ -290,7 +290,7 @@ impl Engine {
290290 client.send_queue().enable_upload_progress(true);
291291 let (note_sender, notes) = mpsc::unbounded_channel();
292292 let (stop, stops) = mpsc::unbounded_channel();
293- let (upload_progress, upload_progress_queue) = mpsc::channel(config.event_capacity);
293+ let (upload_reports, upload_report_queue) = mpsc::channel(config.event_capacity);
294294 let (mutations, mutation_queue) = mpsc::channel(config.command_capacity);
295295 let (notifications, notification_queue) = mpsc::channel(config.command_capacity);
296296 let (encryption, encryption_queue) = mpsc::channel(config.command_capacity);
@@ -316,9 +316,9 @@ impl Engine {
316316 notifications: notification_queue,
317317 encryption: encryption_queue,
318318 session_changes,
319- upload_progress: upload_progress_queue,
319+ upload_reports: upload_report_queue,
320320 }),
321- upload_progress,
321+ upload_reports,
322322 uploads: HashMap::new(),
323323 verification_handler: None,
324324 stop,
@@ -410,7 +410,7 @@ impl Engine {
410410 Box::pin(watchers::uploads(
411411 client.clone(),
412412 client.send_queue().subscribe(),
413- queues.upload_progress,
413+ queues.upload_reports,
414414 events(),
415415 )),
416416 ),
@@ -654,7 +654,7 @@ impl Engine {
654654 let (subscribed, ready) = watch::channel(false);
655655 self.tasks.spawn(
656656 TaskContext::Command("upload watcher"),
657- watchers::room_uploads(room.clone(), self.upload_progress.clone(), subscribed),
657+ watchers::room_uploads(room.clone(), self.upload_reports.clone(), subscribed),
658658 );
659659 self.uploads
660660 .insert(room.room_id().to_owned(), ready.clone());
Modified file: src/engine/watchers.rs+143 additions−68 deletions
@@ -145,10 +145,12 @@ pub(super) async fn send_queue_errors(
145145 }
146146 }
147147
148-/// upload progress only goes out on the room's own channel, never the client-wide one
148+/// upload progress only goes out on the room's own channel, never the client-wide one.
149+/// the room's sends and cancels come along behind it, so an upload's end can't
150+/// overtake its own progress
149151 pub(super) async fn room_uploads(
150152 room: Room,
151- progress: mpsc::Sender<UploadProgress>,
153+ tracker: mpsc::Sender<RoomUpload>,
152154 ready: watch::Sender<bool>,
153155 ) {
154156 let room_id = room.room_id().to_owned();
@@ -161,38 +163,49 @@ pub(super) async fn room_uploads(
161163 };
162164 ready.send_replace(true);
163165 loop {
164- let update = match updates.recv().await {
165- Ok(update) => update,
166- // a missed tick is fine, the next one says where it's got to
167- Err(RecvError::Lagged(_)) => continue,
166+ let report = match updates.recv().await {
167+ Ok(RoomSendQueueUpdate::MediaUpload {
168+ related_to,
169+ file,
170+ progress,
171+ ..
172+ }) => RoomUpload::Progress {
173+ upload: (room_id.clone(), related_to),
174+ uploaded_bytes: progress.current as u64,
175+ total_bytes: progress.total as u64,
176+ complete: file.is_some() && progress.current == progress.total,
177+ },
178+ Ok(RoomSendQueueUpdate::SentEvent {
179+ transaction_id,
180+ event_id,
181+ }) => RoomUpload::Ended(
182+ (room_id.clone(), transaction_id),
183+ UploadOutcome::Sent { event_id },
184+ ),
185+ Ok(RoomSendQueueUpdate::CancelledLocalEvent { transaction_id }) => RoomUpload::Ended(
186+ (room_id.clone(), transaction_id),
187+ UploadOutcome::Cancelled,
188+ ),
189+ Ok(_) => continue,
190+ Err(RecvError::Lagged(_)) => RoomUpload::Lagged,
168191 Err(RecvError::Closed) => return,
169192 };
170- let RoomSendQueueUpdate::MediaUpload {
171- related_to,
172- file,
173- progress: done,
174- ..
175- } = update
176- else {
177- continue;
178- };
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,
184- };
185- if progress.send(tick).await.is_err() {
193+ if tracker.send(report).await.is_err() {
186194 return;
187195 }
188196 }
189197 }
190198
191-pub(super) struct UploadProgress {
192- upload: Upload,
193- uploaded_bytes: u64,
194- total_bytes: u64,
195- complete: bool,
199+pub(super) enum RoomUpload {
200+ Progress {
201+ upload: Upload,
202+ uploaded_bytes: u64,
203+ total_bytes: u64,
204+ complete: bool,
205+ },
206+ Ended(Upload, UploadOutcome),
207+ /// an end may have been lost with the ticks
208+ Lagged,
196209 }
197210
198211 /// the one place upload events leave from, so an upload is never reported after it
@@ -201,7 +214,7 @@ pub(super) struct UploadProgress {
201214 pub(super) async fn uploads(
202215 client: Client,
203216 mut updates: tokio::sync::broadcast::Receiver<SendQueueUpdate>,
204- mut progress: mpsc::Receiver<UploadProgress>,
217+ mut rooms_reports: mpsc::Receiver<RoomUpload>,
205218 events: EventSender,
206219 ) {
207220 let (_, mut rooms) = client.rooms_stream();
@@ -214,13 +227,15 @@ pub(super) async fn uploads(
214227 update = updates.recv() => match update {
215228 Ok(update) => tracker.update(update).into_iter().collect(),
216229 Err(RecvError::Lagged(skipped)) => {
217- tracing::warn!(skipped, "upload watcher lagged");
218- let current = attachment_echoes(&client).await;
219- tracker.resnapshot(current)
230+ tracing::warn!(skipped, "upload tracker lagged");
231+ tracker.resnapshot(attachment_echoes(&client).await)
220232 }
221233 Err(RecvError::Closed) => return,
222234 },
223- Some(tick) = progress.recv() => tracker.progress(tick).into_iter().collect(),
235+ Some(report) = rooms_reports.recv() => match report {
236+ RoomUpload::Lagged => tracker.resnapshot(attachment_echoes(&client).await),
237+ report => tracker.room(report).into_iter().collect(),
238+ },
224239 update = rooms.next() => {
225240 if update.is_none() {
226241 return;
@@ -241,9 +256,9 @@ pub(super) async fn uploads(
241256
242257 type Upload = (OwnedRoomId, OwnedTransactionId);
243258
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-wide
246-/// channel has already seen sent
259+/// everything reported gets exactly one `MediaUploadFinished`. an upload ended from
260+/// anywhere but its room's forwarder goes in `ended` until the forwarder's own end for
261+/// it arrives, since until then the forwarder can still be holding progress for it
247262 struct UploadTracker {
248263 live: HashSet<Upload>,
249264 ended: HashSet<Upload>,
@@ -286,22 +301,36 @@ impl UploadTracker {
286301 }
287302 _ => return None,
288303 };
289- self.finish((room_id, transaction_id), outcome)
304+ self.finish((room_id, transaction_id), outcome, true)
290305 }
291306
292- fn progress(&mut self, tick: UploadProgress) -> Option<Event> {
293- if self.ended.contains(&tick.upload) {
294- return None;
307+ fn room(&mut self, report: RoomUpload) -> Option<Event> {
308+ match report {
309+ RoomUpload::Progress {
310+ upload,
311+ uploaded_bytes,
312+ total_bytes,
313+ complete,
314+ } => {
315+ if self.ended.contains(&upload) {
316+ return None;
317+ }
318+ self.live.insert(upload.clone());
319+ let (room_id, transaction_id) = upload;
320+ Some(Event::MediaUploadProgress {
321+ room_id,
322+ transaction_id,
323+ uploaded_bytes,
324+ total_bytes,
325+ complete,
326+ })
327+ }
328+ RoomUpload::Ended(upload, outcome) => {
329+ self.ended.remove(&upload);
330+ self.finish(upload, outcome, false)
331+ }
332+ RoomUpload::Lagged => None,
295333 }
296- self.live.insert(tick.upload.clone());
297- let (room_id, transaction_id) = tick.upload;
298- Some(Event::MediaUploadProgress {
299- room_id,
300- transaction_id,
301- uploaded_bytes: tick.uploaded_bytes,
302- total_bytes: tick.total_bytes,
303- complete: tick.complete,
304- })
305334 }
306335
307336 fn resnapshot(&mut self, current: HashSet<Upload>) -> Vec<Event> {
@@ -325,15 +354,17 @@ impl UploadTracker {
325354 .collect::<Vec<_>>();
326355 ending
327356 .into_iter()
328- .filter_map(|upload| self.finish(upload, outcome.clone()))
357+ .filter_map(|upload| self.finish(upload, outcome.clone(), true))
329358 .collect()
330359 }
331360
332- fn finish(&mut self, upload: Upload, outcome: UploadOutcome) -> Option<Event> {
361+ fn finish(&mut self, upload: Upload, outcome: UploadOutcome, remember: bool) -> Option<Event> {
333362 if !self.live.remove(&upload) {
334363 return None;
335364 }
336- self.ended.insert(upload.clone());
365+ if remember {
366+ self.ended.insert(upload.clone());
367+ }
337368 let (room_id, transaction_id) = upload;
338369 Some(Event::MediaUploadFinished {
339370 room_id,
@@ -445,27 +476,49 @@ mod tests {
445476 use matrix_sdk::send_queue::{RoomSendQueueUpdate, SendQueueUpdate};
446477 use ruma::{OwnedTransactionId, event_id, owned_room_id};
447478
448- use super::{HashSet, UploadProgress, UploadTracker};
479+ use super::{HashSet, RoomUpload, Upload, UploadTracker};
449480 use crate::{Event, UploadOutcome};
450481
451- #[test]
452- fn an_upload_only_progress_saw_still_ends_once() {
453- let upload = (
482+ fn upload() -> Upload {
483+ (
454484 owned_room_id!("!room:localhost"),
455485 OwnedTransactionId::from("upload"),
456- );
457- let tick = || UploadProgress {
458- upload: upload.clone(),
486+ )
487+ }
488+
489+ fn tick() -> RoomUpload {
490+ RoomUpload::Progress {
491+ upload: upload(),
459492 uploaded_bytes: 1,
460493 total_bytes: 2,
461494 complete: false,
462- };
463- let mut tracker = UploadTracker {
495+ }
496+ }
497+
498+ fn sent() -> UploadOutcome {
499+ UploadOutcome::Sent {
500+ event_id: event_id!("$sent:localhost").to_owned(),
501+ }
502+ }
503+
504+ fn tracker() -> UploadTracker {
505+ UploadTracker {
464506 live: HashSet::new(),
465507 ended: HashSet::new(),
466- };
467- assert!(tracker.progress(tick()).is_some());
508+ }
509+ }
510+
511+ fn finished(event: Option<Event>) -> Option<UploadOutcome> {
512+ match event? {
513+ Event::MediaUploadFinished { outcome, .. } => Some(outcome),
514+ _ => None,
515+ }
516+ }
468517
518+ #[test]
519+ fn a_stale_tick_after_a_missed_end_changes_nothing() {
520+ let mut tracker = tracker();
521+ assert!(tracker.room(tick()).is_some());
469522 let missed = tracker.resnapshot(HashSet::new());
470523 assert!(matches!(
471524 missed.as_slice(),
@@ -475,15 +528,37 @@ mod tests {
475528 }]
476529 ));
477530
478- assert!(tracker.progress(tick()).is_none());
479- let sent = SendQueueUpdate {
480- room_id: upload.0.clone(),
531+ assert!(tracker.room(tick()).is_none());
532+ let (room_id, transaction_id) = upload();
533+ let global = SendQueueUpdate {
534+ room_id,
535+ update: RoomSendQueueUpdate::SentEvent {
536+ transaction_id,
537+ event_id: event_id!("$sent:localhost").to_owned(),
538+ },
539+ };
540+ assert!(tracker.update(global).is_none());
541+ assert!(tracker.room(RoomUpload::Ended(upload(), sent())).is_none());
542+ assert!(tracker.live.is_empty() && tracker.ended.is_empty());
543+ }
544+
545+ #[test]
546+ fn an_end_the_tracker_never_knew_about_still_follows_its_progress() {
547+ let mut tracker = tracker();
548+ let (room_id, transaction_id) = upload();
549+ let global = SendQueueUpdate {
550+ room_id,
481551 update: RoomSendQueueUpdate::SentEvent {
482- transaction_id: upload.1.clone(),
552+ transaction_id,
483553 event_id: event_id!("$sent:localhost").to_owned(),
484554 },
485555 };
486- assert!(tracker.update(sent).is_none());
487- assert!(tracker.resnapshot(HashSet::from([upload])).is_empty());
556+ assert!(tracker.update(global).is_none());
557+ assert!(tracker.room(tick()).is_some());
558+ assert_eq!(
559+ finished(tracker.room(RoomUpload::Ended(upload(), sent()))),
560+ Some(sent())
561+ );
562+ assert!(tracker.live.is_empty() && tracker.ended.is_empty());
488563 }
489564 }
Modified file: src/model.rs+3 additions−1 deletions
@@ -681,7 +681,9 @@ pub enum Event {
681681 kind: crate::ErrorKind,
682682 will_retry: bool,
683683 },
684- /// every upload ends with exactly one of these
684+ /// an upload that turned up in a progress or failure event gets exactly one of
685+ /// these while the engine runs, and nothing about it after. stopping ends
686+ /// nothing, `Stopped` covers whatever is still going and the sdk resumes it next run
685687 MediaUploadFinished {
686688 room_id: OwnedRoomId,
687689 transaction_id: OwnedTransactionId,