atagen

macltok

public
Star0
Watch0

Commit 673563a76e43

tarball

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

2 files changed+75-20
engine: let a logout finish and say so

A logout revokes its own token, so the sync it races can hear the rejection first and end the engine as AccessRevoked with the logout aborted halfway. A rejection that turns up while a logout is in flight is now held for that logout, and only speaks if the logout fails. Dropping every handle waits for a logout in flight rather than aborting it, and the stop channel is read before the closed event stream so a host that drops the stream straight after LoggedOut still learns how it ended.
Modified file: src/engine/mod.rs+46 additions−11 deletions
@@ -16,7 +16,7 @@ use std::{
1616 use futures_util::FutureExt;
1717 use matrix_sdk::{Client, SendOutsideWasm, SessionChange};
1818 use matrix_sdk_ui::sync_service::SyncService;
19-use ruma::OwnedRoomId;
19+use ruma::{OwnedDeviceId, OwnedRoomId};
2020 use thiserror::Error as ThisError;
2121 use tokio::sync::{broadcast, mpsc, watch};
2222
@@ -214,6 +214,13 @@ enum Deferred {
214214 Close(Responder),
215215 }
216216
217+/// logouts in flight, and a token rejection held back because one of them caused it
218+#[derive(Default)]
219+pub(super) struct Logouts {
220+ pub(super) in_flight: usize,
221+ pub(super) rejected: Option<Option<OwnedDeviceId>>,
222+}
223+
217224 struct Queues {
218225 mutations: mpsc::Receiver<MutationJob>,
219226 notifications: mpsc::Receiver<Job<NotificationCommand>>,
@@ -250,6 +257,7 @@ pub struct Engine {
250257 verification_handler: Option<matrix_sdk::event_handler::EventHandlerHandle>,
251258 stop: mpsc::UnboundedSender<Termination>,
252259 stops: mpsc::UnboundedReceiver<Termination>,
260+ logouts: Arc<watch::Sender<Logouts>>,
253261 }
254262
255263 impl Engine {
@@ -323,6 +331,7 @@ impl Engine {
323331 verification_handler: None,
324332 stop,
325333 stops,
334+ logouts: Arc::new(watch::Sender::new(Logouts::default())),
326335 };
327336 Ok((engine, handle, events))
328337 }
@@ -357,11 +366,16 @@ impl Engine {
357366 self.sync.start().await;
358367
359368 let mut shutdown_reply = None;
369+ let mut handles_dropped = false;
370+ let mut logouts = self.logouts.subscribe();
360371 let ending = loop {
361372 tokio::select! {
362373 biased;
363- () = events.closed() => break Err(EngineError::EventReceiverClosed),
364374 Some(termination) = self.stops.recv() => break Ok(termination),
375+ () = events.closed() => break Err(EngineError::EventReceiverClosed),
376+ () = settled(&mut logouts), if handles_dropped => {
377+ break Ok(Termination::HandlesDropped);
378+ }
365379 Some((context, panic)) = self.tasks.join_next(), if !self.tasks.is_empty() => {
366380 let Some(message) = panic else { continue };
367381 if let TaskContext::Command(label) = context {
@@ -371,8 +385,12 @@ impl Engine {
371385 break Err(EngineError::TaskPanicked { task: context.label() });
372386 }
373387 Some(note) = self.notes.recv() => self.actor_note(note).await,
374- request = self.port.recv() => {
375- let Some(request) = request else { break Ok(Termination::HandlesDropped) };
388+ request = self.port.recv(), if !handles_dropped => {
389+ // nobody's left to ask, but a logout they asked for still deserves to land
390+ let Some(request) = request else {
391+ handles_dropped = true;
392+ continue;
393+ };
376394 if let Some(reply) = self.dispatch(request).await {
377395 shutdown_reply = Some(reply);
378396 break Ok(Termination::ShutDown);
@@ -431,6 +449,7 @@ impl Engine {
431449 .expect("authentication was checked during construction")
432450 .to_owned(),
433451 self.stop.clone(),
452+ self.logouts.clone(),
434453 )),
435454 ),
436455 (
@@ -525,14 +544,26 @@ impl Engine {
525544 Command::Account(AccountCommand::Logout) => {
526545 let events = self.port.event_sender();
527546 let stop = self.stop.clone();
547+ let logouts = self.logouts.clone();
548+ logouts.send_modify(|logouts| logouts.in_flight += 1);
528549 self.spawn_command("logout", reply, async move {
529- client
530- .logout()
531- .await
532- .map_err(|source| Error::from_sdk("logout failed", source))?;
533- let _ = events.send(Event::LoggedOut).await;
534- let _ = stop.send(Termination::LoggedOut);
535- Ok(())
550+ let result = client.logout().await;
551+ if result.is_ok() {
552+ let _ = events.send(Event::LoggedOut).await;
553+ let _ = stop.send(Termination::LoggedOut);
554+ }
555+ let mut rejected = None;
556+ logouts.send_if_modified(|logouts| {
557+ if result.is_err() && logouts.in_flight == 1 {
558+ rejected = logouts.rejected.take();
559+ }
560+ false
561+ });
562+ if let Some(soft_logout) = rejected {
563+ watchers::reject_session(&events, &stop, soft_logout).await;
564+ }
565+ logouts.send_modify(|logouts| logouts.in_flight -= 1);
566+ result.map_err(|source| Error::from_sdk("logout failed", source))
536567 });
537568 }
538569 Command::Account(AccountCommand::SetAccountData {
@@ -883,6 +914,10 @@ fn defer(pending: &mut VecDeque<Deferred>, deferred: Deferred, capacity: usize)
883914 )));
884915 }
885916
917+async fn settled(logouts: &mut watch::Receiver<Logouts>) {
918+ let _ = logouts.wait_for(|logouts| logouts.in_flight == 0).await;
919+}
920+
886921 fn enqueue<T>(sender: &mpsc::Sender<Job<T>>, job: Job<T>) {
887922 if let Err(error) = sender.try_send(job) {
888923 let (job, error) = match error {
Modified file: src/engine/watchers.rs+29 additions−9 deletions
@@ -10,7 +10,7 @@ use matrix_sdk_ui::sync_service::{State as SyncState, SyncService};
1010 use ruma::{OwnedDeviceId, OwnedRoomId, OwnedTransactionId, time::Instant};
1111 use tokio::sync::{broadcast::error::RecvError, mpsc, watch};
1212
13-use super::executor::TaskSet;
13+use super::{Logouts, executor::TaskSet};
1414 use crate::{
1515 ConnectionState, Event, Termination, UploadOutcome, encryption::encryption_state,
1616 error::classify_sdk_error, transport::EventSender,
@@ -37,21 +37,25 @@ pub(super) async fn session(
3737 events: EventSender,
3838 device_id: OwnedDeviceId,
3939 stop: mpsc::UnboundedSender<Termination>,
40+ logouts: Arc<watch::Sender<Logouts>>,
4041 ) {
4142 loop {
4243 let event = match changes.recv().await {
4344 Ok(SessionChange::TokensRefreshed) => Event::SessionTokensChanged,
4445 Ok(SessionChange::UnknownToken(data)) => {
4546 let soft_logout = data.soft_logout.then_some(device_id);
46- let _ = events
47- .send(Event::SessionAccessRejected {
48- soft_logout: soft_logout.clone(),
49- })
50- .await;
51- let _ = stop.send(match soft_logout {
52- Some(device_id) => Termination::SoftLoggedOut { device_id },
53- None => Termination::AccessRevoked,
47+ let mut held = false;
48+ // our own logout revokes the token, so a rejection meanwhile is its doing
49+ logouts.send_if_modified(|logouts| {
50+ held = logouts.in_flight > 0;
51+ if held {
52+ logouts.rejected = Some(soft_logout.clone());
53+ }
54+ held
5455 });
56+ if !held {
57+ reject_session(&events, &stop, soft_logout).await;
58+ }
5559 return;
5660 }
5761 // a missed refresh costs one needless save, a missed rejection comes round again
@@ -64,6 +68,22 @@ pub(super) async fn session(
6468 }
6569 }
6670
71+pub(super) async fn reject_session(
72+ events: &EventSender,
73+ stop: &mpsc::UnboundedSender<Termination>,
74+ soft_logout: Option<OwnedDeviceId>,
75+) {
76+ let _ = events
77+ .send(Event::SessionAccessRejected {
78+ soft_logout: soft_logout.clone(),
79+ })
80+ .await;
81+ let _ = stop.send(match soft_logout {
82+ Some(device_id) => Termination::SoftLoggedOut { device_id },
83+ None => Termination::AccessRevoked,
84+ });
85+}
86+
6787 /// `states` must be taken before sync starts, a late subscriber never hears the transition it missed
6888 pub(super) async fn connection(
6989 mut states: impl futures_util::Stream<Item = SyncState> + Unpin,