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:
Aleksei Pavlov
2026-06-21 21:30:06 +03:00
parent 630287f02f
commit 1ced06fa2d
2 changed files with 40 additions and 0 deletions
+19
View File
@@ -116,6 +116,23 @@ async function getNextTopic(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);
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]);
}
return { ok: false, error: err.message };
} finally {
await query('SELECT pg_advisory_unlock($1)', [lockKey]).catch(() => {});
}
}