mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
push that bricked err
Signed-off-by: Kenneth Obsequio <k80308392@gmail.com>
This commit is contained in:
@@ -32,6 +32,7 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const sequelize = require('../config/db.config');
|
const sequelize = require('../config/db.config');
|
||||||
|
const { withTransactionRetry } = require('../utils/withTransactionRetry.util');
|
||||||
const { evaluateEntity } = require('../utils/courses/completion_requirements.registry');
|
const { evaluateEntity } = require('../utils/courses/completion_requirements.registry');
|
||||||
const CompletionRequirement = require('../models/courses/completion_requirement.mdl');
|
const CompletionRequirement = require('../models/courses/completion_requirement.mdl');
|
||||||
const CompletionRequirementProgress = require('../models/courses/completion_requirement_progress.mdl');
|
const CompletionRequirementProgress = require('../models/courses/completion_requirement_progress.mdl');
|
||||||
@@ -298,8 +299,7 @@ async function recordWatchProgress(userId, {
|
|||||||
: null;
|
: null;
|
||||||
const durationSeconds = blockId ? getBlockDuration(page, blockId) : 0;
|
const durationSeconds = blockId ? getBlockDuration(page, blockId) : 0;
|
||||||
|
|
||||||
const t = await sequelize.transaction();
|
const { anyCompleted, aggregatePercent, cascade } = await withTransactionRetry(sequelize, async (t) => {
|
||||||
try {
|
|
||||||
let anyCompleted = false;
|
let anyCompleted = false;
|
||||||
let aggregatePercent = null;
|
let aggregatePercent = null;
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
@@ -382,7 +382,8 @@ async function recordWatchProgress(userId, {
|
|||||||
lessonStatus: anyCompleted ? 'completed' : 'in_progress',
|
lessonStatus: anyCompleted ? 'completed' : 'in_progress',
|
||||||
}, t);
|
}, t);
|
||||||
|
|
||||||
await t.commit();
|
return { anyCompleted, aggregatePercent, cascade };
|
||||||
|
});
|
||||||
|
|
||||||
// recomputeCascade skipped its own task-sync since it ran under our externalTransaction
|
// 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
|
// (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 });
|
const completedTasks = await syncCompletedEntitiesToTaskProgress(userId, { lessonUuid, unitUuid, courseUuid });
|
||||||
|
|
||||||
return { progress_percent: aggregatePercent, completed: anyCompleted, cascade: { ...cascade, completed_tasks: completedTasks } };
|
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;
|
if (!requirement) return null;
|
||||||
|
|
||||||
const t = await sequelize.transaction();
|
const result = await withTransactionRetry(sequelize, async (t) => {
|
||||||
try {
|
|
||||||
await CompletionRequirementProgress.upsert({
|
await CompletionRequirementProgress.upsert({
|
||||||
requirement_id: requirement.requirement_id,
|
requirement_id: requirement.requirement_id,
|
||||||
user_id: userId,
|
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) };
|
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
|
// Same reasoning as recordWatchProgress — recomputeCascade (lesson branch) skipped its
|
||||||
// own sync under our externalTransaction; the unit/course branches never called it at
|
// 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);
|
result.completed_tasks = await syncCompletedEntitiesToTaskProgress(userId, syncUuids);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
} catch (err) {
|
|
||||||
await t.rollback();
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
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