Каналы уведомлений из системы
Назначение
Система сообщает внешним клиентам о происходящем через события (классы событий). Клиент подписывается на интересующие события и получает уведомления по мере их возникновения, вместо того чтобы периодически опрашивать REST-API. Статья описывает доступные каналы, различия событий общего вида и событий модели данных (modelevents), а также подход к поддержанию согласованных данных у нескольких рабочих мест (АРМ).
Каналы
| Канал | Кто подписывается | Какие события | Доставка |
|---|---|---|---|
Websocket, User-API subscr |
Пользователь в рамках своей сессии |
Только |
В открытое websocket-соединение; подписки удаляются при его закрытии |
Websocket, Token-API subscr |
Внешняя система по токену канала интеграции |
Все классы, в том числе modelevents |
В открытое websocket-соединение; подписки удаляются при его закрытии |
HTTP веб-хуки, /rest/v1/service/webhooks |
Внешняя система по токену канала интеграции |
Все классы, кроме modelevents |
HTTP-запрос на URL подписки или канала интеграции; без подтверждения и повторов |
Kafka |
Настраивается в домене (параметры kafka_params, kafka_mustache) |
Классы, отмеченные в Классы событий общего вида; modelevents — только встроенной модели доменного центра (см. Kafka и изменения динамической модели данных) |
Запись в топики брокера |
Прямые уведомления |
Пользователь в рамках своей сессии, без подписки |
Состояние пользователя (user_state_changed), уведомления сценариев (capability scriptnotify) |
В открытое websocket-соединение пользователя |
Рекомендации по выбору:
-
Интерфейс пользователя, которому нужны изменения данных в реальном времени, — websocket User-API, события modelevents.
-
Внешняя система с постоянным соединением — websocket Token-API: все классы событий, порядок доставки сохраняется в пределах соединения.
-
HTTP веб-хуки не рекомендуются (см. Почему веб-хуки дороже websocket); применяются, только если внешняя система не может держать websocket-соединение.
-
Аналитика и шина событий — Kafka.
Почему веб-хуки дороже websocket
-
На каждое уведомление открывается отдельное HTTP-соединение (для https — с TLS-рукопожатием) и закрывается после ответа. Websocket передаёт уведомления кадрами в уже открытом соединении.
-
Запрос синхронный: система ждёт ответа приёмника (до 10 секунд). Уведомления на один адрес отправляются последовательно, поэтому медленный или недоступный приёмник задерживает все следующие уведомления на этот адрес и занимает ресурсы обработчиков системы.
-
Доставка не подтверждается и не повторяется: при недоступности приёмника уведомление теряется, и для критичных данных нужна периодическая сверка через REST-API. В websocket потеря соединения обнаруживается сразу, и клиент восстанавливает состояние перечиткой.
-
События модели данных (modelevents) через веб-хуки недоступны.
Kafka и изменения динамической модели данных
События modelevents динамической модели данных в Kafka не отправляются — только подписчикам websocket.
В Kafka попадают modelevents встроенной модели доменного центра (например, /rest/v1/uc/sipusers) и события общего вида по Классы событий общего вида.
Поток изменений классов динамической модели в Kafka даёт история изменений:
-
у класса включается опция store_changehistory_mode (
syncилиasync): каждое изменение сущности записывается в класс истории изменений домена platform/log/HistoryChanges — единый приёмник изменений всех классов домена с этой опцией; -
класс истории изменений размещается в хранилище с режимом хранения transactionlog (Kafka; для чтения — также ClickHouse): записи истории поступают в топик Kafka. По умолчанию класс создаётся с режимом
historyв хранилище по умолчанию, поэтому для потока в Kafka его размещение меняют.
Запись истории содержит время и автора изменения, класс, операцию (kind: create, update, replace, delete, clear; rollback — изменение отозвано из-за сбоя сохранения), идентификатор и название сущности, изменённые поля, значение созданной или удалённой сущности.
В режиме sync операция с классом завершается после записи в историю; в режиме async запись выполняется асинхронно с сохранением порядка изменений.
Общие правила подписки
-
events— список событий в форматеEventClass.EventTypeили"any". -
objects— фильтр по объектам, к которым привязано событие:"any"или список. -
В мастер-домене события callevents и confevents всех доменов доступны по подписке на класс (
callevents.*) или его событие (callevents.invite) с"objects": "any": подписки на"any"этих событий в мастер-домене не получают, подписка с объектами отклоняется. Поток включает все звонки платформы: для веб-хуков он чрезмерен (см. Почему веб-хуки дороже websocket). -
expires— срок действия подписки в секундах. Подписку нужно продлевать повторным запросом с тем жеidдо истечения срока.expires= 0 удаляет подписку. Для веб-хуков срок не больше 86400; применённое значение возвращается в ответе. -
id— идентификатор подписки; если не задан, генерируется и возвращается в ответе. Указывается в каждом уведомлении в полеsid. -
Новая подписка начинает получать события не позже чем через секунду после ответа.
-
Если событие подходит под несколько подписок одного подписчика, уведомление отправляется по каждой из них.
Формат уведомления:
["notify", {"sid": "...", "class": "...", "type": "...", "eventts": 1791975832258, "data": {...}}]
Для веб-хуков тело HTTP-запроса — объект из второго элемента массива.
События общего вида и события модели данных
События делятся на два вида, и подписка на них оформляется по-разному:
-
События общего вида сообщают о происходящем в системе: звонки, конференции, сценарии, состояния провайдеров и пользователей (Классы событий общего вида). У каждого класса свой набор событий. Подписка отбирает события по их списку и по объектам, к которым событие относится (Объекты событий общего вида); по содержимому события не фильтрует.
-
События модели данных (modelevents) сообщают об изменении записей коллекции: создание, изменение, удаление сущности. Событие одно —
modelevents.data_changed, операция передаётся в уведомлении. Подписка оформляется на одну коллекцию и может отбирать записи по содержимому, ограничивать передаваемые поля и операции.
Различия в параметрах подписки:
| Параметр | События общего вида | События модели данных |
|---|---|---|
|
События классов, в том числе маска класса событий |
Только |
|
|
Одна коллекция: поле |
|
Не применяется |
|
Разрешение подписки |
Право подписки канала интеграции |
Права IAM на чтение коллекции, политики безопасности модели данных (Политики доступа для modelevents) |
Классы событий общего вида
| Класс | Token-API (websocket) | Веб-хуки | Kafka |
|---|---|---|---|
да |
да |
да |
|
да |
да |
да |
|
да |
да |
да |
|
да |
да |
да |
|
да |
да |
да |
|
да |
да |
да |
|
ctievents (мастер-домен) |
да |
да |
да |
События сценариев (компонент «Генерация произвольного события», класс задаёт сценарий) |
да (направление «Подписчики») |
да (направление «Подписчики») |
да (направления KAFKA) |
нет |
нет |
да |
|
нет |
нет |
да |
События класса registrarevents не генерируются с версии 1.8.27; состояния регистраций доступны событиями modelevents коллекции /rest/v1/uc/sipuser_states.
Объекты событий общего вида
Событие связано с объектами, к которым оно относится, и отправляется по подписке, если хотя бы один из них указан в objects.
Объект задаётся строкой:
| Класс | Объект |
|---|---|
Логин SIP-пользователя или номер внешнего абонента |
|
Идентификатор конференции или номер комнаты |
|
Код учётной записи провайдера |
|
Логин SIP-пользователя или идентификатор пользователя |
|
Объекта нет, указывается |
|
События сценариев |
Логин из поля |
Объект можно задать также в виде {"type": "sipuser", "sipuser": "sip1"}; поддерживаются типы sipuser, other, conf, provider, user.
События модели данных
Класс указывается в classpath:
-
динамическая модель —
/rest/v1/model/<section>/<Class>; -
коллекции статической модели, поддерживающие события, — например
/rest/v1/uc/sipusers; -
коллекции пользовательского раздела
/rest/v1/user/….
Операция передаётся в поле operation уведомления:
-
create— сущность создана, либо изменена так, что стала попадать под фильтр подписки; -
update— сущность изменена, и прежнее, и новое значение попадают под фильтр; -
delete— сущность удалена, либо изменена так, что перестала попадать под фильтр; -
clear— коллекция очищена; -
reload— обработка изменений по классу перезапущена; состояние коллекции нужно перечитать; -
corrupt— отменяет предыдущее уведомление об изменении сущности: изменение не сохранено из-за сбоя хранилища; сущность нужно перечитать.
Операции clear и reload касаются всей коллекции и приходят независимо от фильтра.
Операция corrupt возникает только у классов с режимом integrity_mode = sync_fast_notify, где уведомление отправляется до сохранения в хранилище.
Если сохранение затем не удалось, по той же сущности отправляется corrupt; фильтр подписки применяется так же, как к исходному уведомлению, поэтому corrupt получают те подписчики, которые получили отменяемое изменение. Затронута одна сущность (eid): её нужно перечитать — в хранилище осталось прежнее значение. Запрошенная операция не выполнена: инициатор получает ошибку и при необходимости повторяет операцию.
Параметры exargs:
-
filter— выражение в виде списка, как в параметреfilterREST-API коллекции. Событие отправляется, если под фильтр попадает прежнее или новое значение сущности. -
mask— список полей сущности, передаваемых в уведомлении. -
operations— список операций, о которых нужно уведомлять;reload,clear,corruptприходят всегда. -
content_mode— содержимое уведомления: 0 — без сущности, 1 — текущее значение сущности (по умолчанию), 2 — дляupdateтакже прежнее значение. -
index— индексное условие вида["==",["property","<поле>"],["const",<значение>]]. Действует как фильтр «значение поля равно константе в прежнем или новом значении сущности», с тем же приведением операций к create и delete. Подписки с индексом раскладываются по значению поля, и при изменении сущности нужные подписки находятся поиском по значению, без проверки фильтра каждой подписки. Применяется вместо фильтра, когда на один класс подписано много подписчиков, каждый — на свою часть записей (например, исполнитель — на записи, адресованные ему). Если заданы иindex, иfilter, событие отправляется при выполнении обоих условий.
Политики доступа для modelevents
-
Подписка разрешена, если у пользователя (для Token-API — у пользователя, привязанного к каналу интеграции, иначе у ролей самого канала) есть право IAM на чтение (
GET) элемента или коллекции класса. -
Для динамической модели к подписке применяются политики безопасности класса: фильтр политики объединяется с фильтром подписки через
И, маска политики ограничивает маску подписки. Политики не применяются для пользователей с ролью admin, для классов с отключённой безопасностью и для каналов интеграции без привязки к пользователю. -
Для коллекций статической модели применяется только проверка прав IAM.
Согласованность данных у нескольких рабочих мест
Все клиенты, подписанные на один класс с одинаковым фильтром, получают одни и те же уведомления, поэтому их представления данных совпадают, если каждый клиент строит своё состояние по правилу «сначала подпишись, затем считай»:
-
Подписаться на события коллекции с нужным фильтром и накапливать поступающие уведомления.
-
Прочитать коллекцию через REST-API с тем же фильтром.
-
Применить накопленные уведомления в порядке поступления, затем применять новые по мере поступления.
-
При уведомлении
reload, а также после восстановления соединения, повторить чтение коллекции; приcorrupt— перечитать сущностьeid. -
Продлевать подписку до истечения срока.
Порядок уведомлений по одной сущности сохраняется. Номер версии в уведомлении не передаётся; для контроля целостности кеша можно сравнивать контрольную сумму коллекции (операция hc в websocket rest) с сохранённой.
Дополнительно рекомендуется периодическая полная перечитка с большим интервалом.
Загрузка кеша по всей коллекции
Для начальной загрузки большой коллекции в кеш вместо постраничного чтения используйте snapshot-чтение (параметр snapshot при чтении коллекции, REST-API динамической модели):
-
snapshot=true— выборка целиком одним JSON-документом: через HTTP REST-API — сразу в ответе, через websocket — ссылкойdownload_urlна временный файл, который нужно скачать в течение минуты; -
snapshot=<идентификатор>— постраничное чтение из зафиксированного снимка идентификаторов, не меняющегося между страницами (для коллекций от 3000 записей).
Snapshot-чтение поддерживают коллекции с хранением в памяти (ets, ram, runtime), а также category с полным кешированием (только snapshot=true).