atagen

macltok

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