1use sea_orm::{DbBackend, DbErr};
39
40const MYSQL_DEADLOCK_SQLSTATE: &str = "40001";
42
43const PG_SERIALIZATION_FAILURE: &str = "40001";
45const PG_DEADLOCK_DETECTED: &str = "40P01";
46
47const SQLITE_BUSY_CODE: &str = "(code: 5)";
51const SQLITE_BUSY_SNAPSHOT_CODE: &str = "(code: 517)";
52const SQLITE_LOCKED_MSG: &str = "database is locked";
53
54#[must_use]
66pub fn is_retryable_contention(backend: DbBackend, err: &DbErr) -> bool {
67 match err {
68 DbErr::Exec(runtime_err) | DbErr::Query(runtime_err) => {
69 let msg = runtime_err.to_string();
70 match backend {
71 DbBackend::MySql => is_mysql_deadlock(&msg),
72 DbBackend::Postgres => is_pg_contention(&msg),
73 DbBackend::Sqlite => is_sqlite_busy(&msg),
74 }
75 }
76 _ => false,
77 }
78}
79
80fn is_mysql_deadlock(msg: &str) -> bool {
81 msg.contains(MYSQL_DEADLOCK_SQLSTATE)
82}
83
84fn is_pg_contention(msg: &str) -> bool {
85 msg.contains(PG_SERIALIZATION_FAILURE) || msg.contains(PG_DEADLOCK_DETECTED)
86}
87
88fn is_sqlite_busy(msg: &str) -> bool {
89 (msg.contains(SQLITE_BUSY_CODE) || msg.contains(SQLITE_BUSY_SNAPSHOT_CODE))
90 && msg.contains(SQLITE_LOCKED_MSG)
91}
92
93#[cfg(test)]
94mod tests {
95 use sea_orm::RuntimeErr;
96
97 use super::*;
98
99 fn exec_err(msg: &str) -> DbErr {
100 DbErr::Exec(RuntimeErr::Internal(msg.to_owned()))
101 }
102
103 fn query_err(msg: &str) -> DbErr {
104 DbErr::Query(RuntimeErr::Internal(msg.to_owned()))
105 }
106
107 #[test]
110 fn mysql_deadlock_detected() {
111 let err = exec_err("MySqlError { ... SQLSTATE 40001: Deadlock found ... }");
112 assert!(is_retryable_contention(DbBackend::MySql, &err));
113 }
114
115 #[test]
118 fn pg_serialization_failure_detected() {
119 let err = exec_err("error returned from database: error with SQLSTATE 40001");
120 assert!(is_retryable_contention(DbBackend::Postgres, &err));
121 }
122
123 #[test]
124 fn pg_deadlock_detected() {
125 let err = exec_err("error returned from database: error with SQLSTATE 40P01");
126 assert!(is_retryable_contention(DbBackend::Postgres, &err));
127 }
128
129 #[test]
132 fn sqlite_busy_exec_detected() {
133 let err =
134 exec_err("Execution Error: error returned from database: (code: 5) database is locked");
135 assert!(is_retryable_contention(DbBackend::Sqlite, &err));
136 }
137
138 #[test]
139 fn sqlite_busy_query_detected() {
140 let err =
141 query_err("Query Error: error returned from database: (code: 5) database is locked");
142 assert!(is_retryable_contention(DbBackend::Sqlite, &err));
143 }
144
145 #[test]
148 fn sqlite_busy_snapshot_detected() {
149 let err = exec_err(
150 "Execution Error: error returned from database: (code: 517) database is locked",
151 );
152 assert!(is_retryable_contention(DbBackend::Sqlite, &err));
153 }
154
155 #[test]
158 fn sqlstate_40001_not_retryable_on_sqlite() {
159 let err = exec_err("SQLSTATE 40001");
160 assert!(!is_retryable_contention(DbBackend::Sqlite, &err));
161 }
162
163 #[test]
164 fn sqlite_busy_not_retryable_on_mysql() {
165 let err =
166 exec_err("Execution Error: error returned from database: (code: 5) database is locked");
167 assert!(!is_retryable_contention(DbBackend::MySql, &err));
168 }
169
170 #[test]
173 fn sqlite_constraint_not_retryable() {
174 let err = exec_err(
175 "Execution Error: error returned from database: (code: 19) UNIQUE constraint failed",
176 );
177 assert!(!is_retryable_contention(DbBackend::Sqlite, &err));
178 }
179
180 #[test]
181 fn unrelated_errors_not_retryable() {
182 assert!(!is_retryable_contention(
183 DbBackend::Sqlite,
184 &DbErr::Custom("something".into()),
185 ));
186 assert!(!is_retryable_contention(
187 DbBackend::Postgres,
188 &DbErr::RecordNotFound("x".into()),
189 ));
190 }
191
192 #[test]
193 fn code_5_without_locked_msg_not_retryable() {
194 let err = exec_err("error returned from database: (code: 5) something else");
195 assert!(!is_retryable_contention(DbBackend::Sqlite, &err));
196 }
197}