use tracing::{error, info, warn};
pub async fn checkpoint_all_databases() {
let stores = [
("board", crate::board::BOARD.get().map(|s| &s.conn)),
(
"chat_history",
crate::chat_history::CHAT_HISTORY.get().map(|s| &s.conn),
),
(
"config",
crate::config_db::CONFIG_STORE.get().map(|s| &s.conn),
),
("logs", crate::logs::LOG_STORE.get().map(|s| &s.conn)),
("sessions", crate::session::SESSIONS.get().map(|s| &s.conn)),
("stats", crate::stats::STATS_STORE.get().map(|s| &s.conn)),
("users", crate::users::USER_STORE.get().map(|s| &s.conn)),
(
"workspaces",
crate::workspace::WORKSPACES.get().map(|s| &s.conn),
),
];
for (name, conn_opt) in &stores {
let Some(conn) = conn_opt else {
continue;
};
match conn.checkpoint().await {
Ok(()) => info!(db = %name, "Database WAL checkpointed"),
Err(e) => warn!(error = %e, db = %name, "Failed to checkpoint database WAL"),
}
}
}
const AUTO_CHECKPOINT_INTERVAL: std::time::Duration = std::time::Duration::from_mins(5);
pub fn spawn_auto_checkpoint_loop(
tasks: &mut tokio::task::JoinSet<()>,
shutdown_token: &tokio_util::sync::CancellationToken,
) {
use futures_util::FutureExt;
use std::panic::AssertUnwindSafe;
let cancel = shutdown_token.clone();
tasks.spawn(async move {
loop {
tokio::select! {
biased; () = cancel.cancelled() => {
info!("Auto-checkpoint loop stopped (shutdown)");
break;
}
() = tokio::time::sleep(AUTO_CHECKPOINT_INTERVAL) => {
let result = AssertUnwindSafe(checkpoint_all_databases())
.catch_unwind()
.await;
if let Err(payload) = result {
error!(
"Background task panicked [auto-checkpoint]: {}",
crate::util::panic_message(&*payload),
);
}
}
}
}
});
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
use tokio::task::JoinSet;
#[tokio::test]
async fn exits_on_cancellation() {
let token = tokio_util::sync::CancellationToken::new();
let mut tasks = JoinSet::new();
spawn_auto_checkpoint_loop(&mut tasks, &token);
tokio::time::sleep(Duration::from_millis(50)).await;
token.cancel();
let result = tokio::time::timeout(Duration::from_millis(500), tasks.join_next()).await;
assert!(result.is_ok(), "task did not exit after cancellation");
}
#[tokio::test]
async fn noop_when_no_stores() {
checkpoint_all_databases().await;
}
#[test]
fn interval_is_reasonable() {
assert!(
AUTO_CHECKPOINT_INTERVAL >= Duration::from_secs(30),
"auto-checkpoint interval should be at least 30 seconds"
);
assert!(
AUTO_CHECKPOINT_INTERVAL <= Duration::from_mins(10),
"auto-checkpoint interval should be at most 10 minutes"
);
}
#[test]
fn all_store_names_appear_in_checkpoint() {
let checkpoint_stores: &[&str] = &[
"board",
"chat_history",
"config",
"logs",
"sessions",
"stats",
"users",
"workspaces",
];
for name in crate::turso::ALL_STORE_NAMES {
assert!(
checkpoint_stores.contains(name),
"store '{name}' is in ALL_STORE_NAMES but missing from \
checkpoint_all_databases — WAL frames for this store will \
never be flushed on hard exit, causing data loss"
);
}
for name in checkpoint_stores {
assert!(
crate::turso::ALL_STORE_NAMES.contains(name),
"store '{name}' is checkpointed but missing from \
ALL_STORE_NAMES — add it to the canonical list"
);
}
}
}