Введение

Агенты - технология, позволяющая запускать произвольные PHP функции (агенты) с заданной периодичностью. Технически агент - это запись в специальной таблице состоящая из:

  • выполняемого кода,
  • даты выполнения,
  • периода выполнения,
  • каким способом назначать время следующего запуска агента (см. ‘Периодические и непериодические’).

Агенты и почтовые события работают на основе механизма “Фоновых заданий” и выполняются после выполнения страницы.

До версии 20.5.0 агенты выполнялись в самом начале загрузки каждой страницы (непосредственно перед открытием сессии и событием OnPageStart) система автоматически проверяет, есть ли агент, который нуждается в запуске, и исполняет его в случае необходимости.

И если “агенты” это технология, то что же физически из себя представляет агент? Это php-код, занимающий в ячейки таблицы не более 64Кбайт текста, который будет выполнен через eval-функцию языка один или некоторое количество раз. При выпонении агент обязан вернуть строковый результат: либо пустая строка (не продолжать выполнение), либо название следующего агента. И хотя технически в код агента можно записать циклы, запросы и т.д. не рекомендуется создавать агенты который состоят из чего-то большего чем вызов функции или статического метода класса.

Периодические и непериодические виды агентов

Условно все агенты можно разделить на две большие группы по характеру выполнения: “периодические” (“точно в указанное время”) и “непериодические” (“через заданный интервал”), хотя правильнее было бы сказать: “неповторяющиеся” и “повторяющиеся”. Исторически сложившиеся названия решили не менять.

Периодические агенты

Агенты выполняющиеся “точно в указанное время”, т.е. их интервал вычисляется по формуле:

Время назначенного следующего выполнения = Старое назначенное время агента + интервал

Например, раз в день запускается агент очистки старых почтовых событий или агент с ежедневным отчетом о посещаемости сайта.

Важное примечание: агенты выполняется строгое количество раз. Что это значит? Если агенты работают на хитах и с момента последнего хита было пропущено Н выполнений, то после подачи хитов на сайт, агент будет выполнен Н раз

Важное примечание: На проектах с низкой посещаемостью возможна ситуация, когда большое количество агентов может тормозить работу сайта. Монитор производительности показывает низкую (в районе 1) оценку. Статистика выполнения страниц показывает время генерации от 1 секунды и больше, а некоторые страницы вообще отказываются открываться. При таких симптомах посмотрите внимательнее статистику выполнения страницы. Если обнаружится что 90% времени генерации страницы занимает пролог сайта, то вероятно проблема в агентах. Достаточно 5-и агентов, у которых выставлено свойство Периодический. При выставлении этого свойства система при пропуске выполнения агента в следующие разы пытается компенсировать пропуски, что приводит к перегрузке сервера. Решение: Выключить периодичность агентов.

Непериодические агенты

Агенты выполняющиеся “через заданный интервал”, т.е. их интервал вычисляется по формуле:

Время назначенного следующего выполнения = Время завершения последнего запуска + интервал

Важное примечание: в отличие от периодического вида агентов в подобной ситуации агент поведет себя подругому - будет вызван только один раз.

Абсолютное большинство агентов непериодические.

Механизмы запуска агентов

В Битрикс24 существует несколько механизмов запуска агентов:

  • Выполнение всех агентов на хитах
  • Выполнение всех агентов на cron
  • Комбинированный способ: непериодические агенты на cron, периодические на хитах (по-умолчанию для Bitrix Env)

Почему существует несколько механизмов запуска? Битрикс24 разработан на Bitrix Framework, который имеет богатую историю и печальную славу “legacy”-кода. Механизм работы агентов на хитах был использован в тот момент, когда настройка cron была прирогативой разработчиков, а продукт позиционировался как максимально удобный для пользователя. Т.е. он должен был работать одинаково везде - и на хостинг-провайдерах и на VPS. Изначально такой независимой технологией и были - агенты, выполняемые на хитах.

Почему нельзя ультимативно перевести все на cron? Не смотря на наличие технической возможности выполнять все на cron, некоторые задачи могут требовать более частого выполнения, в то время как минимальный интервал крона - 1 минута. К тому же не стоит забывать, что все так же есть установки на разных платформах, где cron может быть закрыт для установки.

Подробнее о механизме запусков можно узнать в документации (см. ссылку в подвале “Запуск агентов”).

Применение

Использовать агенты удобно для выполнения любых операций связанных с повторением или фоновыми (не зависящими от пользователей) действиями. В Битриксе агенты имеют самое разнообразное применение:

  • CUser::CleanUpAgent(); - агент удаляющий пользователей с неподтвержденной регистрацией если прошло больше Н дней после регистрации из главного модуля
  • \Bitrix\Tasks\Internals\Counter\Agent::expiredSoon(6); - агент уведомляющий пользователя о скором окончании задач для пользователя ID:6 из модуля задач
  • CTicket::AutoClose(); - агент автоматический закрывающий задачи из модуля техподдержка
  • CCalendar::ReminderAgent(16, 1, 'https://some-portal.tech/company/personal/user/1/calendar/?EVENT_ID=16', 'user', 1, 0); - агент отправляющий нотификацию пользователю ID:1 по событию ID:16 (EVENT_ID)

Как видно - агенты достаточно распространенная и широкоприменя в продукте технология, однако как и любая другая технология она имеет свои ограничения.

Ограничения

Разработчик должен учитывать, что код который будет завернут в агент имееет достаточно много ограничений:

  1. Объекта пользователя ($USER) в агентах нет.

Поэтому любой код, который запрашивает должен проверяться на наличие опций отключающих проверку прав. Важно: В агентах нельзя проводить авторизацию методом Authorize, поскольку она может отправить cookie файлы авторизации случайному посетителю.

Примечание: даже наличие глобальной переменной $USER не является гарантией того, что в этой переменной находится объект класса CUser

  1. Отсутствует константа SITE_ID Агент может выполнять не только на страницах конкретного сайта, но и на хитах в административной панели и даже в cli-режиме (например на cron)

  2. На многоязычных сайтах нельзя заранее узнать какой будет язык.

  3. Агенты выполняются в однопоточном режиме с блокировкой на MySQL на 10 минут. Новый вызов агента возможен только после того, как отработает предыдущий вызов. Блокировка БД может потеряться если закроется соединение с ней. 10 минут ожидания это - дополнительная защита от повторного запуска, агентов которые не корректно отработали.

  4. В случае выполнения агентов на хитах временная точность запуска агентов напрямую зависит от равномерности и плотности посещаемости сайта. Реальное время запуска агента обычно чуть-чуть позже, чем время, на которое назначен агент (при равномерной посещаемости). Момент запуска - это когда кто-то зашел на страницу сайта. Если вам необходимо организовать запуск каких-либо PHP функций в абсолютно точно заданное время, то необходимо воспользоваться стандартной утилитой cron, предоставляемой большинством хостингов.

  5. Кроме этого, не рекомендуется вешать на агенты ресурсоёмкие операции, для них существует фоновый запуск по cron’у.

Выбор между агентами и cron

У опытных разработчиков обычно не возникает сомнений в выборе способа реализации той или иной бизнес-функции, однако есть общие рекомендации по выбору:

  1. Ресурсоемкие оперции (выполняющиеся 5+ секунд или требующие значительного объема оперативной памяти) следует выполнять отдельными cron-скриптами
  2. Если это будет использоваться в тиражируемом модуле, то выбор стоит отдать в пользу агентов.
  3. Во всех остальных случаях предпочтение стоит отдать агентам.

Да, последний пункт является “вкусовщиной” и нет единого мнения как стоит реализовывать тот или иной код. Лично я предпочитаю использовать cron-скрипты, вместо агентов и использую последних только в тиражируемых модулях или когда это выгоднее с точки зрения времени на написание.

API

Поскольку агенты это системный механизм у него присутствует 2 набора публичного api: верхнеуровневый и низкоуровневый, однако не смотря на наличие обоих API в доступности строго не рекомендуется использовать низкий уровень.

Создание и редактирование агента

В создании агента участвует 2 статических метода: высокоуровневый CAgent::AddAgent и низкоуровневый CAgent::Add.

Сигнатура высокоуровневного метода:

CAgent::AddAgent(
	$name, // PHP function name
	$module = "", // module
	$period = "N", // check for agent execution count in period of time
	$interval = 86400, // time interval between execution
	$datecheck = "", // first check for execution time
	$active = "Y", // is the agent active or not
	$next_exec = "", // first execution time
	$sort = 100, // order
	$user_id = false, // user
	$existError = true // return error, if agent already exist
);

Сигнатура низкоуровневого метода:

CAgent::Add($arFields);

Примечательным является тот факт, что метод AddAgent по факту выполняет обертку над методом Add, проверяя дубли по названию агента и пользователю, т.е. выполнение 2 и более раза метода \CAgent::AddAgent не создаст несколько агентов, а повторное выполнение вернет false, вместо идентификатора добавленного агента.

Предположим у нас есть собственный модуль “Технической поддержки” vendor.support, с агентом который закрывает все тикеты, на которые нет ответа более недели. Мы разработали идемпотентный код \Vendor\Support\Ticket::AutoClose(); и из консоли php разработчика он прекрасно работает. Теперь мы хотим создать агент который будет работать 24 часа для автозакрытия таких тикетов.

\CAgent::AddAgent(
    "\Vendor\Support\Ticket::AutoClose();", // имя функции
    "vendor.support",                       // идентификатор модуля
    "N",                                    // агент не критичен к кол-ву запусков
    86400,                                  // интервал запуска - 1 сутки
    "",                                     // дата первой проверки - текущее
    "Y",                                    // агент активен
    "",                                     // дата первого запуска - текущее
    30                                      // сортировка (по-умолчанию 100)
);

Важно отметить, что имя функции подается с точкой с запятой на конце. Это должен быть валидный php-код, который исполняется через eval

Что касается обновления агентов, здесь представлен только низкоуровневый метод CAgent::Update($agentId, $arFields), который аналогичен другим update методам старого ядра.

Удаление агентов

Удаление агентов представлено более широким ассортиментом методов: CAgent::RemoveAgent(), CAgent::Delete() и CAgent::RemoveModuleAgents().

В отличии от CAgent::AddAgent метод CAgent::RemoveAgent() не вызывает внутри себя метод CAgent::Delete, а является более широким по возможностям методом.

CAgent::RemoveAgent - удаление одного конкретного агента по имени, с возможностью дополнительного ограничения по модулю и пользователю.

Сигнатура метода:

\CAgent::RemoveAgent(
	$name,
	$module = "",
	$user_id = false
);

\CAgent::Delete($id) - удаление агента по его идентификатору в таблице b_agent

\CAgent::RemoveModuleAgents($module) - удаление всех агентов, привязанных к модулю $module.

Получение агентов

Методы для получения списка агентов традиционно для старого ядра осуществляются двумя статическими методами: \CAgent::GetById и \CAgent::GetList.

Поскольку метод \CAgent::GetById($ID) так же традиционно является прослойкой в \CAgent::GetList с фиксированным фильтром, то рассматривать его нет никакого смысла. Что же касается метода \CAgent::GetList, то он несколько отличается от техже методов элемента инфоблоков.

Сигнатура метода:

\CAgent::GetList($arOrder = Array("ID" => "DESC"), $arFilter = array());

Он имеет следующие особенности:

  1. На выход принимаются только 2 параметра: Порядок (Сортировка, $arOrder) и Фильтр ($arFilter). Указать постраничную навигацию, группировку или интересующие поля вы не сможете.
  2. В фильтре не работает сложная логика. Допустимые параметры фильтрации:
Код Пояснение
ID Строгое сравнение по идентификатору
ACTIVE Если значнение Y или N то строгое сравнение. Любое другое значение - игнорирование параметра фильтрации
IS_PERIOD Аналогично ACTIVE параметру
NAME LIKE-сравнение со значением
=NAME Строгое сравнение по названию
MODULE_ID Строгое сравнение по модулю
USER_ID Если больше 0 - поиск по пользователю, если любое другое значение - поиск агента с NULL-указанным пользователем
LAST_EXEC Дата (без времени) последнего выполнения агента
NEXT_EXEC Дата (без времени) следующего выполнения агента

Несмотря на схождесть LAST_EXEC и NEXT_EXEC, в многосайтовых и кластерных конфигурациях есть некоторое различие в парсинге аргументов. LAST_EXEC - принимает дату в формате языка (CLang::GetDateFormat()), в то время как NEXT_EXEC будет ожидать дату в формате определенной в константе FORMAT_DATETIME. В обычном случае это один и тот же формат даты, но в сложных конфигурациях это может сыграть злую шутку.

Важное примечание: если в фильтре какому-нибудь ключу будет соответствовать пустое строковое значение, то такой ключ фильтра будет проигнорирован. Точно так же, как если в ключ USER_ID на данный момент не обрабатывается.

Отладка агентов

Иногда возникает необходимость отладить какие-то сложные механизмы или алгоритмы и для этого существует debug-функция агентов.

  1. Определяем в проекте произвольную функцию с следующей сигнатурой:
function agentDebug($arAgent, $strEvent, $strEvalResult = null, $error = ""): void

Пояснение к аргументами:

  • $arAgent - данные агента из таблицы b_agent
  • $strEvent - код события, поясняющего момент вызова (start - инициализация, finish - успешное завершение, not_callable - неопределенное поведение)
  • $strEvalResult - результат выполнения агента
  • $error - Throwable-наследник.
  1. Определяем константу BX_AGENTS_LOG_FUNCTION с названием лог-функции
define('BX_AGENTS_LOG_FUNCTION', 'agentDebug');
  1. Любуемся результатом.

Важное примечание: в последних релизах (21 и 22) механизм отладки агентов - сломан. Если агент во время выполнения бросит исключение, то события с кодами finish и not_callable не будут вызваны! Вместо этого нужно смотреть Exception-лог из главного модуля.

Пример функции-логгирования для размещения в legacy.php (см. структуру проекта)

function agentDebug($arAgent, $strEvent, $strEvalResult = null, $error = "")
{
	\Bitrix\Main\Diag\Debug::dumpToFile(
		[
			'$arAgent'       => $arAgent,
			'$strEvent'      => $strEvent,
			'$strEvalResult' => $strEvalResult,
			'$error'         => $error,
		],
		"data",
		str_replace($_SERVER['DOCUMENT_ROOT'], "", __DIR__."/log.log")
	);
}
define('BX_AGENTS_LOG_FUNCTION', 'agentDebug');

Дополнительная информация

Агенты в курсе Bitrix Framework Агенты в Пользовательской документации Запуск агентов