use std::{future::Future, time::Duration};
use sea_orm::{DbErr, RuntimeErr};
const SQLITE_BUSY: &str = "5";
const SQLITE_BUSY_SNAPSHOT: &str = "517";
const MAX_BUSY_RETRY_ATTEMPTS: u32 = 8;
const INITIAL_DELAY: Duration = Duration::from_millis(10);
const MAX_DELAY: Duration = Duration::from_millis(500);
pub fn is_sqlite_busy(err: &DbErr) -> bool {
let runtime_err = match err {
DbErr::Conn(e) | DbErr::Exec(e) | DbErr::Query(e) => e,
_ => return false,
};
let RuntimeErr::SqlxError(sqlx::Error::Database(db_err)) = runtime_err else {
return false;
};
matches!(
db_err.code().as_deref(),
Some(SQLITE_BUSY) | Some(SQLITE_BUSY_SNAPSHOT)
)
}
pub trait IsSqliteBusy {
fn is_sqlite_busy(&self) -> bool;
}
impl IsSqliteBusy for DbErr {
fn is_sqlite_busy(&self) -> bool {
is_sqlite_busy(self)
}
}
pub async fn retry_on_busy<F, Fut, T, E>(mut f: F) -> Result<T, E>
where
F: FnMut() -> Fut,
Fut: Future<Output = Result<T, E>>,
E: IsSqliteBusy,
{
let mut delay = INITIAL_DELAY;
for attempt in 1..=MAX_BUSY_RETRY_ATTEMPTS {
match f().await {
Ok(value) => {
if attempt > 1 {
tracing::warn!(attempts = attempt, "db busy resolved after retries");
}
return Ok(value);
}
Err(err) if err.is_sqlite_busy() && attempt < MAX_BUSY_RETRY_ATTEMPTS => {
tracing::warn!(
attempt,
delay_ms = delay.as_millis() as u64,
"SQLITE_BUSY, retrying"
);
tokio::time::sleep(delay).await;
delay = (delay * 2).min(MAX_DELAY);
}
Err(err) if err.is_sqlite_busy() => {
tracing::error!(
attempts = attempt,
"SQLITE_BUSY exhausted retries, giving up"
);
return Err(err);
}
Err(err) => return Err(err),
}
}
unreachable!("loop returns or errors before exhausting attempts")
}