1. Введение
6. Проверка перезапуска рабочего процесса
1. Введение
С выходом версии 8.3.26 очень порадовала новая функциональность 1С Предприятия, а именно подсистема УведомленияКлиента. Она позволяет невозможное ранее (кроме счастливых обладателей Системы взаимодействия, конечно) - отправлять любую информацию с сервера на клиент независимо от источника вызова. То есть если ранее сервер отправлял информацию на клиента, это однозначно был вызов сервера с клиента и возврат управления. Теперь нам может прийти вызов http-сервиса или регламентного задания, который отправит уведомление на обычный клиентский сеанс, например, о том, что поступил заказ - обработай или произошла ошибка - вот подробности, при этом информация может быть практически любой, лишь бы ее можно было прочитать на клиенте!
Конечно, такое не может не радовать, сразу же начал внедрять при переходе на новую версию и спустя пару месяцев наткнулся на первые проблемы: допустим, есть клиентский сеанс, который обрабатывает вызовы с http-сервиса (потому что нужное COM-соединение доступно только на выделенном клиенте). Иногда при сбоях в работе 1С он перезапускается, есть специальная проверка. Так вот этот сеанс в какие-то моменты, обычно после ночных регламентных заданий, переставал принимать уведомления клиента, несмотря на имеющуюся подписку. Причем не важно, давно ли был последний перезапуск или только вчера вечером. Пришлось добавлять новую проверку, а также отправлять уведомление только в один сеанс, а не во все как раньше, для снижения нагрузки. Проверка сработала два раза за одну ночь и более не выдавала себя, но как говорится ложки нашлись, а осадочек остался. Найти баг на простых примерах не удалось, оставлял на другом сервере сеанс на ночь - все стабильно, решил проверить, а можно ли найти условия, в которых сервис уведомлений клиента перестанет работать стабильно? А?
2. Краткий экскурс в функционал
Но для начала о том, как вообще пользоваться новыми уведомлениями. Суть их в том, что мы можем из любого, повторюсь, любого серверного вызова отправить информацию на клиента. Для того, чтобы получать уведомления нужно подписаться на них, причем с тем же ключом, под которым будем отправлять. Для этого подойдет код:
...
Оповещение = Новый ОписаниеОповещения("ПодпискаНаСобытияПолучение", ЭтотОбъект);
УведомленияКлиента.ПодключитьОбработчик("ПодпискаНаСобытия", Оповещение);
...
&НаКлиенте
Процедура ПодпискаНаСобытияПолучение(Результат, ДополнительныеПараметры) Экспорт
Если Результат = Неопределено Тогда
Возврат;
КонецЕсли;
ЗаполнитьЗначенияСвойств(События.Добавить(), Результат);
КонецПроцедуры
То есть используется обычное описание оповещения, где первый аргумент - это имя экспортной клиентской функции, а второй - клиентский модуль либо как в нашем случае ЭтотОбъект, то есть форма, в которой и расположена функция. Третьим, как всегда, могли быть дополнительные параметры, которые потом в неизменном виде перейдут в принимающую уведомления функцию. Функция же в данном случае простая - она принимает структуру и заводит ее новой записью в таблицу.
Для подписки на уведомления, как вы уже догадались, служит специальная функция УведомленияКлиента.ПодключитьОбработчик с ключом "ПодпискаНаСобытия", а для отправки с сервера будет УведомленияКлиента.ОтправитьУведомление с тем же ключом:
ДанныеКОтправке = Новый Структура("ВремяСобытия, Сообщение, Продолжительность", ТекущаяДатаСеанса(),
"Слишком долгое проведение документа", ТекущаяУниверсальнаяДатаВМиллисекундах() - НачалоЗамера);
ДанныеКОтправке.Вставить("Счетчик", Товары.Количество());
ДанныеКОтправке.Вставить("ТипСчетчика", "Количество товаров");
ДанныеКОтправке.Вставить("Объект", Ссылка);
УведомленияКлиента.ОтправитьУведомление("ПодпискаНаСобытия", ДанныеКОтправке,
УведомленияКлиентаВызовСервера.ПолучитьПодписки("ПодпискаНаСобытия"));
Уже на этом примере прорисовывается основное преимущество уведомлений клиента: набор допустимых данных, передаваемых вторым параметром, очень широк. Даже если мы не можем отправить таблицу значений, мы можем ее сериализовать или преобразовать в массив структур. То же и с другими типами. В случае с подчиненным фоновым заданием что-то можно просто перенаправить через временное хранилище, туда вообще можно поместить все, что сериализуется. Третьим параметром не идет ничего, либо массив номеров сеансов, который в данном случае получается специальной функцией.
Не могу не отметить, что подписка осуществляется на клиенте, а отправление происходит с сервера (но можно и с толстого клиента), причем вызов сервера с клиента также подходит, то есть можно нажать кнопку и отправить уведомление, ничего не помешает, но подозреваю, что основной сценарий использования предполагает серверные вызовы не с формы. Особенно удобно отправлять уведомления из фоновых заданий, потому что там для определения номера сеанса легко использовать свойство НомерРодительскогоСеанса текущего фонового задания:
ТекущийСеанс = ПолучитьТекущийСеансИнформационнойБазы();
Задание = ТекущийСеанс.ПолучитьФоновоеЗадание();
Если Задание = Неопределено Тогда
СеансыКОтправке = Неопределено;
Иначе
СеансыКОтправке = ОбщегоНазначенияКлиентСервер.ЗначениеВМассиве(Задание.НомерРодительскогоСеанса);
КонецЕсли;
В остальных сценариях нужно понимать, что уведомление получат все сеансы, подписанные по данному ключу, если вы не передали третьим параметром номера сеансов. Вопрос только в том, затратнее ли будет отправка при большом количестве сеансов, если список адресатов не задан.
Ну и, как водится, если можно на событие подписаться, должна быть возможность и отписаться. Для этого служит третья функция подсистемы ОтключитьОбработчик, куда мы просто передаем тот самый ключ, который мне почему-то хочется назвать каналом, по аналогии с чатами, где мы также подписываемся и получаем разнородную информацию.
3. Проверка на скорость
Ну что же, кратко суть подсистемы изложил - перейдем к опытам. Для начала проверим простую мысль, а что если отправить очень большое количество уведомлений за короткий промежуток. Все ли дойдут и в том же порядке? Разместим на форме таблицу ПроверкаСкорости и будем в нее записывать результаты замеров, в которых отправим в фоновое задание 10000 Уникальных Идентификаторов строкой (почему строкой - УведомленияКлиента не могут отправить уникальный идентификатор в чистом виде, выходит ошибка несоответствия типа). Затем из этого фонового задания будем отправлять УИД в том же порядке вместе с индексом, чтобы проверить будут ли переданы все идентификаторы и в том ли порядке, в котором отправлены. Сразу уточню, что проверки осуществлялись на миниПК с i5 и 32ГБ на борту, впрочем замеры на рабочем тестовом показали то же.

Как видно на рисунке, при использовании отправки в сеанс уведомления приходят быстрее, причем это заметно даже при небольшом количестве сеансов. А что такое с отображением? В ходе экспериментов выяснилось, что клиент-серверный вариант отправки уведомлений зависит от загруженности формы отображаемыми элементами. В файловом варианте такого нет:

Причем видно, что количество сеансов влияет на скорость как сеансовых отправлений, так и широковещательных. Но это на файловой, а что в клиент-серверной:
Более 100 сеансов
Для 10 продолжительность во все сеансы составляет: 719
Для 10 продолжительность в сеанс составляет: 317
Для 100 продолжительность во все сеансы составляет: 7 199
Для 100 продолжительность в сеанс составляет: 2 91620 сеансов
Для 10 продолжительность во все сеансы составляет: 249
Для 10 продолжительность в сеанс составляет: 215
Для 100 продолжительность во все сеансы составляет: 2 800
Для 100 продолжительность в сеанс составляет: 2 353
Как видно, даже на более чем ста сеансах среднее время отправки в сеанс составляет 30 мс, а во все - 70, что конечно на порядки выше 1 мс при 1-3 сеансах, но незначительно больше чем при 20. Отсюда делаем однозначный вывод: количество сеансов важно! И лучше отправлять в сеанс при большом количестве сеансов.
Ну хорошо, видно, что скорость отправки достаточно высока, а что если обработка будет занимать продолжительное время: добавим паузу в обработку и проверяем - нет тоже все идентификаторы доставлены в точном порядке, несмотря на задержку

Значит, брокер сообщений всегда отправляет уведомления в том же порядке, в котором они получены, независимо от их обработки. Этот же факт подтверждает и то, что я так и не получил сообщения ""Нарушен порядок получения сообщений: пришло №" + Результат.Индекс + " за номером " + ТекущаяСтрока.Получено!
А что если нагрузить сервер какими-нибудь вычислениями и вместе с этим рассылать уведомления:

Нет, все так же хорошо даже при более чем 80% загрузки процессора 15 потоками поиска и удаления дублей товаров (в демобазе можно наполнить и обработать дубли через примеры в консоли кода, которая там тоже включена, только не забудьте включить многопоточность и указать количество потоков в зависимости от количества ядер вашего процессора - я использую в качестве источника слов первый том Войны и мира). Пробовал отправить и миллион уведомлений. Все проходит также стабильно, не медленней и не быстрее.
4. Проверка объемом
А что если проверять не количество уведомлений, а их объем, например, отправить пару гигабайт! Как выясняется, проблем нет, будет получено и столько, но достаточно медленно и с большим потреблением оперативной памяти. Например, на моем компьютере с 32ГБ после двух таких отправок было занято более 90% памяти, при том что до этого - только 43%. Впрочем через какое-то время этот объем восстановился, но за считанные минуты только до 59, и лишь через час снова до 43%, то есть память высвобождалась неохотно. Чего не скажешь об отправке через временное хранилище: там оперативная память вообще не задействуется, только диск и время лучше:

То есть очевидно, что большие объемы информации лучше отправлять через временное хранилище, если есть такая возможность. Недостаток в том, что если использовать временное хранилище вкупе с фоновым заданием, то нужно, чтобы задание завершилось, только тогда результат будет доступен. А учитывая, что это единственная возможность получить данные через временное хранилище из другого сеанса, возможно, я лишь зря заставил кого-то улыбнуться, но может, и пригодится кому, не я один балдею от фоновых! В остальном же, уведомлениям клиента в плане объема есть куда расти и у чего учиться! Хотя все доставляется и без проблем, но долго.
5. Проверка типов
Как вы уже знаете УникальныйИдентификатор пересылать нельзя, хотя в описании синтакс-помощника указано - Произвольный тип. Проверим другие:
СписокКВыбору = Новый СписокЗначений;
СписокКВыбору.Добавить(Новый ОписаниеТипов("УникальныйИдентификатор"), "Уникальный идентификатор");
СписокКВыбору.Добавить(Новый ОписаниеТипов("ДокументPDF"), "Документ PDF");
СписокКВыбору.Добавить(Новый ОписаниеТипов("СписокЗначений"), "Список значений");
СписокКВыбору.Добавить(Новый ОписаниеТипов("ТаблицаЗначений"), "Таблица значений");
СписокКВыбору.Добавить(Новый ОписаниеТипов("ФорматированнаяСтрока"), "Форматированная строка");
СписокКВыбору.Добавить(Новый ОписаниеТипов("ТабличныйДокумент"), "Табличный документ");
СписокКВыбору.Добавить(Новый ОписаниеТипов("ФорматированныйДокумент"), "Форматированный документ");
СписокКВыбору.Добавить(Новый ОписаниеТипов("Null"), "NULL");
СписокКВыбору.Добавить(Новый ОписаниеТипов("ОписаниеТипов"), "Описание типов");
СписокКВыбору.Добавить(Новый ОписаниеТипов("Шрифт"), "Шрифт");
СписокКВыбору.Добавить(Новый ОписаниеТипов("СтандартныйПериод"), "Стандартный период");
СписокКВыбору.Добавить(Новый ОписаниеТипов("КомпоновщикНастроекКомпоновкиДанных"), "Компоновщик настроек компоновки данных");
СписокКВыбору.Добавить(Новый ОписаниеТипов("Картинка"), "Картинка");
СписокКВыбору.Добавить(Новый ОписаниеТипов("СправочникСсылка.Банки"), "Банк");
СписокКВыбору.Добавить(Новый ОписаниеТипов("ДвоичныеДанные"), "Двоичные данные");
СписокКВыбору.Добавить(Новый ОписаниеТипов("Строка"), "Строка с недопустимым символом");
Как и ожидалось, пересылается все, кроме таблицы значений. Ну и кроме первого и последнего пункта. Причем последний пункт, с недопустимым символом XML, пересылается совсем уж странно: отправка срабатывает, но на клиент приходит пустая строка, независимо от положения недопустимого символа. Заявил о баге, такого быть явно не должно: либо ошибка, либо подавай всю строку. Также почему-то очень медленно пересылается форматированная строка, хотя она пустая, без текста - больше 10 секунд на моем компьютере.
6. Проверка перезапуска серверного процесса
Как я уже говорил, на моем предприятии проблемы с уведомлениями клиента были после ночных регламентных заданий. А что если это было связано с перезапуском процессов, ведь нагрузка там большая, в том числе по памяти. Здесь замечено, что фоновые задания через раз пропадают при перезапуске, поэтому использовал два клиентских сеанса, один из которых отсылает уведомления, а другой получает. Настроил перезапуск кластера каждую минуту (* * * * *, если вы понимаете о чем я) и запустил проверку на несколько минут - все стабильно приходит, только во время передачи управления другому процессу наблюдается небольшой простой. Но на то они и обработчики ожидания, их нельзя использовать в качестве таймера. Так что и здесь, как ни странно, границ не обнаружено!

7. Примеры использования
Ну что же, можно сказать, что подсистема Уведомлений клиента нами проверена и признана вполне и вполне пригодной для ежедневного применения, давайте рассмотрим пару примеров, где она покажет себя с лучшей стороны.
Не буду приводить сложных реализаций, уже упоминал обработку уведомлений http-сервиса с обратной передачей результата, теперь обратимся к простому. Например, к интеллектуальному отображению состояния прогресса. Обычно для этого используется обработчик ожидания с обработкой текста сообщений фонового задания, здесь же есть возможность передать больше информации и в любой момент (когда другой код не исполняется). Допустим нужно обработать (перепровести) пакет документов, отбираем их и в процессе обработки выборочно отправляем информацию о прогрессе:
Выборка = РезультатЗапроса.Выбрать();
Индекс = 0;
ВремяОбработкиПорции = ТекущаяДатаСеанса();
Пока Выборка.Следующий() Цикл
ДокументОбъект = Выборка.Ссылка.ПолучитьОбъект();
Попытка
ДокументОбъект.Записать(РежимЗаписиДокумента.Проведение);
Исключение
Сообщение = Новый СообщениеПользователю();
Сообщение.Текст = "Не удалось провести документ " + Выборка.Ссылка + " по причине: " + ОписаниеОшибки();
Результат.Сообщения.Добавить(Сообщение);
КонецПопытки;
Индекс = Индекс + 1;
Если Индекс % 5 = 0 Или ТекущаяДатаСеанса() - ВремяОбработкиПорции > 5 Тогда
Результат.Прогресс = Цел(Индекс * 100 / Выборка.Количество());
Результат.Вставить("ТекстСообщения", "Обработано " + Индекс + " из " + Выборка.Количество());
Результат.Вставить("Пояснение", Строка(Выборка.Ссылка));
УведомленияКлиента.ОтправитьУведомление("ИнтеллектуальноеСостояние", Результат, СеансыКОтправке);
Результат.Сообщения.Очистить();
ВремяОбработкиПорции = ТекущаяДатаСеанса();
КонецЕсли;
КонецЦикла;
Результат.Завершено = Истина;
Результат.Прогресс = 100;
Результат.Вставить("ТекстСообщения", "Обработаны все документы");
Результат.Вставить("Пояснение", "Обработка завершена");
УведомленияКлиента.ОтправитьУведомление("ИнтеллектуальноеСостояние", Результат, СеансыКОтправке);
Обработка на стороне клиента также проста:
&НаКлиенте
Процедура ИнтеллектуальноеСостояниеПолучение(Результат, ДополнительныеПараметры) Экспорт
Если Результат = Неопределено Тогда
Возврат;
КонецЕсли;
Если Результат.Завершено Тогда
УведомленияКлиента.ОтключитьОбработчик("ИнтеллектуальноеСостояние");
КонецЕсли;
Состояние(Результат.ТекстСообщения, Результат.Прогресс, Результат.Пояснение);
Если Результат.Сообщения.Количество() Тогда
Для Каждого Сообщение Из Результат.Сообщения Цикл
Сообщение.Сообщить();
КонецЦикла;
КонецЕсли;
КонецПроцедуры
В результате имеем:

Преимущество в том, что можно легко допилить обработку получения даже до того, чтобы можно было открыть сбойный документ, например. Что конечно, ранее не было возможно - по крайней мере, в процессе.
Рассмотрим другой пример. Не знаю, как Вам, а мне зачастую при обновлениях хочется узнать, как работает новый код или просто требуется изучить особенности выполнения старого. Разумеется, для этого я использую журнал регистрации, который неудобен, например, тем, что ссылку можно указать только одну, все остальное только текстом. С уведомлениями клиента все гораздо удобнее. Я уже приводил в самом начале статьи код отправки и получения уведомления о событии. Не умолчал и об основном преимуществе - любые необходимые данные. В нашем случае это количество строк товаров и время проведения вместе с ссылкой на документ:

Но повторюсь, здесь упростил для примера, а так можно гораздо больше информации предоставлять. Также это удобнее тем, что больше не нужно настраивать фильтры журнала регистрации и держать его открытым, постоянно обновляя - оповещения появляются одномоментно с событиями. Кроме того, держим в памяти, что при большом количестве сеансов лучше отправлять в сеанс, поэтому как по мне лучше завести совсем небольшой регистр сведений с ключом подписки и номером сеанса в качестве измерений, чтобы потом при отправке можно было просто отобрать сеансы по отправляемому ключу:
Функция ПолучитьПодписки(КлючУведомления) Экспорт
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ПолучателиУведомленийКлиента.НомерСеанса КАК НомерСеанса
|ИЗ
| РегистрСведений.ПолучателиУведомленийКлиента КАК ПолучателиУведомленийКлиента
|ГДЕ ПолучателиУведомленийКлиента.КлючУведомления = &КлючУведомления";
Запрос.УстановитьПараметр("КлючУведомления", КлючУведомления);
НомераСеансов = Запрос.Выполнить().Выгрузить().ВыгрузитьКолонку("НомерСеанса");
ТекущиеСеансы = ПолучитьСеансыИнформационнойБазы();
НомераСеансовТекущихПользователей = Новый Массив;
Для Каждого ТекущийСеанс Из ТекущиеСеансы Цикл
НомераСеансовТекущихПользователей.Добавить(ТекущийСеанс.НомерСеанса);
КонецЦикла;
Индекс = НомераСеансов.Количество();
Пока Индекс > 0 Цикл
Индекс = Индекс - 1;
Если НомераСеансовТекущихПользователей.Найти(НомераСеансов[Индекс]) = Неопределено Тогда
УбратьИзПодписки(КлючУведомления, НомераСеансов[Индекс]);
НомераСеансов.Удалить(Индекс);
КонецЕсли;
КонецЦикла;
Если НомераСеансов.Количество() Тогда
Возврат НомераСеансов;
Иначе
Возврат Неопределено;
КонецЕсли;
КонецФункции
Это вкупе с УведомленияКлиентаВызовСервера.ДобавитьВПодписку(ИмяПодписки) позволит дополнить недостающее звено в подсистеме уведомлений клиента.
Ну и не забываем, что сама подсистема пришла к нам из Системы взаимодействия, поэтому можно подумать например о том, чтобы реализовать на его основе какой-нибудь чат - ведь транспорт у нас уже есть! Чат я в рамках этой статьи расписывать не буду, а вот поделиться документом - запросто:
Асинх Процедура ПоделитьсяДокументом(МассивДокументов, ДополнительныеПараметры) Экспорт
Если МассивДокументов = Неопределено Или МассивДокументов.Количество() = 0 Тогда
Возврат;
КонецЕсли;
СписокКлиентов = УведомленияКлиентаВызовСервера.СписокТекущихПользователей();
ВыбранноеЗначение = Ждать СписокКлиентов.ВыбратьЭлементАсинх(НСтр("ru = 'Выберите пользователя'"));
Если ВыбранноеЗначение = Неопределено Тогда
Возврат;
КонецЕсли;
Сообщение = Ждать ВвестиСтрокуАсинх("", НСтр("ru = 'Введите сообщение'"), 1000);
Если Сообщение = Неопределено Тогда
Возврат;
КонецЕсли;
Сообщение = Строка(ПользователиКлиент.ТекущийПользователь()) + ": " + Сообщение;
УведомленияКлиентаВызовСервера.ПоделитьсяДокументом(МассивДокументов[0], Сообщение, ВыбранноеЗначение.Значение);
ПоказатьОповещениеПользователя(НСтр("ru = 'Уведомление отправлено'"), , , БиблиотекаКартинок.Успешно32);
КонецПроцедуры
Процедура ПоделитьсяДокументом(Документ, Сообщение, НомерСеанса) Экспорт
УведомленияКлиента.ОтправитьУведомление("ПодпискаНаДокумент",
Новый Структура("Документ, Сообщение", Документ, Сообщение),
ОбщегоНазначенияКлиентСервер.ЗначениеВМассиве(НомерСеанса));
КонецПроцедуры
Процедура ПоказатьУведомление(Результат, ДополнительныеПараметры) Экспорт
Если Результат = Неопределено Тогда
Возврат;
КонецЕсли;
СсылкаНаДокумент = ПолучитьНавигационнуюСсылку(Результат.Документ, "Открываемый документ", Новый Структура);
ПерейтиПоНавигационнойСсылке(СсылкаНаДокумент);
Сообщение = Новый СообщениеПользователю;
Сообщение.Текст = Результат.Сообщение;
Сообщение.Сообщить();
КонецПроцедуры
...
Оповещение = Новый ОписаниеОповещения("ПоказатьУведомление", УведомленияКлиентаКлиент);
УведомленияКлиента.ПодключитьОбработчик("ПодпискаНаДокумент", Оповещение);
...
Как видите, весь код умещается на экран - настолько все просто, а уже получается такое:
8. Вместо заключения
Что же, я попытался исследовать границы системы Уведомления клиента и нашел, что они весьма широки, если не сказать больше. Не знаю, как вас, а меня новая подсистема даже после этих экспериментов несомненно радует. Уверен, что я еще не раз обращусь к ней и не разочаруюсь, чего и вам желаю! Проект EDT без расширения также доступен на gitflic по тегу "фоновые задания", там сразу три моих статьи представлены обработками для проверки.
Проверено на следующих конфигурациях и релизах:
- 1С:Библиотека стандартных подсистем, редакция 3.1, релизы 3.1.11.335
Вступайте в нашу телеграмм-группу Инфостарт