atagen

macltok

public
Star0
Watch0
5a52012c9b1d7ec241e293fb63039a10884d6fdc
3
ssh://brewery@pls.lobotomise.me:33/atagen/macltok.git
src/engine/room.rsRust453 lines15.1 KB
1
use std::collections::{HashMap, VecDeque};
2
3
use matrix_sdk::{Room, RoomState};
4
use matrix_sdk_ui::timeline::{RoomExt, Timeline, TimelineFocus, TimelineReadReceiptTracking};
5
use ruma::{OwnedEventId, OwnedRoomId};
6
use tokio::{
7
    sync::mpsc,
8
    task::{AbortHandle, JoinSet},
9
};
10
11
use super::timeline::{SessionContext, TaskGuard, TimelineSession};
12
use crate::{
13
    Error, ErrorKind, Event, TimelineCommand, TimelineId, TypingState, transport::Responder,
14
};
15
16
pub(super) enum ActorRequest {
17
    /// opening an open room answers with a fresh reset
18
    Open(Responder),
19
    Execute(TimelineCommand, Responder),
20
    Close(Responder),
21
}
22
23
impl ActorRequest {
24
    pub(super) fn into_reply(self) -> Responder {
25
        match self {
26
            Self::Open(reply) | Self::Execute(_, reply) | Self::Close(reply) => reply,
27
        }
28
    }
29
}
30
31
pub(super) enum ActorNote {
32
    /// stopping on its own, after a leave or a kick. anything sent its way now comes back in `Exited`
33
    Leaving {
34
        room_id: OwnedRoomId,
35
        generation: u64,
36
    },
37
    Exited {
38
        room_id: OwnedRoomId,
39
        generation: u64,
40
        /// requests that arrived too late to be served, for the engine to answer or replay
41
        leftovers: Vec<ActorRequest>,
42
    },
43
}
44
45
pub(super) struct RoomActor {
46
    pub(super) room_id: OwnedRoomId,
47
    pub(super) generation: u64,
48
    pub(super) room: Room,
49
    pub(super) requests: mpsc::Receiver<ActorRequest>,
50
    pub(super) notes: mpsc::UnboundedSender<ActorNote>,
51
    pub(super) context: SessionContext,
52
    pub(super) panics: mpsc::UnboundedReceiver<&'static str>,
53
    pub(super) opening: Vec<Responder>,
54
}
55
56
impl RoomActor {
57
    pub(super) async fn run(mut self) {
58
        self.serve().await;
59
        self.requests.close();
60
        let mut leftovers = Vec::new();
61
        while let Ok(request) = self.requests.try_recv() {
62
            leftovers.push(request);
63
        }
64
        let _ = self.notes.send(ActorNote::Exited {
65
            room_id: self.room_id.clone(),
66
            generation: self.generation,
67
            leftovers,
68
        });
69
    }
70
71
    async fn serve(&mut self) {
72
        let timeline = match build_timeline(
73
            &self.room,
74
            TimelineFocus::Live {
75
                hide_threaded_events: true,
76
            },
77
        )
78
        .await
79
        {
80
            Ok(timeline) => timeline,
81
            Err(error) => return self.refuse_everything(error).await,
82
        };
83
        let Ok(room_timeline) = TimelineSession::start(
84
            TimelineId::Room(self.room_id.clone()),
85
            timeline,
86
            self.context.clone(),
87
        )
88
        .await
89
        else {
90
            return;
91
        };
92
        for reply in self.opening.drain(..) {
93
            reply.done(Ok(()));
94
        }
95
        let (typing_guard, typing) = self.room.subscribe_to_typing_notifications();
96
        let typing_watcher = TaskGuard(self.context.supervisor.spawn(
97
            "typing watcher",
98
            watch_typing(self.room_id.clone(), typing, self.context.events.clone()),
99
        ));
100
        let mut timelines = Timelines {
101
            room: room_timeline,
102
            threads: Threads::new(self.requests.max_capacity()),
103
            work: JoinSet::new(),
104
        };
105
        let close = self.serve_open(&mut timelines).await;
106
        if close.is_none() {
107
            self.requests.close();
108
            let _ = self.notes.send(ActorNote::Leaving {
109
                room_id: self.room_id.clone(),
110
                generation: self.generation,
111
            });
112
        }
113
114
        // let redactions and receipts that already left finish rather than lie about them
115
        while timelines.work.join_next().await.is_some() {}
116
        drop(typing_guard);
117
        drop(typing_watcher);
118
        timelines.threads.close().await;
119
        timelines.room.close().await;
120
        if let Some(reply) = close {
121
            reply.done(Ok(()));
122
        }
123
    }
124
125
    /// returns the close request's responder, if a close is why we stopped
126
    async fn serve_open(&mut self, timelines: &mut Timelines) -> Option<Responder> {
127
        let mut room_info = self.room.subscribe_info();
128
        // a leave that landed while the timeline was building won't come round again
129
        if matches!(self.room.state(), RoomState::Left | RoomState::Banned) {
130
            return None;
131
        }
132
        loop {
133
            tokio::select! {
134
                Some(info) = room_info.next() => {
135
                    if matches!(info.state(), RoomState::Left | RoomState::Banned) {
136
                        tracing::debug!(room_id = %self.room_id, "room actor stopped because we left the room");
137
                        return None;
138
                    }
139
                }
140
                request = self.requests.recv() => match request {
141
                    None => return None,
142
                    Some(ActorRequest::Close(reply)) => return Some(reply),
143
                    Some(ActorRequest::Open(reply)) => reply.done(timelines.room.resync().await),
144
                    Some(ActorRequest::Execute(command, reply)) => {
145
                        timelines.execute(command, reply, &self.room).await;
146
                    }
147
                },
148
                Some(built) = timelines.threads.builds.join_next(), if !timelines.threads.builds.is_empty() => {
149
                    timelines.thread_built(built, &self.room_id, &self.context).await;
150
                }
151
                Some(label) = self.panics.recv() => panic!("{label} panicked in room {}", self.room_id),
152
                Some(done) = timelines.work.join_next(), if !timelines.work.is_empty() => {
153
                    if let Err(error) = done && error.is_panic() {
154
                        tracing::warn!(room_id = %self.room_id, %error, "timeline operation panicked");
155
                    }
156
                }
157
            }
158
        }
159
    }
160
161
    async fn refuse_everything(&mut self, error: matrix_sdk_ui::timeline::Error) {
162
        tracing::warn!(%error, room_id = %self.room_id, "room actor could not build its timeline");
163
        let error = Error::with_source(ErrorKind::Timeline, "room timeline build failed", error);
164
        for reply in self.opening.drain(..) {
165
            reply.done(Err(error.clone()));
166
        }
167
        self.requests.close();
168
        while let Some(request) = self.requests.recv().await {
169
            request.into_reply().done(Err(error.clone()));
170
        }
171
    }
172
}
173
174
async fn build_timeline(
175
    room: &Room,
176
    focus: TimelineFocus,
177
) -> Result<Timeline, matrix_sdk_ui::timeline::Error> {
178
    room.timeline_builder()
179
        .with_focus(focus)
180
        .track_read_marker_and_receipts(TimelineReadReceiptTracking::AllEvents)
181
        .build()
182
        .await
183
}
184
185
async fn watch_typing(
186
    room_id: OwnedRoomId,
187
    mut typing: tokio::sync::broadcast::Receiver<Vec<ruma::OwnedUserId>>,
188
    events: crate::transport::EventSender,
189
) {
190
    loop {
191
        match typing.recv().await {
192
            Ok(users) => {
193
                let state = TypingState {
194
                    room_id: room_id.clone(),
195
                    users,
196
                };
197
                if events.send(Event::TypingChanged(state)).await.is_err() {
198
                    return;
199
                }
200
            }
201
            Err(tokio::sync::broadcast::error::RecvError::Lagged(skipped)) => {
202
                tracing::warn!(%room_id, skipped, "typing watcher lagged");
203
            }
204
            Err(tokio::sync::broadcast::error::RecvError::Closed) => return,
205
        }
206
    }
207
}
208
209
struct Timelines {
210
    room: TimelineSession,
211
    threads: Threads,
212
    work: JoinSet<()>,
213
}
214
215
impl Timelines {
216
    async fn execute(&mut self, command: TimelineCommand, reply: Responder, room: &Room) {
217
        match command {
218
            TimelineCommand::OpenThread { root_event_id, .. } => {
219
                self.threads.open(root_event_id, reply, room);
220
            }
221
            TimelineCommand::CloseThread { root_event_id, .. } => {
222
                reply.done(self.threads.close_one(&root_event_id).await);
223
            }
224
            TimelineCommand::Resync {
225
                timeline_id: TimelineId::Room(_),
226
            } => reply.done(self.room.resync().await),
227
            command => {
228
                let root = command
229
                    .timeline_id()
230
                    .and_then(TimelineId::thread_root)
231
                    .cloned();
232
                let Some(root) = root else {
233
                    return self.room.execute(command, reply, &mut self.work).await;
234
                };
235
                if let TimelineCommand::MarkRead {
236
                    timeline_id,
237
                    event_id,
238
                } = &command
239
                    && !self.threads.open.contains_key(&root)
240
                {
241
                    let client = room.client();
242
                    let (timeline_id, event_id) = (timeline_id.clone(), event_id.clone());
243
                    self.work.spawn(async move {
244
                        reply.done(
245
                            super::commands::mark_read(&client, &timeline_id, event_id).await,
246
                        );
247
                    });
248
                    return;
249
                }
250
                match self.threads.route(root, command, reply) {
251
                    Routed::Open(session, TimelineCommand::Resync { .. }, reply) => {
252
                        reply.done(session.resync().await);
253
                    }
254
                    Routed::Open(session, command, reply) => {
255
                        session.execute(command, reply, &mut self.work).await;
256
                    }
257
                    Routed::Queued => {}
258
                }
259
            }
260
        }
261
    }
262
263
    async fn thread_built(
264
        &mut self,
265
        built: Result<Built, tokio::task::JoinError>,
266
        room_id: &OwnedRoomId,
267
        context: &SessionContext,
268
    ) {
269
        let Some((root, queued, timeline)) = self.threads.built(built) else {
270
            return;
271
        };
272
        let timeline = match timeline {
273
            Ok(timeline) => timeline,
274
            Err(error) => {
275
                tracing::warn!(%error, %room_id, %root, "thread timeline build failed");
276
                let error =
277
                    Error::with_source(ErrorKind::Timeline, "thread timeline build failed", error);
278
                for (_, reply) in queued {
279
                    reply.done(Err(error.clone()));
280
                }
281
                return;
282
            }
283
        };
284
        let id = TimelineId::thread(room_id.clone(), root.clone());
285
        let Ok(session) = TimelineSession::start(id, timeline, context.clone()).await else {
286
            return;
287
        };
288
        let session = self
289
            .threads
290
            .open
291
            .entry(root)
292
            .insert_entry(session)
293
            .into_mut();
294
        for (command, reply) in queued {
295
            match command {
296
                None => reply.done(Ok(())),
297
                Some(TimelineCommand::Resync { .. }) => reply.done(session.resync().await),
298
                Some(command) => session.execute(command, reply, &mut self.work).await,
299
            }
300
        }
301
    }
302
}
303
304
type Built = (
305
    OwnedEventId,
306
    u64,
307
    Result<Timeline, matrix_sdk_ui::timeline::Error>,
308
);
309
310
/// `None` is the open itself, everything else waits behind it
311
type Queued = VecDeque<(Option<TimelineCommand>, Responder)>;
312
313
enum Routed<'a> {
314
    Open(&'a mut TimelineSession, TimelineCommand, Responder),
315
    Queued,
316
}
317
318
struct OpeningThread {
319
    generation: u64,
320
    build: AbortHandle,
321
    queued: Queued,
322
}
323
324
struct Threads {
325
    open: HashMap<OwnedEventId, TimelineSession>,
326
    opening: HashMap<OwnedEventId, OpeningThread>,
327
    builds: JoinSet<Built>,
328
    next_generation: u64,
329
    queued: usize,
330
    capacity: usize,
331
}
332
333
impl Threads {
334
    fn new(capacity: usize) -> Self {
335
        Self {
336
            open: HashMap::new(),
337
            opening: HashMap::new(),
338
            builds: JoinSet::new(),
339
            next_generation: 0,
340
            queued: 0,
341
            capacity,
342
        }
343
    }
344
345
    fn enqueue(&mut self, root: &OwnedEventId, command: Option<TimelineCommand>, reply: Responder) {
346
        if self.queued >= self.capacity {
347
            return reply.done(Err(Error::new(
348
                ErrorKind::QueueFull,
349
                "thread command queue is full",
350
            )));
351
        }
352
        let Some(opening) = self.opening.get_mut(root) else {
353
            return reply.done(Err(Error::new(
354
                ErrorKind::TimelineNotOpen,
355
                "thread timeline is not open",
356
            )));
357
        };
358
        opening.queued.push_back((command, reply));
359
        self.queued += 1;
360
    }
361
362
    fn open(&mut self, root: OwnedEventId, reply: Responder, room: &Room) {
363
        if self.open.contains_key(&root) {
364
            return reply.done(Ok(()));
365
        }
366
        if !self.opening.contains_key(&root) {
367
            let generation = self.next_generation;
368
            self.next_generation = self.next_generation.wrapping_add(1);
369
            let room = room.clone();
370
            let build_root = root.clone();
371
            let build = self.builds.spawn(async move {
372
                let focus = TimelineFocus::Thread {
373
                    root_event_id: build_root.clone(),
374
                };
375
                (build_root, generation, build_timeline(&room, focus).await)
376
            });
377
            self.opening.insert(
378
                root.clone(),
379
                OpeningThread {
380
                    generation,
381
                    build,
382
                    queued: VecDeque::new(),
383
                },
384
            );
385
        }
386
        self.enqueue(&root, None, reply);
387
    }
388
389
    fn route(
390
        &mut self,
391
        root: OwnedEventId,
392
        command: TimelineCommand,
393
        reply: Responder,
394
    ) -> Routed<'_> {
395
        if !self.open.contains_key(&root) {
396
            self.enqueue(&root, Some(command), reply);
397
            return Routed::Queued;
398
        }
399
        let session = self.open.get_mut(&root).expect("thread was just checked");
400
        Routed::Open(session, command, reply)
401
    }
402
403
    /// `None` when the build belongs to a thread that was closed while it ran
404
    fn built(
405
        &mut self,
406
        built: Result<Built, tokio::task::JoinError>,
407
    ) -> Option<(
408
        OwnedEventId,
409
        Queued,
410
        Result<Timeline, matrix_sdk_ui::timeline::Error>,
411
    )> {
412
        let (root, generation, timeline) = match built {
413
            Ok(built) => built,
414
            Err(error) if error.is_cancelled() => return None,
415
            Err(error) => std::panic::resume_unwind(error.into_panic()),
416
        };
417
        if self
418
            .opening
419
            .get(&root)
420
            .is_none_or(|opening| opening.generation != generation)
421
        {
422
            return None;
423
        }
424
        let opening = self.opening.remove(&root)?;
425
        self.queued -= opening.queued.len();
426
        Some((root, opening.queued, timeline))
427
    }
428
429
    async fn close_one(&mut self, root: &OwnedEventId) -> Result<(), Error> {
430
        if let Some(session) = self.open.remove(root) {
431
            session.close().await;
432
            return Ok(());
433
        }
434
        let Some(opening) = self.opening.remove(root) else {
435
            return Err(Error::new(
436
                ErrorKind::TimelineNotOpen,
437
                "thread timeline is not open",
438
            ));
439
        };
440
        opening.build.abort();
441
        self.queued -= opening.queued.len();
442
        Ok(())
443
    }
444
445
    async fn close(&mut self) {
446
        self.builds.abort_all();
447
        while self.builds.join_next().await.is_some() {}
448
        self.opening.clear();
449
        for (_, session) in self.open.drain() {
450
            session.close().await;
451
        }
452
    }
453
}