Skip to content

Commit c3c96ac

Browse files
committed
fix(modkit-db): match PG serialization/deadlock by message, not only SQLSTATE code
sea-orm/sqlx put the condition in the error message text, not the numeric SQLSTATE, so the code-only substring match missed real 40001 serialization failures — concurrent tenant DELETE surfaced 500 instead of retry-to-idempotent. Adds canonical-message matching + tests. Signed-off-by: Diffora <ddiffora@gmail.com>
1 parent d817f05 commit c3c96ac

1 file changed

Lines changed: 31 additions & 1 deletion

File tree

libs/modkit-db/src/contention.rs

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,16 @@ const MYSQL_DEADLOCK_SQLSTATE: &str = "40001";
4444
const PG_SERIALIZATION_FAILURE: &str = "40001";
4545
const PG_DEADLOCK_DETECTED: &str = "40P01";
4646

47+
/// `PostgreSQL` retryable error MESSAGE fragments. sea-orm/sqlx surface the
48+
/// condition through the error's *message text* on the `Display` path, not the
49+
/// numeric SQLSTATE — so matching the code alone (`40001` / `40P01`) misses real
50+
/// serialization failures and deadlocks (e.g. `"could not serialize access due
51+
/// to concurrent update"` carries no `"40001"` substring). Matching the message
52+
/// is the reliable signal here. Message text is `lc_messages`-dependent, so the
53+
/// numeric-code checks above remain as a locale-independent belt-and-suspenders.
54+
const PG_SERIALIZATION_MSG: &str = "could not serialize access";
55+
const PG_DEADLOCK_MSG: &str = "deadlock detected";
56+
4757
/// `SQLite` error codes for write contention.
4858
///
4959
/// sqlx surfaces these as `"error returned from database: (code: N) database is locked"`.
@@ -82,7 +92,10 @@ fn is_mysql_deadlock(msg: &str) -> bool {
8292
}
8393

8494
fn is_pg_contention(msg: &str) -> bool {
85-
msg.contains(PG_SERIALIZATION_FAILURE) || msg.contains(PG_DEADLOCK_DETECTED)
95+
msg.contains(PG_SERIALIZATION_FAILURE)
96+
|| msg.contains(PG_DEADLOCK_DETECTED)
97+
|| msg.contains(PG_SERIALIZATION_MSG)
98+
|| msg.contains(PG_DEADLOCK_MSG)
8699
}
87100

88101
fn is_sqlite_busy(msg: &str) -> bool {
@@ -126,6 +139,23 @@ mod tests {
126139
assert!(is_retryable_contention(DbBackend::Postgres, &err));
127140
}
128141

142+
#[test]
143+
fn pg_serialization_failure_by_message_detected() {
144+
// Real sea-orm/sqlx Display carries the message, not the numeric
145+
// SQLSTATE — this is the exact text Postgres emits for 40001 and the
146+
// concurrent-DELETE regression that the numeric-only match missed.
147+
let err = exec_err(
148+
"error returned from database: could not serialize access due to concurrent update",
149+
);
150+
assert!(is_retryable_contention(DbBackend::Postgres, &err));
151+
}
152+
153+
#[test]
154+
fn pg_deadlock_by_message_detected() {
155+
let err = exec_err("error returned from database: deadlock detected");
156+
assert!(is_retryable_contention(DbBackend::Postgres, &err));
157+
}
158+
129159
// ── SQLite BUSY (code 5) ─────────────────────────────────────────
130160

131161
#[test]

0 commit comments

Comments
 (0)