push that bricked err
Signed-off-by: Kenneth Obsequio <k80308392@gmail.com>
This commit is contained in:
@@ -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,19 +382,16 @@ 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
|
||||
// regardless of courseId — a standalone (no parent course) lesson can satisfy a
|
||||
// read_lesson task requirement via watch_video/listen_audio/watch_percent too.
|
||||
const completedTasks = await syncCompletedEntitiesToTaskProgress(userId, { lessonUuid, unitUuid, courseUuid });
|
||||
// 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
|
||||
// regardless of courseId — a standalone (no parent course) lesson can satisfy a
|
||||
// read_lesson task requirement via watch_video/listen_audio/watch_percent too.
|
||||
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;
|
||||
}
|
||||
return { progress_percent: aggregatePercent, completed: anyCompleted, cascade: { ...cascade, completed_tasks: completedTasks } };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -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,24 +433,21 @@ async function recordManualComplete(userId, { entityType, entityId, lessonId = n
|
||||
result = { course: await recomputeAndPersist({ entityType: 'course', entityId, userId, courseId: entityId, referenceId: courseUuid }, t) };
|
||||
}
|
||||
|
||||
await t.commit();
|
||||
|
||||
// Same reasoning as recordWatchProgress — recomputeCascade (lesson branch) skipped its
|
||||
// own sync under our externalTransaction; the unit/course branches never called it at
|
||||
// all. Runs regardless of courseId — a standalone (no parent course) lesson/unit
|
||||
// manual_complete can satisfy a read_lesson/read_unit task requirement too.
|
||||
const syncUuids = entityType === 'lesson'
|
||||
? { lessonUuid, unitUuid, courseUuid }
|
||||
: entityType === 'unit'
|
||||
? { unitUuid, courseUuid }
|
||||
: { courseUuid };
|
||||
result.completed_tasks = await syncCompletedEntitiesToTaskProgress(userId, syncUuids);
|
||||
|
||||
return result;
|
||||
} catch (err) {
|
||||
await t.rollback();
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
|
||||
// Same reasoning as recordWatchProgress — recomputeCascade (lesson branch) skipped its
|
||||
// own sync under our externalTransaction; the unit/course branches never called it at
|
||||
// all. Runs regardless of courseId — a standalone (no parent course) lesson/unit
|
||||
// manual_complete can satisfy a read_lesson/read_unit task requirement too.
|
||||
const syncUuids = entityType === 'lesson'
|
||||
? { lessonUuid, unitUuid, courseUuid }
|
||||
: entityType === 'unit'
|
||||
? { unitUuid, courseUuid }
|
||||
: { courseUuid };
|
||||
result.completed_tasks = await syncCompletedEntitiesToTaskProgress(userId, syncUuids);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
||||
@@ -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 };
|
||||
Reference in New Issue
Block a user