atagen

macltok

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