Идемпотентный агент с блокировкой (lock file/DB)
Шаблон агента или cron-задачи с защитой от параллельного запуска через файловую блокировку или DB lock.
Как использовать
- Файловый lock: создайте lock-файл, flock(LOCK_EX | LOCK_NB); при неудаче выйдите без выполнения; в finally — LOCK_UN и fclose.
- DB lock (кластер): SELECT GET_LOCK('name', 0) для non-blocking; в finally — RELEASE_LOCK('name').
- Регистрируйте агента через CAgent::AddAgent() или в админке (Настройки → Агенты).
Агенты и cron-задачи могут запускаться снова до завершения предыдущего запуска или выполняться на нескольких нодах кластера. Без блокировки одна и та же тяжёлая операция (импорт, синхронизация) выполняется параллельно — дублирование, гонки, ошибки целостности. Проблема: интервал агента меньше времени выполнения или несколько воркеров вызывают один агент. Симптомы: двойная обработка записей, конфликты в БД, высокая нагрузка. Ниже — шаблон с файловой блокировкой (flock) для одной ноды и блокировкой через MySQL GET_LOCK/RELEASE_LOCK для кластера; проверка и когда что использовать.
Решение
Защита агента от параллельного запуска через файловую блокировку или блокировку в БД. Используйте для долгих задач, которые не должны выполняться одновременно.
Файловая блокировка (одна нода):
use Bitrix\Main\Application;
function MyAgentTask() {
$lockFile = $_SERVER['DOCUMENT_ROOT'] . '/upload/locks/my_agent.lock';
$lockHandle = @fopen($lockFile, 'w');
if (!$lockHandle) {
return "MyAgentTask();";
}
if (!flock($lockHandle, LOCK_EX | LOCK_NB)) {
fclose($lockHandle);
return "MyAgentTask();";
}
try {
// Ваша логика агента
// ...
sleep(5);
} finally {
flock($lockHandle, LOCK_UN);
fclose($lockHandle);
@unlink($lockFile);
}
return "MyAgentTask();";
}
Блокировка через БД (кластер):
function MyAgentTaskWithDBLock() {
$connection = Application::getConnection();
$lockName = 'my_agent_lock';
$timeout = 0; // 0 = не ждать (non-blocking)
$result = $connection->query("SELECT GET_LOCK('{$lockName}', {$timeout}) as lock_acquired")->fetch();
if (!$result || !$result['lock_acquired']) {
return "MyAgentTaskWithDBLock();";
}
try {
// Ваша логика
// ...
} finally {
$connection->query("SELECT RELEASE_LOCK('{$lockName}')");
}
return "MyAgentTaskWithDBLock();";
}
Для кластера используйте DB lock (GET_LOCK): файловый lock виден только на одной ноде. Папка /upload/locks/ должна существовать и быть доступна для записи. Имя lock в БД должно быть уникальным для агента (например с префиксом модуля).
Проверка
-
Файловый lock — запустите агент вручную дважды подряд (или дождитесь двух срабатываний по расписанию). Второй запуск должен выйти по flock без выполнения тела (проверьте по логу или побочному эффекту). После завершения первого lock освобождается, следующий запуск выполнится.
-
DB lock — на одной ноде два процесса с одним именем lock: второй запрос GET_LOCK(…, 0) вернёт 0 (не получен). После RELEASE_LOCK в первом процессе второй сможет получить блокировку. В кластере проверьте с двух серверов с общей MySQL.
-
Папка для lock-файла — убедитесь, что
/upload/locks/создана и PHP может создавать там файлы (права, владелец). Иначе fopen вернёт false и агент будет каждый раз выходить без выполнения. -
RELEASE_LOCK в finally — при исключении в try блокировка должна сниматься. Убедитесь, что RELEASE_LOCK (и flock LOCK_UN) вызываются в finally.
Типичные ошибки
- Кластер и файловый lock — flock работает только в пределах одной ФС. На другой ноде свой lock-файл, блокировка не общая. Для нескольких серверов используйте только DB lock.
- GET_LOCK с большим timeout — второй параметр GET_LOCK — время ожидания в секундах. 0 — не ждать (сразу вернуть 0, если lock занят). Большое значение (например 300) — ждать до 5 минут; для агента обычно нужен 0, чтобы не блокировать очередь.
- Lock не снимается при падении — при фатальной ошибке или kill процесса finally может не выполниться. MySQL снимает GET_LOCK при разрыве соединения; файловый lock снимается ядром при закрытии дескриптора (завершение процесса). Зависший процесс — смотрите PID в lock-файле (если пишете его) и при необходимости завершайте вручную.
- Один lock на разных агентов — имена lock должны быть разными для разных задач, иначе один агент будет блокировать другой без необходимости.
Где применять
- Prod: агенты Bitrix (импорт, синхронизация, рассылки), cron-скрипты на одной ноде (файловый lock) или в кластере (DB lock).
- Dev: тот же шаблон для локального теста; убедитесь, что папка locks доступна для записи.
Связанные сниппеты: Cron/Agent: шаблон lock-файла, Безопасная ротация логов.