atagen

macltok

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