atagen

macltok

public
Star0
Watch0
b83edf681ed279f96918ddcd4678007864ede41c
3
ssh://brewery@pls.lobotomise.me:33/atagen/macltok.git
src/engine/room.rsRust562 lines20.0 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, label, resume_unwind};
9
use super::repair::{Completion, Outcome, RepairController};
10
use super::timeline::{SessionContext, TaskGuard, TimelineSession};
11
use crate::{
12
    Error, ErrorKind, Event, HistoryRepairCommand, RecentHistoryState, TimelineCommand, TimelineId,
13
    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
    Repair(HistoryRepairCommand, Responder),
21
    Close(Responder),
22
}
23
24
impl ActorRequest {
25
    pub(super) fn into_reply(self) -> Responder {
26
        match self {
27
            Self::Open(reply)
28
            | Self::Execute(_, reply)
29
            | Self::Repair(_, reply)
30
            | Self::Close(reply) => reply,
31
        }
32
    }
33
}
34
35
pub(super) enum ActorNote {
36
    /// stopping on its own, after a leave or a kick. anything sent its way now comes back in `Exited`
37
    Leaving {
38
        room_id: OwnedRoomId,
39
        generation: u64,
40
    },
41
    Exited {
42
        room_id: OwnedRoomId,
43
        generation: u64,
44
        /// requests that arrived too late to be served, for the engine to answer or replay
45
        leftovers: Vec<ActorRequest>,
46
    },
47
}
48
49
pub(super) struct RoomActor {
50
    pub(super) room_id: OwnedRoomId,
51
    pub(super) generation: u64,
52
    pub(super) room: Room,
53
    pub(super) requests: mpsc::Receiver<ActorRequest>,
54
    pub(super) notes: mpsc::UnboundedSender<ActorNote>,
55
    pub(super) context: SessionContext,
56
    pub(super) panics: mpsc::UnboundedReceiver<&'static str>,
57
    pub(super) opening: Vec<Responder>,
58
}
59
60
impl RoomActor {
61
    pub(super) async fn run(mut self) {
62
        self.serve().await;
63
        self.requests.close();
64
        let mut leftovers = Vec::new();
65
        while let Ok(request) = self.requests.try_recv() {
66
            leftovers.push(request);
67
        }
68
        let _ = self.notes.send(ActorNote::Exited {
69
            room_id: self.room_id.clone(),
70
            generation: self.generation,
71
            leftovers,
72
        });
73
    }
74
75
    async fn serve(&mut self) {
76
        tracing::debug!(room_id = %self.room_id, "building the room timeline");
77
        let timeline = match build_timeline(
78
            &self.room,
79
            TimelineFocus::Live {
80
                hide_threaded_events: true,
81
            },
82
        )
83
        .await
84
        {
85
            Ok(timeline) => timeline,
86
            Err(error) => return self.refuse_everything(error).await,
87
        };
88
        tracing::debug!(room_id = %self.room_id, "starting the room timeline session");
89
        let Ok(room_timeline) = TimelineSession::start(
90
            TimelineId::Room(self.room_id.clone()),
91
            timeline,
92
            self.context.clone(),
93
        )
94
        .await
95
        else {
96
            return;
97
        };
98
        let (typing_guard, typing) = self.room.subscribe_to_typing_notifications();
99
        let mut typing_watcher = TaskGuard(self.context.supervisor.spawn(
100
            "typing watcher",
101
            watch_typing(self.room_id.clone(), typing, self.context.events.clone()),
102
        ));
103
        let mut timelines = Timelines {
104
            room: room_timeline,
105
            threads: Threads::new(self.requests.max_capacity()),
106
            work: TaskSet::new(),
107
        };
108
        let mut repair = RepairController::new(self.room.clone(), self.context.clone());
109
        tracing::debug!(room_id = %self.room_id, "starting recent history");
110
        repair.start_recent().await;
111
        tracing::debug!(room_id = %self.room_id, "room opened");
112
        for reply in self.opening.drain(..) {
113
            reply.done(Ok(()));
114
        }
115
        let close = self.serve_open(&mut timelines, &mut repair).await;
116
        let recent_settled = match repair.shutdown().await {
117
            Some(completion) => {
118
                let recent = matches!(completion.outcome, Outcome::Recent(_));
119
                self.finish_repair(completion, &mut timelines).await;
120
                recent
121
            }
122
            None => false,
123
        };
124
        if !recent_settled {
125
            let _ = self
126
                .context
127
                .events
128
                .send(Event::RecentHistoryChanged {
129
                    room_id: self.room_id.clone(),
130
                    state: RecentHistoryState::Idle,
131
                })
132
                .await;
133
        }
134
        if close.is_none() {
135
            self.requests.close();
136
            let _ = self.notes.send(ActorNote::Leaving {
137
                room_id: self.room_id.clone(),
138
                generation: self.generation,
139
            });
140
        }
141
142
        // let redactions and receipts that already left finish rather than lie about them
143
        while timelines.work.join_next().await.is_some() {}
144
        drop(typing_guard);
145
        typing_watcher.0.abort();
146
        let _ = (&mut typing_watcher.0).await;
147
        let _ = self
148
            .context
149
            .events
150
            .send(Event::TypingChanged(TypingState {
151
                room_id: self.room_id.clone(),
152
                users: Vec::new(),
153
            }))
154
            .await;
155
        timelines.threads.close().await;
156
        timelines.room.close().await;
157
        if let Some(reply) = close {
158
            reply.done(Ok(()));
159
        }
160
    }
161
162
    async fn finish_repair(&self, mut completion: Completion, timelines: &mut Timelines) {
163
        let mut cancellation = Ok(());
164
        // pages a failed or cancelled operation already committed still have to be on
165
        // screen before anyone hears it stopped
166
        if completion.committed
167
            && !completion.closing
168
            && let Err(error) = timelines.room.publish_history().await
169
        {
170
            if completion.result.is_ok() {
171
                completion.unpublished(error.clone());
172
            }
173
            cancellation = Err(error);
174
        }
175
        let event = match &completion.outcome {
176
            Outcome::Recent(state) => Event::RecentHistoryChanged {
177
                room_id: self.room_id.clone(),
178
                state: state.clone(),
179
            },
180
            Outcome::Fill(report) => Event::HistoryRepairChanged(report.clone()),
181
        };
182
        let mut resolved = completion.result.clone();
183
        let events = &self.context.events;
184
        if events.send(event).await.is_err() || events.publish_barrier().await.is_err() {
185
            resolved = Err(Error::interrupted());
186
            cancellation = Err(Error::interrupted());
187
        }
188
        let limit = self.context.initial_history_limit;
189
        if matches!(
190
            completion.outcome,
191
            Outcome::Recent(RecentHistoryState::Failed(_))
192
        ) && limit > 0
193
            && !timelines.room.has_events().await
194
        {
195
            timelines.room.paginate(limit, None);
196
        }
197
        completion.resolve(resolved, cancellation);
198
    }
199
200
    /// returns the close request's responder, if a close is why we stopped
201
    async fn serve_open(
202
        &mut self,
203
        timelines: &mut Timelines,
204
        repair: &mut RepairController,
205
    ) -> Option<Responder> {
206
        let mut room_info = self.room.subscribe_info();
207
        // a leave that landed while the timeline was building won't come round again
208
        if matches!(self.room.state(), RoomState::Left | RoomState::Banned) {
209
            return None;
210
        }
211
        loop {
212
            tokio::select! {
213
                Some(info) = room_info.next() => {
214
                    if matches!(info.state(), RoomState::Left | RoomState::Banned) {
215
                        tracing::debug!(room_id = %self.room_id, "room actor stopped because we left the room");
216
                        return None;
217
                    }
218
                }
219
                request = self.requests.recv() => match request {
220
                    None => return None,
221
                    Some(ActorRequest::Close(reply)) => return Some(reply),
222
                    Some(ActorRequest::Open(reply)) => reply.done(timelines.room.resync().await),
223
                    Some(ActorRequest::Execute(command, reply)) => {
224
                        let label = label(&command);
225
                        tracing::debug!(room_id = %self.room_id, command = %label, "room command");
226
                        timelines.execute(command, reply, &self.room).await;
227
                        tracing::debug!(room_id = %self.room_id, command = %label, "room command handed off");
228
                    }
229
                    Some(ActorRequest::Repair(command, reply)) => {
230
                        let label = label(&command);
231
                        tracing::debug!(room_id = %self.room_id, command = %label, "room repair command");
232
                        repair.execute(command, reply).await;
233
                        tracing::debug!(room_id = %self.room_id, command = %label, "room repair command handed off");
234
                    }
235
                },
236
                completion = repair.next_completion(), if repair.is_running() => {
237
                    if let Some(completion) = completion {
238
                        self.finish_repair(completion, timelines).await;
239
                    }
240
                }
241
                Some(built) = timelines.threads.builds.join_next(), if !timelines.threads.builds.is_empty() => {
242
                    timelines.thread_built(built, &self.room_id, &self.context).await;
243
                }
244
                Some(label) = self.panics.recv() => panic!("{label} panicked in room {}", self.room_id),
245
                Some(done) = timelines.work.join_next(), if !timelines.work.is_empty() => {
246
                    if let Err(error) = done && is_panic(&error) {
247
                        tracing::warn!(room_id = %self.room_id, %error, "timeline operation panicked");
248
                    }
249
                }
250
            }
251
        }
252
    }
253
254
    async fn refuse_everything(&mut self, error: matrix_sdk_ui::timeline::Error) {
255
        tracing::warn!(%error, room_id = %self.room_id, "room actor could not build its timeline");
256
        let error = Error::with_source(ErrorKind::Timeline, "room timeline build failed", error);
257
        for reply in self.opening.drain(..) {
258
            reply.done(Err(error.clone()));
259
        }
260
        self.requests.close();
261
        while let Some(request) = self.requests.recv().await {
262
            request.into_reply().done(Err(error.clone()));
263
        }
264
    }
265
}
266
267
async fn build_timeline(
268
    room: &Room,
269
    focus: TimelineFocus,
270
) -> Result<Timeline, matrix_sdk_ui::timeline::Error> {
271
    room.timeline_builder()
272
        .with_focus(focus)
273
        .track_read_marker_and_receipts(TimelineReadReceiptTracking::AllEvents)
274
        .build()
275
        .await
276
}
277
278
async fn watch_typing(
279
    room_id: OwnedRoomId,
280
    mut typing: tokio::sync::broadcast::Receiver<Vec<ruma::OwnedUserId>>,
281
    events: crate::transport::EventSender,
282
) {
283
    loop {
284
        match typing.recv().await {
285
            Ok(users) => {
286
                let state = TypingState {
287
                    room_id: room_id.clone(),
288
                    users,
289
                };
290
                if events.send(Event::TypingChanged(state)).await.is_err() {
291
                    return;
292
                }
293
            }
294
            Err(tokio::sync::broadcast::error::RecvError::Lagged(skipped)) => {
295
                tracing::warn!(%room_id, skipped, "typing watcher lagged");
296
            }
297
            Err(tokio::sync::broadcast::error::RecvError::Closed) => return,
298
        }
299
    }
300
}
301
302
struct Timelines {
303
    room: TimelineSession,
304
    threads: Threads,
305
    work: TaskSet<()>,
306
}
307
308
impl Timelines {
309
    async fn execute(&mut self, command: TimelineCommand, reply: Responder, room: &Room) {
310
        match command {
311
            TimelineCommand::OpenThread {
312
                room_id,
313
                root_event_id,
314
            } => self
315
                .threads
316
                .open(TimelineId::thread(room_id, root_event_id), reply, room),
317
            TimelineCommand::CloseThread {
318
                room_id,
319
                root_event_id,
320
            } => {
321
                let id = TimelineId::thread(room_id, root_event_id);
322
                reply.done(self.threads.close_one(&id).await);
323
            }
324
            TimelineCommand::OpenPinned(room_id) => {
325
                self.threads.open(TimelineId::Pinned(room_id), reply, room);
326
            }
327
            TimelineCommand::ClosePinned(room_id) => {
328
                reply.done(self.threads.close_one(&TimelineId::Pinned(room_id)).await);
329
            }
330
            TimelineCommand::Resync {
331
                timeline_id: TimelineId::Room(_),
332
            } => reply.done(self.room.resync().await),
333
            command => {
334
                let Some(id) = command
335
                    .timeline_id()
336
                    .filter(|id| !matches!(id, TimelineId::Room(_)))
337
                    .cloned()
338
                else {
339
                    return self.room.execute(command, reply, &mut self.work).await;
340
                };
341
                if matches!(id, TimelineId::Pinned(_))
342
                    && matches!(
343
                        command,
344
                        TimelineCommand::Send { .. } | TimelineCommand::Paginate { .. }
345
                    )
346
                {
347
                    return reply.done(Err(Error::new(
348
                        ErrorKind::Invalid,
349
                        "the pinned timeline can't send or paginate",
350
                    )));
351
                }
352
                if let TimelineCommand::MarkRead {
353
                    timeline_id,
354
                    event_id,
355
                } = &command
356
                    && !self.threads.open.contains_key(&id)
357
                {
358
                    let client = room.client();
359
                    let (timeline_id, event_id) = (timeline_id.clone(), event_id.clone());
360
                    self.work.spawn(async move {
361
                        reply.done(
362
                            super::commands::mark_read(&client, &timeline_id, event_id).await,
363
                        );
364
                    });
365
                    return;
366
                }
367
                match self.threads.route(id, command, reply) {
368
                    Routed::Open(session, TimelineCommand::Resync { .. }, reply) => {
369
                        reply.done(session.resync().await);
370
                    }
371
                    Routed::Open(session, command, reply) => {
372
                        session.execute(command, reply, &mut self.work).await;
373
                    }
374
                    Routed::Queued => {}
375
                }
376
            }
377
        }
378
    }
379
380
    async fn thread_built(
381
        &mut self,
382
        built: Result<Built, JoinError>,
383
        room_id: &OwnedRoomId,
384
        context: &SessionContext,
385
    ) {
386
        let Some((id, queued, timeline)) = self.threads.built(built) else {
387
            return;
388
        };
389
        let timeline = match timeline {
390
            Ok(timeline) => timeline,
391
            Err(error) => {
392
                tracing::warn!(%error, %room_id, ?id, "timeline build failed");
393
                let error = Error::with_source(ErrorKind::Timeline, "timeline build failed", error);
394
                for (_, reply) in queued {
395
                    reply.done(Err(error.clone()));
396
                }
397
                return;
398
            }
399
        };
400
        let Ok(session) = TimelineSession::start(id.clone(), timeline, context.clone()).await
401
        else {
402
            return;
403
        };
404
        let session = self.threads.open.entry(id).insert_entry(session).into_mut();
405
        for (command, reply) in queued {
406
            match command {
407
                None => reply.done(Ok(())),
408
                Some(TimelineCommand::Resync { .. }) => reply.done(session.resync().await),
409
                Some(command) => session.execute(command, reply, &mut self.work).await,
410
            }
411
        }
412
    }
413
}
414
415
type Built = (
416
    TimelineId,
417
    u64,
418
    Result<Timeline, matrix_sdk_ui::timeline::Error>,
419
);
420
421
/// `None` is the open itself, everything else waits behind it
422
type Queued = VecDeque<(Option<TimelineCommand>, Responder)>;
423
424
enum Routed<'a> {
425
    Open(&'a mut TimelineSession, TimelineCommand, Responder),
426
    Queued,
427
}
428
429
struct OpeningThread {
430
    generation: u64,
431
    build: AbortHandle,
432
    queued: Queued,
433
}
434
435
/// threads and the pinned timeline, which build off the actor and queue what arrives meanwhile
436
struct Threads {
437
    open: HashMap<TimelineId, TimelineSession>,
438
    opening: HashMap<TimelineId, OpeningThread>,
439
    builds: TaskSet<Built>,
440
    next_generation: u64,
441
    queued: usize,
442
    capacity: usize,
443
}
444
445
impl Threads {
446
    fn new(capacity: usize) -> Self {
447
        Self {
448
            open: HashMap::new(),
449
            opening: HashMap::new(),
450
            builds: TaskSet::new(),
451
            next_generation: 0,
452
            queued: 0,
453
            capacity,
454
        }
455
    }
456
457
    fn enqueue(&mut self, id: &TimelineId, command: Option<TimelineCommand>, reply: Responder) {
458
        if self.queued >= self.capacity {
459
            return reply.done(Err(Error::new(
460
                ErrorKind::QueueFull,
461
                "thread command queue is full",
462
            )));
463
        }
464
        let Some(opening) = self.opening.get_mut(id) else {
465
            return reply.done(Err(Error::new(
466
                ErrorKind::TimelineNotOpen,
467
                "timeline is not open",
468
            )));
469
        };
470
        opening.queued.push_back((command, reply));
471
        self.queued += 1;
472
    }
473
474
    fn open(&mut self, id: TimelineId, reply: Responder, room: &Room) {
475
        if self.open.contains_key(&id) {
476
            return reply.done(Ok(()));
477
        }
478
        if !self.opening.contains_key(&id) {
479
            let generation = self.next_generation;
480
            self.next_generation = self.next_generation.wrapping_add(1);
481
            let room = room.clone();
482
            let build_id = id.clone();
483
            let build = self.builds.spawn(async move {
484
                let focus = match &build_id {
485
                    TimelineId::Thread { root_event_id, .. } => TimelineFocus::Thread {
486
                        root_event_id: root_event_id.clone(),
487
                    },
488
                    _ => TimelineFocus::PinnedEvents,
489
                };
490
                (build_id, generation, build_timeline(&room, focus).await)
491
            });
492
            self.opening.insert(
493
                id.clone(),
494
                OpeningThread {
495
                    generation,
496
                    build,
497
                    queued: VecDeque::new(),
498
                },
499
            );
500
        }
501
        self.enqueue(&id, None, reply);
502
    }
503
504
    fn route(&mut self, id: TimelineId, command: TimelineCommand, reply: Responder) -> Routed<'_> {
505
        if !self.open.contains_key(&id) {
506
            self.enqueue(&id, Some(command), reply);
507
            return Routed::Queued;
508
        }
509
        let session = self.open.get_mut(&id).expect("timeline was just checked");
510
        Routed::Open(session, command, reply)
511
    }
512
513
    /// `None` when the build belongs to a thread that was closed while it ran
514
    fn built(
515
        &mut self,
516
        built: Result<Built, JoinError>,
517
    ) -> Option<(
518
        TimelineId,
519
        Queued,
520
        Result<Timeline, matrix_sdk_ui::timeline::Error>,
521
    )> {
522
        let (id, generation, timeline) = match built {
523
            Ok(built) => built,
524
            Err(error) if error.is_cancelled() => return None,
525
            Err(error) => resume_unwind(error),
526
        };
527
        if self
528
            .opening
529
            .get(&id)
530
            .is_none_or(|opening| opening.generation != generation)
531
        {
532
            return None;
533
        }
534
        let opening = self.opening.remove(&id)?;
535
        self.queued -= opening.queued.len();
536
        Some((id, opening.queued, timeline))
537
    }
538
539
    async fn close_one(&mut self, id: &TimelineId) -> Result<(), Error> {
540
        if let Some(session) = self.open.remove(id) {
541
            session.close().await;
542
            return Ok(());
543
        }
544
        let Some(opening) = self.opening.remove(id) else {
545
            return Err(Error::new(
546
                ErrorKind::TimelineNotOpen,
547
                "timeline is not open",
548
            ));
549
        };
550
        opening.build.abort();
551
        self.queued -= opening.queued.len();
552
        Ok(())
553
    }
554
555
    async fn close(&mut self) {
556
        self.builds.shutdown().await;
557
        self.opening.clear();
558
        for (_, session) in self.open.drain() {
559
            session.close().await;
560
        }
561
    }
562
}