Как гарантировать, чтобы каждую Jenkins Job из базы данных обрабатывал только один воркер?

«Как гарантировать, чтобы каждую Jenkins Job из базы данных обрабатывал только один воркер?» — вопрос из категории DevOps, который задают на 24% собеседований PHP Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Чтобы избежать дублирующего выполнения задач, нужно реализовать механизм блокировки на уровне базы данных. Я использовал паттерн "блокировка на уровне строк" с 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) выполняются вне транзакции БД, чтобы не держать блокировки долго.