atagen

macltok

public
Star0
Watch0
9eabb6b7587ddbf872d0ee2dfb752650c3f823f9
3
ssh://brewery@pls.lobotomise.me:33/atagen/macltok.git
src/engine/repair.rsRust453 lines15.6 KB
1
use matrix_sdk::{Room, event_cache::HistoryGapFillOutcome};
2
use tokio::sync::watch;
3
4
use super::{
5
    executor::{JoinError, JoinHandle, spawn},
6
    timeline::SessionContext,
7
};
8
use crate::{
9
    CommandOutput, Error, ErrorKind, HistoryContinuity, HistoryRepairBudget, HistoryRepairCommand,
10
    HistoryRepairPhase, HistoryRepairPolicy, HistoryRepairReport, HistoryRepairStop,
11
    HistoryRepairStrategy, RecentHistoryState, transport::Responder,
12
};
13
14
const MAX_GAP_PAGES: u32 = 4;
15
16
struct Running {
17
    task: JoinHandle<CompletionResult>,
18
    cancel: watch::Sender<bool>,
19
    responders: Vec<PendingResponder>,
20
    recent: bool,
21
}
22
23
struct PendingResponder {
24
    responder: Responder,
25
    cancel_ack: bool,
26
}
27
28
pub(super) struct Completion {
29
    pub(super) report: HistoryRepairReport,
30
    pub(super) recent_state: Option<RecentHistoryState>,
31
    pub(super) result: Result<CommandOutput, Error>,
32
    responders: Vec<PendingResponder>,
33
}
34
35
impl Completion {
36
    pub(super) fn resolve(
37
        self,
38
        result: Result<CommandOutput, Error>,
39
        cancellation: Result<(), Error>,
40
    ) {
41
        for responder in self.responders {
42
            if responder.cancel_ack {
43
                responder.responder.done(cancellation.clone());
44
            } else {
45
                responder.responder.send(result.clone());
46
            }
47
        }
48
    }
49
}
50
51
struct CompletionResult {
52
    report: HistoryRepairReport,
53
    recent_state: Option<RecentHistoryState>,
54
    result: Result<CommandOutput, Error>,
55
}
56
57
pub(super) struct RepairController {
58
    room: Room,
59
    context: SessionContext,
60
    running: Option<Running>,
61
}
62
63
impl RepairController {
64
    pub(super) fn new(room: Room, context: SessionContext) -> Self {
65
        Self {
66
            room,
67
            context,
68
            running: None,
69
        }
70
    }
71
72
    pub(super) fn is_running(&self) -> bool {
73
        self.running.is_some()
74
    }
75
76
    pub(super) async fn start_recent(&mut self) {
77
        self.start_recent_with(HistoryRepairPolicy::default(), None, true, Vec::new())
78
            .await;
79
    }
80
81
    pub(super) async fn execute(&mut self, command: HistoryRepairCommand, reply: Responder) {
82
        match command {
83
            HistoryRepairCommand::Cancel { .. } => {
84
                if let Some(running) = self.running.as_mut() {
85
                    running.responders.push(PendingResponder {
86
                        responder: reply,
87
                        cancel_ack: true,
88
                    });
89
                    running.cancel.send_replace(true);
90
                } else {
91
                    reply.done(Ok(()));
92
                }
93
            }
94
            HistoryRepairCommand::Start { policy, budget, .. } => {
95
                if let Err(error) = validate_budget(&budget) {
96
                    reply.done(Err(error));
97
                    return;
98
                }
99
                let policy = policy.unwrap_or_default();
100
                self.start_recent_with(policy, Some(reply), true, Vec::new())
101
                    .await;
102
            }
103
            HistoryRepairCommand::StartRecent { .. } => {
104
                self.start_recent_with(
105
                    HistoryRepairPolicy::default(),
106
                    Some(reply),
107
                    true,
108
                    Vec::new(),
109
                )
110
                .await;
111
            }
112
            HistoryRepairCommand::FillGap { gap_id, .. } => {
113
                self.start_gap_fill(gap_id, reply).await;
114
            }
115
            HistoryRepairCommand::Continue { .. } => reply.done(Err(Error::new(
116
                ErrorKind::Invalid,
117
                "continue requires a history gap ID, use FillGap",
118
            ))),
119
            HistoryRepairCommand::GetPolicy => {
120
                reply.send(
121
                    self.context
122
                        .mentions
123
                        .repair_policy()
124
                        .await
125
                        .map(CommandOutput::HistoryRepairPolicy),
126
                );
127
            }
128
            HistoryRepairCommand::SetPolicy(policy) => {
129
                reply.done(self.context.mentions.set_repair_policy(policy).await);
130
            }
131
        }
132
    }
133
134
    async fn start_recent_with(
135
        &mut self,
136
        policy: HistoryRepairPolicy,
137
        responder: Option<Responder>,
138
        publish_recent: bool,
139
        mut responders: Vec<PendingResponder>,
140
    ) {
141
        if let Some(running) = &mut self.running {
142
            if let Some(responder) = responder {
143
                if running.recent {
144
                    running.responders.push(PendingResponder {
145
                        responder,
146
                        cancel_ack: false,
147
                    });
148
                } else {
149
                    responder.done(Err(room_busy()));
150
                }
151
            }
152
            return;
153
        }
154
        if let Some(responder) = responder {
155
            responders.push(PendingResponder {
156
                responder,
157
                cancel_ack: false,
158
            });
159
        }
160
        if publish_recent
161
            && self
162
                .context
163
                .events
164
                .send(crate::Event::RecentHistoryChanged {
165
                    room_id: self.room.room_id().to_owned(),
166
                    state: RecentHistoryState::Loading,
167
                })
168
                .await
169
                .is_err()
170
        {
171
            for responder in responders {
172
                responder.responder.done(Err(Error::interrupted()));
173
            }
174
            return;
175
        }
176
        let (cancel, receiver) = watch::channel(false);
177
        let room = self.room.clone();
178
        let room_id = self.room.room_id().to_owned();
179
        let task = spawn(async move { load_recent(room, room_id, policy, receiver).await });
180
        self.running = Some(Running {
181
            task,
182
            cancel,
183
            responders,
184
            recent: true,
185
        });
186
    }
187
188
    async fn start_gap_fill(&mut self, gap_id: ruma::OwnedTransactionId, responder: Responder) {
189
        if self.is_running() {
190
            responder.done(Err(room_busy()));
191
            return;
192
        }
193
        let room = self.room.clone();
194
        let room_id = self.room.room_id().to_owned();
195
        let mut progress = report(room_id.clone(), HistoryRepairPolicy::default());
196
        progress.gap_id = Some(gap_id.clone());
197
        progress.strategy = HistoryRepairStrategy::Bridge;
198
        progress.phase = HistoryRepairPhase::Bridging;
199
        if self
200
            .context
201
            .events
202
            .send(crate::Event::HistoryRepairChanged(progress))
203
            .await
204
            .is_err()
205
        {
206
            responder.done(Err(Error::interrupted()));
207
            return;
208
        }
209
        let (cancel, receiver) = watch::channel(false);
210
        let task = spawn(async move { fill_gap(room, room_id, gap_id, receiver).await });
211
        self.running = Some(Running {
212
            task,
213
            cancel,
214
            responders: vec![PendingResponder {
215
                responder,
216
                cancel_ack: false,
217
            }],
218
            recent: false,
219
        });
220
    }
221
222
    pub(super) async fn next_completion(&mut self) -> Option<Completion> {
223
        let running = self.running.as_mut()?;
224
        let result = (&mut running.task).await;
225
        Some(self.finish_task(result))
226
    }
227
228
    pub(super) async fn shutdown(&mut self) -> Option<Completion> {
229
        let running = self.running.as_mut()?;
230
        running.cancel.send_replace(true);
231
        let result = (&mut running.task).await;
232
        Some(self.finish_task(result))
233
    }
234
235
    fn finish_task(&mut self, result: Result<CompletionResult, JoinError>) -> Completion {
236
        let running = self.running.take().expect("repair task exists");
237
        let result = match result {
238
            Ok(result) => result,
239
            Err(_) => CompletionResult {
240
                report: failed_report(
241
                    self.room.room_id().to_owned(),
242
                    HistoryRepairPolicy::default(),
243
                    Error::interrupted(),
244
                ),
245
                recent_state: running.recent.then(|| {
246
                    RecentHistoryState::Failed(
247
                        "recent history operation was interrupted".to_owned(),
248
                    )
249
                }),
250
                result: Err(Error::interrupted()),
251
            },
252
        };
253
        Completion {
254
            report: result.report,
255
            recent_state: result.recent_state,
256
            result: result.result,
257
            responders: running.responders,
258
        }
259
    }
260
}
261
262
fn validate_budget(budget: &HistoryRepairBudget) -> Result<(), Error> {
263
    if !(1..=100).contains(&budget.max_pages)
264
        || !(50..=10_000).contains(&budget.max_events)
265
        || !(1..=60_000).contains(&budget.max_duration_ms)
266
    {
267
        return Err(Error::new(
268
            ErrorKind::Invalid,
269
            "repair budget requires 1 to 100 pages, 50 to 10000 events, and 1 to 60000 milliseconds",
270
        ));
271
    }
272
    Ok(())
273
}
274
275
async fn load_recent(
276
    room: Room,
277
    room_id: ruma::OwnedRoomId,
278
    policy: HistoryRepairPolicy,
279
    mut cancel: watch::Receiver<bool>,
280
) -> CompletionResult {
281
    let base = report(room_id.clone(), policy);
282
    let outcome = tokio::select! {
283
        biased;
284
        _ = cancel.changed() => Err(Error::interrupted()),
285
        result = async {
286
            let (cache, _handles) = room.event_cache().await.map_err(cache_error)?;
287
            cache.load_recent_history().await.map_err(cache_error)
288
        } => result,
289
    };
290
    match outcome {
291
        Ok(outcome) => {
292
            let mut report = base;
293
            report.pages = outcome.pages;
294
            report.events = outcome.events.min(u32::MAX as usize) as u32;
295
            report.recent_available = true;
296
            report.phase = HistoryRepairPhase::Ready;
297
            match outcome.gap.map(|gap| gap.status) {
298
                None => report.continuity = HistoryContinuity::AccessibleStart,
299
                Some(matrix_sdk::event_cache::HistoryGapStatus::Joined) => {
300
                    report.continuity = HistoryContinuity::Verified
301
                }
302
                Some(matrix_sdk::event_cache::HistoryGapStatus::Unavailable) => {
303
                    report.continuity = HistoryContinuity::AccessibleStart;
304
                    report.stop = Some(HistoryRepairStop::HistoryUnavailable);
305
                    report.phase = HistoryRepairPhase::Paused;
306
                }
307
                Some(matrix_sdk::event_cache::HistoryGapStatus::Paused) => {
308
                    report.stop = Some(HistoryRepairStop::CursorStalled);
309
                    report.phase = HistoryRepairPhase::Paused;
310
                }
311
                Some(matrix_sdk::event_cache::HistoryGapStatus::Failed { message }) => {
312
                    let error = Error::new(ErrorKind::Network, message.clone());
313
                    report.failure = Some(error.clone());
314
                    report.phase = HistoryRepairPhase::Paused;
315
                }
316
                Some(matrix_sdk::event_cache::HistoryGapStatus::Idle) => {}
317
            }
318
            CompletionResult {
319
                report: report.clone(),
320
                recent_state: Some(RecentHistoryState::Loaded),
321
                result: Ok(CommandOutput::HistoryRepair(report)),
322
            }
323
        }
324
        Err(error) => CompletionResult {
325
            report: failed_report(room_id, base.requested_policy, error.clone()),
326
            recent_state: Some(RecentHistoryState::Failed(error.message.clone())),
327
            result: Err(error),
328
        },
329
    }
330
}
331
332
async fn fill_gap(
333
    room: Room,
334
    room_id: ruma::OwnedRoomId,
335
    gap_id: ruma::OwnedTransactionId,
336
    mut cancel: watch::Receiver<bool>,
337
) -> CompletionResult {
338
    let mut report = report(room_id, HistoryRepairPolicy::default());
339
    report.gap_id = Some(gap_id.clone());
340
    report.strategy = HistoryRepairStrategy::Bridge;
341
    let outcome = async {
342
        let (cache, _handles) = room.event_cache().await.map_err(cache_error)?;
343
        let mut last_gap = None;
344
        for _ in 0..MAX_GAP_PAGES {
345
            let next = tokio::select! {
346
                biased;
347
                _ = cancel.changed() => return Err(Error::interrupted()),
348
                result = cache.fill_history_gap_once(gap_id.clone()) => result.map_err(cache_error)?,
349
            };
350
            match next {
351
                HistoryGapFillOutcome::NotFound => {
352
                    return Err(Error::new(ErrorKind::EventNotFound, "history gap was not found"));
353
                }
354
                HistoryGapFillOutcome::Busy => return Err(room_busy()),
355
                HistoryGapFillOutcome::Updated { gap, events } => {
356
                    report.pages += 1;
357
                    report.events = report.events.saturating_add(events.min(u32::MAX as usize) as u32);
358
                    match gap.status.clone() {
359
                        matrix_sdk::event_cache::HistoryGapStatus::Joined => {
360
                            report.continuity = HistoryContinuity::Verified;
361
                            last_gap = Some(gap);
362
                            break;
363
                        }
364
                        matrix_sdk::event_cache::HistoryGapStatus::Unavailable => {
365
                            report.continuity = HistoryContinuity::AccessibleStart;
366
                            report.stop = Some(HistoryRepairStop::HistoryUnavailable);
367
                            last_gap = Some(gap);
368
                            break;
369
                        }
370
                        matrix_sdk::event_cache::HistoryGapStatus::Paused => {
371
                            report.stop = Some(HistoryRepairStop::CursorStalled);
372
                            last_gap = Some(gap);
373
                            break;
374
                        }
375
                        matrix_sdk::event_cache::HistoryGapStatus::Failed { message } => {
376
                            return Err(Error::new(ErrorKind::Network, message));
377
                        }
378
                        matrix_sdk::event_cache::HistoryGapStatus::Idle => last_gap = Some(gap),
379
                    }
380
                }
381
            }
382
        }
383
        if report.pages == MAX_GAP_PAGES
384
            && report.continuity == HistoryContinuity::Unresolved
385
            && report.stop.is_none()
386
        {
387
            report.stop = Some(HistoryRepairStop::Budget);
388
        }
389
        Ok::<_, Error>(last_gap)
390
    }
391
    .await;
392
    let command_result = match outcome {
393
        Ok(_) => {
394
            report.recent_available = true;
395
            report.phase = if report.stop.is_some() {
396
                HistoryRepairPhase::Paused
397
            } else {
398
                HistoryRepairPhase::Ready
399
            };
400
            Ok(CommandOutput::Done)
401
        }
402
        Err(error) => {
403
            report.failure = Some(error.clone());
404
            report.phase = HistoryRepairPhase::Paused;
405
            Err(error)
406
        }
407
    };
408
    CompletionResult {
409
        report,
410
        recent_state: None,
411
        result: command_result,
412
    }
413
}
414
415
fn report(room_id: ruma::OwnedRoomId, policy: HistoryRepairPolicy) -> HistoryRepairReport {
416
    HistoryRepairReport {
417
        room_id,
418
        gap_id: None,
419
        requested_policy: policy,
420
        strategy: HistoryRepairStrategy::Recent,
421
        coverage_duration_ms: None,
422
        threshold_evaluated: false,
423
        pages: 0,
424
        events: 0,
425
        phase: HistoryRepairPhase::FetchingRecent,
426
        continuity: HistoryContinuity::Unresolved,
427
        recent_available: false,
428
        stop: None,
429
        failure: None,
430
    }
431
}
432
433
fn failed_report(
434
    room_id: ruma::OwnedRoomId,
435
    policy: HistoryRepairPolicy,
436
    error: Error,
437
) -> HistoryRepairReport {
438
    let mut report = report(room_id, policy);
439
    report.phase = HistoryRepairPhase::Paused;
440
    report.failure = Some(error);
441
    report
442
}
443
444
fn cache_error(source: matrix_sdk::event_cache::EventCacheError) -> Error {
445
    Error::with_source(ErrorKind::Store, "room history operation failed", source)
446
}
447
448
fn room_busy() -> Error {
449
    Error::new(
450
        ErrorKind::RoomBusy,
451
        "room history operation is already running",
452
    )
453
}