atagen

macltok

public
Star0
Watch0
3e5a785a7273c52abdc58fb07ceb86b59eb59727
3
ssh://brewery@pls.lobotomise.me:33/atagen/macltok.git
src/engine/room.rsRust602 lines21.9 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
        timelines.work.spawn(probe_sync_hole(self.room.clone()));
227
        let mut probe = Box::pin(matrix_sdk::sleep::sleep(PROBE_INTERVAL));
228
        loop {
229
            tokio::select! {
230
                () = &mut probe => {
231
                    timelines.work.spawn(probe_sync_hole(self.room.clone()));
232
                    probe = Box::pin(matrix_sdk::sleep::sleep(PROBE_INTERVAL));
233
                }
234
                Some(info) = room_info.next() => {
235
                    if matches!(info.state(), RoomState::Left | RoomState::Banned) {
236
                        tracing::debug!(room_id = %self.room_id, "room actor stopped because we left the room");
237
                        return None;
238
                    }
239
                }
240
                request = self.requests.recv() => match request {
241
                    None => return None,
242
                    Some(ActorRequest::Close(reply)) => return Some(reply),
243
                    Some(ActorRequest::Open(reply)) => reply.done(timelines.room.resync().await),
244
                    Some(ActorRequest::Execute(command, reply)) => {
245
                        let label = label(&command);
246
                        tracing::debug!(room_id = %self.room_id, command = %label, "room command");
247
                        timelines.execute(command, reply, &self.room).await;
248
                        tracing::debug!(room_id = %self.room_id, command = %label, "room command handed off");
249
                    }
250
                    Some(ActorRequest::Repair(command, reply)) => {
251
                        let label = label(&command);
252
                        tracing::debug!(room_id = %self.room_id, command = %label, "room repair command");
253
                        repair.execute(command, reply).await;
254
                        tracing::debug!(room_id = %self.room_id, command = %label, "room repair command handed off");
255
                    }
256
                },
257
                completion = repair.next_completion(), if repair.is_running() => {
258
                    if let Some(completion) = completion {
259
                        self.finish_repair(completion, timelines).await;
260
                    }
261
                }
262
                Some(built) = timelines.threads.builds.join_next(), if !timelines.threads.builds.is_empty() => {
263
                    timelines.thread_built(built, &self.room_id, &self.context).await;
264
                }
265
                Some(label) = self.panics.recv() => panic!("{label} panicked in room {}", self.room_id),
266
                Some(done) = timelines.work.join_next(), if !timelines.work.is_empty() => {
267
                    if let Err(error) = done && is_panic(&error) {
268
                        tracing::warn!(room_id = %self.room_id, %error, "timeline operation panicked");
269
                    }
270
                }
271
            }
272
        }
273
    }
274
275
    async fn refuse_everything(&mut self, error: Error) {
276
        for reply in self.opening.drain(..) {
277
            reply.done(Err(error.clone()));
278
        }
279
        self.requests.close();
280
        while let Some(request) = self.requests.recv().await {
281
            request.into_reply().done(Err(error.clone()));
282
        }
283
    }
284
}
285
286
/// a server that under-reports `limited` hides skipped events behind a sync token the sdk drops,
287
/// so an open room looks behind the newest one now and then. without one recorded it costs nothing
288
const PROBE_INTERVAL: std::time::Duration = std::time::Duration::from_secs(30);
289
290
async fn probe_sync_hole(room: Room) {
291
    let result = async {
292
        let (cache, _handles) = room.event_cache().await?;
293
        cache.probe_sync_hole().await
294
    }
295
    .await;
296
    match result {
297
        Ok(Some(found)) if found > 0 => {
298
            tracing::info!(room_id = %room.room_id(), found, "history gap probe: new events");
299
        }
300
        Ok(_) => {}
301
        Err(error) => {
302
            tracing::debug!(room_id = %room.room_id(), %error, "history gap probe failed")
303
        }
304
    }
305
}
306
307
async fn build_timeline(
308
    room: &Room,
309
    focus: TimelineFocus,
310
) -> Result<Timeline, matrix_sdk_ui::timeline::Error> {
311
    room.timeline_builder()
312
        .with_focus(focus)
313
        .track_read_marker_and_receipts(TimelineReadReceiptTracking::AllEvents)
314
        .build()
315
        .await
316
}
317
318
async fn watch_typing(
319
    room_id: OwnedRoomId,
320
    mut typing: tokio::sync::broadcast::Receiver<Vec<ruma::OwnedUserId>>,
321
    events: crate::transport::EventSender,
322
) {
323
    loop {
324
        match typing.recv().await {
325
            Ok(users) => {
326
                let state = TypingState {
327
                    room_id: room_id.clone(),
328
                    users,
329
                };
330
                if events.send(Event::TypingChanged(state)).await.is_err() {
331
                    return;
332
                }
333
            }
334
            Err(tokio::sync::broadcast::error::RecvError::Lagged(skipped)) => {
335
                tracing::warn!(%room_id, skipped, "typing watcher lagged");
336
            }
337
            Err(tokio::sync::broadcast::error::RecvError::Closed) => return,
338
        }
339
    }
340
}
341
342
struct Timelines {
343
    room: TimelineSession,
344
    threads: Threads,
345
    work: TaskSet<()>,
346
}
347
348
impl Timelines {
349
    async fn execute(&mut self, command: TimelineCommand, reply: Responder, room: &Room) {
350
        match command {
351
            TimelineCommand::OpenThread {
352
                room_id,
353
                root_event_id,
354
            } => self
355
                .threads
356
                .open(TimelineId::thread(room_id, root_event_id), reply, room),
357
            TimelineCommand::CloseThread {
358
                room_id,
359
                root_event_id,
360
            } => {
361
                let id = TimelineId::thread(room_id, root_event_id);
362
                reply.done(self.threads.close_one(&id).await);
363
            }
364
            TimelineCommand::OpenPinned(room_id) => {
365
                self.threads.open(TimelineId::Pinned(room_id), reply, room);
366
            }
367
            TimelineCommand::ClosePinned(room_id) => {
368
                reply.done(self.threads.close_one(&TimelineId::Pinned(room_id)).await);
369
            }
370
            TimelineCommand::Resync {
371
                timeline_id: TimelineId::Room(_),
372
            } => reply.done(self.room.resync().await),
373
            command => {
374
                let Some(id) = command
375
                    .timeline_id()
376
                    .filter(|id| !matches!(id, TimelineId::Room(_)))
377
                    .cloned()
378
                else {
379
                    return self.room.execute(command, reply, &mut self.work).await;
380
                };
381
                if matches!(id, TimelineId::Pinned(_))
382
                    && matches!(
383
                        command,
384
                        TimelineCommand::Send { .. } | TimelineCommand::Paginate { .. }
385
                    )
386
                {
387
                    return reply.done(Err(Error::new(
388
                        ErrorKind::Invalid,
389
                        "the pinned timeline can't send or paginate",
390
                    )));
391
                }
392
                if let TimelineCommand::MarkRead {
393
                    timeline_id,
394
                    event_id,
395
                } = &command
396
                    && !self.threads.open.contains_key(&id)
397
                {
398
                    let client = room.client();
399
                    let (timeline_id, event_id) = (timeline_id.clone(), event_id.clone());
400
                    self.work.spawn(async move {
401
                        reply.done(
402
                            super::commands::mark_read(&client, &timeline_id, event_id).await,
403
                        );
404
                    });
405
                    return;
406
                }
407
                match self.threads.route(id, command, reply) {
408
                    Routed::Open(session, TimelineCommand::Resync { .. }, reply) => {
409
                        reply.done(session.resync().await);
410
                    }
411
                    Routed::Open(session, command, reply) => {
412
                        session.execute(command, reply, &mut self.work).await;
413
                    }
414
                    Routed::Queued => {}
415
                }
416
            }
417
        }
418
    }
419
420
    async fn thread_built(
421
        &mut self,
422
        built: Result<Built, JoinError>,
423
        room_id: &OwnedRoomId,
424
        context: &SessionContext,
425
    ) {
426
        let Some((id, queued, timeline)) = self.threads.built(built) else {
427
            return;
428
        };
429
        let timeline = match timeline {
430
            Ok(timeline) => timeline,
431
            Err(error) => {
432
                tracing::warn!(%error, %room_id, ?id, "timeline build failed");
433
                let error = Error::with_source(ErrorKind::Timeline, "timeline build failed", error);
434
                for (_, reply) in queued {
435
                    reply.done(Err(error.clone()));
436
                }
437
                return;
438
            }
439
        };
440
        let Ok(session) = TimelineSession::start(id.clone(), timeline, context.clone()).await
441
        else {
442
            return;
443
        };
444
        let session = self.threads.open.entry(id).insert_entry(session).into_mut();
445
        for (command, reply) in queued {
446
            match command {
447
                None => reply.done(Ok(())),
448
                Some(TimelineCommand::Resync { .. }) => reply.done(session.resync().await),
449
                Some(command) => session.execute(command, reply, &mut self.work).await,
450
            }
451
        }
452
    }
453
}
454
455
type Built = (
456
    TimelineId,
457
    u64,
458
    Result<Timeline, matrix_sdk_ui::timeline::Error>,
459
);
460
461
/// `None` is the open itself, everything else waits behind it
462
type Queued = VecDeque<(Option<TimelineCommand>, Responder)>;
463
464
enum Routed<'a> {
465
    Open(&'a mut TimelineSession, TimelineCommand, Responder),
466
    Queued,
467
}
468
469
struct OpeningThread {
470
    generation: u64,
471
    build: AbortHandle,
472
    queued: Queued,
473
}
474
475
/// threads and the pinned timeline, which build off the actor and queue what arrives meanwhile
476
struct Threads {
477
    open: HashMap<TimelineId, TimelineSession>,
478
    opening: HashMap<TimelineId, OpeningThread>,
479
    builds: TaskSet<Built>,
480
    next_generation: u64,
481
    queued: usize,
482
    capacity: usize,
483
}
484
485
impl Threads {
486
    fn new(capacity: usize) -> Self {
487
        Self {
488
            open: HashMap::new(),
489
            opening: HashMap::new(),
490
            builds: TaskSet::new(),
491
            next_generation: 0,
492
            queued: 0,
493
            capacity,
494
        }
495
    }
496
497
    fn enqueue(&mut self, id: &TimelineId, command: Option<TimelineCommand>, reply: Responder) {
498
        if self.queued >= self.capacity {
499
            return reply.done(Err(Error::new(
500
                ErrorKind::QueueFull,
501
                "thread command queue is full",
502
            )));
503
        }
504
        let Some(opening) = self.opening.get_mut(id) else {
505
            return reply.done(Err(Error::new(
506
                ErrorKind::TimelineNotOpen,
507
                "timeline is not open",
508
            )));
509
        };
510
        opening.queued.push_back((command, reply));
511
        self.queued += 1;
512
    }
513
514
    fn open(&mut self, id: TimelineId, reply: Responder, room: &Room) {
515
        if self.open.contains_key(&id) {
516
            return reply.done(Ok(()));
517
        }
518
        if !self.opening.contains_key(&id) {
519
            let generation = self.next_generation;
520
            self.next_generation = self.next_generation.wrapping_add(1);
521
            let room = room.clone();
522
            let build_id = id.clone();
523
            let build = self.builds.spawn(async move {
524
                let focus = match &build_id {
525
                    TimelineId::Thread { root_event_id, .. } => TimelineFocus::Thread {
526
                        root_event_id: root_event_id.clone(),
527
                    },
528
                    _ => TimelineFocus::PinnedEvents,
529
                };
530
                (build_id, generation, build_timeline(&room, focus).await)
531
            });
532
            self.opening.insert(
533
                id.clone(),
534
                OpeningThread {
535
                    generation,
536
                    build,
537
                    queued: VecDeque::new(),
538
                },
539
            );
540
        }
541
        self.enqueue(&id, None, reply);
542
    }
543
544
    fn route(&mut self, id: TimelineId, command: TimelineCommand, reply: Responder) -> Routed<'_> {
545
        if !self.open.contains_key(&id) {
546
            self.enqueue(&id, Some(command), reply);
547
            return Routed::Queued;
548
        }
549
        let session = self.open.get_mut(&id).expect("timeline was just checked");
550
        Routed::Open(session, command, reply)
551
    }
552
553
    /// `None` when the build belongs to a thread that was closed while it ran
554
    fn built(
555
        &mut self,
556
        built: Result<Built, JoinError>,
557
    ) -> Option<(
558
        TimelineId,
559
        Queued,
560
        Result<Timeline, matrix_sdk_ui::timeline::Error>,
561
    )> {
562
        let (id, generation, timeline) = match built {
563
            Ok(built) => built,
564
            Err(error) if error.is_cancelled() => return None,
565
            Err(error) => resume_unwind(error),
566
        };
567
        if self
568
            .opening
569
            .get(&id)
570
            .is_none_or(|opening| opening.generation != generation)
571
        {
572
            return None;
573
        }
574
        let opening = self.opening.remove(&id)?;
575
        self.queued -= opening.queued.len();
576
        Some((id, opening.queued, timeline))
577
    }
578
579
    async fn close_one(&mut self, id: &TimelineId) -> Result<(), Error> {
580
        if let Some(session) = self.open.remove(id) {
581
            session.close().await;
582
            return Ok(());
583
        }
584
        let Some(opening) = self.opening.remove(id) else {
585
            return Err(Error::new(
586
                ErrorKind::TimelineNotOpen,
587
                "timeline is not open",
588
            ));
589
        };
590
        opening.build.abort();
591
        self.queued -= opening.queued.len();
592
        Ok(())
593
    }
594
595
    async fn close(&mut self) {
596
        self.builds.shutdown().await;
597
        self.opening.clear();
598
        for (_, session) in self.open.drain() {
599
            session.close().await;
600
        }
601
    }
602
}