atagen

macltok

public
Star0
Watch0
2006a11ce142bf4e8a1e5ea7fdc6f96f79c4a603
3
ssh://brewery@pls.lobotomise.me:33/atagen/macltok.git

Blame: src/engine/repair.rs

d9b96952ec60atagen2026-10-25 09:07 UTC1
use matrix_sdk::{
d9b96952ec60atagen2026-10-25 09:07 UTC2
    Room,
d9b96952ec60atagen2026-10-25 09:07 UTC3
    event_cache::{EventCacheError, HistoryGapFillOutcome, HistoryGapStatus},
d9b96952ec60atagen2026-10-25 09:07 UTC4
};
d9b96952ec60atagen2026-10-25 09:07 UTC5
use ruma::{OwnedRoomId, OwnedTransactionId};
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC6
use tokio::sync::watch;
80381e06a573atagen2026-10-25 03:02 UTC7
80381e06a573atagen2026-10-25 03:02 UTC8
use super::{
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC9
    executor::{JoinError, JoinHandle, spawn},
80381e06a573atagen2026-10-25 03:02 UTC10
    timeline::SessionContext,
80381e06a573atagen2026-10-25 03:02 UTC11
};
80381e06a573atagen2026-10-25 03:02 UTC12
use crate::{
d9b96952ec60atagen2026-10-25 09:07 UTC13
    CommandOutput, Error, ErrorKind, Event, HistoryContinuity, HistoryRepairCommand,
d9b96952ec60atagen2026-10-25 09:07 UTC14
    HistoryRepairPhase, HistoryRepairReport, HistoryRepairStop, RecentHistoryState,
d9b96952ec60atagen2026-10-25 09:07 UTC15
    transport::Responder,
80381e06a573atagen2026-10-25 03:02 UTC16
};
80381e06a573atagen2026-10-25 03:02 UTC17
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC18
const MAX_GAP_PAGES: u32 = 4;
80381e06a573atagen2026-10-25 03:02 UTC19
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC20
struct Running {
d9b96952ec60atagen2026-10-25 09:07 UTC21
    task: JoinHandle<Finished>,
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC22
    cancel: watch::Sender<bool>,
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC23
    responders: Vec<PendingResponder>,
d9b96952ec60atagen2026-10-25 09:07 UTC24
    gap_id: Option<OwnedTransactionId>,
d9b96952ec60atagen2026-10-25 09:07 UTC25
    closing: bool,
80381e06a573atagen2026-10-25 03:02 UTC26
}
80381e06a573atagen2026-10-25 03:02 UTC27
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC28
struct PendingResponder {
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC29
    responder: Responder,
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC30
    cancel_ack: bool,
80381e06a573atagen2026-10-25 03:02 UTC31
}
80381e06a573atagen2026-10-25 03:02 UTC32
d9b96952ec60atagen2026-10-25 09:07 UTC33
pub(super) enum Outcome {
d9b96952ec60atagen2026-10-25 09:07 UTC34
    Recent(RecentHistoryState),
d9b96952ec60atagen2026-10-25 09:07 UTC35
    Fill(HistoryRepairReport),
d9b96952ec60atagen2026-10-25 09:07 UTC36
}
d9b96952ec60atagen2026-10-25 09:07 UTC37
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC38
pub(super) struct Completion {
d9b96952ec60atagen2026-10-25 09:07 UTC39
    pub(super) outcome: Outcome,
d9b96952ec60atagen2026-10-25 09:07 UTC40
    /// history went into the cache, so the room timeline has to show it before anyone hears
d9b96952ec60atagen2026-10-25 09:07 UTC41
    pub(super) committed: bool,
d9b96952ec60atagen2026-10-25 09:07 UTC42
    pub(super) closing: bool,
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC43
    pub(super) result: Result<CommandOutput, Error>,
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC44
    responders: Vec<PendingResponder>,
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC45
}
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC46
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC47
impl Completion {
d9b96952ec60atagen2026-10-25 09:07 UTC48
    /// the room timeline couldn't show what was committed
d9b96952ec60atagen2026-10-25 09:07 UTC49
    pub(super) fn unpublished(&mut self, error: Error) {
d9b96952ec60atagen2026-10-25 09:07 UTC50
        match &mut self.outcome {
d9b96952ec60atagen2026-10-25 09:07 UTC51
            Outcome::Recent(state) => *state = RecentHistoryState::Failed(error.message.clone()),
d9b96952ec60atagen2026-10-25 09:07 UTC52
            Outcome::Fill(report) => {
d9b96952ec60atagen2026-10-25 09:07 UTC53
                report.phase = HistoryRepairPhase::Paused;
d9b96952ec60atagen2026-10-25 09:07 UTC54
                report.failure = Some(error.clone());
d9b96952ec60atagen2026-10-25 09:07 UTC55
            }
d9b96952ec60atagen2026-10-25 09:07 UTC56
        }
d9b96952ec60atagen2026-10-25 09:07 UTC57
        self.result = Err(error);
d9b96952ec60atagen2026-10-25 09:07 UTC58
    }
d9b96952ec60atagen2026-10-25 09:07 UTC59
9eabb6b7587datagen2026-10-25 07:08 UTC60
    pub(super) fn resolve(
9eabb6b7587datagen2026-10-25 07:08 UTC61
        self,
9eabb6b7587datagen2026-10-25 07:08 UTC62
        result: Result<CommandOutput, Error>,
9eabb6b7587datagen2026-10-25 07:08 UTC63
        cancellation: Result<(), Error>,
9eabb6b7587datagen2026-10-25 07:08 UTC64
    ) {
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC65
        for responder in self.responders {
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC66
            if responder.cancel_ack {
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC67
                responder.responder.done(cancellation.clone());
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC68
            } else {
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC69
                responder.responder.send(result.clone());
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC70
            }
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC71
        }
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC72
    }
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC73
}
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC74
d9b96952ec60atagen2026-10-25 09:07 UTC75
enum Stopped {
d9b96952ec60atagen2026-10-25 09:07 UTC76
    Done,
d9b96952ec60atagen2026-10-25 09:07 UTC77
    Cancelled,
d9b96952ec60atagen2026-10-25 09:07 UTC78
    Failed(Error),
d9b96952ec60atagen2026-10-25 09:07 UTC79
}
d9b96952ec60atagen2026-10-25 09:07 UTC80
d9b96952ec60atagen2026-10-25 09:07 UTC81
struct Finished {
d9b96952ec60atagen2026-10-25 09:07 UTC82
    stopped: Stopped,
d9b96952ec60atagen2026-10-25 09:07 UTC83
    committed: bool,
d9b96952ec60atagen2026-10-25 09:07 UTC84
    report: Option<HistoryRepairReport>,
80381e06a573atagen2026-10-25 03:02 UTC85
}
80381e06a573atagen2026-10-25 03:02 UTC86
80381e06a573atagen2026-10-25 03:02 UTC87
pub(super) struct RepairController {
80381e06a573atagen2026-10-25 03:02 UTC88
    room: Room,
80381e06a573atagen2026-10-25 03:02 UTC89
    context: SessionContext,
80381e06a573atagen2026-10-25 03:02 UTC90
    running: Option<Running>,
80381e06a573atagen2026-10-25 03:02 UTC91
}
80381e06a573atagen2026-10-25 03:02 UTC92
80381e06a573atagen2026-10-25 03:02 UTC93
impl RepairController {
80381e06a573atagen2026-10-25 03:02 UTC94
    pub(super) fn new(room: Room, context: SessionContext) -> Self {
80381e06a573atagen2026-10-25 03:02 UTC95
        Self {
80381e06a573atagen2026-10-25 03:02 UTC96
            room,
80381e06a573atagen2026-10-25 03:02 UTC97
            context,
80381e06a573atagen2026-10-25 03:02 UTC98
            running: None,
80381e06a573atagen2026-10-25 03:02 UTC99
        }
80381e06a573atagen2026-10-25 03:02 UTC100
    }
80381e06a573atagen2026-10-25 03:02 UTC101
80381e06a573atagen2026-10-25 03:02 UTC102
    pub(super) fn is_running(&self) -> bool {
80381e06a573atagen2026-10-25 03:02 UTC103
        self.running.is_some()
80381e06a573atagen2026-10-25 03:02 UTC104
    }
80381e06a573atagen2026-10-25 03:02 UTC105
417da9e719bcatagen2026-10-26 02:40 UTC106
    /// a room with retained events already shows the live end, and a limited sync has put its
417da9e719bcatagen2026-10-26 02:40 UTC107
    /// own gap there, so only an empty room walks back from the live end. otherwise the newest
417da9e719bcatagen2026-10-26 02:40 UTC108
    /// gap gets one page, so a limited sync that skipped nothing leaves no gap behind
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC109
    pub(super) async fn start_recent(&mut self) {
417da9e719bcatagen2026-10-26 02:40 UTC110
        let retained = async {
417da9e719bcatagen2026-10-26 02:40 UTC111
            let (cache, _handles) = self.room.event_cache().await?;
417da9e719bcatagen2026-10-26 02:40 UTC112
            if !cache.has_retained_events().await? {
417da9e719bcatagen2026-10-26 02:40 UTC113
                return Ok(None);
417da9e719bcatagen2026-10-26 02:40 UTC114
            }
417da9e719bcatagen2026-10-26 02:40 UTC115
            let gaps = cache.history_gaps().await?;
417da9e719bcatagen2026-10-26 02:40 UTC116
            Ok::<_, EventCacheError>(Some(gaps.into_iter().rev().find(|gap| {
417da9e719bcatagen2026-10-26 02:40 UTC117
                matches!(
417da9e719bcatagen2026-10-26 02:40 UTC118
                    gap.status,
417da9e719bcatagen2026-10-26 02:40 UTC119
                    HistoryGapStatus::Idle | HistoryGapStatus::Failed { .. }
417da9e719bcatagen2026-10-26 02:40 UTC120
                )
417da9e719bcatagen2026-10-26 02:40 UTC121
            })))
417da9e719bcatagen2026-10-26 02:40 UTC122
        }
417da9e719bcatagen2026-10-26 02:40 UTC123
        .await;
417da9e719bcatagen2026-10-26 02:40 UTC124
        match retained {
417da9e719bcatagen2026-10-26 02:40 UTC125
            Ok(Some(Some(gap))) => self.start_gap_fill(gap.id, None, 1, false).await,
417da9e719bcatagen2026-10-26 02:40 UTC126
            Ok(Some(None)) => {}
417da9e719bcatagen2026-10-26 02:40 UTC127
            Ok(None) | Err(_) => self.start_recent_with(None).await,
417da9e719bcatagen2026-10-26 02:40 UTC128
        }
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC129
    }
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC130
80381e06a573atagen2026-10-25 03:02 UTC131
    pub(super) async fn execute(&mut self, command: HistoryRepairCommand, reply: Responder) {
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC132
        match command {
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC133
            HistoryRepairCommand::Cancel { .. } => {
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC134
                if let Some(running) = self.running.as_mut() {
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC135
                    running.responders.push(PendingResponder {
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC136
                        responder: reply,
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC137
                        cancel_ack: true,
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC138
                    });
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC139
                    running.cancel.send_replace(true);
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC140
                } else {
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC141
                    reply.done(Ok(()));
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC142
                }
80381e06a573atagen2026-10-25 03:02 UTC143
            }
d9b96952ec60atagen2026-10-25 09:07 UTC144
            HistoryRepairCommand::RetryRecent { .. } => self.start_recent_with(Some(reply)).await,
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC145
            HistoryRepairCommand::FillGap { gap_id, .. } => {
417da9e719bcatagen2026-10-26 02:40 UTC146
                self.start_gap_fill(gap_id, Some(reply), MAX_GAP_PAGES, true)
417da9e719bcatagen2026-10-26 02:40 UTC147
                    .await;
80381e06a573atagen2026-10-25 03:02 UTC148
            }
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC149
        }
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC150
    }
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC151
d9b96952ec60atagen2026-10-25 09:07 UTC152
    async fn start_recent_with(&mut self, responder: Option<Responder>) {
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC153
        if let Some(running) = &mut self.running {
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC154
            if let Some(responder) = responder {
d9b96952ec60atagen2026-10-25 09:07 UTC155
                if running.gap_id.is_none() {
9eabb6b7587datagen2026-10-25 07:08 UTC156
                    running.responders.push(PendingResponder {
9eabb6b7587datagen2026-10-25 07:08 UTC157
                        responder,
9eabb6b7587datagen2026-10-25 07:08 UTC158
                        cancel_ack: false,
9eabb6b7587datagen2026-10-25 07:08 UTC159
                    });
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC160
                } else {
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC161
                    responder.done(Err(room_busy()));
80381e06a573atagen2026-10-25 03:02 UTC162
                }
80381e06a573atagen2026-10-25 03:02 UTC163
            }
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC164
            return;
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC165
        }
d9b96952ec60atagen2026-10-25 09:07 UTC166
        let event = Event::RecentHistoryChanged {
d9b96952ec60atagen2026-10-25 09:07 UTC167
            room_id: self.room.room_id().to_owned(),
d9b96952ec60atagen2026-10-25 09:07 UTC168
            state: RecentHistoryState::Loading,
d9b96952ec60atagen2026-10-25 09:07 UTC169
        };
d9b96952ec60atagen2026-10-25 09:07 UTC170
        if self.context.events.send(event).await.is_err() {
d9b96952ec60atagen2026-10-25 09:07 UTC171
            if let Some(responder) = responder {
d9b96952ec60atagen2026-10-25 09:07 UTC172
                responder.done(Err(Error::interrupted()));
80381e06a573atagen2026-10-25 03:02 UTC173
            }
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC174
            return;
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC175
        }
80381e06a573atagen2026-10-25 03:02 UTC176
        let (cancel, receiver) = watch::channel(false);
80381e06a573atagen2026-10-25 03:02 UTC177
        let room = self.room.clone();
d9b96952ec60atagen2026-10-25 09:07 UTC178
        let task = spawn(async move { load_recent(room, receiver).await });
80381e06a573atagen2026-10-25 03:02 UTC179
        self.running = Some(Running {
80381e06a573atagen2026-10-25 03:02 UTC180
            task,
80381e06a573atagen2026-10-25 03:02 UTC181
            cancel,
d9b96952ec60atagen2026-10-25 09:07 UTC182
            responders: responder
d9b96952ec60atagen2026-10-25 09:07 UTC183
                .map(|responder| PendingResponder {
d9b96952ec60atagen2026-10-25 09:07 UTC184
                    responder,
d9b96952ec60atagen2026-10-25 09:07 UTC185
                    cancel_ack: false,
d9b96952ec60atagen2026-10-25 09:07 UTC186
                })
d9b96952ec60atagen2026-10-25 09:07 UTC187
                .into_iter()
d9b96952ec60atagen2026-10-25 09:07 UTC188
                .collect(),
d9b96952ec60atagen2026-10-25 09:07 UTC189
            gap_id: None,
d9b96952ec60atagen2026-10-25 09:07 UTC190
            closing: false,
80381e06a573atagen2026-10-25 03:02 UTC191
        });
80381e06a573atagen2026-10-25 03:02 UTC192
    }
80381e06a573atagen2026-10-25 03:02 UTC193
417da9e719bcatagen2026-10-26 02:40 UTC194
    async fn start_gap_fill(
417da9e719bcatagen2026-10-26 02:40 UTC195
        &mut self,
417da9e719bcatagen2026-10-26 02:40 UTC196
        gap_id: OwnedTransactionId,
417da9e719bcatagen2026-10-26 02:40 UTC197
        responder: Option<Responder>,
417da9e719bcatagen2026-10-26 02:40 UTC198
        pages: u32,
417da9e719bcatagen2026-10-26 02:40 UTC199
        spend_budget: bool,
417da9e719bcatagen2026-10-26 02:40 UTC200
    ) {
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC201
        if self.is_running() {
417da9e719bcatagen2026-10-26 02:40 UTC202
            if let Some(responder) = responder {
417da9e719bcatagen2026-10-26 02:40 UTC203
                responder.done(Err(room_busy()));
417da9e719bcatagen2026-10-26 02:40 UTC204
            }
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC205
            return;
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC206
        }
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC207
        let room = self.room.clone();
d9b96952ec60atagen2026-10-25 09:07 UTC208
        let progress = report(self.room.room_id().to_owned(), gap_id.clone());
9eabb6b7587datagen2026-10-25 07:08 UTC209
        if self
9eabb6b7587datagen2026-10-25 07:08 UTC210
            .context
9eabb6b7587datagen2026-10-25 07:08 UTC211
            .events
d9b96952ec60atagen2026-10-25 09:07 UTC212
            .send(Event::HistoryRepairChanged(progress.clone()))
9eabb6b7587datagen2026-10-25 07:08 UTC213
            .await
9eabb6b7587datagen2026-10-25 07:08 UTC214
            .is_err()
9eabb6b7587datagen2026-10-25 07:08 UTC215
        {
417da9e719bcatagen2026-10-26 02:40 UTC216
            if let Some(responder) = responder {
417da9e719bcatagen2026-10-26 02:40 UTC217
                responder.done(Err(Error::interrupted()));
417da9e719bcatagen2026-10-26 02:40 UTC218
            }
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC219
            return;
80381e06a573atagen2026-10-25 03:02 UTC220
        }
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC221
        let (cancel, receiver) = watch::channel(false);
417da9e719bcatagen2026-10-26 02:40 UTC222
        let task =
417da9e719bcatagen2026-10-26 02:40 UTC223
            spawn(async move { fill_gap(room, progress, receiver, pages, spend_budget).await });
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC224
        self.running = Some(Running {
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC225
            task,
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC226
            cancel,
417da9e719bcatagen2026-10-26 02:40 UTC227
            responders: responder
417da9e719bcatagen2026-10-26 02:40 UTC228
                .map(|responder| PendingResponder {
417da9e719bcatagen2026-10-26 02:40 UTC229
                    responder,
417da9e719bcatagen2026-10-26 02:40 UTC230
                    cancel_ack: false,
417da9e719bcatagen2026-10-26 02:40 UTC231
                })
417da9e719bcatagen2026-10-26 02:40 UTC232
                .into_iter()
417da9e719bcatagen2026-10-26 02:40 UTC233
                .collect(),
d9b96952ec60atagen2026-10-25 09:07 UTC234
            gap_id: Some(gap_id),
d9b96952ec60atagen2026-10-25 09:07 UTC235
            closing: false,
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC236
        });
80381e06a573atagen2026-10-25 03:02 UTC237
    }
80381e06a573atagen2026-10-25 03:02 UTC238
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC239
    pub(super) async fn next_completion(&mut self) -> Option<Completion> {
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC240
        let running = self.running.as_mut()?;
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC241
        let result = (&mut running.task).await;
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC242
        Some(self.finish_task(result))
80381e06a573atagen2026-10-25 03:02 UTC243
    }
80381e06a573atagen2026-10-25 03:02 UTC244
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC245
    pub(super) async fn shutdown(&mut self) -> Option<Completion> {
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC246
        let running = self.running.as_mut()?;
d9b96952ec60atagen2026-10-25 09:07 UTC247
        running.closing = true;
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC248
        running.cancel.send_replace(true);
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC249
        let result = (&mut running.task).await;
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC250
        Some(self.finish_task(result))
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC251
    }
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC252
d9b96952ec60atagen2026-10-25 09:07 UTC253
    fn finish_task(&mut self, result: Result<Finished, JoinError>) -> Completion {
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC254
        let running = self.running.take().expect("repair task exists");
d9b96952ec60atagen2026-10-25 09:07 UTC255
        let finished = result.unwrap_or(Finished {
d9b96952ec60atagen2026-10-25 09:07 UTC256
            stopped: Stopped::Failed(Error::interrupted()),
d9b96952ec60atagen2026-10-25 09:07 UTC257
            committed: false,
d9b96952ec60atagen2026-10-25 09:07 UTC258
            report: None,
d9b96952ec60atagen2026-10-25 09:07 UTC259
        });
d9b96952ec60atagen2026-10-25 09:07 UTC260
        let result = match &finished.stopped {
d9b96952ec60atagen2026-10-25 09:07 UTC261
            Stopped::Done => Ok(CommandOutput::Done),
d9b96952ec60atagen2026-10-25 09:07 UTC262
            Stopped::Cancelled => Err(Error::interrupted()),
d9b96952ec60atagen2026-10-25 09:07 UTC263
            Stopped::Failed(error) => Err(error.clone()),
d9b96952ec60atagen2026-10-25 09:07 UTC264
        };
d9b96952ec60atagen2026-10-25 09:07 UTC265
        let outcome = match running.gap_id {
d9b96952ec60atagen2026-10-25 09:07 UTC266
            None => Outcome::Recent(match finished.stopped {
d9b96952ec60atagen2026-10-25 09:07 UTC267
                _ if running.closing => RecentHistoryState::Idle,
d9b96952ec60atagen2026-10-25 09:07 UTC268
                Stopped::Done => RecentHistoryState::Loaded,
d9b96952ec60atagen2026-10-25 09:07 UTC269
                Stopped::Cancelled => RecentHistoryState::Cancelled,
d9b96952ec60atagen2026-10-25 09:07 UTC270
                Stopped::Failed(error) => RecentHistoryState::Failed(error.message),
d9b96952ec60atagen2026-10-25 09:07 UTC271
            }),
d9b96952ec60atagen2026-10-25 09:07 UTC272
            Some(gap_id) => {
d9b96952ec60atagen2026-10-25 09:07 UTC273
                let mut report = finished
d9b96952ec60atagen2026-10-25 09:07 UTC274
                    .report
d9b96952ec60atagen2026-10-25 09:07 UTC275
                    .unwrap_or_else(|| report(self.room.room_id().to_owned(), gap_id));
d9b96952ec60atagen2026-10-25 09:07 UTC276
                match finished.stopped {
d9b96952ec60atagen2026-10-25 09:07 UTC277
                    Stopped::Done => {}
d9b96952ec60atagen2026-10-25 09:07 UTC278
                    Stopped::Cancelled => report.stop = Some(HistoryRepairStop::Cancelled),
d9b96952ec60atagen2026-10-25 09:07 UTC279
                    Stopped::Failed(error) => report.failure = Some(error),
d9b96952ec60atagen2026-10-25 09:07 UTC280
                }
d9b96952ec60atagen2026-10-25 09:07 UTC281
                report.phase = if report.stop.is_none() && report.failure.is_none() {
d9b96952ec60atagen2026-10-25 09:07 UTC282
                    HistoryRepairPhase::Ready
d9b96952ec60atagen2026-10-25 09:07 UTC283
                } else {
d9b96952ec60atagen2026-10-25 09:07 UTC284
                    HistoryRepairPhase::Paused
d9b96952ec60atagen2026-10-25 09:07 UTC285
                };
d9b96952ec60atagen2026-10-25 09:07 UTC286
                Outcome::Fill(report)
d9b96952ec60atagen2026-10-25 09:07 UTC287
            }
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC288
        };
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC289
        Completion {
d9b96952ec60atagen2026-10-25 09:07 UTC290
            outcome,
d9b96952ec60atagen2026-10-25 09:07 UTC291
            committed: finished.committed,
d9b96952ec60atagen2026-10-25 09:07 UTC292
            closing: running.closing,
d9b96952ec60atagen2026-10-25 09:07 UTC293
            result,
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC294
            responders: running.responders,
80381e06a573atagen2026-10-25 03:02 UTC295
        }
80381e06a573atagen2026-10-25 03:02 UTC296
    }
80381e06a573atagen2026-10-25 03:02 UTC297
}
80381e06a573atagen2026-10-25 03:02 UTC298
d9b96952ec60atagen2026-10-25 09:07 UTC299
async fn load_recent(room: Room, mut cancel: watch::Receiver<bool>) -> Finished {
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC300
    let outcome = tokio::select! {
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC301
        biased;
d9b96952ec60atagen2026-10-25 09:07 UTC302
        _ = cancel.changed() => None,
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC303
        result = async {
d9b96952ec60atagen2026-10-25 09:07 UTC304
            let (cache, _handles) = room.event_cache().await?;
d9b96952ec60atagen2026-10-25 09:07 UTC305
            cache.load_recent_history().await
d9b96952ec60atagen2026-10-25 09:07 UTC306
        } => Some(result),
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC307
    };
d9b96952ec60atagen2026-10-25 09:07 UTC308
    let stopped = match outcome {
d9b96952ec60atagen2026-10-25 09:07 UTC309
        None => Stopped::Cancelled,
d9b96952ec60atagen2026-10-25 09:07 UTC310
        Some(Ok(_)) => Stopped::Done,
d9b96952ec60atagen2026-10-25 09:07 UTC311
        Some(Err(error)) => Stopped::Failed(cache_error(error)),
d9b96952ec60atagen2026-10-25 09:07 UTC312
    };
d9b96952ec60atagen2026-10-25 09:07 UTC313
    Finished {
d9b96952ec60atagen2026-10-25 09:07 UTC314
        committed: matches!(stopped, Stopped::Done),
d9b96952ec60atagen2026-10-25 09:07 UTC315
        stopped,
d9b96952ec60atagen2026-10-25 09:07 UTC316
        report: None,
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC317
    }
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC318
}
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC319
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC320
async fn fill_gap(
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC321
    room: Room,
d9b96952ec60atagen2026-10-25 09:07 UTC322
    mut report: HistoryRepairReport,
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC323
    mut cancel: watch::Receiver<bool>,
417da9e719bcatagen2026-10-26 02:40 UTC324
    pages: u32,
417da9e719bcatagen2026-10-26 02:40 UTC325
    spend_budget: bool,
d9b96952ec60atagen2026-10-25 09:07 UTC326
) -> Finished {
d9b96952ec60atagen2026-10-25 09:07 UTC327
    let mut committed = false;
d9b96952ec60atagen2026-10-25 09:07 UTC328
    let stopped = async {
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC329
        let (cache, _handles) = room.event_cache().await.map_err(cache_error)?;
417da9e719bcatagen2026-10-26 02:40 UTC330
        for _ in 0..pages {
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC331
            let next = tokio::select! {
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC332
                biased;
d9b96952ec60atagen2026-10-25 09:07 UTC333
                _ = cancel.changed() => return Ok(Stopped::Cancelled),
002c79e4c6ecatagen2026-10-25 10:42 UTC334
                result = cache.fill_history_gap_once(report.gap_id.clone()) => match result {
002c79e4c6ecatagen2026-10-25 10:42 UTC335
                    Ok(next) => next,
002c79e4c6ecatagen2026-10-25 10:42 UTC336
                    Err(error) => {
002c79e4c6ecatagen2026-10-25 10:42 UTC337
                        committed |= matches!(error, EventCacheError::PaginationError(_));
002c79e4c6ecatagen2026-10-25 10:42 UTC338
                        return Err(cache_error(error));
002c79e4c6ecatagen2026-10-25 10:42 UTC339
                    }
d9b96952ec60atagen2026-10-25 09:07 UTC340
                }
80381e06a573atagen2026-10-25 03:02 UTC341
            };
d9b96952ec60atagen2026-10-25 09:07 UTC342
            let (gap, events) = match next {
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC343
                HistoryGapFillOutcome::NotFound => {
d9b96952ec60atagen2026-10-25 09:07 UTC344
                    return Err(Error::new(
d9b96952ec60atagen2026-10-25 09:07 UTC345
                        ErrorKind::EventNotFound,
d9b96952ec60atagen2026-10-25 09:07 UTC346
                        "history gap was not found",
d9b96952ec60atagen2026-10-25 09:07 UTC347
                    ));
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC348
                }
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC349
                HistoryGapFillOutcome::Busy => return Err(room_busy()),
d9b96952ec60atagen2026-10-25 09:07 UTC350
                HistoryGapFillOutcome::Updated { gap, events } => (gap, events),
d9b96952ec60atagen2026-10-25 09:07 UTC351
            };
06c73391cbaeatagen2026-10-25 10:26 UTC352
            // the sdk answers an already unavailable gap without asking the server, and a joined
06c73391cbaeatagen2026-10-25 10:26 UTC353
            // one with NotFound. an empty first page that hits the end looks the same, uncounted
924b7c011591atagen2026-10-25 10:28 UTC354
            let settled_before =
924b7c011591atagen2026-10-25 10:28 UTC355
                report.pages == 0 && events == 0 && gap.status == HistoryGapStatus::Unavailable;
d9b96952ec60atagen2026-10-25 09:07 UTC356
            if !settled_before {
d9b96952ec60atagen2026-10-25 09:07 UTC357
                report.pages += 1;
d9b96952ec60atagen2026-10-25 09:07 UTC358
                committed = true;
d9b96952ec60atagen2026-10-25 09:07 UTC359
            }
d9b96952ec60atagen2026-10-25 09:07 UTC360
            report.events = report
d9b96952ec60atagen2026-10-25 09:07 UTC361
                .events
d9b96952ec60atagen2026-10-25 09:07 UTC362
                .saturating_add(u32::try_from(events).unwrap_or(u32::MAX));
d9b96952ec60atagen2026-10-25 09:07 UTC363
            match gap.status {
d9b96952ec60atagen2026-10-25 09:07 UTC364
                HistoryGapStatus::Joined => {
d9b96952ec60atagen2026-10-25 09:07 UTC365
                    report.continuity = HistoryContinuity::Verified;
d9b96952ec60atagen2026-10-25 09:07 UTC366
                    return Ok(Stopped::Done);
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC367
                }
d9b96952ec60atagen2026-10-25 09:07 UTC368
                HistoryGapStatus::Unavailable => {
d9b96952ec60atagen2026-10-25 09:07 UTC369
                    report.continuity = HistoryContinuity::AccessibleStart;
d9b96952ec60atagen2026-10-25 09:07 UTC370
                    report.stop = Some(HistoryRepairStop::HistoryUnavailable);
d9b96952ec60atagen2026-10-25 09:07 UTC371
                    return Ok(Stopped::Done);
d9b96952ec60atagen2026-10-25 09:07 UTC372
                }
d9b96952ec60atagen2026-10-25 09:07 UTC373
                HistoryGapStatus::Paused => {
d9b96952ec60atagen2026-10-25 09:07 UTC374
                    report.stop = Some(HistoryRepairStop::CursorStalled);
d9b96952ec60atagen2026-10-25 09:07 UTC375
                    return Ok(Stopped::Done);
d9b96952ec60atagen2026-10-25 09:07 UTC376
                }
d9b96952ec60atagen2026-10-25 09:07 UTC377
                HistoryGapStatus::Failed { message } => {
d9b96952ec60atagen2026-10-25 09:07 UTC378
                    return Err(Error::new(ErrorKind::Network, message));
d9b96952ec60atagen2026-10-25 09:07 UTC379
                }
417da9e719bcatagen2026-10-26 02:40 UTC380
                HistoryGapStatus::Idle | HistoryGapStatus::BudgetSpent => {}
80381e06a573atagen2026-10-25 03:02 UTC381
            }
80381e06a573atagen2026-10-25 03:02 UTC382
        }
d9b96952ec60atagen2026-10-25 09:07 UTC383
        report.stop = Some(HistoryRepairStop::Budget);
417da9e719bcatagen2026-10-26 02:40 UTC384
        if spend_budget {
417da9e719bcatagen2026-10-26 02:40 UTC385
            if let Err(error) = cache.spend_history_budget(report.gap_id.clone()).await {
417da9e719bcatagen2026-10-26 02:40 UTC386
                tracing::debug!(%error, "marking a history gap's spent budget failed");
417da9e719bcatagen2026-10-26 02:40 UTC387
            }
417da9e719bcatagen2026-10-26 02:40 UTC388
            committed = true;
417da9e719bcatagen2026-10-26 02:40 UTC389
        }
d9b96952ec60atagen2026-10-25 09:07 UTC390
        Ok(Stopped::Done)
80381e06a573atagen2026-10-25 03:02 UTC391
    }
d9b96952ec60atagen2026-10-25 09:07 UTC392
    .await
d9b96952ec60atagen2026-10-25 09:07 UTC393
    .unwrap_or_else(Stopped::Failed);
d9b96952ec60atagen2026-10-25 09:07 UTC394
    Finished {
d9b96952ec60atagen2026-10-25 09:07 UTC395
        stopped,
d9b96952ec60atagen2026-10-25 09:07 UTC396
        committed,
d9b96952ec60atagen2026-10-25 09:07 UTC397
        report: Some(report),
80381e06a573atagen2026-10-25 03:02 UTC398
    }
80381e06a573atagen2026-10-25 03:02 UTC399
}
80381e06a573atagen2026-10-25 03:02 UTC400
d9b96952ec60atagen2026-10-25 09:07 UTC401
fn report(room_id: OwnedRoomId, gap_id: OwnedTransactionId) -> HistoryRepairReport {
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC402
    HistoryRepairReport {
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC403
        room_id,
d9b96952ec60atagen2026-10-25 09:07 UTC404
        gap_id,
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC405
        pages: 0,
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC406
        events: 0,
d9b96952ec60atagen2026-10-25 09:07 UTC407
        phase: HistoryRepairPhase::Bridging,
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC408
        continuity: HistoryContinuity::Unresolved,
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC409
        stop: None,
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC410
        failure: None,
80381e06a573atagen2026-10-25 03:02 UTC411
    }
80381e06a573atagen2026-10-25 03:02 UTC412
}
80381e06a573atagen2026-10-25 03:02 UTC413
d9b96952ec60atagen2026-10-25 09:07 UTC414
fn cache_error(source: EventCacheError) -> Error {
d9b96952ec60atagen2026-10-25 09:07 UTC415
    let kind = match &source {
d9b96952ec60atagen2026-10-25 09:07 UTC416
        EventCacheError::PaginationError(error) => crate::error::classify_sdk_error(error),
d9b96952ec60atagen2026-10-25 09:07 UTC417
        EventCacheError::InitialPaginationError(_) => ErrorKind::Network,
d9b96952ec60atagen2026-10-25 09:07 UTC418
        EventCacheError::RoomNotFound { .. } => ErrorKind::RoomUnknown,
d9b96952ec60atagen2026-10-25 09:07 UTC419
        EventCacheError::ClientDropped => ErrorKind::Interrupted,
d9b96952ec60atagen2026-10-25 09:07 UTC420
        _ => ErrorKind::Store,
d9b96952ec60atagen2026-10-25 09:07 UTC421
    };
d9b96952ec60atagen2026-10-25 09:07 UTC422
    Error::with_source(kind, "room history operation failed", source)
80381e06a573atagen2026-10-25 03:02 UTC423
}
80381e06a573atagen2026-10-25 03:02 UTC424
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC425
fn room_busy() -> Error {
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC426
    Error::new(
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC427
        ErrorKind::RoomBusy,
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC428
        "room history operation is already running",
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC429
    )
80381e06a573atagen2026-10-25 03:02 UTC430
}