Skip to main content

m4a_agent/engine/
driver.rs

1//! One session's engine run: requests the core releases are executed by the backend, `/sync` runs
2//! on a worker so a long poll does not stall everything else, answers go back into the core.
3
4use std::sync::mpsc::{self, Receiver};
5use std::sync::Arc;
6use std::thread::{self, JoinHandle};
7use std::time::Duration;
8
9use mail4agent_messenger::{
10    HttpResponseDescriptor, MessengerCore, MessengerEvent, OutgoingRequest, OutgoingRequestKind, RecordCodec, RequestId,
11};
12
13use super::http::clip_public;
14use crate::backend::Backend;
15use crate::error::{AgentError, Result};
16
17struct SyncFlight {
18    id: RequestId,
19    rx: Receiver<std::result::Result<HttpResponseDescriptor, String>>,
20    /// Not joined on drop: a blocked poll must not hold the process.
21    _worker: JoinHandle<()>,
22}
23
24/// Flushes the core's sealed records to wherever the host keeps them.
25pub type Persist<C> = Box<dyn FnMut(&mut MessengerCore<C>) -> Result<()> + Send>;
26
27pub struct Driver<C: RecordCodec> {
28    pub core: MessengerCore<C>,
29    backend: Arc<dyn Backend>,
30    persist: Persist<C>,
31    flight: Option<SyncFlight>,
32    /// Kind and status of calls made. No bodies.
33    pub http_trace: Vec<(OutgoingRequestKind, u16)>,
34    /// Peer device key changes seen on `/keys/query`.
35    pub security_alerts: Vec<String>,
36}
37
38pub fn sync_timeout_ms(request: &OutgoingRequest) -> u64 {
39    request.query.iter().find(|(n, _)| n == "timeout").and_then(|(_, v)| v.parse().ok()).unwrap_or(0)
40}
41
42impl<C: RecordCodec> Driver<C> {
43    pub fn new(core: MessengerCore<C>, backend: Arc<dyn Backend>, persist: Persist<C>) -> Self {
44        Self { core, backend, persist, flight: None, http_trace: Vec::new(), security_alerts: Vec::new() }
45    }
46
47    pub fn backend(&self) -> &Arc<dyn Backend> {
48        &self.backend
49    }
50
51    /// Puts another transport under the same core (the shell's in-process bus does this).
52    pub fn set_backend(&mut self, backend: Arc<dyn Backend>) {
53        self.backend = backend;
54    }
55
56    pub fn sync_inflight(&self) -> bool {
57        self.flight.is_some()
58    }
59
60    pub fn persist(&mut self) -> Result<()> {
61        (self.persist)(&mut self.core)
62    }
63
64    /// Drops a sync that is still on the wire (its answer is never read).
65    pub fn abandon_sync(&mut self, now_ms: i64) {
66        if let Some(f) = self.flight.take() {
67            self.core.on_transport_error(&f.id, now_ms);
68        }
69    }
70
71    fn note_security_events(&mut self, events: &[MessengerEvent]) {
72        for event in events {
73            if let MessengerEvent::DeviceKeyChanged { user_id, device_id } = event {
74                self.security_alerts.push(format!("{} reset its keys (device {}); trust cleared, room keys re-shared", user_id.as_str(), device_id.as_str()));
75            }
76        }
77    }
78
79    pub fn release_after_flush(&mut self, now_ms: i64) -> Result<Vec<OutgoingRequest>> {
80        self.persist()?;
81        let mut released = self.core.releasable_requests(now_ms);
82        if released.is_empty() {
83            self.persist()?;
84            released = self.core.releasable_requests(now_ms);
85        }
86        Ok(released)
87    }
88
89    pub fn roundtrip(&mut self, request: &OutgoingRequest, now_ms: i64) -> Result<()> {
90        match self.backend.execute(request) {
91            Ok(response) => {
92                self.http_trace.push((request.kind, response.status));
93                let events = self.core.on_response(request.id.clone(), response, now_ms);
94                self.note_security_events(&events);
95                Ok(())
96            }
97            Err(err) => {
98                self.core.on_transport_error(&request.id, now_ms);
99                Err(err)
100            }
101        }
102    }
103
104    pub fn spawn_sync(&mut self, request: OutgoingRequest) -> Result<()> {
105        if self.flight.is_some() {
106            self.core.on_transport_error(&request.id, 0);
107            return Err(AgentError::Transport("a sync was already on the wire".into()));
108        }
109        let backend = self.backend.frozen().unwrap_or_else(|| Arc::clone(&self.backend));
110        let id = request.id.clone();
111        let (tx, rx) = mpsc::channel();
112        let worker = thread::spawn(move || {
113            let result = backend.execute(&request).map_err(|e| match e {
114                AgentError::Transport(t) => t,
115                other => clip_public(other.to_string()),
116            });
117            let _ = tx.send(result);
118        });
119        self.flight = Some(SyncFlight { id, rx, _worker: worker });
120        Ok(())
121    }
122
123    fn drop_flight(&mut self, now_ms: i64, why: &str) -> AgentError {
124        if let Some(f) = self.flight.take() {
125            self.core.on_transport_error(&f.id, now_ms);
126        }
127        AgentError::Transport(why.to_string())
128    }
129
130    /// `Ok(true)` only when this call blocked on a poll that had not already finished. An
131    /// already-buffered answer is `Ok(false)`.
132    pub fn harvest_sync(&mut self, now_ms: i64, wait: bool) -> Result<bool> {
133        let Some(flight) = self.flight.as_ref() else { return Ok(false) };
134        let (received, blocked) = match flight.rx.try_recv() {
135            Ok(r) => (Some(r), false),
136            Err(mpsc::TryRecvError::Disconnected) => return Err(self.drop_flight(now_ms, "sync worker dropped")),
137            Err(mpsc::TryRecvError::Empty) if !wait => (None, false),
138            Err(mpsc::TryRecvError::Empty) => match flight.rx.recv_timeout(Duration::from_secs(45)) {
139                Ok(r) => (Some(r), true),
140                Err(mpsc::RecvTimeoutError::Timeout) => return Err(self.drop_flight(now_ms, "sync timed out")),
141                Err(mpsc::RecvTimeoutError::Disconnected) => return Err(self.drop_flight(now_ms, "sync worker dropped")),
142            },
143        };
144        let Some(result) = received else { return Ok(false) };
145        let flight = self.flight.take().expect("flight present");
146        match result {
147            Ok(response) => {
148                self.http_trace.push((OutgoingRequestKind::Sync, response.status));
149                let events = self.core.on_response(flight.id, response, now_ms);
150                self.note_security_events(&events);
151            }
152            Err(err) => {
153                self.core.on_transport_error(&flight.id, now_ms);
154                return Err(AgentError::Transport(err));
155            }
156        }
157        self.persist()?;
158        if let Some(err) = self.core.take_ingest_error() {
159            return Err(AgentError::Ingest(clip_public(err)));
160        }
161        Ok(blocked)
162    }
163
164    /// One round of the drive loop: what the core releases is executed, syncs on the worker, the
165    /// state persisted. `Ok(false)` when nothing was released (the loop is done). `waited` tells
166    /// whether a long poll was already waited for in this drive.
167    pub fn step(&mut self, now_ms: i64, wait_for_sync: bool, waited: &mut bool) -> Result<bool> {
168        let released = self.release_after_flush(now_ms)?;
169        if released.is_empty() {
170            return Ok(false);
171        }
172        let (syncs, others): (Vec<_>, Vec<_>) = released.into_iter().partition(|r| r.kind == OutgoingRequestKind::Sync);
173        for request in &syncs {
174            self.spawn_sync(request.clone())?;
175        }
176        for request in others {
177            self.roundtrip(&request, now_ms)?;
178        }
179        self.persist()?;
180        for request in &syncs {
181            let timeout = sync_timeout_ms(request);
182            if timeout == 0 || (wait_for_sync && !*waited) {
183                self.harvest_sync(now_ms, true)?;
184                if timeout != 0 {
185                    *waited = true;
186                }
187            }
188        }
189        self.persist()?;
190        if let Some(err) = self.core.take_ingest_error() {
191            return Err(AgentError::Ingest(clip_public(err)));
192        }
193        Ok(true)
194    }
195}