Перейти к содержимому

Обработчик меняет данные - рекурсия, флаг, массовые операции

Пишем обработчик, который сам меняет запись: защита от рекурсии, поведение при массовых операциях и вынос тяжёлой работы из обработчика.

Что нужно знать заранее

Запись внутри обработчика события поднимает то же самое событие ещё раз. Обработчик входит в себя повторно, и без защиты это заканчивается ошибкой сервера или зависанием запроса.

Флаг блокировки хранят в статическом свойстве класса обработчика. Он сбрасывается в конце работы, и повторный вход в тот же обработчик на этом же запросе просто ничего не делает.

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

Шаги

  1. Решить, действительно ли правку нужно делать внутри обработчика события.
  2. Поставить флаг блокировки в начале обработчика и снять его в самом конце.
  3. Для массовых операций хранить флаг по идентификатору записи, а не общим значением.
  4. Вынести тяжёлую работу из обработчика в фоновое задание или в очередь.
  5. Проверить сценарий на массовой правке из административного раздела и при обмене.

Решение

Ставим флаг блокировки в обработчике:

class ElementHandler
{
private static array $busy = []; // флаг по идентификаторам записей
public static function onAfterUpdate(array &$fields): void
{
$id = (int)$fields['ID'];
if (isset(self::$busy[$id])) { return; } // повторный вход прерываем сразу
self::$busy[$id] = true;
\CIBlockElement::SetPropertyValuesEx($id, false, ['SLUG' => makeSlug($fields['NAME'])]);
unset(self::$busy[$id]); // снимаем флаг в конце работы обработчика
}
}

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

Меняем поля без повторного сохранения:

public static function onBeforeUpdate(array &$fields): void
{
$fields['CODE'] = mb_strtolower($fields['CODE'] ?? ''); // правка до записи
}
// изменение массива по ссылке до сохранения не поднимает событие заново

Правка полей до сохранения - самый дешёвый способ вообще избежать рекурсии. Событие до записи получает массив по ссылке, и изменённые значения уходят в базу вместе с остальными.

Выносим тяжёлую работу в фон:

public static function onAfterAdd(array $fields): void
{
\Bitrix\Main\Application::getInstance()->addBackgroundJob(
static fn() => (new Indexer())->rebuild((int)$fields['ID'])); // после ответа
}

Фоновое выполнение снимает сразу две проблемы: скорость ответа и риск рекурсии. Работа идёт после отдачи страницы, а повторные события внутри неё уже не мешают исходной операции.

Ловим зацикливание при разборе:

public static function onAfterUpdate(array &$fields): void
{
\Bitrix\Main\Diag\Debug::writeToFile([$fields['ID'], count(debug_backtrace())],
date('H:i:s.u'), 'handler.log'); // растущая глубина стека выдаёт рекурсию
}

Растущая глубина стека в журнале - самый быстрый признак зацикливания. Строки идут с одинаковым идентификатором записи и всё большей глубиной, пока запрос не упирается в предел.

Проверяем сценарий массовой правки:

foreach ([101, 102, 103] as $id) {
(new \CIBlockElement())->Update($id, ['NAME' => 'Товар ' . $id]);
}
// проверка повторяет групповую операцию администратора и обмен с учётной системой

Типичные проблемы

Сохранение элемента заканчивается ошибкой сервера.

Обработчик события сам вызывает запись и входит в себя повторно без ограничений. Повторный вход прерывают статическим флагом блокировки в начале обработчика.

Групповая правка обрабатывает не все записи.

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

Сохранение записи стало заметно медленным.

В обработчике выполняется тяжёлая работа вроде переиндексации или запроса наружу. Такую работу выносят в фоновое задание после отдачи ответа.

Правка полей в обработчике не сохраняется.

Массив полей изменён в событии после записи, когда данные уже ушли в базу. Поля правят в событии до сохранения, изменяя массив по ссылке.

Обмен с учётной системой стал падать по времени.

Каждая запись обмена запускает цепочку обработчиков со своими записями в базу. На время обмена такие обработчики отключают или переводят в фоновый режим.

Частые вопросы

Почему запись внутри обработчика опасна?

Она поднимает то же событие ещё раз и приводит к повторному входу в обработчик. Без флага блокировки это заканчивается ошибкой сервера или зависанием запроса.

Где хранить флаг блокировки?

В статическом свойстве класса обработчика: он живёт ровно один запрос. Хранилища и настройки для этого не нужны и только замедляют работу.

Как отключить обработчики на время обмена?

Проверять в обработчике признак режима обмена и выходить сразу. Признак ставят в начале обмена и снимают после его завершения.

Можно ли менять чужие записи в обработчике?

Можно, и рекурсии там обычно нет: событие поднимется для другой записи. Следят за объёмом: правка сотни связанных записей в обработчике замедляет сохранение.

Что лучше: событие до записи или после?

Правка полей самой записи - до записи, действия с другими данными - после. Событие до записи умеет ещё и отменить операцию с сообщением об ошибке.

Смежное

Первоисточник