fix(autogen): атомарный захват темы — исключает дубли статей
Проблема: одна и та же тема из blog_topics использовалась несколько раз (см. 7 дублей в БД). Причина: getNextTopic читал is_used=false, возвращал тему, но помечал её использованной только ПОСЛЕ INSERT статьи (через 30-60 сек пока AI генерирует текст). За это время повторный запуск видел ту же is_used=false и выбирал её снова. Фикс: атомарная операция UPDATE...RETURNING с FOR UPDATE SKIP LOCKED — тема помечается is_used=true В МОМЕНТ ВЫБОРА, до вызова AI. Параллельные или повторные запуски видят is_used=true и пропускают тему. Дополнительно: fallback TOPIC_BANK теперь перемешивает пул случайно (вместо Math.random() на весь массив) — более равномерное распределение.
This commit is contained in:
+16
-4
@@ -70,9 +70,14 @@ async function getNextTopic(category) {
|
|||||||
return { id: rows[0].id, topic: rows[0].topic, tags: rows[0].tags || [], keywords: rows[0].keywords || [] };
|
return { id: rows[0].id, topic: rows[0].topic, tags: rows[0].tags || [], keywords: rows[0].keywords || [] };
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. DB-банк тем — неиспользованные
|
// 2. DB-банк тем — атомарно захватываем следующую свободную тему.
|
||||||
|
// FOR UPDATE SKIP LOCKED + немедленный UPDATE is_used=true устраняет race condition:
|
||||||
|
// параллельные генерации не могут выбрать одну и ту же тему.
|
||||||
const { rows: dbTopics } = await query(`
|
const { rows: dbTopics } = await query(`
|
||||||
SELECT bt.id, bt.topic FROM blog_topics bt
|
UPDATE blog_topics
|
||||||
|
SET is_used=true, used_at=NOW()
|
||||||
|
WHERE id = (
|
||||||
|
SELECT bt.id FROM blog_topics bt
|
||||||
WHERE bt.category = $1
|
WHERE bt.category = $1
|
||||||
AND bt.is_used = false
|
AND bt.is_used = false
|
||||||
AND NOT EXISTS (
|
AND NOT EXISTS (
|
||||||
@@ -81,13 +86,17 @@ async function getNextTopic(category) {
|
|||||||
)
|
)
|
||||||
ORDER BY bt.priority DESC, bt.created_at ASC
|
ORDER BY bt.priority DESC, bt.created_at ASC
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
|
FOR UPDATE SKIP LOCKED
|
||||||
|
)
|
||||||
|
RETURNING id, topic
|
||||||
`, [category]);
|
`, [category]);
|
||||||
|
|
||||||
if (dbTopics.length) {
|
if (dbTopics.length) {
|
||||||
return { id: null, topic: dbTopics[0].topic, tags: [], keywords: [], blog_topic_id: dbTopics[0].id };
|
return { id: null, topic: dbTopics[0].topic, tags: [], keywords: [], blog_topic_id: dbTopics[0].id };
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Fallback: хардкод если DB пустой
|
// 3. Fallback: хардкод если DB-банк пустой или все темы использованы.
|
||||||
|
// Проверяем использованные темы (всё время) чтобы не повторяться.
|
||||||
const bank = TOPIC_BANK[category] || TOPIC_BANK['ai-tools'];
|
const bank = TOPIC_BANK[category] || TOPIC_BANK['ai-tools'];
|
||||||
const { rows: usedTopics } = await query(
|
const { rows: usedTopics } = await query(
|
||||||
`SELECT source_topic FROM articles WHERE category=$1 AND source_topic IS NOT NULL`,
|
`SELECT source_topic FROM articles WHERE category=$1 AND source_topic IS NOT NULL`,
|
||||||
@@ -95,8 +104,11 @@ async function getNextTopic(category) {
|
|||||||
);
|
);
|
||||||
const usedSet = new Set(usedTopics.map(r => r.source_topic?.toLowerCase().trim()).filter(Boolean));
|
const usedSet = new Set(usedTopics.map(r => r.source_topic?.toLowerCase().trim()).filter(Boolean));
|
||||||
const unused = bank.filter(t => !usedSet.has(t.toLowerCase().trim()));
|
const unused = bank.filter(t => !usedSet.has(t.toLowerCase().trim()));
|
||||||
|
// Если все темы уже использованы — берём рандомную (лучше повтор чем пустой контент)
|
||||||
const pool = unused.length > 0 ? unused : bank;
|
const pool = unused.length > 0 ? unused : bank;
|
||||||
const topic = pool[Math.floor(Math.random() * pool.length)];
|
// Перемешиваем и берём первую (вместо случайного — детерминированно для одного запуска)
|
||||||
|
const shuffled = [...pool].sort(() => Math.random() - 0.5);
|
||||||
|
const topic = shuffled[0];
|
||||||
return { id: null, topic, tags: [], keywords: [] };
|
return { id: null, topic, tags: [], keywords: [] };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user