atagen

macltok

public
Star0
Watch0
5572a11e8823a38565ef9d03d2033208a39e95a3
3
ssh://brewery@pls.lobotomise.me:33/atagen/macltok.git
src/engine/room.rsRust483 lines16.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::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 mut 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
        typing_watcher.0.abort();
116
        let _ = (&mut typing_watcher.0).await;
117
        let _ = self
118
            .context
119
            .events
120
            .send(Event::TypingChanged(TypingState {
121
                room_id: self.room_id.clone(),
122
                users: Vec::new(),
123
            }))
124
            .await;
125
        timelines.threads.close().await;
126
        timelines.room.close().await;
127
        if let Some(reply) = close {
128
            reply.done(Ok(()));
129
        }
130
    }
131
132
    /// returns the close request's responder, if a close is why we stopped
133
    async fn serve_open(&mut self, timelines: &mut Timelines) -> Option<Responder> {
134
        let mut room_info = self.room.subscribe_info();
135
        // a leave that landed while the timeline was building won't come round again
136
        if matches!(self.room.state(), RoomState::Left | RoomState::Banned) {
137
            return None;
138
        }
139
        loop {
140
            tokio::select! {
141
                Some(info) = room_info.next() => {
142
                    if matches!(info.state(), RoomState::Left | RoomState::Banned) {
143
                        tracing::debug!(room_id = %self.room_id, "room actor stopped because we left the room");
144
                        return None;
145
                    }
146
                }
147
                request = self.requests.recv() => match request {
148
                    None => return None,
149
                    Some(ActorRequest::Close(reply)) => return Some(reply),
150
                    Some(ActorRequest::Open(reply)) => {
151
                        match timelines.room.resync().await {
152
                            Ok(()) if self.context.initial_history_limit > 0 => {
153
                                timelines.room.paginate(self.context.initial_history_limit, Some(reply));
154
                            }
155
                            result => reply.done(result),
156
                        }
157
                    }
158
                    Some(ActorRequest::Execute(command, reply)) => {
159
                        timelines.execute(command, reply, &self.room).await;
160
                    }
161
                },
162
                Some(built) = timelines.threads.builds.join_next(), if !timelines.threads.builds.is_empty() => {
163
                    timelines.thread_built(built, &self.room_id, &self.context).await;
164
                }
165
                Some(label) = self.panics.recv() => panic!("{label} panicked in room {}", self.room_id),
166
                Some(done) = timelines.work.join_next(), if !timelines.work.is_empty() => {
167
                    if let Err(error) = done && is_panic(&error) {
168
                        tracing::warn!(room_id = %self.room_id, %error, "timeline operation panicked");
169
                    }
170
                }
171
            }
172
        }
173
    }
174
175
    async fn refuse_everything(&mut self, error: matrix_sdk_ui::timeline::Error) {
176
        tracing::warn!(%error, room_id = %self.room_id, "room actor could not build its timeline");
177
        let error = Error::with_source(ErrorKind::Timeline, "room timeline build failed", error);
178
        for reply in self.opening.drain(..) {
179
            reply.done(Err(error.clone()));
180
        }
181
        self.requests.close();
182
        while let Some(request) = self.requests.recv().await {
183
            request.into_reply().done(Err(error.clone()));
184
        }
185
    }
186
}
187
188
async fn build_timeline(
189
    room: &Room,
190
    focus: TimelineFocus,
191
) -> Result<Timeline, matrix_sdk_ui::timeline::Error> {
192
    room.timeline_builder()
193
        .with_focus(focus)
194
        .track_read_marker_and_receipts(TimelineReadReceiptTracking::AllEvents)
195
        .build()
196
        .await
197
}
198
199
async fn watch_typing(
200
    room_id: OwnedRoomId,
201
    mut typing: tokio::sync::broadcast::Receiver<Vec<ruma::OwnedUserId>>,
202
    events: crate::transport::EventSender,
203
) {
204
    loop {
205
        match typing.recv().await {
206
            Ok(users) => {
207
                let state = TypingState {
208
                    room_id: room_id.clone(),
209
                    users,
210
                };
211
                if events.send(Event::TypingChanged(state)).await.is_err() {
212
                    return;
213
                }
214
            }
215
            Err(tokio::sync::broadcast::error::RecvError::Lagged(skipped)) => {
216
                tracing::warn!(%room_id, skipped, "typing watcher lagged");
217
            }
218
            Err(tokio::sync::broadcast::error::RecvError::Closed) => return,
219
        }
220
    }
221
}
222
223
struct Timelines {
224
    room: TimelineSession,
225
    threads: Threads,
226
    work: TaskSet<()>,
227
}
228
229
impl Timelines {
230
    async fn execute(&mut self, command: TimelineCommand, reply: Responder, room: &Room) {
231
        match command {
232
            TimelineCommand::OpenThread {
233
                room_id,
234
                root_event_id,
235
            } => self
236
                .threads
237
                .open(TimelineId::thread(room_id, root_event_id), reply, room),
238
            TimelineCommand::CloseThread {
239
                room_id,
240
                root_event_id,
241
            } => {
242
                let id = TimelineId::thread(room_id, root_event_id);
243
                reply.done(self.threads.close_one(&id).await);
244
            }
245
            TimelineCommand::OpenPinned(room_id) => {
246
                self.threads.open(TimelineId::Pinned(room_id), reply, room);
247
            }
248
            TimelineCommand::ClosePinned(room_id) => {
249
                reply.done(self.threads.close_one(&TimelineId::Pinned(room_id)).await);
250
            }
251
            TimelineCommand::Resync {
252
                timeline_id: TimelineId::Room(_),
253
            } => reply.done(self.room.resync().await),
254
            command => {
255
                let Some(id) = command
256
                    .timeline_id()
257
                    .filter(|id| !matches!(id, TimelineId::Room(_)))
258
                    .cloned()
259
                else {
260
                    return self.room.execute(command, reply, &mut self.work).await;
261
                };
262
                if matches!(id, TimelineId::Pinned(_))
263
                    && matches!(
264
                        command,
265
                        TimelineCommand::Send { .. } | TimelineCommand::Paginate { .. }
266
                    )
267
                {
268
                    return reply.done(Err(Error::new(
269
                        ErrorKind::Invalid,
270
                        "the pinned timeline can't send or paginate",
271
                    )));
272
                }
273
                if let TimelineCommand::MarkRead {
274
                    timeline_id,
275
                    event_id,
276
                } = &command
277
                    && !self.threads.open.contains_key(&id)
278
                {
279
                    let client = room.client();
280
                    let (timeline_id, event_id) = (timeline_id.clone(), event_id.clone());
281
                    self.work.spawn(async move {
282
                        reply.done(
283
                            super::commands::mark_read(&client, &timeline_id, event_id).await,
284
                        );
285
                    });
286
                    return;
287
                }
288
                match self.threads.route(id, command, reply) {
289
                    Routed::Open(session, TimelineCommand::Resync { .. }, reply) => {
290
                        reply.done(session.resync().await);
291
                    }
292
                    Routed::Open(session, command, reply) => {
293
                        session.execute(command, reply, &mut self.work).await;
294
                    }
295
                    Routed::Queued => {}
296
                }
297
            }
298
        }
299
    }
300
301
    async fn thread_built(
302
        &mut self,
303
        built: Result<Built, JoinError>,
304
        room_id: &OwnedRoomId,
305
        context: &SessionContext,
306
    ) {
307
        let Some((id, queued, timeline)) = self.threads.built(built) else {
308
            return;
309
        };
310
        let timeline = match timeline {
311
            Ok(timeline) => timeline,
312
            Err(error) => {
313
                tracing::warn!(%error, %room_id, ?id, "timeline build failed");
314
                let error = Error::with_source(ErrorKind::Timeline, "timeline build failed", error);
315
                for (_, reply) in queued {
316
                    reply.done(Err(error.clone()));
317
                }
318
                return;
319
            }
320
        };
321
        let Ok(session) = TimelineSession::start(id.clone(), timeline, context.clone()).await
322
        else {
323
            return;
324
        };
325
        let session = self.threads.open.entry(id).insert_entry(session).into_mut();
326
        for (command, reply) in queued {
327
            match command {
328
                None => reply.done(Ok(())),
329
                Some(TimelineCommand::Resync { .. }) => reply.done(session.resync().await),
330
                Some(command) => session.execute(command, reply, &mut self.work).await,
331
            }
332
        }
333
    }
334
}
335
336
type Built = (
337
    TimelineId,
338
    u64,
339
    Result<Timeline, matrix_sdk_ui::timeline::Error>,
340
);
341
342
/// `None` is the open itself, everything else waits behind it
343
type Queued = VecDeque<(Option<TimelineCommand>, Responder)>;
344
345
enum Routed<'a> {
346
    Open(&'a mut TimelineSession, TimelineCommand, Responder),
347
    Queued,
348
}
349
350
struct OpeningThread {
351
    generation: u64,
352
    build: AbortHandle,
353
    queued: Queued,
354
}
355
356
/// threads and the pinned timeline, which build off the actor and queue what arrives meanwhile
357
struct Threads {
358
    open: HashMap<TimelineId, TimelineSession>,
359
    opening: HashMap<TimelineId, OpeningThread>,
360
    builds: TaskSet<Built>,
361
    next_generation: u64,
362
    queued: usize,
363
    capacity: usize,
364
}
365
366
impl Threads {
367
    fn new(capacity: usize) -> Self {
368
        Self {
369
            open: HashMap::new(),
370
            opening: HashMap::new(),
371
            builds: TaskSet::new(),
372
            next_generation: 0,
373
            queued: 0,
374
            capacity,
375
        }
376
    }
377
378
    fn enqueue(&mut self, id: &TimelineId, command: Option<TimelineCommand>, reply: Responder) {
379
        if self.queued >= self.capacity {
380
            return reply.done(Err(Error::new(
381
                ErrorKind::QueueFull,
382
                "thread command queue is full",
383
            )));
384
        }
385
        let Some(opening) = self.opening.get_mut(id) else {
386
            return reply.done(Err(Error::new(
387
                ErrorKind::TimelineNotOpen,
388
                "timeline is not open",
389
            )));
390
        };
391
        opening.queued.push_back((command, reply));
392
        self.queued += 1;
393
    }
394
395
    fn open(&mut self, id: TimelineId, reply: Responder, room: &Room) {
396
        if self.open.contains_key(&id) {
397
            return reply.done(Ok(()));
398
        }
399
        if !self.opening.contains_key(&id) {
400
            let generation = self.next_generation;
401
            self.next_generation = self.next_generation.wrapping_add(1);
402
            let room = room.clone();
403
            let build_id = id.clone();
404
            let build = self.builds.spawn(async move {
405
                let focus = match &build_id {
406
                    TimelineId::Thread { root_event_id, .. } => TimelineFocus::Thread {
407
                        root_event_id: root_event_id.clone(),
408
                    },
409
                    _ => TimelineFocus::PinnedEvents,
410
                };
411
                (build_id, generation, build_timeline(&room, focus).await)
412
            });
413
            self.opening.insert(
414
                id.clone(),
415
                OpeningThread {
416
                    generation,
417
                    build,
418
                    queued: VecDeque::new(),
419
                },
420
            );
421
        }
422
        self.enqueue(&id, None, reply);
423
    }
424
425
    fn route(&mut self, id: TimelineId, command: TimelineCommand, reply: Responder) -> Routed<'_> {
426
        if !self.open.contains_key(&id) {
427
            self.enqueue(&id, Some(command), reply);
428
            return Routed::Queued;
429
        }
430
        let session = self.open.get_mut(&id).expect("timeline was just checked");
431
        Routed::Open(session, command, reply)
432
    }
433
434
    /// `None` when the build belongs to a thread that was closed while it ran
435
    fn built(
436
        &mut self,
437
        built: Result<Built, JoinError>,
438
    ) -> Option<(
439
        TimelineId,
440
        Queued,
441
        Result<Timeline, matrix_sdk_ui::timeline::Error>,
442
    )> {
443
        let (id, generation, timeline) = match built {
444
            Ok(built) => built,
445
            Err(error) if error.is_cancelled() => return None,
446
            Err(error) => resume_unwind(error),
447
        };
448
        if self
449
            .opening
450
            .get(&id)
451
            .is_none_or(|opening| opening.generation != generation)
452
        {
453
            return None;
454
        }
455
        let opening = self.opening.remove(&id)?;
456
        self.queued -= opening.queued.len();
457
        Some((id, opening.queued, timeline))
458
    }
459
460
    async fn close_one(&mut self, id: &TimelineId) -> Result<(), Error> {
461
        if let Some(session) = self.open.remove(id) {
462
            session.close().await;
463
            return Ok(());
464
        }
465
        let Some(opening) = self.opening.remove(id) else {
466
            return Err(Error::new(
467
                ErrorKind::TimelineNotOpen,
468
                "timeline is not open",
469
            ));
470
        };
471
        opening.build.abort();
472
        self.queued -= opening.queued.len();
473
        Ok(())
474
    }
475
476
    async fn close(&mut self) {
477
        self.builds.shutdown().await;
478
        self.opening.clear();
479
        for (_, session) in self.open.drain() {
480
            session.close().await;
481
        }
482
    }
483
}