Ответ
Чтобы избежать дублирующего выполнения задач, нужно реализовать механизм блокировки на уровне базы данных. Я использовал паттерн "блокировка на уровне строк" с SELECT ... FOR UPDATE SKIP LOCKED (поддерживается в PostgreSQL 9.5+, MySQL 8.0+, MariaDB 10.3+).
Пример реализации на PHP с PDO и PostgreSQL:
// Воркер выполняет этот код для получения следующей задачи
$pdo->beginTransaction();
try {
// 1. Находим и блокируем первую незаблокированную задачу
$stmt = $pdo->prepare('
SELECT id, job_name, parameters
FROM jenkins_jobs_queue
WHERE status = :status_pending
AND scheduled_at <= NOW()
ORDER BY priority DESC, scheduled_at ASC
LIMIT 1
FOR UPDATE SKIP LOCKED
');
$stmt->execute([':status_pending' => 'pending']);
$job = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$job) {
$pdo->commit();
return null; // Нет задач для обработки
}
// 2. Немедленно обновляем статус, чтобы визуально отразить это в мониторинге
$updateStmt = $pdo->prepare('UPDATE jenkins_jobs_queue SET status = :status_processing WHERE id = :id');
$updateStmt->execute([
':status_processing' => 'processing',
':id' => $job['id']
]);
$pdo->commit(); // Освобождаем блокировку строки, транзакция завершена
// 3. ВНЕ транзакции: выполняем саму Jenkins Job
$this->runJenkinsJob($job['job_name'], $job['parameters']);
// 4. Отмечаем задачу как выполненную (можно в новой транзакции)
$this->markJobAsCompleted($job['id']);
} catch (Exception $e) {
$pdo->rollBack();
throw $e;
}
Ключевые моменты:
FOR UPDATEблокирует выбранные строки для других транзакций.SKIP LOCKEDкритически важен: если одна строка уже заблокирована другим воркером, этот запрос просто пропустит ее и возьмет следующую, избегая ожидания. Это предотвращает "очередь" из воркеров.- Вся операция "выборка-обновление" происходит в одной транзакции, что гарантирует атомарность.
- Длительные операции (запуск Jenkins) выполняются вне транзакции БД, чтобы не держать блокировки долго.