use sea_orm::{DbBackend, DbErr};
const MYSQL_DEADLOCK_SQLSTATE: &str = "40001";
const PG_SERIALIZATION_FAILURE: &str = "40001";
const PG_DEADLOCK_DETECTED: &str = "40P01";
const SQLITE_BUSY_CODE: &str = "(code: 5)";
const SQLITE_BUSY_SNAPSHOT_CODE: &str = "(code: 517)";
const SQLITE_LOCKED_MSG: &str = "database is locked";
#[must_use]
pub fn is_retryable_contention(backend: DbBackend, err: &DbErr) -> bool {
match err {
DbErr::Exec(runtime_err) | DbErr::Query(runtime_err) => {
let msg = runtime_err.to_string();
match backend {
DbBackend::MySql => is_mysql_deadlock(&msg),
DbBackend::Postgres => is_pg_contention(&msg),
DbBackend::Sqlite => is_sqlite_busy(&msg),
}
}
_ => false,
}
}
fn is_mysql_deadlock(msg: &str) -> bool {
msg.contains(MYSQL_DEADLOCK_SQLSTATE)
}
fn is_pg_contention(msg: &str) -> bool {
msg.contains(PG_SERIALIZATION_FAILURE) || msg.contains(PG_DEADLOCK_DETECTED)
}
fn is_sqlite_busy(msg: &str) -> bool {
(msg.contains(SQLITE_BUSY_CODE) || msg.contains(SQLITE_BUSY_SNAPSHOT_CODE))
&& msg.contains(SQLITE_LOCKED_MSG)
}
#[cfg(test)]
mod tests {
use sea_orm::RuntimeErr;
use super::*;
fn exec_err(msg: &str) -> DbErr {
DbErr::Exec(RuntimeErr::Internal(msg.to_owned()))
}
fn query_err(msg: &str) -> DbErr {
DbErr::Query(RuntimeErr::Internal(msg.to_owned()))
}
#[test]
fn mysql_deadlock_detected() {
let err = exec_err("MySqlError { ... SQLSTATE 40001: Deadlock found ... }");
assert!(is_retryable_contention(DbBackend::MySql, &err));
}
#[test]
fn pg_serialization_failure_detected() {
let err = exec_err("error returned from database: error with SQLSTATE 40001");
assert!(is_retryable_contention(DbBackend::Postgres, &err));
}
#[test]
fn pg_deadlock_detected() {
let err = exec_err("error returned from database: error with SQLSTATE 40P01");
assert!(is_retryable_contention(DbBackend::Postgres, &err));
}
#[test]
fn sqlite_busy_exec_detected() {
let err =
exec_err("Execution Error: error returned from database: (code: 5) database is locked");
assert!(is_retryable_contention(DbBackend::Sqlite, &err));
}
#[test]
fn sqlite_busy_query_detected() {
let err =
query_err("Query Error: error returned from database: (code: 5) database is locked");
assert!(is_retryable_contention(DbBackend::Sqlite, &err));
}
#[test]
fn sqlite_busy_snapshot_detected() {
let err = exec_err(
"Execution Error: error returned from database: (code: 517) database is locked",
);
assert!(is_retryable_contention(DbBackend::Sqlite, &err));
}
#[test]
fn sqlstate_40001_not_retryable_on_sqlite() {
let err = exec_err("SQLSTATE 40001");
assert!(!is_retryable_contention(DbBackend::Sqlite, &err));
}
#[test]
fn sqlite_busy_not_retryable_on_mysql() {
let err =
exec_err("Execution Error: error returned from database: (code: 5) database is locked");
assert!(!is_retryable_contention(DbBackend::MySql, &err));
}
#[test]
fn sqlite_constraint_not_retryable() {
let err = exec_err(
"Execution Error: error returned from database: (code: 19) UNIQUE constraint failed",
);
assert!(!is_retryable_contention(DbBackend::Sqlite, &err));
}
#[test]
fn unrelated_errors_not_retryable() {
assert!(!is_retryable_contention(
DbBackend::Sqlite,
&DbErr::Custom("something".into()),
));
assert!(!is_retryable_contention(
DbBackend::Postgres,
&DbErr::RecordNotFound("x".into()),
));
}
#[test]
fn code_5_without_locked_msg_not_retryable() {
let err = exec_err("error returned from database: (code: 5) something else");
assert!(!is_retryable_contention(DbBackend::Sqlite, &err));
}
}