atagen

macltok

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