Как исправить ошибку «Переполнение стека встроенного языка на сервере» при записи номенклатуры?

Программист 1С v8.3 (Обычные формы) IT и автоматизация бизнеса
← На главную

Ошибка «Переполнение стека встроенного языка на сервере» — это классический индикатор возникновения бесконечной рекурсии в коде. В контексте платформы 1С:Предприятие это чаще всего означает, что при записи одного объекта (например, справочника «Номенклатура») срабатывает алгоритм, который инициирует запись другого объекта (например, связанного справочника или регистра), а тот, в свою очередь, снова пытается записать первый объект. Давайте подробно разберем, почему это происходит и как правильно разорвать этот замкнутый круг.

Проанализируем причину возникновения ошибки

Рассмотрим ситуацию, описанную в теме, используя веб-инструмент для анализа зависимостей. Существуют два справочника: Номенклатура и БИТ_СТ_ГруппыНоменклатурыВКаталоге. Проблема кроется в их «взаимной вежливости», когда каждый из них считает своим долгом обновить соседа при собственном изменении:

  1. В модуле объекта Номенклатура при записи проверяются условия синхронизации с группой. Если данные расходятся, вызывается метод Записать() для объекта группы.
  2. В модуле объекта БИТ_СТ_ГруппыНоменклатурыВКаталоге прописана логика: при записи группы нужно найти всю входящую в неё номенклатуру и обновить в ней определенные реквизиты, вызывая метод Записать() для каждой карточки номенклатуры.

В итоге получается бесконечный цикл: Номенклатура записывает Группу — Группа записывает Номенклатуру — Номенклатура снова записывает Группу. Платформа 1С имеет жесткое ограничение на глубину вложенности вызовов (стек). Как только количество вложенных процедур превышает допустимый предел (обычно около 1000 уровней), сервер аварийно завершает поток, чтобы не допустить падения всей службы. Даже если в логах (которые можно собирать через tjclick) вы видите «17 вызовов», помните, что внутри каждой записи работают еще и типовые подписки на события, механизмы БСП и проверки, которые мгновенно забивают память стека.

Решение №1: Использование механизма ДополнительныеСвойства

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

Сначала изменим код в процедуре, которая выполняет обновление связанных объектов (в нашем примере это перезапись номенклатуры из группы):


Процедура ПерезаписатьНоменклатуруЗапретитьВыгрузкуНаWebSite(ЭлементНК)
    Попытка
        ТекущийОбъект = ЭлементНК.ПолучитьОбъект();
        // Устанавливаем флаг, чтобы избежать рекурсии
        ТекущийОбъект.ДополнительныеСвойства.Вставить("ЭтоТехническаяЗапись", Истина);
        ТекущийОбъект.Записать();
    Исключение
        ОбщегоНазначения.СообщитьОбОшибке("Не удалось перезаписать номенклатуру: " + ТекущийОбъект.Наименование);
    КонецПопытки;
КонецПроцедуры

Затем в модуле объекта справочника Номенклатура, в самом начале обработчика ПередЗаписью, добавим проверку этого флага:


Процедура ПередЗаписью(Отказ)
    // Если флаг установлен, прерываем выполнение логики синхронизации
    Если ДополнительныеСвойства.Свойство("ЭтоТехническаяЗапись") Тогда
        Возврат;
    КонецЕсли;

    // Далее идет ваша обычная логика синхронизации с группой
    Если ЗначениеЗаполнено(БИТ_СТ_ГруппаНомеклатурыВКаталоге) Тогда
        // Проверка условий и вызов записи Группы...
    КонецЕсли;
КонецПроцедуры

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

Решение №2: Использование свойства ОбменДанными.Загрузка

Существует более «радикальный» метод прерывания обработчиков — установка свойства ОбменДанными.Загрузка в значение Истина. Когда это свойство установлено, платформа игнорирует большинство проверок и событий в модулях объекта.

Выясним, как это выглядит в коде:


ТекущийОбъект = ЭлементНК.ПолучитьОбъект();
ТекущийОбъект.ОбменДанными.Загрузка = Истина;
ТекущийОбъект.Записать();

Однако будьте осторожны: при использовании этого метода не сработают не только ваши обработчики, но и типовые механизмы (например, регистрация изменений для обмена данными, заполнение связанных регистров или проверка обязательных полей). Рассмотрим этот вариант только в том случае, если вы уверены, что объекту не требуется выполнение никакой бизнес-логики при данной конкретной записи.

Почему ошибка появилась «вдруг», если раньше всё работало?

Если код не менялся, а ошибка начала проявляться только сегодня, проанализируем возможные внешние факторы:

Проблемы транзакционных блокировок при записи в цикле

Разберем еще один критический аспект кода из примера. Выполнение записи множества объектов номенклатуры (в примере их 17) внутри транзакции записи основного объекта (Группы) крайне нежелательно по двум причинам:

  1. Длительные блокировки: Пока выполняется цикл записи 17 карточек, таблицы базы данных заблокированы. Другие пользователи не смогут в этот момент записывать номенклатуру, что приведет к ошибкам «Конфликт блокировок».
  2. Производительность: Каждая команда Записать() инициирует полноценный цикл работы с СУБД. Пользователь будет вынужден ждать окончания всей цепочки действий.

Рекомендация: Если бизнес-логика позволяет, лучше выносить массовое обновление связанных объектов в Фоновое задание. Это позволит мгновенно вернуть управление пользователю, а система обновит зависимые данные в фоновом режиме.

Итоговое резюме

Для устранения переполнения стека мы рекомендуем:

← На главную