Files
Portabase.Agent/src/services/backup/runner.rs
T
charles-gauthereau b0da2e40a3 feat: retry the database backup
Each attempt now dumps into its own tmp_path/attempt-{n} directory,
which is removed when the attempt fails. Without the per-attempt
directory pg_dump -Fd would refuse every retry, because it will not
write into a directory a previous attempt left behind; removing it on
failure keeps peak disk at one attempt's artifacts rather than five.

A backup blocked by a concurrent job is retried before surfacing the
same backup_already_in_progress code, since FileLock reports it as an
ordinary error and the combinator cannot tell it apart.

Reshapes retry()'s bound from the native AsyncFnMut sugar to the
classic F: FnMut(u32) -> Fut, Fut: Future<Output = Result<T, E>> + Send
shape (the pattern tokio-retry and backoff both use). AsyncFnMut's
produced future is a lifetime-quantified associated type
(F::CallRefFuture<'_>) that cannot be named or bounded as Send on
stable Rust, so wiring a retried call through it into a future that
eventually gets polled inside tokio::spawn (dispatcher.rs, via
execute_backup) made rustc's opaque-type Send inference fail with
"implementation of Send is not general enough" at the spawn site,
several call layers away from the actual retry call. Naming Fut as its
own type parameter lets Send be asserted on it directly instead, which
resolves cleanly. The combinator's control flow, log messages, and
formats are unchanged; only the bound and the call sites' closure
shape (async |x| { } becomes |x| async { }, with shared references
bound outside a move closure so the inner async move block only moves
Copy references, not the originals) are affected.
2026-08-27 18:39:41 +02:00

100 lines
3.0 KiB
Rust

use super::logger::JobLogger;
use super::models::BackupResult;
use super::service::BackupService;
use crate::domain::factory::DatabaseFactory;
use crate::services::config::DatabaseConfig;
use crate::utils::retry::{RetryPolicy, retry};
use anyhow::Result;
use std::path::Path;
use std::sync::Arc;
use tracing::error;
impl BackupService {
pub async fn run(cfg: DatabaseConfig, tmp_path: &Path, logger: Arc<JobLogger>) -> Result<BackupResult> {
let db = DatabaseFactory::create_for_backup(cfg.clone()).await;
let generated_id = cfg.generated_id.clone();
let db_type = cfg.db_type.clone();
let reachable = match db.ping().await {
Ok(v) => v,
Err(e) => {
error!("Ping failed: {}", e);
logger.log("error", format!("Ping failed: {}", e));
return Err(e.into());
}
};
logger.log("info", format!("Database reachable: {}", reachable));
if !reachable {
logger.log("error", "Database unreachable, backup aborted");
return Ok(BackupResult {
generated_id,
db_type,
status: "failed".into(),
backup_file: None,
code: None,
});
}
let policy = RetryPolicy::default();
let db_ref = &db;
let logger_ref = &logger;
let outcome = retry("Database backup", &logger, &policy, move |attempt| {
let dir = tmp_path.join(format!("attempt-{attempt}"));
async move {
if let Err(e) = tokio::fs::create_dir_all(&dir).await {
return Err(anyhow::Error::from(e));
}
match db_ref.backup(&dir, Arc::clone(logger_ref)).await {
Ok(f) => Ok(f),
Err(e) => {
let _ = tokio::fs::remove_dir_all(&dir).await;
Err(e)
}
}
}
})
.await;
match outcome {
Ok(file) => Ok(BackupResult {
generated_id,
db_type,
status: "success".into(),
backup_file: Some(file),
code: None,
}),
Err(e) if e.to_string() == "backup_already_in_progress" => {
logger.log("warn", "Backup already in progress");
Ok(BackupResult {
generated_id,
db_type,
status: "failed".into(),
backup_file: None,
code: Some("backup_already_in_progress".into()),
})
}
Err(e) => {
logger.log("error", format!("Backup failed: {}", e));
Ok(BackupResult {
generated_id,
db_type,
status: "failed".into(),
backup_file: None,
code: None,
})
}
}
}
}