atagen

macltok

public
Star0
Watch0
ssh://brewery@pls.lobotomise.me:33/atagen/macltok.git
src/engine/repair.rsRust386 lines12.6 KB
1
use matrix_sdk::{
2
    Room,
3
    event_cache::{EventCacheError, HistoryGapFillOutcome, HistoryGapStatus},
4
};
5
use ruma::{OwnedRoomId, OwnedTransactionId};
6
use tokio::sync::watch;
7
8
use super::{
9
    executor::{JoinError, JoinHandle, spawn},
10
    timeline::SessionContext,
11
};
12
use crate::{
13
    CommandOutput, Error, ErrorKind, Event, HistoryContinuity, HistoryRepairCommand,
14
    HistoryRepairPhase, HistoryRepairReport, HistoryRepairStop, RecentHistoryState,
15
    transport::Responder,
16
};
17
18
const MAX_GAP_PAGES: u32 = 4;
19
20
struct Running {
21
    task: JoinHandle<Finished>,
22
    cancel: watch::Sender<bool>,
23
    responders: Vec<PendingResponder>,
24
    gap_id: Option<OwnedTransactionId>,
25
    closing: bool,
26
}
27
28
struct PendingResponder {
29
    responder: Responder,
30
    cancel_ack: bool,
31
}
32
33
pub(super) enum Outcome {
34
    Recent(RecentHistoryState),
35
    Fill(HistoryRepairReport),
36
}
37
38
pub(super) struct Completion {
39
    pub(super) outcome: Outcome,
40
    /// history went into the cache, so the room timeline has to show it before anyone hears
41
    pub(super) committed: bool,
42
    pub(super) closing: bool,
43
    pub(super) result: Result<CommandOutput, Error>,
44
    responders: Vec<PendingResponder>,
45
}
46
47
impl Completion {
48
    /// the room timeline couldn't show what was committed
49
    pub(super) fn unpublished(&mut self, error: Error) {
50
        match &mut self.outcome {
51
            Outcome::Recent(state) => *state = RecentHistoryState::Failed(error.message.clone()),
52
            Outcome::Fill(report) => {
53
                report.phase = HistoryRepairPhase::Paused;
54
                report.failure = Some(error.clone());
55
            }
56
        }
57
        self.result = Err(error);
58
    }
59
60
    pub(super) fn resolve(
61
        self,
62
        result: Result<CommandOutput, Error>,
63
        cancellation: Result<(), Error>,
64
    ) {
65
        for responder in self.responders {
66
            if responder.cancel_ack {
67
                responder.responder.done(cancellation.clone());
68
            } else {
69
                responder.responder.send(result.clone());
70
            }
71
        }
72
    }
73
}
74
75
enum Stopped {
76
    Done,
77
    Cancelled,
78
    Failed(Error),
79
}
80
81
struct Finished {
82
    stopped: Stopped,
83
    committed: bool,
84
    report: Option<HistoryRepairReport>,
85
}
86
87
pub(super) struct RepairController {
88
    room: Room,
89
    context: SessionContext,
90
    running: Option<Running>,
91
}
92
93
impl RepairController {
94
    pub(super) fn new(room: Room, context: SessionContext) -> Self {
95
        Self {
96
            room,
97
            context,
98
            running: None,
99
        }
100
    }
101
102
    pub(super) fn is_running(&self) -> bool {
103
        self.running.is_some()
104
    }
105
106
    pub(super) async fn start_recent(&mut self) {
107
        self.start_recent_with(None).await;
108
    }
109
110
    pub(super) async fn execute(&mut self, command: HistoryRepairCommand, reply: Responder) {
111
        match command {
112
            HistoryRepairCommand::Cancel { .. } => {
113
                if let Some(running) = self.running.as_mut() {
114
                    running.responders.push(PendingResponder {
115
                        responder: reply,
116
                        cancel_ack: true,
117
                    });
118
                    running.cancel.send_replace(true);
119
                } else {
120
                    reply.done(Ok(()));
121
                }
122
            }
123
            HistoryRepairCommand::RetryRecent { .. } => self.start_recent_with(Some(reply)).await,
124
            HistoryRepairCommand::FillGap { gap_id, .. } => {
125
                self.start_gap_fill(gap_id, reply).await;
126
            }
127
        }
128
    }
129
130
    async fn start_recent_with(&mut self, responder: Option<Responder>) {
131
        if let Some(running) = &mut self.running {
132
            if let Some(responder) = responder {
133
                if running.gap_id.is_none() {
134
                    running.responders.push(PendingResponder {
135
                        responder,
136
                        cancel_ack: false,
137
                    });
138
                } else {
139
                    responder.done(Err(room_busy()));
140
                }
141
            }
142
            return;
143
        }
144
        let event = Event::RecentHistoryChanged {
145
            room_id: self.room.room_id().to_owned(),
146
            state: RecentHistoryState::Loading,
147
        };
148
        if self.context.events.send(event).await.is_err() {
149
            if let Some(responder) = responder {
150
                responder.done(Err(Error::interrupted()));
151
            }
152
            return;
153
        }
154
        let (cancel, receiver) = watch::channel(false);
155
        let room = self.room.clone();
156
        let task = spawn(async move { load_recent(room, receiver).await });
157
        self.running = Some(Running {
158
            task,
159
            cancel,
160
            responders: responder
161
                .map(|responder| PendingResponder {
162
                    responder,
163
                    cancel_ack: false,
164
                })
165
                .into_iter()
166
                .collect(),
167
            gap_id: None,
168
            closing: false,
169
        });
170
    }
171
172
    async fn start_gap_fill(&mut self, gap_id: OwnedTransactionId, responder: Responder) {
173
        if self.is_running() {
174
            responder.done(Err(room_busy()));
175
            return;
176
        }
177
        let room = self.room.clone();
178
        let progress = report(self.room.room_id().to_owned(), gap_id.clone());
179
        if self
180
            .context
181
            .events
182
            .send(Event::HistoryRepairChanged(progress.clone()))
183
            .await
184
            .is_err()
185
        {
186
            responder.done(Err(Error::interrupted()));
187
            return;
188
        }
189
        let (cancel, receiver) = watch::channel(false);
190
        let task = spawn(async move { fill_gap(room, progress, receiver).await });
191
        self.running = Some(Running {
192
            task,
193
            cancel,
194
            responders: vec![PendingResponder {
195
                responder,
196
                cancel_ack: false,
197
            }],
198
            gap_id: Some(gap_id),
199
            closing: false,
200
        });
201
    }
202
203
    pub(super) async fn next_completion(&mut self) -> Option<Completion> {
204
        let running = self.running.as_mut()?;
205
        let result = (&mut running.task).await;
206
        Some(self.finish_task(result))
207
    }
208
209
    pub(super) async fn shutdown(&mut self) -> Option<Completion> {
210
        let running = self.running.as_mut()?;
211
        running.closing = true;
212
        running.cancel.send_replace(true);
213
        let result = (&mut running.task).await;
214
        Some(self.finish_task(result))
215
    }
216
217
    fn finish_task(&mut self, result: Result<Finished, JoinError>) -> Completion {
218
        let running = self.running.take().expect("repair task exists");
219
        let finished = result.unwrap_or(Finished {
220
            stopped: Stopped::Failed(Error::interrupted()),
221
            committed: false,
222
            report: None,
223
        });
224
        let result = match &finished.stopped {
225
            Stopped::Done => Ok(CommandOutput::Done),
226
            Stopped::Cancelled => Err(Error::interrupted()),
227
            Stopped::Failed(error) => Err(error.clone()),
228
        };
229
        let outcome = match running.gap_id {
230
            None => Outcome::Recent(match finished.stopped {
231
                _ if running.closing => RecentHistoryState::Idle,
232
                Stopped::Done => RecentHistoryState::Loaded,
233
                Stopped::Cancelled => RecentHistoryState::Cancelled,
234
                Stopped::Failed(error) => RecentHistoryState::Failed(error.message),
235
            }),
236
            Some(gap_id) => {
237
                let mut report = finished
238
                    .report
239
                    .unwrap_or_else(|| report(self.room.room_id().to_owned(), gap_id));
240
                match finished.stopped {
241
                    Stopped::Done => {}
242
                    Stopped::Cancelled => report.stop = Some(HistoryRepairStop::Cancelled),
243
                    Stopped::Failed(error) => report.failure = Some(error),
244
                }
245
                report.phase = if report.stop.is_none() && report.failure.is_none() {
246
                    HistoryRepairPhase::Ready
247
                } else {
248
                    HistoryRepairPhase::Paused
249
                };
250
                Outcome::Fill(report)
251
            }
252
        };
253
        Completion {
254
            outcome,
255
            committed: finished.committed,
256
            closing: running.closing,
257
            result,
258
            responders: running.responders,
259
        }
260
    }
261
}
262
263
async fn load_recent(room: Room, mut cancel: watch::Receiver<bool>) -> Finished {
264
    let outcome = tokio::select! {
265
        biased;
266
        _ = cancel.changed() => None,
267
        result = async {
268
            let (cache, _handles) = room.event_cache().await?;
269
            cache.load_recent_history().await
270
        } => Some(result),
271
    };
272
    let stopped = match outcome {
273
        None => Stopped::Cancelled,
274
        Some(Ok(_)) => Stopped::Done,
275
        Some(Err(error)) => Stopped::Failed(cache_error(error)),
276
    };
277
    Finished {
278
        committed: matches!(stopped, Stopped::Done),
279
        stopped,
280
        report: None,
281
    }
282
}
283
284
async fn fill_gap(
285
    room: Room,
286
    mut report: HistoryRepairReport,
287
    mut cancel: watch::Receiver<bool>,
288
) -> Finished {
289
    let mut committed = false;
290
    let stopped = async {
291
        let (cache, _handles) = room.event_cache().await.map_err(cache_error)?;
292
        for _ in 0..MAX_GAP_PAGES {
293
            let next = tokio::select! {
294
                biased;
295
                _ = cancel.changed() => return Ok(Stopped::Cancelled),
296
                result = cache.fill_history_gap_once(report.gap_id.clone()) => match result {
297
                    Ok(next) => next,
298
                    Err(error) => {
299
                        committed |= matches!(error, EventCacheError::PaginationError(_));
300
                        return Err(cache_error(error));
301
                    }
302
                }
303
            };
304
            let (gap, events) = match next {
305
                HistoryGapFillOutcome::NotFound => {
306
                    return Err(Error::new(
307
                        ErrorKind::EventNotFound,
308
                        "history gap was not found",
309
                    ));
310
                }
311
                HistoryGapFillOutcome::Busy => return Err(room_busy()),
312
                HistoryGapFillOutcome::Updated { gap, events } => (gap, events),
313
            };
314
            // the sdk answers an already unavailable gap without asking the server, and a joined
315
            // one with NotFound. an empty first page that hits the end looks the same, uncounted
316
            let settled_before =
317
                report.pages == 0 && events == 0 && gap.status == HistoryGapStatus::Unavailable;
318
            if !settled_before {
319
                report.pages += 1;
320
                committed = true;
321
            }
322
            report.events = report
323
                .events
324
                .saturating_add(u32::try_from(events).unwrap_or(u32::MAX));
325
            match gap.status {
326
                HistoryGapStatus::Joined => {
327
                    report.continuity = HistoryContinuity::Verified;
328
                    return Ok(Stopped::Done);
329
                }
330
                HistoryGapStatus::Unavailable => {
331
                    report.continuity = HistoryContinuity::AccessibleStart;
332
                    report.stop = Some(HistoryRepairStop::HistoryUnavailable);
333
                    return Ok(Stopped::Done);
334
                }
335
                HistoryGapStatus::Paused => {
336
                    report.stop = Some(HistoryRepairStop::CursorStalled);
337
                    return Ok(Stopped::Done);
338
                }
339
                HistoryGapStatus::Failed { message } => {
340
                    return Err(Error::new(ErrorKind::Network, message));
341
                }
342
                HistoryGapStatus::Idle => {}
343
            }
344
        }
345
        report.stop = Some(HistoryRepairStop::Budget);
346
        Ok(Stopped::Done)
347
    }
348
    .await
349
    .unwrap_or_else(Stopped::Failed);
350
    Finished {
351
        stopped,
352
        committed,
353
        report: Some(report),
354
    }
355
}
356
357
fn report(room_id: OwnedRoomId, gap_id: OwnedTransactionId) -> HistoryRepairReport {
358
    HistoryRepairReport {
359
        room_id,
360
        gap_id,
361
        pages: 0,
362
        events: 0,
363
        phase: HistoryRepairPhase::Bridging,
364
        continuity: HistoryContinuity::Unresolved,
365
        stop: None,
366
        failure: None,
367
    }
368
}
369
370
fn cache_error(source: EventCacheError) -> Error {
371
    let kind = match &source {
372
        EventCacheError::PaginationError(error) => crate::error::classify_sdk_error(error),
373
        EventCacheError::InitialPaginationError(_) => ErrorKind::Network,
374
        EventCacheError::RoomNotFound { .. } => ErrorKind::RoomUnknown,
375
        EventCacheError::ClientDropped => ErrorKind::Interrupted,
376
        _ => ErrorKind::Store,
377
    };
378
    Error::with_source(kind, "room history operation failed", source)
379
}
380
381
fn room_busy() -> Error {
382
    Error::new(
383
        ErrorKind::RoomBusy,
384
        "room history operation is already running",
385
    )
386
}