Best practices для разработчиков плагинов iikoFront API
v10
Рекомендации выработаны по итогам разбора реальных инцидентов с высокой нагрузкой на кассу. Главный принцип: нагрузку создают не «количество заказов», а поток событий и число их сериализаций/обработок. Каждая лишняя подписка и каждый лишний запрос умножаются на работу ядра и сборщика мусора.
1. Подписывайтесь на события один раз
Каждая подписка на событие (например, OrderChanged) — это отдельная полная сериализация и доставка данных в плагин. Дублирующиеся подписки умножают нагрузку.
- Один плагин = одна подписка на одно событие.
- Если нужны несколько обработчиков — делите поток внутри себя (через
Subject/ несколькоSubscribeна один полученныйIObservable), а не подписывайтесь на API несколько раз. - Реальный кейс: плагин с 7 подписками на
OrderChangedполучал в 7 раз больше нагрузки, чем нужно.
2. Фильтруйте события как можно раньше внутри плагина
Важно понимать границы: фильтр .Where(...) в плагине выполняется после того, как ядро уже сериализовало заказ и передало его по каналу. Поэтому:
- фильтр не отменяет сериализацию события на стороне ядра;
- но он избавляет плагин от лишней работы и, главное, от повторных дорогих запросов (повторный
GetOrderи т.п.).
Пример из инцидента: плагину нужны были только закрытые заказы, а он реагировал на каждый OrderChanged.
PluginContext.Notifications.OrderChanged
.Select(e => e.Entity)
.Where(o => o.Status == OrderStatus.Closed)
.Subscribe(OnOrderClosed);
Саму сериализацию убирают только две вещи:
- меньше подписчиков на событие (см. пункт 1);
- чтобы ядро не порождало лишних событий (это уже зона ответственности платформы, а не плагина).
3. Не запрашивайте весь заказ на каждое уведомление
На каждое изменение заказа повторный GetOrder / GetOrderItemProductGroups / TryGetOrderExternalDataByKey сериализует весь граф заказа заново.
- Если пришёл
OrderChangedс сущностью — используйте данные из аргумента, не перечитывайте заказ. - Держите кэш/ревизию и читайте заново только при реальном изменении.
- Массовый
GetOrders— дорого: вызывайте редко и только при реальной необходимости полного среза.
4. Не вызывайте одни и те же методы по тысячи раз
GetExternalOperations, GetPrice, GetModifiers и аналогичные — дорогие вызовы с сериализацией ответа.
- Получите нужный ключ/объект один раз и переиспользуйте.
- Реальный кейс:
GetExternalOperationsвызывался ~700 000 раз в день там, где хватило бы одного получения ключа.
5. Держите обработчики событий лёгкими
События обрабатываются на общих потоках API. Долгий или блокирующий обработчик задерживает все остальные.
- Тяжёлую работу (сеть, диск, длительные вычисления) по возможности переносите в фон и готовьте заранее.
- Если по бизнесу нужно дождаться актуальных данных — это допустимо, но делайте это осознанно: помните, что ваша задержка складывается с задержками других плагинов и ложится на UI.
6. Синхронные нотификации: интеракция с юзером — да, тяжёлая работа — нет
Синхронные нотификации (INotification<...>, например NavigatingToPaymentScreen, BeforeProceedOrderPayment, BeforeDoCheque) существуют именно для того, чтобы плагин мог взаимодействовать с пользователем и повлиять на операцию:
- показать диалог/сообщение через
IViewManager(ShowOkPopup,ShowYesNoPopup,ShowInputDialogи т.д.); - читать/писать данные в контекст операции (например,
context.ChequeAdditionalInfo); - отменить операцию, выбросив
OperationCanceledException.
Это легитимный сценарий — пользоваться им можно и нужно.
Ограничение: по возможности не делайте сетевых/дисковых/тяжёлых вычислений в обработчике. Показать диалог — быстро. А вот сходить по HTTP за данными, чтобы «подготовить» этот диалог, — уже секунды.
При этом, если бизнес-логика требует дождаться актуальных данных синхронно — это допустимо, но делайте это осознанно: задержка ляжет на UI и сложится с другими плагинами.
Реальный кейс: «вход в заказ ~10 сек» полностью раскладывался на три последовательных обработчика NavigatingToPaymentScreen — 5,5 с + 2,2 с + 2,0 с. Это почти наверняка сетевые вызовы, а не показ диалога.
Как делать правильно:
- Данные для интеракции готовьте заранее, по подпискам на изменения (пункты 2–4), а в момент синхронной нотификации — только показывайте диалог и читайте готовый кэш.
- Если нужны свежие данные, а кэш уже неактуален, — синхронный запрос допустим как осознанный компромисс, но помните про сумму задержек.
- Учитывайте, что обработчики складываются: UI ждёт сумму времени всех плагинов, а не максимум. «Каждый по чуть-чуть» — тоже проблема.
Итого: взаимодействие с юзером — это назначение синхронных нотификаций, используйте IViewManager. Сеть и тяжёлые вычисления внутри обработчика — по возможности в фон; синхронно — только когда этого действительно требует бизнес.
7. Освобождайте подписки
Не забывайте Dispose() подписок и ресурсов при остановке плагина. «Забытая» подписка продолжает получать события и создавать нагрузку, даже когда плагин уже не использует данные.
8. Помните про главный терминал и синхронизацию
На главном терминале группы данные синхронизируются на ведомые терминалы. Избыточные операции на главном умножаются на число ведомых.
- На главном терминале минимизируйте частоту изменений, которые рассылаются в группу.
9. Не полагайтесь на детали реализации API
- Некоторые расчёты (например, скидки iikoCard) применяются асинхронно и могут попадать в
OrderChangedне сразу — учитывайте это, не делайте жёстких допущений о «мгновенности». - Работайте только с публичным контрактом API, не завязывайтесь на конкретную версию/реализацию.
Итог одной фразой
Одна подписка на событие, ранний фильтр внутри плагина, не перечитывать данные, которые уже пришли, кэшировать справочники, не блокировать обработчики (особенно синхронные) — это убирает подавляющую часть избыточной нагрузки, которую мы видим в реальных инцидентах.