Ошибка «Переполнение стека встроенного языка на сервере» — это классический индикатор возникновения бесконечной рекурсии в коде. В контексте платформы 1С:Предприятие это чаще всего означает, что при записи одного объекта (например, справочника «Номенклатура») срабатывает алгоритм, который инициирует запись другого объекта (например, связанного справочника или регистра), а тот, в свою очередь, снова пытается записать первый объект. Давайте подробно разберем, почему это происходит и как правильно разорвать этот замкнутый круг.
Рассмотрим ситуацию, описанную в теме, используя веб-инструмент для анализа зависимостей. Существуют два справочника: Номенклатура и БИТ_СТ_ГруппыНоменклатурыВКаталоге. Проблема кроется в их «взаимной вежливости», когда каждый из них считает своим долгом обновить соседа при собственном изменении:
Номенклатура при записи проверяются условия синхронизации с группой. Если данные расходятся, вызывается метод Записать() для объекта группы.БИТ_СТ_ГруппыНоменклатурыВКаталоге прописана логика: при записи группы нужно найти всю входящую в неё номенклатуру и обновить в ней определенные реквизиты, вызывая метод Записать() для каждой карточки номенклатуры.В итоге получается бесконечный цикл: Номенклатура записывает Группу — Группа записывает Номенклатуру — Номенклатура снова записывает Группу. Платформа 1С имеет жесткое ограничение на глубину вложенности вызовов (стек). Как только количество вложенных процедур превышает допустимый предел (обычно около 1000 уровней), сервер аварийно завершает поток, чтобы не допустить падения всей службы. Даже если в логах (которые можно собирать через tjclick) вы видите «17 вызовов», помните, что внутри каждой записи работают еще и типовые подписки на события, механизмы БСП и проверки, которые мгновенно забивают память стека.
Самый правильный и безопасный способ разорвать рекурсию — передать объекту «флаг», который сообщит ему, что запись уже инициирована программно и повторно обновлять связанные объекты не нужно. Для этого используется коллекция ДополнительныеСвойства. Проанализируем пошагово, как внедрить этот механизм.
Сначала изменим код в процедуре, которая выполняет обновление связанных объектов (в нашем примере это перезапись номенклатуры из группы):
Процедура ПерезаписатьНоменклатуруЗапретитьВыгрузкуНаWebSite(ЭлементНК)
Попытка
ТекущийОбъект = ЭлементНК.ПолучитьОбъект();
// Устанавливаем флаг, чтобы избежать рекурсии
ТекущийОбъект.ДополнительныеСвойства.Вставить("ЭтоТехническаяЗапись", Истина);
ТекущийОбъект.Записать();
Исключение
ОбщегоНазначения.СообщитьОбОшибке("Не удалось перезаписать номенклатуру: " + ТекущийОбъект.Наименование);
КонецПопытки;
КонецПроцедуры
Затем в модуле объекта справочника Номенклатура, в самом начале обработчика ПередЗаписью, добавим проверку этого флага:
Процедура ПередЗаписью(Отказ)
// Если флаг установлен, прерываем выполнение логики синхронизации
Если ДополнительныеСвойства.Свойство("ЭтоТехническаяЗапись") Тогда
Возврат;
КонецЕсли;
// Далее идет ваша обычная логика синхронизации с группой
Если ЗначениеЗаполнено(БИТ_СТ_ГруппаНомеклатурыВКаталоге) Тогда
// Проверка условий и вызов записи Группы...
КонецЕсли;
КонецПроцедуры
Важный момент: проверку флага необходимо ставить в обоих справочниках, участвующих в обмене, чтобы цикл мог быть разорван с любой стороны.
Существует более «радикальный» метод прерывания обработчиков — установка свойства ОбменДанными.Загрузка в значение Истина. Когда это свойство установлено, платформа игнорирует большинство проверок и событий в модулях объекта.
Выясним, как это выглядит в коде:
ТекущийОбъект = ЭлементНК.ПолучитьОбъект();
ТекущийОбъект.ОбменДанными.Загрузка = Истина;
ТекущийОбъект.Записать();
Однако будьте осторожны: при использовании этого метода не сработают не только ваши обработчики, но и типовые механизмы (например, регистрация изменений для обмена данными, заполнение связанных регистров или проверка обязательных полей). Рассмотрим этот вариант только в том случае, если вы уверены, что объекту не требуется выполнение никакой бизнес-логики при данной конкретной записи.
Если код не менялся, а ошибка начала проявляться только сегодня, проанализируем возможные внешние факторы:
Записать(), из-за чего лимит стека стал достигаться быстрее.Разберем еще один критический аспект кода из примера. Выполнение записи множества объектов номенклатуры (в примере их 17) внутри транзакции записи основного объекта (Группы) крайне нежелательно по двум причинам:
Записать() инициирует полноценный цикл работы с СУБД. Пользователь будет вынужден ждать окончания всей цепочки действий.Рекомендация: Если бизнес-логика позволяет, лучше выносить массовое обновление связанных объектов в Фоновое задание. Это позволит мгновенно вернуть управление пользователю, а система обновит зависимые данные в фоновом режиме.
Для устранения переполнения стека мы рекомендуем:
ДополнительныеСвойства для маркировки программных (технических) записей.ПередЗаписью и ПриЗаписи.Записать(): значения действительно должны отличаться, и после записи они должны стать равными, чтобы следующая проверка не инициировала повторную запись.