С чего все началось. После перехода на версию платформы 8.3.18.1208 х64 и релиз ЗУП 3.1.17.135 перестали отправляться расчетные листки сотрудникам одним скопом (получателей 250+). А именно, начали ловить ошибку "Превышен максимальный расход памяти сервера за один вызов" (когда сеанс "отъедает" всю доступную серверу 1С оперативную память). В нашем случае, при отправке расчеток, формировалось фоновое задание, которое и расходовало всю доступную оперативную память. Обновление версии ЗУП до 3.1.17.138 и "танцы" с настройками сервера 1С не дали результата, отправился искать отладчиком "узкое" место встроенного языка, где происходит увеличение памяти.
На скрине представлен фрагмент типового кода конфигурации ЗУП 3.1.17.138 , логика приведенного цикла в формировании и сохранении результата отчета "Анализ начислений и удержаний" (расчетный листок) для каждого сотрудника.
Судить о том, что данный фрагмент является "узким" местом встроенного языка не берусь, но именно в этом месте, на каждой итерации цикла наблюдалось увеличение потребляемой оперативной памяти. И как следствие - появление ошибки указанной выше.
Решение. При рассылке отчетов типовым алгоритмом предусмотрено: формирование одного фонового задания, которое обеспечивает формирование и отправку результатов отчетов всем получателям. Так как памяти не хватает обрабатывать всех получателей за одно фоновое задание. Появилась идея формировать для каждого получателя отдельное фоновое задание (контролируя статус завершения фонового задания) и при этом минимально изменяя типовой алгоритм, в надежде, что когда нибудь это исправят... Результатом идеи и успешного решения поставленной задачи стало написанное расширение конфигурации. Логику и фрагменты которого распишу ниже.
Логика:
1. В имеющемся общем модуле перехватим типовую процедуру по формированию фонового задания для всего массива получателей и передадим в параметр создания фонового задания первый элемент массива получателей, затем сохраним исходный массив получателей и УИД задания во временном хранилище и хранилище общих настроек соответственно.
2. Далее подключим обработчик ожидания, который будет повторять создание фонового задания, пока массив получателей не будет пустым.
3. Организуем проверку статуса выполнения фонового задания. Если завершен, тогда удаляем из массива получателей первый элемент и вновь формируем фоновое задание, иначе отключаем обработчик ожидания.
&ИзменениеИКонтроль("ВыбратьПолучателяЗавершение")
Процедура Расш5_ВыбратьПолучателяЗавершение(ВыбранныйЭлемент, ДополнительныеПараметры) Экспорт
Если ВыбранныйЭлемент = Неопределено Тогда
Результат = Неопределено;
Иначе
Если ДополнительныеПараметры.ВозвращатьСоответствие Тогда
Если ВыбранныйЭлемент.Значение = Неопределено Тогда
Результат = ДополнительныеПараметры.Получатели;
Иначе
Результат = Новый Соответствие;
Результат.Вставить(ВыбранныйЭлемент.Значение, ДополнительныеПараметры.Получатели[ВыбранныйЭлемент.Значение]);
КонецЕсли;
Иначе
Результат = Новый Структура("Получатель, ПочтовыйАдрес", ВыбранныйЭлемент.Значение, ДополнительныеПараметры.Получатели[ВыбранныйЭлемент.Значение]);
КонецЕсли;
КонецЕсли;
#Удаление
ВыполнитьОбработкуОповещения(ДополнительныеПараметры.ОбработчикРезультата, Результат);
#КонецУдаления
#Вставка
Если Строка(ДополнительныеПараметры.ОбработчикРезультата.ДополнительныеПараметры.МассивРассылок[0]) = "расчетный листок" Тогда
//Формируем массив соответствий получателей (по одному)
МассивСоответствийПолучателей = Новый Массив;
Для каждого Получатель из Результат Цикл
КопияПолучателей = Новый Соответствие;
КопияПолучателей.Вставить(Получатель.Ключ,Получатель.Значение);
МассивСоответствийПолучателей.Добавить(КопияПолучателей);
КонецЦикла;
//Сохраним наш массив получателей во временное хранилище, и сохраним адрес к этому значению в хранилище общих настроек
АдресМассиваПолучателейВременноеХранилище = ПоместитьВоВременноеХранилище(МассивСоответствийПолучателей, Новый УникальныйИдентификатор);
Расш5_ПроверкаФоновогоЗадания.СохранитьАдресПользователей(АдресМассиваПолучателейВременноеХранилище);
ДополнительныеПараметры.Получатели = МассивСоответствийПолучателей[0];
Результат = МассивСоответствийПолучателей[0];
ВыполнитьОбработкуОповещения(ДополнительныеПараметры.ОбработчикРезультата, Результат);
//Сохраним Параметры, а перед сохранением удалим из параметров саму форму
Расш5_ПроверкаФоновогоЗадания.СохранитьУидФормы(ДополнительныеПараметры.ОбработчикРезультата.ДополнительныеПараметры.Форма.УникальныйИдентификатор);
ДополнительныеПараметры.Удалить("ОбработчикРезультата");
АдресПараметровВременноеХранилище = ПоместитьВоВременноеХранилище(ДополнительныеПараметры, Новый УникальныйИдентификатор);
Расш5_ПроверкаФоновогоЗадания.СохранитьПараметры(АдресПараметровВременноеХранилище);
ОжиданиеОбработчика();
Иначе
ВыполнитьОбработкуОповещения(ДополнительныеПараметры.ОбработчикРезультата, Результат);
КонецЕсли;
#КонецВставки
КонецПроцедуры
В данном фрагменте типового общего модуля "РассылкаОтчетовКлиент" перехватим процедуру "ВыбратьПолучателяЗавершение". В которой "ВыполнитьОбработкуОповещения(ДополнительныеПараметры.ОбработчикРезультата, Результат)" дает условный старт формирования фонового задания, посему перед этой строкой мы сохраняем наш массив получателей во временное хранилище, а так же необходимые дополнительные параметры. (помещаем во временное хранилище обязательно с параметром "Новый УникальныйИдентификатор", чтобы наш массив получателей жил на протяжении всего сеанса пользователя).
Т.к. подключить обработчик ожидания мы не можем в текущем общем модуле, создадим и перейдем для этого в глобальный модуль "ОжиданиеОбработчика()". (подробнее в "Пример кода и описание пункта 2." ниже)
После выполнения "ВыполнитьОбработкуОповещения(ДополнительныеПараметры.ОбработчикРезультата, Результат)" и еще ряда процедур, исполнение кода переходит в процедуру "ВыполнитьСейчасВФоне", которую мы так же перехватим в расширении:
&ИзменениеИКонтроль("ВыполнитьСейчасВФоне")
Процедура Расш5_ВыполнитьСейчасВФоне(Получатели, Параметры) Экспорт
ПредварительныеНастройки = Неопределено;
Если Параметры.ЭтоФормаЭлемента Тогда
Если Параметры.Форма.Объект.ИспользоватьЭлектроннуюПочту Тогда
Если Получатели = Неопределено Тогда
Возврат;
КонецЕсли;
ПредварительныеНастройки = Новый Структура("Получатели", Получатели);
КонецЕсли;
ТекстСостояния = НСтр("ru = 'Выполняется рассылка отчетов.'");
Иначе
ТекстСостояния = НСтр("ru = 'Выполняются рассылки отчетов.'");
КонецЕсли;
ПараметрыМетода = Новый Структура;
ПараметрыМетода.Вставить("МассивРассылок", Параметры.МассивРассылок);
ПараметрыМетода.Вставить("ПредварительныеНастройки", ПредварительныеНастройки);
#Удаление
Задание = РассылкаОтчетовВызовСервера.ЗапуститьФоновоеЗадание(ПараметрыМетода, Параметры.Форма.УникальныйИдентификатор);
НастройкиОжидания = ДлительныеОперацииКлиент.ПараметрыОжидания(Параметры.Форма);
НастройкиОжидания.ВыводитьОкноОжидания = Истина;
НастройкиОжидания.ТекстСообщения = ТекстСостояния;
Обработчик = Новый ОписаниеОповещения("ВыполнитьСейчасВФонеЗавершение", ЭтотОбъект, Параметры);
ДлительныеОперацииКлиент.ОжидатьЗавершение(Задание, Обработчик, НастройкиОжидания);
#КонецУдаления
#Вставка
Если Строка(Параметры.МассивРассылок[0]) = "расчетный листок" Тогда
Задание = РассылкаОтчетовВызовСервера.ЗапуститьФоновоеЗадание(ПараметрыМетода, Параметры.Форма.УникальныйИдентификатор);
//Сохраним УИД задания для дальнейшего контроля статуса
Расш5_ПроверкаФоновогоЗадания.СохранитьУидЗадания(Задание.ИдентификаторЗадания);
НастройкиОжидания = ДлительныеОперацииКлиент.ПараметрыОжидания(Параметры.Форма);
НастройкиОжидания.ВыводитьОкноОжидания = Истина;
НастройкиОжидания.ТекстСообщения = ТекстСостояния;
Обработчик = Новый ОписаниеОповещения("ВыполнитьСейчасВФонеЗавершение", ЭтотОбъект, Параметры);
ДлительныеОперацииКлиент.ОжидатьЗавершение(Задание, Обработчик, НастройкиОжидания);
Иначе
Задание = РассылкаОтчетовВызовСервера.ЗапуститьФоновоеЗадание(ПараметрыМетода, Параметры.Форма.УникальныйИдентификатор);
НастройкиОжидания = ДлительныеОперацииКлиент.ПараметрыОжидания(Параметры.Форма);
НастройкиОжидания.ВыводитьОкноОжидания = Истина;
НастройкиОжидания.ТекстСообщения = ТекстСостояния;
Обработчик = Новый ОписаниеОповещения("ВыполнитьСейчасВФонеЗавершение", ЭтотОбъект, Параметры);
ДлительныеОперацииКлиент.ОжидатьЗавершение(Задание, Обработчик, НастройкиОжидания);
КонецЕсли;
#КонецВставки
КонецПроцедуры
В данном фрагменте проверяем, что текущая рассылка у нас по расчетным листкам, затем после формирования фонового задания сохраняем его УИД в хранилище общих настроек.
Описанный выше код прописываем в заимствованном в расширении типовом общем модуле "РассылкаОтчетовКлиент".
Процедура ОжиданиеОбработчика() Экспорт
//Подключим свой обработчик ожидания, который будет ждать статус завершения фонового задания
ПодключитьОбработчикОжидания("ОбработчикСтатусаВыполненияЗадания", 1, Ложь);
КонецПроцедуры
Данный фрагмент кода используется, чтобы организовать повторение действий по созданию фонового задания для каждого получателя отдельно.
Процедура ОбработчикСтатусаВыполненияЗадания() Экспорт
СостояниеЗадания = Расш5_ПроверкаФоновогоЗадания.ПолучитьПрогрессВыполнения();
Если СостояниеЗадания = "Задание выполнено" Тогда
//Если заданее запущенное ранее заврешено, то удаляем 1 получателя из общего массива получателей и запускаем новое
АдресМассива = Расш5_ПроверкаФоновогоЗадания.ПолучитьАдресПользователей();
МассивСоответствийПолучателей = ПолучитьИзВременногоХранилища(АдресМассива);
МассивСоответствийПолучателей.Удалить(0);
Если МассивСоответствийПолучателей.Количество() = 0 Тогда
ОтключитьОбработчикОжидания("ОбработчикСтатусаВыполненияЗадания");
Сообщить("Выполнение рассылки успешно завершено!");
ПоказатьОповещениеПользователя("Выполнение рассылки успешно завершено!");
Иначе
АдресПараметры = Расш5_ПроверкаФоновогоЗадания.ПолучитьПараметры();
ДополнительныеПараметрыОбщие = ПолучитьИзВременногоХранилища(АдресПараметры);
ДополнительныеПараметрыОбщие.Получатели = МассивСоответствийПолучателей[0];
Результат = МассивСоответствийПолучателей[0];
ИмяПроцедуры = "ВыполнитьСейчасВФоне";
Модуль = РассылкаОтчетовКлиент;
МассивРассылок = Новый Массив;
МассивРассылок.Добавить(Расш5_ПроверкаФоновогоЗадания.ПолучитьРассылку());
ДополнительныеПараметры = Новый Структура;
УИДФормы = Расш5_ПроверкаФоновогоЗадания.ПолучитьУидФормы();
ФормаРассылки = ПолучитьФорму("Справочник.РассылкиОтчетов.Форма.ФормаЭлемента",,,Новый УникальныйИдентификатор(УИДФормы));
ДополнительныеПараметры.Вставить("Форма", ФормаРассылки);
ДополнительныеПараметры.Вставить("ЭтоФормаЭлемента", Истина);
ДополнительныеПараметры.Вставить("МассивРассылок", МассивРассылок);
ОбработчикРезультата = Новый ОписаниеОповещения(ИмяПроцедуры, Модуль, ДополнительныеПараметры);
ДополнительныеПараметрыОбщие.Вставить("ОбработчикРезультата", ОбработчикРезультата);
ВыполнитьОбработкуОповещения(ДополнительныеПараметрыОбщие.ОбработчикРезультата, Результат);
//Сохраним наш массив получателей во временное хранилище
ПоместитьВоВременноеХранилище(МассивСоответствийПолучателей, АдресМассива);
КонецЕсли;
ИначеЕсли СостояниеЗадания = "Задание отменено пользователем" Тогда
//Если фоновое задание было отменено, то мы создаем новое задание для того же пользователя
АдресМассива = Расш5_ПроверкаФоновогоЗадания.ПолучитьАдресПользователей();
МассивСоответствийПолучателей = ПолучитьИзВременногоХранилища(АдресМассива);
АдресПараметры = Расш5_ПроверкаФоновогоЗадания.ПолучитьПараметры();
ДополнительныеПараметрыОбщие = ПолучитьИзВременногоХранилища(АдресПараметры);
ДополнительныеПараметрыОбщие.Получатели = МассивСоответствийПолучателей[0];
Результат = МассивСоответствийПолучателей[0];
ИмяПроцедуры = "ВыполнитьСейчасВФоне";
Модуль = РассылкаОтчетовКлиент;
МассивРассылок = Новый Массив;
МассивРассылок.Добавить(Расш5_ПроверкаФоновогоЗадания.ПолучитьРассылку());
ДополнительныеПараметры = Новый Структура;
УИДФормы = Расш5_ПроверкаФоновогоЗадания.ПолучитьУидФормы();
ФормаРассылки = ПолучитьФорму("Справочник.РассылкиОтчетов.Форма.ФормаЭлемента",,,Новый УникальныйИдентификатор(УИДФормы));
ДополнительныеПараметры.Вставить("Форма", ФормаРассылки);
ДополнительныеПараметры.Вставить("ЭтоФормаЭлемента", Истина);
ДополнительныеПараметры.Вставить("МассивРассылок", МассивРассылок);
ОбработчикРезультата = Новый ОписаниеОповещения(ИмяПроцедуры, Модуль, ДополнительныеПараметры);
ДополнительныеПараметрыОбщие.Вставить("ОбработчикРезультата", ОбработчикРезультата);
ВыполнитьОбработкуОповещения(ДополнительныеПараметрыОбщие.ОбработчикРезультата, Результат);
//Сохраним наш массив получателей во временное хранилище
ПоместитьВоВременноеХранилище(МассивСоответствийПолучателей, АдресМассива);
КонецЕсли;
КонецПроцедуры
Проверим завершено ли созданное нами ранее фоновое задание по одному получателю "Расш5_ПроверкаФоновогоЗадания.ПолучитьПрогрессВыполнения()". Если статус "Задание выполнено", тогда необходимо удалить из исходного массива получателей первый элемент (порядок элементов массива не меняется при "перегонке" его из хранилища в хранилище). Затем, если в массиве получателей еще есть элементы, создадим новое фоновое задание по аналогии фрагмента кода из пункта 1 логики, описанного выше.
Т.к. через примерно 40-50 итераций созданное фоновое задание иногда завершалось со статусом "Задание отменено пользователем"(не понятно почему), то заново формируем новое фоновое задание по тому же получателю.
Процедуры "ОжиданиеОбработчика()" и "ОбработчикСтатусаВыполненияЗадания()" помещаем в созаднный нами общий модуль "Расш5_ПодключениеОжидания" в расширении.
#Область сохранения
Процедура СохранитьАдресПользователей(АдресПользователей) Экспорт
ХранилищеОбщихНастроек.Сохранить("MMP", "AdrUser", АдресПользователей, , );
КонецПроцедуры
Процедура СохранитьУидЗадания(УидЗадания) Экспорт
ХранилищеОбщихНастроек.Сохранить("MMP", "AdrUid", УидЗадания, , );
КонецПроцедуры
Процедура СохранитьУидФормы(УидФормы) Экспорт
ХранилищеОбщихНастроек.Сохранить("MMP", "UidForm", УидФормы, , );
КонецПроцедуры
Процедура СохранитьПараметрыМетода(ПараметрыМетода) Экспорт
ХранилищеОбщихНастроек.Сохранить("MMP", "ParamMetod", ПараметрыМетода, , );
КонецПроцедуры
Процедура СохранитьПараметры(Параметры) Экспорт
ХранилищеОбщихНастроек.Сохранить("MMP", "Param", Параметры, , );
КонецПроцедуры
#КонецОбласти
#Область получения
Функция ПолучитьАдресПользователей() Экспорт
Возврат ХранилищеОбщихНастроек.Загрузить("MMP", "AdrUser", ,);
КонецФункции
Функция ПолучитьУидЗадания() Экспорт
Возврат ХранилищеОбщихНастроек.Загрузить("MMP", "AdrUid", ,);
КонецФункции
Функция ПолучитьПараметрыМетода() Экспорт
Возврат ХранилищеОбщихНастроек.Загрузить("MMP", "ParamMetod", ,);
КонецФункции
Функция ПолучитьПараметры() Экспорт
Возврат ХранилищеОбщихНастроек.Загрузить("MMP", "Param", ,);
КонецФункции
Функция ПолучитьУидФормы() Экспорт
Возврат ХранилищеОбщихНастроек.Загрузить("MMP", "UidForm", ,);
КонецФункции
Функция ПолучитьРассылку() Экспорт
Возврат Справочники.РассылкиОтчетов.НайтиПоНаименованию("расчетный листок",Истина);
КонецФункции
Функция ПолучитьПрогрессВыполнения() Экспорт
УникальныйИдентификатор = ХранилищеОбщихНастроек.Загрузить("MMP", "AdrUid", ,);
Отбор = Новый Структура;
Отбор.Вставить("УникальныйИдентификатор", УникальныйИдентификатор);
НайденныеЗадания = ФоновыеЗадания.ПолучитьФоновыеЗадания(Отбор);
Возврат Строка(НайденныеЗадания[0].Состояние);
КонецФункции
#КонецОбласти
Поместим в общий модуль "Расш5_ПроверкаФоновогоЗадания".
P.s. Расширение тестировалось на версии ЗУП 3.1.17.138. Надеюсь данный материал будет кому то полезен.