Cron/Agent: шаблон lock-файла для защиты от параллельного запуска
Шаблон агента или cron-задачи с файловой блокировкой (flock) для предотвращения параллельного выполнения на одной ноде.
Как использовать
- Скопируйте шаблон функции с flock(LOCK_EX | LOCK_NB), подставьте путь к lock-файлу (например /upload/locks/имя_агента.lock) и свою логику в try.
- В finally обязательно вызовите flock(LOCK_UN) и fclose(); при необходимости удалите lock-файл.
- Зарегистрируйте агента через CAgent::AddAgent() или в админке (Настройки → Агенты).
Агенты и cron-задачи в 1С-Битрикс могут вызываться чаще, чем успевает завершиться предыдущий запуск. Без блокировки два процесса выполняют одну и ту же тяжёлую операцию (импорт, синхронизация, рассылка) параллельно — дублирование данных, гонки, нагрузка на БД. Проблема: агент запускается по расписанию каждые N минут, а выполнение занимает больше N минут. Симптомы: двойная обработка одних и тех же записей, ошибки уникальности в БД, высокий CPU. Ниже — шаблон с файловой блокировкой через flock(): при занятой блокировке следующий запуск сразу выходит без выполнения; плюс вариант с проверкой по времени и команды для проверки.
Решение
Шаблон агента или 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();";
}
Регистрация агента:
// Регистрация агента
CAgent::AddAgent(
"MyLongRunningAgent();",
"",
"N",
3600, // Интервал в секундах
"",
"Y",
"",
100
);
// Или через админку: Настройки -> Настройки продукта -> Агенты
flock() действует только на одной ноде. Для кластера используйте блокировку через БД (например GET_LOCK в MySQL). Всегда снимайте блокировку в finally. Lock-файлы храните в каталоге с правами на запись (например /upload/locks/). PID в файле помогает отлаживать зависшие процессы.
Проверка
- Проверить, что lock-файл создаётся при запуске — после первого запуска агента (по расписанию или вручную через «Выполнить агента» в админке):
ls -la /path/to/site/upload/locks/
cat /path/to/site/upload/locks/my_agent.lock
В файле должен быть PID процесса. Пока агент выполняется, файл заблокирован.
-
Диагностика: второй запуск не выполняет логику — запустите агент вручную дважды подряд (или дождитесь двух срабатываний по cron). В логах или по побочному эффекту (например запись в БД) должен быть только один результат выполнения; второй запуск выходит по
flock(LOCK_EX | LOCK_NB)без выполнения тела. -
Проверка зависшего lock — если процесс упал без снятия блокировки, lock освободится при закрытии файла ядром. Если lock «завис» из-за долгого процесса, посмотрите PID в lock-файле:
ps -p PID(под Linux). При необходимости завершите процесс и удалите lock-файл вручную.
Типичные ошибки
- Блокировка не снимается при исключении — без
finallyпри выбросе исключения в try блокировка остаётся, следующие запуски всегда будут пропускать выполнение. Всегда вызывайтеflock($handle, LOCK_UN)иfclose($handle)вfinally. - Папка для lock-файлов недоступна для записи — создайте
/upload/locks/и выставите права (например 755 или 775 в зависимости от пользователя PHP). Иначеfopenвернёт false и агент будет каждый раз выходить без выполнения. - Кластер или несколько воркеров —
flock()на одном сервере не видна на другом. Для распределённой среды нужна блокировка в БД (GET_LOCK/RELEASE_LOCK в MySQL или аналог). См. Идемпотентный агент с блокировкой при необходимости блокировки через БД. - LOCK_NB не указан — без LOCK_NB вызов
flock(LOCK_EX)будет ждать освобождения файла. Для агента обычно нужен non-blocking режим: не ждать, а сразу выйти и запланировать следующий запуск.
Где применять
- Prod: агенты Bitrix (импорт, синхронизация с внешними системами, тяжёлые отчёты), cron-скрипты PHP на одной ноде.
- Dev: тот же шаблон для локального теста агентов; убедитесь, что путь к lock-файлу доступен для записи.
Связанные сниппеты: Идемпотентный агент с блокировкой, Безопасная ротация логов.