When using the REPEATABLE READ or SERIALIZABLE isolation levels, you should be prepared to handle serialization (error code 40001) and deadlock failures (error code 40P01) in your code. Well this is how you do it:
let retry = true;
while (retry) {
const t = await sequelize.transaction({
isolationLevel: Transaction.ISOLATION_LEVELS.SERIALIZABLE
});
try {
let id = await the_function_that_actually_does_the_work(t);
await t.commit();
retry = false;
} catch (err) {
retry = is_serialization_failure(err); // serialization failures should be retried
if (!is_rollback_failure(err)) {
await t.rollback();
} // else transaction has already been rolled back and you should NOT roll it back again!
if (!retry) {
throw err; // re-throw so caller knows we failed
}
}
}
where
export const is_serialization_failure = (e): boolean => {
// https://www.postgresql.org/docs/current/mvcc-serialization-failure-handling.html
// The optional chaining (?.) operator accesses an object's property or calls a function.
// If the object accessed or function called using this operator is undefined or null,
// the expression short circuits and evaluates to undefined instead of throwing an error.
// more accurately only 40001 is serialization failure. 40P01 is a deadlock failure.
return (e.original?.code === '40001' || e.original?.code === '40P01');
}
export const is_rollback_failure = (e): boolean => {
// https://www.postgresql.org/docs/current/errcodes-appendix.html
return e.original?.code?.startsWith('40');
}
That’s it for this post! Let me know what you think.