use crate::constants::{MAX_SQLITE_BUSY_RETRIES, SQLITE_BUSY_BASE_DELAY_MS};
use crate::errors::AppError;
use rusqlite::ErrorCode;
use std::thread;
use std::time::Duration;
pub fn is_sqlite_busy(err: &AppError) -> bool {
match err {
AppError::Database(rusqlite::Error::SqliteFailure(e, _)) => {
e.code == ErrorCode::DatabaseBusy || e.code == ErrorCode::DatabaseLocked
}
_ => false,
}
}
pub fn with_busy_retry<F>(op: F) -> Result<(), AppError>
where
F: Fn() -> Result<(), AppError>,
{
for attempt in 0..MAX_SQLITE_BUSY_RETRIES {
match op() {
Ok(()) => return Ok(()),
Err(e) if is_sqlite_busy(&e) => {
let delay_ms = SQLITE_BUSY_BASE_DELAY_MS * (1u64 << attempt);
thread::sleep(Duration::from_millis(delay_ms));
}
Err(other) => return Err(other),
}
}
Err(AppError::DbBusy(format!(
"SQLITE_BUSY after {MAX_SQLITE_BUSY_RETRIES} retries"
)))
}
#[cfg(test)]
mod testes {
use super::*;
use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::Arc;
fn make_busy_error() -> AppError {
let ffi_err = rusqlite::ffi::Error {
code: ErrorCode::DatabaseBusy,
extended_code: 5,
};
AppError::Database(rusqlite::Error::SqliteFailure(ffi_err, None))
}
fn make_locked_error() -> AppError {
let ffi_err = rusqlite::ffi::Error {
code: ErrorCode::DatabaseLocked,
extended_code: 6,
};
AppError::Database(rusqlite::Error::SqliteFailure(ffi_err, None))
}
#[test]
fn is_sqlite_busy_detecta_database_busy() {
assert!(is_sqlite_busy(&make_busy_error()));
}
#[test]
fn is_sqlite_busy_detecta_database_locked() {
assert!(is_sqlite_busy(&make_locked_error()));
}
#[test]
fn is_sqlite_busy_rejeita_outros_erros() {
let err = AppError::Validation("campo inválido".into());
assert!(!is_sqlite_busy(&err));
}
#[test]
fn with_busy_retry_propagates_non_busy_error() {
let calls = Arc::new(AtomicU32::new(0));
let calls_clone = Arc::clone(&calls);
let result = with_busy_retry(|| {
calls_clone.fetch_add(1, Ordering::SeqCst);
Err(AppError::Validation("campo x".into()))
});
assert_eq!(calls.load(Ordering::SeqCst), 1);
assert!(matches!(result, Err(AppError::Validation(_))));
}
#[test]
fn with_busy_retry_succeeds_on_third_attempt() {
let calls = Arc::new(AtomicU32::new(0));
let calls_clone = Arc::clone(&calls);
let result = with_busy_retry(|| {
let n = calls_clone.fetch_add(1, Ordering::SeqCst);
if n < 2 {
Err(make_busy_error())
} else {
Ok(())
}
});
assert_eq!(calls.load(Ordering::SeqCst), 3);
assert!(result.is_ok(), "expected Ok after 3rd attempt");
}
#[test]
fn with_busy_retry_returns_db_busy_after_all_retries() {
let calls = Arc::new(AtomicU32::new(0));
let calls_clone = Arc::clone(&calls);
let result = with_busy_retry(|| {
calls_clone.fetch_add(1, Ordering::SeqCst);
Err(make_busy_error())
});
assert_eq!(
calls.load(Ordering::SeqCst),
MAX_SQLITE_BUSY_RETRIES,
"must attempt exactly MAX_SQLITE_BUSY_RETRIES times"
);
assert!(
matches!(result, Err(AppError::DbBusy(_))),
"must convert to DbBusy after exhausting retries"
);
}
}