atagen

macltok

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