push that bricked err

Signed-off-by: Kenneth Obsequio <k80308392@gmail.com>
This commit is contained in:
2026-07-17 15:20:42 +08:00
parent d49e3be4d2
commit bae079d5d8
2 changed files with 73 additions and 32 deletions
+7 -14
View File
@@ -32,6 +32,7 @@
'use strict';
const sequelize = require('../config/db.config');
const { withTransactionRetry } = require('../utils/withTransactionRetry.util');
const { evaluateEntity } = require('../utils/courses/completion_requirements.registry');
const CompletionRequirement = require('../models/courses/completion_requirement.mdl');
const CompletionRequirementProgress = require('../models/courses/completion_requirement_progress.mdl');
@@ -298,8 +299,7 @@ async function recordWatchProgress(userId, {
: null;
const durationSeconds = blockId ? getBlockDuration(page, blockId) : 0;
const t = await sequelize.transaction();
try {
const { anyCompleted, aggregatePercent, cascade } = await withTransactionRetry(sequelize, async (t) => {
let anyCompleted = false;
let aggregatePercent = null;
const now = new Date();
@@ -382,7 +382,8 @@ async function recordWatchProgress(userId, {
lessonStatus: anyCompleted ? 'completed' : 'in_progress',
}, t);
await t.commit();
return { anyCompleted, aggregatePercent, cascade };
});
// recomputeCascade skipped its own task-sync since it ran under our externalTransaction
// (would've read pre-commit state) — run it now that everything is durable. Runs
@@ -391,10 +392,6 @@ async function recordWatchProgress(userId, {
const completedTasks = await syncCompletedEntitiesToTaskProgress(userId, { lessonUuid, unitUuid, courseUuid });
return { progress_percent: aggregatePercent, completed: anyCompleted, cascade: { ...cascade, completed_tasks: completedTasks } };
} catch (err) {
await t.rollback();
throw err;
}
}
/**
@@ -408,8 +405,7 @@ async function recordManualComplete(userId, { entityType, entityId, lessonId = n
});
if (!requirement) return null;
const t = await sequelize.transaction();
try {
const result = await withTransactionRetry(sequelize, async (t) => {
await CompletionRequirementProgress.upsert({
requirement_id: requirement.requirement_id,
user_id: userId,
@@ -437,7 +433,8 @@ async function recordManualComplete(userId, { entityType, entityId, lessonId = n
result = { course: await recomputeAndPersist({ entityType: 'course', entityId, userId, courseId: entityId, referenceId: courseUuid }, t) };
}
await t.commit();
return result;
});
// Same reasoning as recordWatchProgress — recomputeCascade (lesson branch) skipped its
// own sync under our externalTransaction; the unit/course branches never called it at
@@ -451,10 +448,6 @@ async function recordManualComplete(userId, { entityType, entityId, lessonId = n
result.completed_tasks = await syncCompletedEntitiesToTaskProgress(userId, syncUuids);
return result;
} catch (err) {
await t.rollback();
throw err;
}
}
module.exports = {
+48
View File
@@ -0,0 +1,48 @@
/***********************************************************************************************************************************************************************
* 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 };