/*********************************************************************************************************************************************************************** * 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 };