Сервер 1С падает с переполнением стека: ищем бесконечную рекурсию

· 9 мин

Содержание

Переполнение стека встроенного языка почти всегда означает одно: какая-то процедура вызывает сама себя и не может остановиться. Настройки сервера тут вторичны. Увеличить «память» не получится: глубина вызовов ограничена платформой, и лечится ошибка в коде, а не в параметрах кластера.

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

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

Что такое стек вызовов и почему он кончается

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

Что важно знать:

  • 🔁 Ошибка возникает при глубокой цепочке вызовов, а не при долгой работе. Цикл на миллион итераций стек не переполнит, а рекурсия в несколько тысяч уровней — запросто.
  • 🧱 Лимит жёсткий. Объём оперативной памяти сервера на него не влияет.
  • 🖥 Серверный вызов обрывается только в рамках текущего сеанса. Рабочий процесс rphost при этом, как правило, продолжает жить, а вот пользователь теряет операцию.

Рекурсия сама по себе не порок. Обход дерева подразделений или номенклатуры рекурсией — нормальный приём. Проблема в рекурсии без условия выхода или с условием, которое не срабатывает на конкретных данных.

Как проявляется ошибка на сервере

Симптомы бывают разные, но у них общее: сбой воспроизводится на одной и той же операции.

  • ❌ Проведение или запись конкретного документа заканчивается ошибкой, остальные документы того же вида проходят.
  • ❌ Отчёт падает при определённых параметрах или периоде.
  • ❌ Регламентное задание завершается с ошибкой, а в списке заданий видна запись со статусом ошибки.
  • ❌ После обновления конфигурации или подключения расширения ошибка появилась там, где её раньше не было.
  • ❌ Обмен данными зависает на одном объекте и повторяет сбой при каждой попытке.

Отличите это от других сбоев. Если падает всё и у всех, ищите проблему в самом сервере или базе. Если ошибка привязана к одному объекту или действию, это почти наверняка рекурсия в прикладном коде.

ПризнакЧто это значит
Падает один документ или один отчётРекурсия на конкретных данных
Ошибка появилась после обновления или расширенияНовый код вступил в конфликт со старым
Ошибка у всех пользователей на любой операцииНе рекурсия; смотрите состояние кластера и базы
Ошибка только на сервере, в файловой базе нетДругой путь выполнения: серверный модуль или фоновое задание

Где в коде живёт бесконечная рекурсия

Прямую рекурсию (процедура вызывает сама себя) видно сразу. Тяжелее с косвенной, когда цепочка идёт через несколько модулей. Типичные сценарии:

Подписка на событие, которая меняет тот же объект

Самый частый случай. Подписка на ПриЗаписи или ПередЗаписью получает объект, что-то в нём правит и снова вызывает Записать(). Запись вызывает то же событие, подписка срабатывает снова — и так до переполнения.

Проверка простая: если в обработчике есть Объект.Записать() или Документ.Записать() для того же самого объекта, это подозреваемый номер один.

Две подписки, которые перезаписывают друг друга

Первая подписка меняет документ A и пишет его. Запись A запускает вторую подписку, которая пишет связанный документ B. Запись B запускает первую подписку на тот же A. Круг замкнулся.

Рекурсивный обход иерархии с циклической ссылкой

Процедура обходит родителей элемента справочника: берёт Родитель, затем его родителя и так далее. Если в данных элемент оказался собственным предком (прямо или через цепочку), конец дерева не наступает никогда.

Условие выхода, которое не срабатывает

Выход написан как Если Значение = 0, а на реальных данных приходит Неопределено или пустая ссылка. Условие ложно всегда, рекурсия бесконечна.

Переопределённый метод в расширении

Расширение перехватывает процедуру через &Вместо и в теле вызывает её же без ПродолжитьВызов(), а напрямую. Либо два расширения перехватывают одну процедуру и вызывают друг друга.

Как найти место, где крутится рекурсия

Идём от безопасного способа к более трудоёмким.

Способ 1. Текст ошибки и журнал регистрации

Откройте журнал регистрации: Администрирование → Обслуживание → Журнал регистрации. Отфильтруйте по уровню «Ошибка» и времени сбоя. Подробное представление ошибки обычно содержит имя модуля и строку, с которой начался вызов. Этого иногда хватает, чтобы сразу увидеть процедуру.

Способ 2. Технологический журнал

Журнал регистрации показывает конец цепочки, а техжурнал — полный стек. Для этого в каталоге конфигурации сервера создаётся файл logging.cfg (каталог conf рядом с установкой платформы) с настройкой на исключения:

<?xml version="1.0" encoding="UTF-8"?>
<config xmlns="http://v8.1c.ru/v8/tech-log">
  <log location="C:\techlog\stack" history="24">
    <event>
      <eq property="name" value="EXCP"/>
    </event>
    <property name="all"/>
  </log>
</config>

Каталог C:\techlog\stack должен существовать, а у учётной записи, под которой работает сервер, должны быть права на запись. Воспроизведите ошибку и откройте свежие файлы журнала: в записи EXCP будет текст исключения и контекст — последовательность вызовов.

Техжурнал с <property name="all"/> быстро разрастается. Включайте его на время воспроизведения и отключайте после. Следите за свободным местом на диске, где лежит каталог журнала.

В контексте ищите повторяющийся фрагмент: одна и та же пара «модуль : процедура» идёт десятки раз подряд. Это и есть петля.

Способ 3. Отладка в Конфигураторе

Если можно воспроизвести на тестовой копии базы:

  1. Откройте Конфигуратор и включите отладку для нужного сеанса (меню «Отладка»).
  2. Задайте остановку по ошибке: в параметрах отладки включите остановку при возникновении ошибки (пункт про «Остановку по ошибке» в меню «Отладка»).
  3. Запустите операцию в режиме «Предприятие» с отладкой.
  4. Когда отладчик остановится, откройте окно «Стек вызовов» и прокрутите вверх: повторяющиеся строки покажут петлю.

Отлаживать удобнее на копии: на рабочей базе остановка точек останова на сервере блокирует работу других пользователей.

Способ 4. Поиск по конфигурации

Если место примерно ясно, откройте конфигурацию и воспользуйтесь поиском по всей конфигурации (меню «Правка» или «Конфигурация → Поиск по конфигурации»). Ищите .Записать( в модулях объектов и подписок, на которые падает ошибка.

Диагностика: выберите симптом

Что делать, если ошибка уже случилась

Порядок действий, чтобы вернуть работу и не потерять данные:

  1. Зафиксируйте контекст: кто, какая операция, какой объект, во сколько. Это сэкономит часы в техжурнале.
  2. Определите, что ломается: один объект или все. Если один, остальных пользователей можно не трогать.
  3. Временно исключите причину: отключите подозрительную подписку, расширение или регламентное задание. На рабочей базе делайте это через согласование, а перед любыми изменениями сделайте резервную копию.
  4. Верните данные в работоспособное состояние: если дело в циклической ссылке в справочнике, поправьте родителя вручную или обработкой.
  5. Исправьте код и проверьте на копии на тех же данных, на которых падало.

Не лечите ошибку перезапуском службы сервера. Операция упрётся в тот же лимит сразу после повторного запуска, а остальные пользователи потеряют сеансы зря.

Как исправить код

Выбор зависит от причины.

ПричинаИсправление
Повторная запись объекта в обработчике его же записиПравьте реквизиты прямо в ПередЗаписью, без вызова Записать(). Если запись всё же нужна, поставьте флаг в ДополнительныеСвойства и выходите из обработчика, когда флаг стоит
Две подписки зацикленыРазорвите круг флагом в ДополнительныеСвойства или объедините логику в одну подписку
Обход иерархииНакапливайте уже пройденные ссылки в массиве или соответствии и прерывайтесь на повторе
Условие выхода не срабатываетПроверяйте на Неопределено и пустую ссылку, а не только на ноль
РасширениеИспользуйте ПродолжитьВызов() вместо прямого повторного вызова

Пример защиты от зацикливания через дополнительные свойства:

Процедура ПриЗаписиОбработчик(Источник, Отказ)

    Если Источник.ДополнительныеСвойства.Свойство("ПропуститьПересчет") Тогда
        Возврат;
    КонецЕсли;

    Источник.ДополнительныеСвойства.Вставить("ПропуститьПересчет", Истина);
    // ... изменения и повторная запись ...

КонецПроцедуры

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

Если рекурсия нужна по смыслу (обход глубокого дерева), перепишите её в цикл с собственным стеком: массив ссылок, из которого берём элементы, пока он не пуст. Такой вариант не упирается в лимит платформы и работает быстрее.

Серверные настройки: что реально можно сделать

Лимит глубины стека настройками кластера не меняется, но есть смежные вещи, которые помогают пережить сбой.

  • 🛡 Перезапуск рабочих процессов. В свойствах кластера задаются интервал перезапуска и допустимый объём памяти процессов. Это защищает от накопления мусора, но рекурсию не лечит.
  • 📊 Мониторинг. Следите за ростом памяти процесса rphost на одной и той же операции: бесконечная рекурсия сопровождается резким скачком.
  • 🧾 Регламентные задания. Для заданий, которые падают с этой ошибкой, включите уведомление о сбое, чтобы не узнавать о нём от пользователей.

Если клиент-серверная база работает на старой платформе, проверьте номер релиза платформы в справке «О программе» и сверьтесь с описанием исправлений в нотах релиза: часть ошибок поведения на сервере исправляют обновлением. Но сначала исключите свой код.

Профилактика

Рекурсию проще не допускать, чем искать.

Перед выпуском доработки
Выполнено: 0 / 6

Что ещё помогает на практике:

  • 🧪 Тестируйте на копии рабочей базы. На чистой демонстрационной базе циклических данных нет, и рекурсия там не проявится.
  • 🔍 На код-ревью смотрите отдельно любые вызовы Записать() внутри подписок.
  • 📝 Описывайте в комментарии, какие подписки срабатывают на один и тот же объект. Через год никто не вспомнит, что одна из них зависит от другой.

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

Можно ли увеличить глубину стека в настройках сервера?
Нет. Лимит задан платформой и в консоли кластера не настраивается. Ошибку устраняют правкой кода.
Почему ошибка есть на сервере, а в файловой базе нет?
В разных режимах код выполняется по разным путям: часть логики идёт в серверных модулях, часть в клиентских. Рекурсия может существовать только в серверной ветке.
Это ошибка платформы или нашей конфигурации?
В подавляющем большинстве случаев причина в прикладном коде: типовом, доработанном или в расширении. Для проверки отключите расширения и сравните поведение на чистой типовой конфигурации.
Поможет ли перезапуск сервера?
Нет. Операция снова упрётся в то же условие, потому что дело в логике, а не в состоянии процесса.
Как понять, какая именно процедура зацикливается?
В технологическом журнале по событию EXCP найдите повторяющуюся пару «модуль : процедура» в стеке. Либо остановите отладчик по ошибке и посмотрите стек вызовов.