atagen

macltok

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