atagen

macltok

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