atagen

macltok

public
Star0
Watch0
3eee54c8ed63154fa8fe3d292e7556b4c19379fa
3
ssh://brewery@pls.lobotomise.me:33/atagen/macltok.git
src/engine/repair.rsRust430 lines14.3 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
    /// a room with retained events already shows the live end, and a limited sync has put its
107
    /// own gap there, so only an empty room walks back from the live end. otherwise the newest
108
    /// gap gets one page, so a limited sync that skipped nothing leaves no gap behind
109
    pub(super) async fn start_recent(&mut self) {
110
        let retained = async {
111
            let (cache, _handles) = self.room.event_cache().await?;
112
            if !cache.has_retained_events().await? {
113
                return Ok(None);
114
            }
115
            let gaps = cache.history_gaps().await?;
116
            Ok::<_, EventCacheError>(Some(gaps.into_iter().rev().find(|gap| {
117
                matches!(
118
                    gap.status,
119
                    HistoryGapStatus::Idle | HistoryGapStatus::Failed { .. }
120
                )
121
            })))
122
        }
123
        .await;
124
        match retained {
125
            Ok(Some(Some(gap))) => self.start_gap_fill(gap.id, None, 1, false).await,
126
            Ok(Some(None)) => {}
127
            Ok(None) | Err(_) => self.start_recent_with(None).await,
128
        }
129
    }
130
131
    pub(super) async fn execute(&mut self, command: HistoryRepairCommand, reply: Responder) {
132
        match command {
133
            HistoryRepairCommand::Cancel { .. } => {
134
                if let Some(running) = self.running.as_mut() {
135
                    running.responders.push(PendingResponder {
136
                        responder: reply,
137
                        cancel_ack: true,
138
                    });
139
                    running.cancel.send_replace(true);
140
                } else {
141
                    reply.done(Ok(()));
142
                }
143
            }
144
            HistoryRepairCommand::RetryRecent { .. } => self.start_recent_with(Some(reply)).await,
145
            HistoryRepairCommand::FillGap { gap_id, .. } => {
146
                self.start_gap_fill(gap_id, Some(reply), MAX_GAP_PAGES, true)
147
                    .await;
148
            }
149
        }
150
    }
151
152
    async fn start_recent_with(&mut self, responder: Option<Responder>) {
153
        if let Some(running) = &mut self.running {
154
            if let Some(responder) = responder {
155
                if running.gap_id.is_none() {
156
                    running.responders.push(PendingResponder {
157
                        responder,
158
                        cancel_ack: false,
159
                    });
160
                } else {
161
                    responder.done(Err(room_busy()));
162
                }
163
            }
164
            return;
165
        }
166
        let event = Event::RecentHistoryChanged {
167
            room_id: self.room.room_id().to_owned(),
168
            state: RecentHistoryState::Loading,
169
        };
170
        if self.context.events.send(event).await.is_err() {
171
            if let Some(responder) = responder {
172
                responder.done(Err(Error::interrupted()));
173
            }
174
            return;
175
        }
176
        let (cancel, receiver) = watch::channel(false);
177
        let room = self.room.clone();
178
        let task = spawn(async move { load_recent(room, receiver).await });
179
        self.running = Some(Running {
180
            task,
181
            cancel,
182
            responders: responder
183
                .map(|responder| PendingResponder {
184
                    responder,
185
                    cancel_ack: false,
186
                })
187
                .into_iter()
188
                .collect(),
189
            gap_id: None,
190
            closing: false,
191
        });
192
    }
193
194
    async fn start_gap_fill(
195
        &mut self,
196
        gap_id: OwnedTransactionId,
197
        responder: Option<Responder>,
198
        pages: u32,
199
        spend_budget: bool,
200
    ) {
201
        if self.is_running() {
202
            if let Some(responder) = responder {
203
                responder.done(Err(room_busy()));
204
            }
205
            return;
206
        }
207
        let room = self.room.clone();
208
        let progress = report(self.room.room_id().to_owned(), gap_id.clone());
209
        if self
210
            .context
211
            .events
212
            .send(Event::HistoryRepairChanged(progress.clone()))
213
            .await
214
            .is_err()
215
        {
216
            if let Some(responder) = responder {
217
                responder.done(Err(Error::interrupted()));
218
            }
219
            return;
220
        }
221
        let (cancel, receiver) = watch::channel(false);
222
        let task =
223
            spawn(async move { fill_gap(room, progress, receiver, pages, spend_budget).await });
224
        self.running = Some(Running {
225
            task,
226
            cancel,
227
            responders: responder
228
                .map(|responder| PendingResponder {
229
                    responder,
230
                    cancel_ack: false,
231
                })
232
                .into_iter()
233
                .collect(),
234
            gap_id: Some(gap_id),
235
            closing: false,
236
        });
237
    }
238
239
    pub(super) async fn next_completion(&mut self) -> Option<Completion> {
240
        let running = self.running.as_mut()?;
241
        let result = (&mut running.task).await;
242
        Some(self.finish_task(result))
243
    }
244
245
    pub(super) async fn shutdown(&mut self) -> Option<Completion> {
246
        let running = self.running.as_mut()?;
247
        running.closing = true;
248
        running.cancel.send_replace(true);
249
        let result = (&mut running.task).await;
250
        Some(self.finish_task(result))
251
    }
252
253
    fn finish_task(&mut self, result: Result<Finished, JoinError>) -> Completion {
254
        let running = self.running.take().expect("repair task exists");
255
        let finished = result.unwrap_or(Finished {
256
            stopped: Stopped::Failed(Error::interrupted()),
257
            committed: false,
258
            report: None,
259
        });
260
        let result = match &finished.stopped {
261
            Stopped::Done => Ok(CommandOutput::Done),
262
            Stopped::Cancelled => Err(Error::interrupted()),
263
            Stopped::Failed(error) => Err(error.clone()),
264
        };
265
        let outcome = match running.gap_id {
266
            None => Outcome::Recent(match finished.stopped {
267
                _ if running.closing => RecentHistoryState::Idle,
268
                Stopped::Done => RecentHistoryState::Loaded,
269
                Stopped::Cancelled => RecentHistoryState::Cancelled,
270
                Stopped::Failed(error) => RecentHistoryState::Failed(error.message),
271
            }),
272
            Some(gap_id) => {
273
                let mut report = finished
274
                    .report
275
                    .unwrap_or_else(|| report(self.room.room_id().to_owned(), gap_id));
276
                match finished.stopped {
277
                    Stopped::Done => {}
278
                    Stopped::Cancelled => report.stop = Some(HistoryRepairStop::Cancelled),
279
                    Stopped::Failed(error) => report.failure = Some(error),
280
                }
281
                report.phase = if report.stop.is_none() && report.failure.is_none() {
282
                    HistoryRepairPhase::Ready
283
                } else {
284
                    HistoryRepairPhase::Paused
285
                };
286
                Outcome::Fill(report)
287
            }
288
        };
289
        Completion {
290
            outcome,
291
            committed: finished.committed,
292
            closing: running.closing,
293
            result,
294
            responders: running.responders,
295
        }
296
    }
297
}
298
299
async fn load_recent(room: Room, mut cancel: watch::Receiver<bool>) -> Finished {
300
    let outcome = tokio::select! {
301
        biased;
302
        _ = cancel.changed() => None,
303
        result = async {
304
            let (cache, _handles) = room.event_cache().await?;
305
            cache.load_recent_history().await
306
        } => Some(result),
307
    };
308
    let stopped = match outcome {
309
        None => Stopped::Cancelled,
310
        Some(Ok(_)) => Stopped::Done,
311
        Some(Err(error)) => Stopped::Failed(cache_error(error)),
312
    };
313
    Finished {
314
        committed: matches!(stopped, Stopped::Done),
315
        stopped,
316
        report: None,
317
    }
318
}
319
320
async fn fill_gap(
321
    room: Room,
322
    mut report: HistoryRepairReport,
323
    mut cancel: watch::Receiver<bool>,
324
    pages: u32,
325
    spend_budget: bool,
326
) -> Finished {
327
    let mut committed = false;
328
    let stopped = async {
329
        let (cache, _handles) = room.event_cache().await.map_err(cache_error)?;
330
        for _ in 0..pages {
331
            let next = tokio::select! {
332
                biased;
333
                _ = cancel.changed() => return Ok(Stopped::Cancelled),
334
                result = cache.fill_history_gap_once(report.gap_id.clone()) => match result {
335
                    Ok(next) => next,
336
                    Err(error) => {
337
                        committed |= matches!(error, EventCacheError::PaginationError(_));
338
                        return Err(cache_error(error));
339
                    }
340
                }
341
            };
342
            let (gap, events) = match next {
343
                HistoryGapFillOutcome::NotFound => {
344
                    return Err(Error::new(
345
                        ErrorKind::EventNotFound,
346
                        "history gap was not found",
347
                    ));
348
                }
349
                HistoryGapFillOutcome::Busy => return Err(room_busy()),
350
                HistoryGapFillOutcome::Updated { gap, events } => (gap, events),
351
            };
352
            // the sdk answers an already unavailable gap without asking the server, and a joined
353
            // one with NotFound. an empty first page that hits the end looks the same, uncounted
354
            let settled_before =
355
                report.pages == 0 && events == 0 && gap.status == HistoryGapStatus::Unavailable;
356
            if !settled_before {
357
                report.pages += 1;
358
                committed = true;
359
            }
360
            report.events = report
361
                .events
362
                .saturating_add(u32::try_from(events).unwrap_or(u32::MAX));
363
            match gap.status {
364
                HistoryGapStatus::Joined => {
365
                    report.continuity = HistoryContinuity::Verified;
366
                    return Ok(Stopped::Done);
367
                }
368
                HistoryGapStatus::Unavailable => {
369
                    report.continuity = HistoryContinuity::AccessibleStart;
370
                    report.stop = Some(HistoryRepairStop::HistoryUnavailable);
371
                    return Ok(Stopped::Done);
372
                }
373
                HistoryGapStatus::Paused => {
374
                    report.stop = Some(HistoryRepairStop::CursorStalled);
375
                    return Ok(Stopped::Done);
376
                }
377
                HistoryGapStatus::Failed { message } => {
378
                    return Err(Error::new(ErrorKind::Network, message));
379
                }
380
                HistoryGapStatus::Idle | HistoryGapStatus::BudgetSpent => {}
381
            }
382
        }
383
        report.stop = Some(HistoryRepairStop::Budget);
384
        if spend_budget {
385
            if let Err(error) = cache.spend_history_budget(report.gap_id.clone()).await {
386
                tracing::debug!(%error, "marking a history gap's spent budget failed");
387
            }
388
            committed = true;
389
        }
390
        Ok(Stopped::Done)
391
    }
392
    .await
393
    .unwrap_or_else(Stopped::Failed);
394
    Finished {
395
        stopped,
396
        committed,
397
        report: Some(report),
398
    }
399
}
400
401
fn report(room_id: OwnedRoomId, gap_id: OwnedTransactionId) -> HistoryRepairReport {
402
    HistoryRepairReport {
403
        room_id,
404
        gap_id,
405
        pages: 0,
406
        events: 0,
407
        phase: HistoryRepairPhase::Bridging,
408
        continuity: HistoryContinuity::Unresolved,
409
        stop: None,
410
        failure: None,
411
    }
412
}
413
414
fn cache_error(source: EventCacheError) -> Error {
415
    let kind = match &source {
416
        EventCacheError::PaginationError(error) => crate::error::classify_sdk_error(error),
417
        EventCacheError::InitialPaginationError(_) => ErrorKind::Network,
418
        EventCacheError::RoomNotFound { .. } => ErrorKind::RoomUnknown,
419
        EventCacheError::ClientDropped => ErrorKind::Interrupted,
420
        _ => ErrorKind::Store,
421
    };
422
    Error::with_source(kind, "room history operation failed", source)
423
}
424
425
fn room_busy() -> Error {
426
    Error::new(
427
        ErrorKind::RoomBusy,
428
        "room history operation is already running",
429
    )
430
}