pub fn classify_error(err: &anyhow::Error) -> (bool, bool, u64) {
if let Some(pg) = err.downcast_ref::<postgres::Error>() {
if let Some(db) = pg.as_db_error() {
return classify_pg_sqlstate(db.code());
}
if pg.is_closed() {
return (true, true, 0);
}
}
if let Some(result) = err
.downcast_ref::<mysql::Error>()
.and_then(classify_mysql_error)
{
return result;
}
let msg = format!("{:#}", err).to_lowercase();
if msg.contains("loading credential")
|| msg.contains("loadcredential")
|| msg.contains("metadata.google.internal")
|| msg.contains("permission denied")
|| msg.contains("access denied")
|| msg.contains("invalid_grant")
|| msg.contains("token has been expired or revoked")
{
return (false, false, 0);
}
if msg.contains("connection reset")
|| msg.contains("broken pipe")
|| msg.contains("connection refused")
|| msg.contains("no route to host")
|| msg.contains("network is unreachable")
|| msg.contains("name resolution")
|| msg.contains("dns")
|| msg.contains("ssl handshake")
|| msg.contains("i/o timeout")
|| msg.contains("unexpected eof")
|| msg.contains("closed the connection unexpectedly")
|| msg.contains("got an error reading communication packets")
{
return (true, true, 0);
}
if msg.contains("gone away")
|| msg.contains("lost connection")
|| msg.contains("the server closed the connection")
|| msg.contains("can't connect to mysql server")
{
return (true, true, 0);
}
if msg.contains("timed out")
|| msg.contains("timeout")
|| msg.contains("canceling statement")
|| msg.contains("lock wait timeout")
|| msg.contains("execution time exceeded")
{
return (true, false, 0);
}
if msg.contains("too many connections")
|| msg.contains("the database system is starting up")
|| msg.contains("the database system is shutting down")
{
return (true, true, 15_000);
}
if msg.contains("deadlock") || msg.contains("could not serialize access") {
return (true, false, 1_000);
}
(false, false, 0)
}
fn classify_pg_sqlstate(code: &postgres::error::SqlState) -> (bool, bool, u64) {
use postgres::error::SqlState;
if *code == SqlState::CONNECTION_EXCEPTION
|| *code == SqlState::CONNECTION_DOES_NOT_EXIST
|| *code == SqlState::CONNECTION_FAILURE
|| *code == SqlState::SQLCLIENT_UNABLE_TO_ESTABLISH_SQLCONNECTION
|| *code == SqlState::SQLSERVER_REJECTED_ESTABLISHMENT_OF_SQLCONNECTION
|| code.code().starts_with("08")
{
return (true, true, 0);
}
if *code == SqlState::ADMIN_SHUTDOWN
|| *code == SqlState::CRASH_SHUTDOWN
|| *code == SqlState::CANNOT_CONNECT_NOW
{
return (true, true, 15_000);
}
if *code == SqlState::TOO_MANY_CONNECTIONS {
return (true, true, 15_000);
}
if *code == SqlState::T_R_SERIALIZATION_FAILURE {
return (true, false, 1_000);
}
if *code == SqlState::T_R_DEADLOCK_DETECTED {
return (true, false, 1_000);
}
if *code == SqlState::QUERY_CANCELED {
return (true, false, 0);
}
if code.code().starts_with("53") {
return (true, false, 5_000);
}
if code.code().starts_with("28") {
return (false, false, 0);
}
if code.code().starts_with("42") {
return (false, false, 0);
}
(false, false, 0)
}
fn classify_mysql_error(err: &mysql::Error) -> Option<(bool, bool, u64)> {
match err {
mysql::Error::MySqlError(me) => {
match me.code {
1213 => Some((true, false, 1_000)),
1205 => Some((true, false, 0)),
1040 => Some((true, true, 15_000)),
1053 => Some((true, true, 15_000)),
1045 | 1044 => Some((false, false, 0)),
1049 | 1146 | 1064 => Some((false, false, 0)),
_ => None,
}
}
mysql::Error::IoError(_) => Some((true, true, 0)),
_ => None,
}
}
#[cfg(test)]
pub(crate) fn is_transient(err: &anyhow::Error) -> bool {
classify_error(err).0
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_transient_matches() {
assert!(is_transient(&anyhow::anyhow!("statement timed out")));
assert!(is_transient(&anyhow::anyhow!("connection reset")));
}
#[test]
fn test_is_transient_rejects() {
assert!(!is_transient(&anyhow::anyhow!("syntax error")));
assert!(!is_transient(&anyhow::anyhow!("permission denied")));
assert!(!is_transient(&anyhow::anyhow!("table not found")));
}
#[test]
fn test_classify_network_errors_need_reconnect() {
let cases = [
"connection refused",
"no route to host",
"network is unreachable",
"broken pipe",
"unexpected eof",
"MySQL server has gone away",
"lost connection to server",
"can't connect to mysql server",
"the server closed the connection",
"got an error reading communication packets",
"ssl handshake failed",
];
for msg in cases {
let (transient, reconnect, _) = classify_error(&anyhow::anyhow!("{}", msg));
assert!(transient, "should be transient: {}", msg);
assert!(reconnect, "should need reconnect: {}", msg);
}
}
#[test]
fn test_classify_timeout_no_reconnect() {
let (t, r, _) = classify_error(&anyhow::anyhow!("statement timed out"));
assert!(t);
assert!(!r, "timeout should not require reconnect");
let (t, r, _) = classify_error(&anyhow::anyhow!("lock wait timeout exceeded"));
assert!(t);
assert!(!r);
}
#[test]
fn test_classify_capacity_errors_extra_delay() {
let (t, r, delay) = classify_error(&anyhow::anyhow!("too many connections"));
assert!(t);
assert!(r);
assert!(
delay >= 10_000,
"capacity errors should have extra delay, got: {}ms",
delay
);
let (t, _, delay) = classify_error(&anyhow::anyhow!("the database system is starting up"));
assert!(t);
assert!(delay >= 10_000);
}
#[test]
fn test_classify_deadlock_retryable() {
let (t, r, delay) = classify_error(&anyhow::anyhow!("deadlock detected"));
assert!(t);
assert!(!r, "deadlock should not require reconnect");
assert!(delay >= 1_000, "deadlock should have small extra delay");
}
#[test]
fn test_classify_permanent_errors() {
let cases = [
"syntax error",
"permission denied",
"relation does not exist",
"column not found",
];
for msg in cases {
let (transient, _, _) = classify_error(&anyhow::anyhow!("{}", msg));
assert!(!transient, "should NOT be transient: {}", msg);
}
}
#[test]
fn test_classify_credential_errors_not_transient() {
let cases = [
"loading credential to sign http request",
"error sending request for url (http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token): dns error",
"invalid_grant: Token has been expired or revoked",
"Access Denied: no permission",
];
for msg in cases {
let (transient, _, _) = classify_error(&anyhow::anyhow!("{}", msg));
assert!(
!transient,
"credential error should NOT be transient: {}",
msg
);
}
}
}