atagen

macltok

public
Star0
Watch0

Commit 411bc1c78b65

tarball

atagen <boss@atagen.co>2026-10-22 09:31 UTC

4 files changed+157-227
uploads: follow progress on the client-wide channel and drop the per-room forwarders

Upstream only sends upload progress on each room's own channel, which needed a forwarder per uploading room, a readiness handshake before queueing, restarts for resumed uploads, and a tracker reconciling two channels' views of the same upload. The fork's enable_global_upload_progress puts it all on one channel. The only ordering left to handle is the sdk's detached progress task trailing an upload's end.
Modified file: docs/design-decisions.md+12 additions−13 deletions
@@ -188,23 +188,20 @@ memory, the very thing the host streams to avoid. `media_source` hands over the
188188 url and keys `media` would have used, picked the same way, and `access_token`
189189 the rest. the byte cap doesn't apply, nothing is buffered
190190
191-uploads are commands that answer once the send queue has them. progress only
192-goes out on a room's own send queue channel, so each room that uploads gets a
193-forwarder, subscribed before the upload is queued and restarted for uploads the
194-sdk resumes from last time. everything but progress reaches the client-wide
195-channel, so one watcher there follows every attachment in the queue and ends
196-each with one `MediaUploadFinished`. sent, cancelled, its room forgotten, or
191+uploads are commands that answer once the send queue has them. the fork puts
192+upload progress on the client-wide send queue channel too, which upstream keeps
193+for each room's own, so one watcher there follows every attachment in the queue,
194+leftovers the sdk resumes from last time included, and ends each with one
195+`MediaUploadFinished`. sent, cancelled, its room forgotten, or
197196 missed when that channel overflowed at the wrong moment, since then there's no
198197 knowing which. a failure isn't an ending, a wedged upload can still be retried.
199198 uploads don't belong to a room's actor, so closing the room leaves them be
200199
201-the forwarders hand their progress to that watcher rather than the stream, so
202-it's the only thing speaking about uploads. anything that ever showed progress
203-or a failure is followed until it gets exactly one `MediaUploadFinished`, and
204-nothing about it goes out after. each forwarder relays its room's sends and
205-cancels behind its progress, so an end never overtakes the ticks before it. an
206-upload ended from anywhere else is remembered until its forwarder's own end
207-turns up, since until then there may be progress still queued for it. the
200+that watcher is the only thing speaking about uploads. anything that ever
201+showed progress or a failure is followed until it gets exactly one
202+`MediaUploadFinished`, and nothing about it goes out after. the sdk sends
203+progress from a detached task, so a tick can turn up behind its own upload's
204+end, and the watcher remembers the last few hundred ends to drop those. the
208205 engine stopping ends nothing, `Stopped` covers it
209206
210207 ## sessions and secrets
@@ -390,6 +387,8 @@ which keep their public sans-io apis
390387 sdk's hard-coded list leaves out. image packs use it
391388 - msc4027 reaction shortcodes, kept on the timeline's `ReactionInfo` and sent
392389 with `Timeline::toggle_reaction_with_shortcode`
390+- `SendQueue::enable_global_upload_progress`, upload progress on the
391+ client-wide send queue channel
393392
394393 ## public model
395394
Modified file: src/engine/mod.rs+1 additions−44 deletions
@@ -246,7 +246,6 @@ struct Queues {
246246 encryption: mpsc::Receiver<Job<EncryptionCommand>>,
247247 session_changes: broadcast::Receiver<SessionChange>,
248248 persistence: Option<Persistence>,
249- upload_reports: mpsc::Receiver<watchers::RoomUpload>,
250249 }
251250
252251 /// requests other devices send us, handed from the sdk's event handler to the encryption worker
@@ -270,9 +269,6 @@ pub struct Engine {
270269 notifications: mpsc::Sender<Job<NotificationCommand>>,
271270 encryption: mpsc::Sender<Job<EncryptionCommand>>,
272271 queues: Option<Queues>,
273- /// one progress forwarder per room that has uploaded, and whether it's subscribed yet
274- uploads: HashMap<OwnedRoomId, watch::Receiver<bool>>,
275- upload_reports: mpsc::Sender<watchers::RoomUpload>,
276272 /// the client can outlive us, so the handler has to come off it when we stop
277273 verification_handler: Option<matrix_sdk::event_handler::EventHandlerHandle>,
278274 stop: mpsc::UnboundedSender<Termination>,
@@ -334,9 +330,9 @@ impl Engine {
334330 config.event_capacity,
335331 );
336332 client.send_queue().enable_upload_progress(true);
333+ client.send_queue().enable_global_upload_progress(true);
337334 let (note_sender, notes) = mpsc::unbounded_channel();
338335 let (stop, stops) = mpsc::unbounded_channel();
339- let (upload_reports, upload_report_queue) = mpsc::channel(config.event_capacity);
340336 let (mutations, mutation_queue) = mpsc::channel(config.command_capacity);
341337 let (notifications, notification_queue) = mpsc::channel(config.command_capacity);
342338 let (encryption, encryption_queue) = mpsc::channel(config.command_capacity);
@@ -363,10 +359,7 @@ impl Engine {
363359 encryption: encryption_queue,
364360 session_changes,
365361 persistence,
366- upload_reports: upload_report_queue,
367362 }),
368- upload_reports,
369- uploads: HashMap::new(),
370363 verification_handler: None,
371364 preparations: prepare::Preparations::default(),
372365 stop,
@@ -403,7 +396,6 @@ impl Engine {
403396 }
404397 }
405398 self.start_tasks();
406- self.resume_upload_forwarders().await;
407399 self.sync.start().await;
408400
409401 let mut shutdown_reply = None;
@@ -470,7 +462,6 @@ impl Engine {
470462 Box::pin(watchers::uploads(
471463 client.clone(),
472464 client.send_queue().subscribe(),
473- queues.upload_reports,
474465 events(),
475466 )),
476467 ),
@@ -740,45 +731,12 @@ impl Engine {
740731 let Some(room) = self.client.get_room(upload.timeline_id.room_id()) else {
741732 return reply.done(Err(Error::room_unknown()));
742733 };
743- let mut ready = self.upload_forwarder(&room);
744734 let max_bytes = self.config.max_media_bytes;
745- // queueing before the forwarder listens would lose the first progress ticks
746735 self.spawn_command("upload", reply, async move {
747- ready
748- .wait_for(|subscribed| *subscribed)
749- .await
750- .map_err(|_| Error::new(ErrorKind::Store, "reading the media send queue failed"))?;
751736 media::queue_upload(room, upload, max_bytes).await
752737 });
753738 }
754739
755- fn upload_forwarder(&mut self, room: &matrix_sdk::Room) -> watch::Receiver<bool> {
756- self.uploads.retain(|_, ready| ready.has_changed().is_ok());
757- if let Some(ready) = self.uploads.get(room.room_id()) {
758- return ready.clone();
759- }
760- let (subscribed, ready) = watch::channel(false);
761- self.tasks.spawn(
762- TaskContext::Command("upload watcher"),
763- watchers::room_uploads(room.clone(), self.upload_reports.clone(), subscribed),
764- );
765- self.uploads
766- .insert(room.room_id().to_owned(), ready.clone());
767- ready
768- }
769-
770- /// the sdk resumes uploads left over from last time, and they deserve progress too
771- async fn resume_upload_forwarders(&mut self) {
772- let Ok(echoes) = self.client.send_queue().local_echoes().await else {
773- return;
774- };
775- for room_id in echoes.keys() {
776- if let Some(room) = self.client.get_room(room_id) {
777- self.upload_forwarder(&room);
778- }
779- }
780- }
781-
782740 fn route_timeline(&mut self, command: TimelineCommand, reply: Responder) {
783741 let sender = match self.rooms.get(command.room_id()) {
784742 Some(RoomSlot::Active { sender, .. }) => Some(sender.clone()),
@@ -948,7 +906,6 @@ impl Engine {
948906 self.port.close();
949907 self.tasks.shutdown().await;
950908 self.rooms.clear();
951- self.uploads.clear();
952909 self.media.clear().await;
953910 self.sync.stop().await;
954911 self.port
Modified file: src/engine/watchers.rs+140 additions−170 deletions
@@ -1,8 +1,12 @@
1-use std::{collections::HashSet, sync::Arc, time::Duration};
1+use std::{
2+ collections::{HashSet, VecDeque},
3+ sync::Arc,
4+ time::Duration,
5+};
26
37 use futures_util::StreamExt;
48 use matrix_sdk::{
5- Client, Room,
9+ Client,
610 ruma::events::{AnyMessageLikeEventContent, room::message::MessageType},
711 send_queue::{LocalEchoContent, RoomSendQueueUpdate, SendQueueUpdate},
812 };
@@ -14,7 +18,7 @@ use ruma::{
1418 serde::Raw,
1519 time::Instant,
1620 };
17-use tokio::sync::{Notify, broadcast::error::RecvError, mpsc, watch};
21+use tokio::sync::{Notify, broadcast::error::RecvError, watch};
1822
1923 use super::executor::TaskSet;
2024 use crate::{
@@ -122,84 +126,20 @@ pub(super) async fn send_queue_errors(
122126 }
123127 }
124128
125-/// upload progress only goes out on the room's own channel, never the client-wide one.
126-/// the room's sends and cancels come along behind it, so an upload's end can't
127-/// overtake its own progress
128-pub(super) async fn room_uploads(
129- room: Room,
130- tracker: mpsc::Sender<RoomUpload>,
131- ready: watch::Sender<bool>,
132-) {
133- let room_id = room.room_id().to_owned();
134- let mut updates = match room.send_queue().subscribe().await {
135- Ok((_, updates)) => updates,
136- Err(error) => {
137- tracing::warn!(%room_id, %error, "reading the media send queue failed");
138- return;
139- }
140- };
141- ready.send_replace(true);
142- loop {
143- let report = match updates.recv().await {
144- Ok(RoomSendQueueUpdate::MediaUpload {
145- related_to,
146- file,
147- progress,
148- ..
149- }) => RoomUpload::Progress {
150- upload: (room_id.clone(), related_to),
151- uploaded_bytes: progress.current as u64,
152- total_bytes: progress.total as u64,
153- complete: file.is_some() && progress.current == progress.total,
154- },
155- Ok(RoomSendQueueUpdate::SentEvent {
156- transaction_id,
157- event_id,
158- }) => RoomUpload::Ended(
159- (room_id.clone(), transaction_id),
160- UploadOutcome::Sent { event_id },
161- ),
162- Ok(RoomSendQueueUpdate::CancelledLocalEvent { transaction_id }) => {
163- RoomUpload::Ended((room_id.clone(), transaction_id), UploadOutcome::Cancelled)
164- }
165- Ok(_) => continue,
166- Err(RecvError::Lagged(_)) => RoomUpload::Lagged,
167- Err(RecvError::Closed) => return,
168- };
169- if tracker.send(report).await.is_err() {
170- return;
171- }
172- }
173-}
174-
175-pub(super) enum RoomUpload {
176- Progress {
177- upload: Upload,
178- uploaded_bytes: u64,
179- total_bytes: u64,
180- complete: bool,
181- },
182- Ended(Upload, UploadOutcome),
183- /// an end may have been lost with the ticks
184- Lagged,
185-}
186-
187129 /// the one place upload events leave from, so an upload is never reported after it
188-/// ended. progress comes in from the room forwarders, the rest from the client-wide
189-/// channel, which exists before any upload does
130+/// ended. the client-wide channel exists before any upload does. it holds 32 updates and
131+/// progress ticks share it, so this keeps draining while the event stream is full
190132 pub(super) async fn uploads(
191133 client: Client,
192134 mut updates: tokio::sync::broadcast::Receiver<SendQueueUpdate>,
193- mut rooms_reports: mpsc::Receiver<RoomUpload>,
194135 events: EventSender,
195136 ) {
196137 let (_, mut rooms) = client.rooms_stream();
197- let mut tracker = UploadTracker {
198- live: attachment_echoes(&client).await,
199- ended: HashSet::new(),
200- };
138+ let mut tracker = UploadTracker::new(attachment_echoes(&client).await);
139+ let mut pending = VecDeque::new();
201140 loop {
202- let reports = tokio::select! {
141+ let reports: Vec<Event> = tokio::select! {
142+ biased;
203143 update = updates.recv() => match update {
204144 Ok(update) => tracker.update(update).into_iter().collect(),
205145 Err(RecvError::Lagged(skipped)) => {
@@ -208,10 +148,6 @@ pub(super) async fn uploads(
208148 }
209149 Err(RecvError::Closed) => return,
210150 },
211- Some(report) = rooms_reports.recv() => match report {
212- RoomUpload::Lagged => tracker.resnapshot(attachment_echoes(&client).await),
213- report => tracker.room(report).into_iter().collect(),
214- },
215151 update = rooms.next() => {
216152 if update.is_none() {
217153 return;
@@ -221,26 +157,77 @@ pub(super) async fn uploads(
221157 &UploadOutcome::RoomForgotten,
222158 )
223159 }
160+ permit = events.reserve(), if !pending.is_empty() => {
161+ let Ok(permit) = permit else { return };
162+ permit.send(pending.pop_front().expect("pending was checked"));
163+ continue;
164+ }
224165 };
225166 for event in reports {
226- if events.send(event).await.is_err() {
227- return;
228- }
167+ queue_upload_event(&mut pending, event);
168+ }
169+ }
170+}
171+
172+/// a progress tick replaces a waiting one for the same upload, unless something else about
173+/// that upload is queued after it
174+fn queue_upload_event(pending: &mut VecDeque<Event>, event: Event) {
175+ if matches!(event, Event::MediaUploadProgress { .. })
176+ && let Some(waiting) = pending
177+ .iter_mut()
178+ .rev()
179+ .find(|waiting| upload_of(waiting) == upload_of(&event))
180+ && matches!(waiting, Event::MediaUploadProgress { .. })
181+ {
182+ *waiting = event;
183+ return;
184+ }
185+ pending.push_back(event);
186+}
187+
188+fn upload_of(event: &Event) -> Option<(&OwnedRoomId, &OwnedTransactionId)> {
189+ match event {
190+ Event::MediaUploadProgress {
191+ room_id,
192+ transaction_id,
193+ ..
194+ }
195+ | Event::MediaUploadFailed {
196+ room_id,
197+ transaction_id,
198+ ..
229199 }
200+ | Event::MediaUploadFinished {
201+ room_id,
202+ transaction_id,
203+ ..
204+ } => Some((room_id, transaction_id)),
205+ _ => None,
230206 }
231207 }
232208
233209 type Upload = (OwnedRoomId, OwnedTransactionId);
234210
235-/// everything reported gets exactly one `MediaUploadFinished`. an upload ended from
236-/// anywhere but its room's forwarder goes in `ended` until the forwarder's own end for
237-/// it arrives, since until then the forwarder can still be holding progress for it
211+/// the sdk sends progress ticks from a detached task, so a tick can land after its
212+/// upload's end. it trails by one task hop, so a short memory of ends covers it
213+const REMEMBERED_ENDS: usize = 256;
214+
215+/// everything reported gets exactly one `MediaUploadFinished` and nothing after it
238216 struct UploadTracker {
239217 live: HashSet<Upload>,
240218 ended: HashSet<Upload>,
219+ ended_order: VecDeque<Upload>,
241220 }
242221
243222 impl UploadTracker {
223+ fn new(live: HashSet<Upload>) -> Self {
224+ Self {
225+ live,
226+ ended: HashSet::new(),
227+ ended_order: VecDeque::new(),
228+ }
229+ }
230+
244231 fn update(&mut self, update: SendQueueUpdate) -> Option<Event> {
245232 let room_id = update.room_id;
246233 let (transaction_id, outcome) = match update.update {
@@ -251,6 +238,26 @@ impl UploadTracker {
251238 }
252239 return None;
253240 }
241+ RoomSendQueueUpdate::MediaUpload {
242+ related_to,
243+ file,
244+ progress,
245+ ..
246+ } => {
247+ let upload = (room_id, related_to);
248+ if self.ended.contains(&upload) {
249+ return None;
250+ }
251+ self.live.insert(upload.clone());
252+ let (room_id, transaction_id) = upload;
253+ return Some(Event::MediaUploadProgress {
254+ room_id,
255+ transaction_id,
256+ uploaded_bytes: progress.current as u64,
257+ total_bytes: progress.total as u64,
258+ complete: file.is_some() && progress.current == progress.total,
259+ });
260+ }
254261 RoomSendQueueUpdate::SendError {
255262 transaction_id,
256263 error,
@@ -277,36 +284,7 @@ impl UploadTracker {
277284 }
278285 _ => return None,
279286 };
280- self.finish((room_id, transaction_id), outcome, true)
281- }
282-
283- fn room(&mut self, report: RoomUpload) -> Option<Event> {
284- match report {
285- RoomUpload::Progress {
286- upload,
287- uploaded_bytes,
288- total_bytes,
289- complete,
290- } => {
291- if self.ended.contains(&upload) {
292- return None;
293- }
294- self.live.insert(upload.clone());
295- let (room_id, transaction_id) = upload;
296- Some(Event::MediaUploadProgress {
297- room_id,
298- transaction_id,
299- uploaded_bytes,
300- total_bytes,
301- complete,
302- })
303- }
304- RoomUpload::Ended(upload, outcome) => {
305- self.ended.remove(&upload);
306- self.finish(upload, outcome, false)
307- }
308- RoomUpload::Lagged => None,
309- }
287+ self.finish((room_id, transaction_id), outcome)
310288 }
311289
312290 fn resnapshot(&mut self, current: HashSet<Upload>) -> Vec<Event> {
@@ -330,17 +308,23 @@ impl UploadTracker {
330308 .collect::<Vec<_>>();
331309 ending
332310 .into_iter()
333- .filter_map(|upload| self.finish(upload, outcome.clone(), true))
311+ .filter_map(|upload| self.finish(upload, outcome.clone()))
334312 .collect()
335313 }
336314
337- fn finish(&mut self, upload: Upload, outcome: UploadOutcome, remember: bool) -> Option<Event> {
315+ /// an end for an upload we never saw is remembered too, its ticks may still be coming
316+ fn finish(&mut self, upload: Upload, outcome: UploadOutcome) -> Option<Event> {
317+ if self.ended.insert(upload.clone()) {
318+ self.ended_order.push_back(upload.clone());
319+ if self.ended_order.len() > REMEMBERED_ENDS
320+ && let Some(oldest) = self.ended_order.pop_front()
321+ {
322+ self.ended.remove(&oldest);
323+ }
324+ }
338325 if !self.live.remove(&upload) {
339326 return None;
340327 }
341- if remember {
342- self.ended.insert(upload.clone());
343- }
344328 let (room_id, transaction_id) = upload;
345329 Some(Event::MediaUploadFinished {
346330 room_id,
@@ -504,10 +488,10 @@ pub(super) async fn notification_settings(client: Client, events: EventSender) {
504488
505489 #[cfg(test)]
506490 mod tests {
507- use matrix_sdk::send_queue::{RoomSendQueueUpdate, SendQueueUpdate};
491+ use matrix_sdk::send_queue::{AbstractProgress, RoomSendQueueUpdate, SendQueueUpdate};
508492 use ruma::{OwnedTransactionId, event_id, owned_room_id};
509493
510- use super::{HashSet, RoomUpload, Upload, UploadTracker};
494+ use super::{HashSet, Upload, UploadTracker};
511495 use crate::{Event, UploadOutcome};
512496
513497 fn upload() -> Upload {
@@ -517,26 +501,30 @@ mod tests {
517501 )
518502 }
519503
520- fn tick() -> RoomUpload {
521- RoomUpload::Progress {
522- upload: upload(),
523- uploaded_bytes: 1,
524- total_bytes: 2,
525- complete: false,
504+ fn update(update: RoomSendQueueUpdate) -> SendQueueUpdate {
505+ SendQueueUpdate {
506+ room_id: upload().0,
507+ update,
526508 }
527509 }
528510
529- fn sent() -> UploadOutcome {
530- UploadOutcome::Sent {
531- event_id: event_id!("$sent:localhost").to_owned(),
532- }
511+ fn tick() -> SendQueueUpdate {
512+ update(RoomSendQueueUpdate::MediaUpload {
513+ related_to: upload().1,
514+ file: None,
515+ index: 0,
516+ progress: AbstractProgress {
517+ current: 1,
518+ total: 2,
519+ },
520+ })
533521 }
534522
535- fn tracker() -> UploadTracker {
536- UploadTracker {
537- live: HashSet::new(),
538- ended: HashSet::new(),
539- }
523+ fn sent() -> SendQueueUpdate {
524+ update(RoomSendQueueUpdate::SentEvent {
525+ transaction_id: upload().1,
526+ event_id: event_id!("$sent:localhost").to_owned(),
527+ })
540528 }
541529
542530 fn finished(event: Option<Event>) -> Option<UploadOutcome> {
@@ -547,9 +535,21 @@ mod tests {
547535 }
548536
549537 #[test]
538+ fn a_tick_trailing_its_end_says_nothing() {
539+ let mut tracker = UploadTracker::new(HashSet::new());
540+ assert!(tracker.update(tick()).is_some());
541+ assert!(matches!(
542+ finished(tracker.update(sent())),
543+ Some(UploadOutcome::Sent { .. })
544+ ));
545+ assert!(tracker.update(tick()).is_none());
546+ assert!(tracker.live.is_empty());
547+ }
548+
549+ #[test]
550550 fn a_stale_tick_after_a_missed_end_changes_nothing() {
551- let mut tracker = tracker();
552- assert!(tracker.room(tick()).is_some());
551+ let mut tracker = UploadTracker::new(HashSet::new());
552+ assert!(tracker.update(tick()).is_some());
553553 let missed = tracker.resnapshot(HashSet::new());
554554 assert!(matches!(
555555 missed.as_slice(),
@@ -558,38 +558,8 @@ mod tests {
558558 ..
559559 }]
560560 ));
561-
562- assert!(tracker.room(tick()).is_none());
563- let (room_id, transaction_id) = upload();
564- let global = SendQueueUpdate {
565- room_id,
566- update: RoomSendQueueUpdate::SentEvent {
567- transaction_id,
568- event_id: event_id!("$sent:localhost").to_owned(),
569- },
570- };
571- assert!(tracker.update(global).is_none());
572- assert!(tracker.room(RoomUpload::Ended(upload(), sent())).is_none());
573- assert!(tracker.live.is_empty() && tracker.ended.is_empty());
574- }
575-
576- #[test]
577- fn an_end_the_tracker_never_knew_about_still_follows_its_progress() {
578- let mut tracker = tracker();
579- let (room_id, transaction_id) = upload();
580- let global = SendQueueUpdate {
581- room_id,
582- update: RoomSendQueueUpdate::SentEvent {
583- transaction_id,
584- event_id: event_id!("$sent:localhost").to_owned(),
585- },
586- };
587- assert!(tracker.update(global).is_none());
588- assert!(tracker.room(tick()).is_some());
589- assert_eq!(
590- finished(tracker.room(RoomUpload::Ended(upload(), sent()))),
591- Some(sent())
592- );
593- assert!(tracker.live.is_empty() && tracker.ended.is_empty());
561+ assert!(tracker.update(tick()).is_none());
562+ assert!(tracker.update(sent()).is_none());
563+ assert!(tracker.live.is_empty());
594564 }
595565 }
Modified file: src/transport.rs+4 additions
@@ -992,6 +992,10 @@ impl EventSender {
992992 self.0.send(event).await.map_err(|_| SendEventError)
993993 }
994994
995+ pub(crate) async fn reserve(&self) -> Result<mpsc::Permit<'_, Event>, SendEventError> {
996+ self.0.reserve().await.map_err(|_| SendEventError)
997+ }
998+
995999 /// resolves once every `EventStream` is gone
9961000 pub(crate) async fn closed(&self) {
9971001 self.0.closed().await;