use std::sync::Arc;
use std::time::Instant;
use super::{checkpoint_once_core, CheckpointConfig, CheckpointCoreOutcome, TruncateState};
use crate::pool::ConnectionPool;
#[cfg(test)]
mod panic_tests;
#[derive(Debug)]
pub(super) struct PanickedCycle {
pub(super) join_error: tokio::task::JoinError,
pub(super) truncate_state: TruncateState,
}
pub(super) async fn run_checkpoint_core_off_worker(
pool: Arc<ConnectionPool>,
conn: rusqlite::Connection,
config: CheckpointConfig,
mut truncate_state: TruncateState,
) -> Result<
(
rusqlite::Connection,
TruncateState,
Result<CheckpointCoreOutcome, rusqlite::Error>,
),
Box<PanickedCycle>,
> {
let kept = kept_across_a_panic(&truncate_state);
tokio::task::spawn_blocking(move || {
let outcome = checkpoint_once_core(&pool, &conn, &config, &mut truncate_state);
(conn, truncate_state, outcome)
})
.await
.map_err(|join_error| {
let mut truncate_state = kept;
truncate_state.last_attempt = Some(Instant::now());
Box::new(PanickedCycle {
join_error,
truncate_state,
})
})
}
fn kept_across_a_panic(state: &TruncateState) -> TruncateState {
TruncateState {
last_attempt: state.last_attempt,
consecutive_failures: state.consecutive_failures,
#[cfg(unix)]
legacy_walpin_fallback_interval: state.legacy_walpin_fallback_interval,
#[cfg(unix)]
walpin_full_scan_interval: state.walpin_full_scan_interval,
#[cfg(unix)]
walpin_full_scan_last_attempt: state.walpin_full_scan_last_attempt,
#[cfg(unix)]
walpin_cached_attribution: None,
#[cfg(unix)]
sidecar_attribution_attempted_this_tick: false,
}
}
#[cfg(test)]
pub(super) mod cycle_panic_seam {
use std::path::{Path, PathBuf};
use std::sync::Mutex;
static HOOK: Mutex<Option<PathBuf>> = Mutex::new(None);
pub(crate) fn install(db_path: PathBuf) {
let replaced = HOOK
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
.replace(db_path);
assert!(replaced.is_none(), "cycle panic hook already installed");
}
pub(crate) fn uninstall() {
*HOOK.lock().unwrap_or_else(|poisoned| poisoned.into_inner()) = None;
}
pub(crate) fn after_attempt_decided(db_path: Option<&Path>) {
let Some(db_path) = db_path else {
return;
};
let armed = {
let mut guard = HOOK.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
match guard.as_ref() {
Some(path) if path.as_path() == db_path => guard.take(),
_ => None,
}
};
if armed.is_some() {
panic!("injected checkpoint cycle panic");
}
}
}