use super::SharedConnection;
use rusqlite::InterruptHandle;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
#[derive(Default)]
pub(crate) struct StartupRestoreControl {
state: Mutex<ControlState>,
}
impl std::fmt::Debug for StartupRestoreControl {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let state = self.state.lock().unwrap_or_else(|e| e.into_inner());
formatter
.debug_struct("StartupRestoreControl")
.field("epoch", &state.epoch)
.field("paused", &state.paused)
.field("active", &state.active.is_some())
.finish()
}
}
#[derive(Default)]
struct ControlState {
epoch: u64,
paused: bool,
active: Option<ActiveRestore>,
}
struct ActiveRestore {
cancelled: Arc<AtomicBool>,
interrupt: InterruptHandle,
}
#[derive(Debug)]
pub(super) struct StartupRestoreDeferred;
impl std::fmt::Display for StartupRestoreDeferred {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str("startup restore deferred until foreground")
}
}
impl std::error::Error for StartupRestoreDeferred {}
impl StartupRestoreControl {
#[cfg(test)]
pub(in crate::core) fn is_active_for_test(&self) -> bool {
self.state
.lock()
.unwrap_or_else(|e| e.into_inner())
.active
.is_some()
}
pub(crate) fn epoch(&self) -> u64 {
self.state.lock().unwrap_or_else(|e| e.into_inner()).epoch
}
pub(crate) fn cancel(&self, epoch: u64) {
let mut state = self.state.lock().unwrap_or_else(|e| e.into_inner());
if state.epoch != epoch {
return;
}
state.paused = true;
if let Some(active) = &state.active {
active.cancelled.store(true, Ordering::Release);
active.interrupt.interrupt();
}
}
pub(crate) fn foregrounded(&self) {
let mut state = self.state.lock().unwrap_or_else(|e| e.into_inner());
state.epoch = state.epoch.wrapping_add(1);
state.paused = false;
}
pub(super) fn begin(
self: &Arc<Self>,
connection: SharedConnection,
) -> anyhow::Result<StartupRestoreGuard> {
let conn = connection
.lock()
.map_err(|_| anyhow::anyhow!("startup storage connection mutex poisoned"))?;
let mut state = self.state.lock().unwrap_or_else(|e| e.into_inner());
anyhow::ensure!(
conn.is_autocommit(),
"startup restore requires an idle connection"
);
if state.paused {
return Err(StartupRestoreDeferred.into());
}
anyhow::ensure!(state.active.is_none(), "startup restore already active");
let cancelled = Arc::new(AtomicBool::new(false));
state.active = Some(ActiveRestore {
cancelled: Arc::clone(&cancelled),
interrupt: conn.get_interrupt_handle(),
});
let check = Arc::clone(&cancelled);
conn.progress_handler(1, Some(move || check.load(Ordering::Acquire)));
drop(state);
drop(conn);
Ok(StartupRestoreGuard {
control: Arc::clone(self),
connection,
cancelled,
registered: true,
cleaned: false,
})
}
}
pub(super) struct StartupRestoreGuard {
control: Arc<StartupRestoreControl>,
connection: SharedConnection,
cancelled: Arc<AtomicBool>,
registered: bool,
cleaned: bool,
}
impl StartupRestoreGuard {
pub(super) fn finish<T>(mut self, result: anyhow::Result<T>) -> anyhow::Result<T> {
let rolled_back = self.cleanup()?;
anyhow::ensure!(
!rolled_back || result.is_err(),
"startup restore left an unfinished transaction"
);
let interrupted = result.as_ref().err().is_some_and(|error| {
error.chain().any(|cause| {
cause
.downcast_ref::<iris_chat_protocol::StorageError>()
.is_some_and(iris_chat_protocol::StorageError::is_interrupted)
|| matches!(
cause.downcast_ref::<rusqlite::Error>(),
Some(rusqlite::Error::SqliteFailure(inner, _))
if inner.code == rusqlite::ErrorCode::OperationInterrupted
)
})
});
if interrupted && self.cancelled.load(Ordering::Acquire) {
Err(StartupRestoreDeferred.into())
} else {
result
}
}
fn cleanup(&mut self) -> anyhow::Result<bool> {
if self.cleaned {
return Ok(false);
}
let conn = self.connection.lock().unwrap_or_else(|e| e.into_inner());
if self.registered {
let mut state = self.control.state.lock().unwrap_or_else(|e| e.into_inner());
if state
.active
.as_ref()
.is_some_and(|active| Arc::ptr_eq(&active.cancelled, &self.cancelled))
{
conn.progress_handler(0, None::<fn() -> bool>);
state.active = None;
}
self.registered = false;
}
let rolled_back = !conn.is_autocommit();
if rolled_back {
conn.execute_batch("ROLLBACK")?;
}
self.cleaned = true;
Ok(rolled_back)
}
}
impl Drop for StartupRestoreGuard {
fn drop(&mut self) {
let _ = self.cleanup();
}
}