PHP
#bitrix#agent#cron#php#lock#flock#concurrency

Cron/Agent: шаблон lock-файла для защиты от параллельного запуска

Шаблон агента или cron-задачи с файловой блокировкой (flock) для предотвращения параллельного выполнения на одной ноде.

Как использовать

  1. Скопируйте нужный фрагмент кода.
  2. Вставьте в свой проект и при необходимости измените под задачу.
  3. Проверьте зависимости и окружение (версии, переменные).

Шаблон агента или cron-задачи с файловой блокировкой через flock() для защиты от параллельного запуска. Используйте для долгих задач, импортов, синхронизаций, которые не должны выполняться одновременно.

use Bitrix\Main\Application;

/**
 * Шаблон агента с файловой блокировкой
 * @return string Строка для повторного запуска агента
 */
function MyLongRunningAgent() {
    $lockFile = $_SERVER['DOCUMENT_ROOT'] . '/upload/locks/my_agent.lock';
    $lockHandle = @fopen($lockFile, 'c+'); // c+ - создаёт файл если нет
    
    if (!$lockHandle) {
        // Не удалось открыть файл, пропускаем выполнение
        return "MyLongRunningAgent();";
    }
    
    // Пытаемся получить эксклюзивную блокировку (non-blocking)
    // LOCK_EX - эксклюзивная блокировка
    // LOCK_NB - non-blocking (не ждём, если файл заблокирован)
    if (!flock($lockHandle, LOCK_EX | LOCK_NB)) {
        // Файл уже заблокирован другим процессом
        fclose($lockHandle);
        return "MyLongRunningAgent();"; // Пропускаем выполнение
    }
    
    // Записываем PID процесса в lock-файл (опционально, для отладки)
    ftruncate($lockHandle, 0);
    fwrite($lockHandle, getmypid());
    fflush($lockHandle);
    
    try {
        // ===== ВАША ЛОГИКА АГЕНТА =====
        
        // Пример: обработка элементов
        $processed = 0;
        $limit = 100;
        
        // Ваш код здесь
        // ...
        
        // ===== КОНЕЦ ЛОГИКИ =====
        
    } catch (\Exception $e) {
        // Логируем ошибку
        if (class_exists('SafeFileLogger')) {
            $logger = new SafeFileLogger('agents.log');
            $logger->error('Agent error', [
                'agent' => 'MyLongRunningAgent',
                'error' => $e->getMessage(),
                'trace' => $e->getTraceAsString()
            ]);
        }
    } finally {
        // ВАЖНО: всегда освобождаем блокировку в finally
        flock($lockHandle, LOCK_UN);
        fclose($lockHandle);
        // Опционально: удаляем lock-файл
        @unlink($lockFile);
    }
    
    // Возвращаем строку для повторного запуска
    return "MyLongRunningAgent();";
}

/**
 * Альтернатива: проверка через время последнего запуска
 */
function MyAgentWithTimeCheck() {
    $lockFile = $_SERVER['DOCUMENT_ROOT'] . '/upload/locks/my_agent_time.lock';
    $lockTimeout = 300; // 5 минут
    
    // Проверяем время последнего запуска
    if (file_exists($lockFile)) {
        $lastRun = filemtime($lockFile);
        if (time() - $lastRun < $lockTimeout) {
            // Ещё не прошло достаточно времени или выполняется
            return "MyAgentWithTimeCheck();";
        }
    }
    
    // Создаём/обновляем lock-файл
    touch($lockFile);
    
    try {
        // Ваша логика
        // ...
        
    } finally {
        // Обновляем время последнего запуска
        touch($lockFile);
    }
    
    return "MyAgentWithTimeCheck();";
}

Usage:

// Регистрация агента
CAgent::AddAgent(
    "MyLongRunningAgent();",
    "",
    "N",
    3600, // Интервал в секундах
    "",
    "Y",
    "",
    100
);

// Или через админку: Настройки -> Настройки продукта -> Агенты

Notes:

⚠️ flock() работает только на одной ноде сервера. Для кластеров используйте блокировку через БД (GET_LOCK в MySQL). Всегда освобождайте блокировку в finally блоке, чтобы она снялась даже при ошибке. Lock-файлы должны быть в /upload/locks/ с правами на запись. PID в lock-файле помогает отлаживать зависшие процессы.