Передача данных между сеансами и Повторное использование значений между сеансами

Программирование - Практика программирования

Вам никогда не хотелось в одном сеансе повторно использовать значения, однажды уже вычисленные в другом сеансе?

Про повторное использование значений в рамках одного сеанса сказано уже достаточно. Давайте подумаем, как сделать то же самое, но глобально для ИБ в рамках всех сеансов. Спойлер: без вэб-сервера ничего не получится.

Upd: Внимание! Приведенный метод не является универсальным инструментом на все случаи жизни и имеет существенные ограничения. Прежде чем пилить код - обязательно читайте главу "Update: Потокобезопасность"

В чем проблема

Банальная инициализация параметров какого-то длительного фонового действия может занимать по нескольку секунд. А если это действие распараллелить - то с ростом производительности за счет многопоточной обработки - мы получим рост издержек на инициализацию параметров. Даже в пользовательских сеансах встречаются операции а-ля получения каких-то значений по-умолчанию, продолжающихся достаточно длительное время. Особой проблемы в рамках одного сеанса это как правило не представляет, но если 200 сеансов "долбят" базу данных одними и теми-же вопросами - это уже становится ощутимо.

Согласитесь, было бы здорово прочитать, например, параметры обмена данными один раз и использовать их дальше из оперативной памяти. В общем то такое уже встречается сплошь и рядом: в типовой УТ наверно сотня модулей с повторным использованием возвращаемых значений. Но есть проблема: все это работает только в рамках одного сеанса и у нас нет средства для передачи данных между сеансами (кроме разве что базы данных, но ее использование в этом качестве само по себе является анти-паттерном)

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

Решение

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

Схематично все это можно изобразить так:

Таким образом, мы как-бы "заворачиваем" вызовы из разных сеансов в один посредством REST сервиса, а в нем уже используем штатную функциональность модулей с повторным использованием возвращаемых значений.

Детали

Для того, чтобы все это взлетело - необходимо создать HTTP сервис и установить в нем параметр Повторное использование сеансов в значение Использовать автоматически. Время жизни сеанса устанавливайте по своему усмотрению, у меня стоит 2.592.000 (месяц). Будьте внимательны: это не время жизни сеанса, это время бездействия сеанса, после которого он будет завершен. Дополнительно, в файле default.vrd (файл описания сервиса, лежит в папке публикации) необходимо установить параметр poolSize="1". Все это вполне легальные действия, никакой партизанщины.

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

Программный интерфейс:

Функция ВыполнитьСПовторнымИспользованием(ИмяМетода, МассивПараметровМетода) Экспорт
    
    ПараметрыСоединения = ПараметрыСоединенияСЭтойБазой();
    
    HTTPСоединение = Новый HTTPСоединение(ПараметрыСоединения.Сервер, ПараметрыСоединения.порт, ПараметрыСоединения.Пользователь, ПараметрыСоединения.Пароль);
    
    HTTPЗапрос = Новый HTTPЗапрос(ПараметрыСоединения.АдресРесурса);
    
    ПараметрыВызова = Новый Структура();
    ПараметрыВызова.Вставить("ИмяМетода", ИмяМетода);
    ПараметрыВызова.Вставить("МассивПараметров", МассивПараметровМетода);
    HTTPЗапрос.УстановитьТелоИзСтроки(XMLСтрока(Новый ХранилищеЗначения(ПараметрыВызова)));
    
    HTTPОтвет = HTTPСоединение.ОтправитьДляОбработки(HTTPЗапрос);
    
    Если HTTPОтвет.КодСостояния = 200 Тогда
        ТекстОтвета = HTTPОтвет.ПолучитьТелоКакСтроку();
        Возврат XMLЗначение(Тип("ХранилищеЗначения"), ТекстОтвета).Получить();
    Иначе
        ВызватьИсключение HTTPОтвет.ПолучитьТелоКакСтроку();
    КонецЕсли;
    
КонецФункции

Функция ПараметрыСоединенияСЭтойБазой()
    
    ПараметрыСоединения = Новый Структура();
    
    ПараметрыСоединения.Вставить("Сервер", "127.0.0.1");
    ПараметрыСоединения.Вставить("Порт", 8008);
    ПараметрыСоединения.Вставить("Пользователь", "Справочная");
    ПараметрыСоединения.Вставить("Пароль", "123");
    ПараметрыСоединения.Вставить("АдресРесурса", "/TM/hs/Reference/GetReference");
    
    Возврат ПараметрыСоединения;
    
КонецФункции

Здесь параметры соединение введены хардкодом, что как бы не очень хорошо, но если мы будем делать все по-правильному и читать их каждый раз из БД - эффект от повторного использования возвращаемых значений может быть существенно снижен.

Метод сервиса:

Функция GetReference(Запрос)
    
    ПараметрыСтрока = Запрос.ПолучитьТелоКакСтроку();
    
    СтруктураПараметров = XMLЗначение(Тип("ХранилищеЗначения"), ПараметрыСтрока).Получить();
    
    Если СтруктураПараметров.МассивПараметров.Количество() = 0 Тогда
        ВозвращаемоеЗначение = СправочнаяСлужебныйПовтИсп.ПовторноИспользуемыйМетод0(СтруктураПараметров.ИмяМетода);
    ИначеЕсли СтруктураПараметров.МассивПараметров.Количество() = 1 Тогда
        ВозвращаемоеЗначение = СправочнаяСлужебныйПовтИсп.ПовторноИспользуемыйМетод1(СтруктураПараметров.ИмяМетода, СтруктураПараметров.МассивПараметров[0]);
    ИначеЕсли СтруктураПараметров.МассивПараметров.Количество() = 2 Тогда
        ВозвращаемоеЗначение = СправочнаяСлужебныйПовтИсп.ПовторноИспользуемыйМетод2(СтруктураПараметров.ИмяМетода, СтруктураПараметров.МассивПараметров[0], СтруктураПараметров.МассивПараметров[1]);
    ИначеЕсли СтруктураПараметров.МассивПараметров.Количество() = 3 Тогда
        ВозвращаемоеЗначение = СправочнаяСлужебныйПовтИсп.ПовторноИспользуемыйМетод3(СтруктураПараметров.ИмяМетода, СтруктураПараметров.МассивПараметров[0], СтруктураПараметров.МассивПараметров[1], СтруктураПараметров.МассивПараметров[2]);
    /// тут дальнейшая идея думаю понятна...
    Иначе
        Ответ = Новый HTTPСервисОтвет(500);
        Ответ.УстановитьТелоИзСтроки("Количество параметров более 9 не поддерживается");
        Возврат Ответ;
    КонецЕсли;
    
    Ответ = Новый HTTPСервисОтвет(200);
    Ответ.УстановитьТелоИзСтроки(XMLСтрока(Новый ХранилищеЗначения(ВозвращаемоеЗначение)));
    Возврат Ответ;
    
КонецФункции

Функции повторно-используемого модуля:

Функция ПовторноИспользуемыйМетод0(ИмяМетода) Экспорт
    
    Возврат Вычислить(ИмяМетода);
    
КонецФункции

Функция ПовторноИспользуемыйМетод1(ИмяМетода, Параметр1) Экспорт
    
    Возврат Вычислить(ИмяМетода + "(Параметр1)");
    
КонецФункции

Функция ПовторноИспользуемыйМетод2(ИмяМетода, Параметр1, Параметр2) Экспорт
    
    Возврат Вычислить(ИмяМетода + "(Параметр1, Параметр2)");
    
КонецФункции

Функция ПовторноИспользуемыйМетод3(ИмяМетода, Параметр1, Параметр2, Параметр3) Экспорт
    
    Возврат Вычислить(ИмяМетода + "(Параметр1, Параметр2, Параметр3)");
    
КонецФункции

/// и так далее...

Подводные камни

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

  • Первый вызов (в рамках которого происходит инициализация общего сеанса) в моем случае длится около 1 секунды. Этот параметр зависит от того, что у вас за конфа. У меня УТ 11.3.
  • Последующие вызовы, которые не инициализируют сеанс, а обращаются к уже готовому, занимают 0,015 - 0,02с, что вполне неплохо (это только время самой сетевой транзакции, если будет выполняться трехэтажный запрос - реальное время будет больше). Но я подозреваю, что тут все сильно зависит от сетевой архитектуры.

Здесь важно обратить внимание: инициализация длинной в 1 секунду будет проходить только один раз для всех возможных методов и наборов их значений, а не для каждого нового метода/значения.

Во-вторых у этого метода есть ограничение сверху, некий "потолок использования", который обусловлен размером оперативной памяти, которую вы готовы выделить под кэширование значений. Хотя, здесь невозможно не заметить, что если вы будете кэшировать эти данные традиционным способом, через повторноиспользуемый модуль для каждого сеанса - это займет памяти больше, кратно количеству сеансов, поэтому здесь есть как ограничение так и простор для экономии.

Дополнительные возможности

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

  • передача данных между сеансами
  • хранение актуальных состояний и управление общим оборудованием, например, одним фискальником на несколько рабочих мест
  • управление соединением с другой ИБ (иметь COM-коннект в одном сеансе и использовать его из остальных)

И не стоит забывать, что все это будет работать только в версии 8.3.9 и выше. Удачи!

Update: Потокобезопасность

Комментарий Юрия Дешина подтолкнул меня к, как теперь кажется, очевидной мысли: заворачивание вызова нескольких сеансов в один, при некоторых обстоятельствах может привести к образованию очереди и массовому отказу в обслуживании вызовов. Так, пока сеанс Х выполняет какие-то вычисления, "заказанные" сеансом 1, сеанс 2 не сможет получить никакие данные (в том числе ранее закэшированные) от сеанса Х. При этом сеанс 2 будет "висеть", пока не будет достигнут тайм-аут (параметр poolTimeout в файле default.vrd), по достижении тайм-аута будет возвращен ответ 500.

В общем, как обычно, когда кто-то встает и спрашивает "а что там с потокобезопастностью?" - все становится сильно сложнее чем было до этого. Но это не значит, что задавать такие вопросы не нужно.

Что же нам со всем этим делать?

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

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

  1. Не использовать универсальных обработчиков, которые отвечают сразу за все (например такие, как описано выше). Т.е. если сервис будет отвечать и за расчет себестоимости и за соединение с соседней базой по COM - неизбежно будут возникать ситуации, когда мы не можем подключиться к соседней базе, потому что считаем себестоимость в этой (я утрирую, но мысль думаю понятна). Однако, если мы примем концепцию типа "одно значение - один сервис", то все будет логично: пока значения нет - все сеансы сидят и ждут когда оно появится. Появилось значение - раздали его всем.
  2. Увеличивать число сеансов, раздающих значения. (параметр poolSize в файле default.vrd). Это экстенсивный путь, но до какой-то степени его можно считать рабочим, поскольку такое решение позволит избежать завешивания: при занятости одного сеанса - платформа автоматически переключит вызов на второй, третий, пятый и т.д., но в этом случае значение вычисленное в сеансе Х1 не будет доступно в сеансе и Х2 и будет вычисляться там заново. Можно думать об этом примерно так: все значения кэшируются в сеансе Х1, но он может быть недоступен, в этом случае вызов будет обработан сеансом Х2 (Х3...Хn), но значение будет вычислено заново.
  3. Городить более сложные схемы взаимодействия с кэширующими сеансами, при которых использование сервиса сильно усложнится, но и вероятность получить отказ будет минимальной. Например такую:

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

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

См. также

Комментарии
1. Andrey BedaNastala (Lem0n) 93 09.06.18 10:47 Сейчас в теме
эту задачу решает РС с измерениями: имя метода, хеш параметров, и ресурсом хранилищем + все названные ограничения снимаются(остается только ограничение на несериализуемые данные)
2. Роман М (m-rv) 477 09.06.18 10:59 Сейчас в теме
3. Andrey BedaNastala (Lem0n) 93 09.06.18 11:13 Сейчас в теме
(2)
1. Британские ученые
2. Сокеты, а не http протокол
Проверить бы все на реалиях 1С
4. Роман М (m-rv) 477 09.06.18 11:19 Сейчас в теме
(3)
1. а вы считаете себя умнее?
2. such as sockets
так кто ж мешает то?
jONES1979; +1 Ответить
6. Василий Казьмин (awk) 689 09.06.18 16:41 Сейчас в теме
7. Виктор Третьяков (t.v.s.) 77 09.06.18 17:36 Сейчас в теме
Плюсик за идею, возьму на вооружение
Оффтоп
8. Юрий Дешин (blackhole321) 656 09.06.18 19:33 Сейчас в теме
У Вас в качестве примера доп. возможностей приведена возможность использования com объектов. Как будут обстоять дела с потокобезопасностью?
Или все сводится к последовательным вызовам в одном сеансе, т.е. по сути это один поток выполнения?
m-rv; kote; +2 Ответить
10. Роман М (m-rv) 477 12.06.18 09:31 Сейчас в теме
(8) Спасибо за комментарий, дополнил статью (раздел Потокобезопасность)
9. Ingvar Vilkman (zeegin) 09.06.18 21:53 Сейчас в теме
Согласитесь, было бы здорово прочитать, например, параметры обмена данными один раз и использовать их дальше из оперативной памяти.


Вы работаете в кластере серверов с разной архитектурой, линукс, виндовс, разная разрядность, в т.ч. разные пути к сетевому хранилищу.
При попытке закешировать в такой среде параметры обмена получим развал.

Все это решение - антипаттерн. Не следует кешировать данные в целом, просто потому что не следует вообще кешировать данные вне сеанса.

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

В целом утверждение, что повторно используемые модули кешируются на время сенаса - ложь. Время жизни 20 минут, но в некоторых случаях 6 минут.

Попытка кэшировать через HTTP сервер - хорошая идея, но глупая. Время жизни сенасов HTTP контроллируется только с версии 8.3.10 и по умолчанию соединение может повторно использоваться только 20 секунд. Фактически вы кеш с 20 минут изменили на 20 секунд.

Кроме того многие повторно используемые модули могут зависит от прав пользователя, которые рассчитаны для конкретного сеанса, кеширование таких значений для базы потенциально приводит к проблемам.
a_titeev; max_st; CSiER; +3 1 Ответить
11. Сергей Афонкин (Pixar0000) 12.06.18 13:45 Сейчас в теме
(9) еще дописать RLS на кешируемые данные )))
идея, голой воды идея, но автору за то, что расписал - респект
Оставьте свое сообщение