use crate::{app_context::AppContext, persistence::lmdb::LmDB};
use heed::CompactionOption;
use std::path::PathBuf;
use std::time::Duration;
use tokio::{task::JoinHandle, time::interval};
use tracing::{error, info};
pub(crate) struct PeriodicBackup {
handle: Option<JoinHandle<()>>,
}
const BACKUP_INTERVAL_DANGERZONE: Duration = Duration::from_secs(30);
impl PeriodicBackup {
pub fn start(context: &AppContext) -> Self {
let backup_interval =
Duration::from_secs(context.config_toml.general.lmdb_backup_interval_s);
let is_disabled = backup_interval.as_secs() == 0;
if is_disabled {
tracing::info!("LMDB backup is disabled.");
return Self { handle: None };
}
if backup_interval < BACKUP_INTERVAL_DANGERZONE {
tracing::warn!(
"The configured LMDB backup interval is less than {}s!.",
BACKUP_INTERVAL_DANGERZONE.as_secs(),
);
}
let db = context.db.clone();
let backup_path = context.data_dir.path().join("backup");
tracing::info!(
"Starting LMDB backup with interval {}s",
backup_interval.as_secs()
);
let handle = tokio::spawn(async move {
backup_lmdb_periodically(db, backup_path, backup_interval).await;
});
Self {
handle: Some(handle),
}
}
}
impl Drop for PeriodicBackup {
fn drop(&mut self) {
if let Some(handle) = self.handle.take() {
handle.abort();
}
}
}
pub async fn backup_lmdb_periodically(db: LmDB, backup_path: PathBuf, period: Duration) {
let mut interval_timer = interval(period);
interval_timer.tick().await;
loop {
interval_timer.tick().await;
let db_clone = db.clone();
let backup_path_clone = backup_path.clone();
tokio::task::spawn_blocking(move || {
do_backup(db_clone, backup_path_clone);
})
.await
.map_err(|e| error!("Backup task panicked: {:?}", e))
.ok();
}
}
fn do_backup(db: LmDB, backup_path: PathBuf) {
let final_backup_path = backup_path.with_extension("mdb");
let temp_backup_path = backup_path.with_extension("tmp");
if let Err(e) = db
.env
.copy_to_file(&temp_backup_path, CompactionOption::Enabled)
{
error!(
"Failed to create temporary LMDB backup at {:?}: {:?}",
temp_backup_path, e
);
return;
}
if let Err(e) = std::fs::rename(&temp_backup_path, &final_backup_path) {
error!(
"Failed to rename temporary backup file {:?} to final backup file {:?}: {:?}",
temp_backup_path, final_backup_path, e
);
return;
}
info!(
"LMDB backup successfully created at {:?}",
final_backup_path
);
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
#[test]
fn test_do_backup_creates_backup_file() {
let db = LmDB::test();
let temp_dir = tempdir().expect("Failed to create temporary directory");
let backup_path = temp_dir.path().join("lmdb_backup");
do_backup(db, backup_path.clone());
let final_backup_file = backup_path.with_extension("mdb");
let temp_backup_file = backup_path.with_extension("tmp");
assert!(
final_backup_file.exists(),
"Expected final backup file at {:?} to exist.",
final_backup_file
);
assert!(
!temp_backup_file.exists(),
"Expected temporary backup file at {:?} to be removed.",
temp_backup_file
);
}
}