Files
starr-philproperties/utils/withTransactionRetry.util.js
T
2026-07-17 15:20:42 +08:00

49 lines
2.3 KiB
JavaScript

/***********************************************************************************************************************************************************************
* File Name: withTransactionRetry.util.js
* Type of Program: Utility
* Description: Runs a Sequelize transaction, automatically retrying on serialization_failure
* (SQLSTATE 40001) — the transient "restart transaction" error CockroachDB (and
* Postgres SERIALIZABLE) throw when concurrent transactions can't be safely
* ordered. Without a retry, these surface as unhandled 500s on otherwise-correct
* code the moment two requests touch the same row close together.
* Author: Kenneth Obsequio (@lash0000)
* Date Created: Jul. 17, 2026
***********************************************************************************************************************************************************************
* HOW TO USE:
* const { withTransactionRetry } = require('../utils/withTransactionRetry.util');
* const result = await withTransactionRetry(sequelize, async (t) => {
* await Model.upsert({...}, { transaction: t });
* return something;
* });
***********************************************************************************************************************************************************************/
'use strict';
const SERIALIZATION_FAILURE = '40001';
const MAX_RETRIES = 5;
const BASE_DELAY_MS = 50;
function isSerializationFailure(err) {
return err?.parent?.code === SERIALIZATION_FAILURE || err?.original?.code === SERIALIZATION_FAILURE;
}
async function withTransactionRetry(sequelize, fn, { maxRetries = MAX_RETRIES } = {}) {
for (let attempt = 0; ; attempt++) {
const t = await sequelize.transaction();
try {
const result = await fn(t);
await t.commit();
return result;
} catch (err) {
await t.rollback();
if (isSerializationFailure(err) && attempt < maxRetries) {
const delay = BASE_DELAY_MS * 2 ** attempt + Math.random() * BASE_DELAY_MS;
await new Promise((resolve) => setTimeout(resolve, delay));
continue;
}
throw err;
}
}
}
module.exports = { withTransactionRetry, isSerializationFailure };