fix(autogen): pg_advisory_lock + catch-up + double-check after lock
Race condition (дубли статей) устранён окончательно: 1. pg_advisory_lock по ключу категории в начале runAutogenForCategory — если два процесса запускаются одновременно, второй ждёт первого. pg_advisory_unlock в finally — освобождается всегда, даже при ошибке. 2. После получения lock — повторная проверка 'уже генерировали сегодня' (SELECT articles WHERE category AND created_at >= CURRENT_DATE). Если да — skip без генерации. Это защита от случая когда первый процесс завершился пока второй ждал lock. 3. draftAutoApprove catch-up при старте: если engine стартовал после 07:00 и есть непрогнанные вчерашние черновики — одобряет их сразу. Раньше deploy в 07:27 приводил к тому что черновики зависали навсегда.
This commit is contained in:
@@ -113,6 +113,27 @@ async function runDraftAutoApprove() {
|
|||||||
function startDraftAutoApproveScheduler() {
|
function startDraftAutoApproveScheduler() {
|
||||||
console.log('[DraftApprove] scheduler started (auto-approve at 07:00 MSK, только вчерашние черновики)');
|
console.log('[DraftApprove] scheduler started (auto-approve at 07:00 MSK, только вчерашние черновики)');
|
||||||
|
|
||||||
|
// Catch-up при старте: если сейчас уже после AUTO_APPROVE_HOUR_MSK —
|
||||||
|
// проверить есть ли вчерашние черновики которые пропустили тик.
|
||||||
|
(async () => {
|
||||||
|
const nowMsk = new Date(Date.now() + 3 * 60 * 60 * 1000);
|
||||||
|
const hourMsk = nowMsk.getUTCHours();
|
||||||
|
if (hourMsk >= AUTO_APPROVE_HOUR_MSK) {
|
||||||
|
try {
|
||||||
|
const { rows: missed } = await query(`
|
||||||
|
SELECT COUNT(*) AS cnt FROM articles
|
||||||
|
WHERE status='draft'
|
||||||
|
AND created_at AT TIME ZONE 'Europe/Moscow' >= (CURRENT_DATE - INTERVAL '1 day')::date
|
||||||
|
AND created_at AT TIME ZONE 'Europe/Moscow' < CURRENT_DATE::date
|
||||||
|
`);
|
||||||
|
if (parseInt(missed[0]?.cnt, 10) > 0) {
|
||||||
|
console.log('[DraftApprove] catch-up при старте: найдены пропущенные вчерашние черновики (' + missed[0].cnt + ' шт)');
|
||||||
|
await runAutoApprove();
|
||||||
|
}
|
||||||
|
} catch (err) { console.error('[DraftApprove] catch-up error:', err.message); }
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const msk = new Date(now.getTime() + 3 * 60 * 60 * 1000);
|
const msk = new Date(now.getTime() + 3 * 60 * 60 * 1000);
|
||||||
|
|||||||
@@ -116,6 +116,23 @@ async function getNextTopic(category) {
|
|||||||
* Запускает генерацию одной статьи для категории.
|
* Запускает генерацию одной статьи для категории.
|
||||||
*/
|
*/
|
||||||
async function runAutogenForCategory(category) {
|
async function runAutogenForCategory(category) {
|
||||||
|
// pg_advisory_lock: транзакционный lock по ключу категории.
|
||||||
|
// Гарантирует что только один процесс генерирует статью для данной категории.
|
||||||
|
// Устраняет race condition когда несколько тиков/запросов запускаются одновременно.
|
||||||
|
const lockKey = Math.abs(category.split('').reduce((h, c) => (Math.imul(31, h) + c.charCodeAt(0)) | 0, 0));
|
||||||
|
await query('SELECT pg_advisory_lock($1)', [lockKey]);
|
||||||
|
|
||||||
|
// После получения lock — проверяем ещё раз что за сегодня ещё не генерировали
|
||||||
|
const { rows: alreadyToday } = await query(
|
||||||
|
`SELECT id FROM articles WHERE category=$1 AND status='draft' AND created_at >= CURRENT_DATE LIMIT 1`,
|
||||||
|
[category]
|
||||||
|
);
|
||||||
|
if (alreadyToday.length) {
|
||||||
|
await query('SELECT pg_advisory_unlock($1)', [lockKey]).catch(() => {});
|
||||||
|
console.log(`[Autogen] category=${category}: already generated today, skipping`);
|
||||||
|
return { ok: false, skipped: true, reason: 'already generated today' };
|
||||||
|
}
|
||||||
|
|
||||||
const { id: queueId, topic, tags, keywords } = await getNextTopic(category);
|
const { id: queueId, topic, tags, keywords } = await getNextTopic(category);
|
||||||
console.log(`[Autogen] category=${category} topic="${topic.slice(0, 60)}"`);
|
console.log(`[Autogen] category=${category} topic="${topic.slice(0, 60)}"`);
|
||||||
|
|
||||||
@@ -152,6 +169,8 @@ async function runAutogenForCategory(category) {
|
|||||||
await query(`UPDATE content_queue SET status='failed' WHERE id=$1`, [queueId]);
|
await query(`UPDATE content_queue SET status='failed' WHERE id=$1`, [queueId]);
|
||||||
}
|
}
|
||||||
return { ok: false, error: err.message };
|
return { ok: false, error: err.message };
|
||||||
|
} finally {
|
||||||
|
await query('SELECT pg_advisory_unlock($1)', [lockKey]).catch(() => {});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user