Шина событий
Мотивация
Нужно стремится делать компоненты системы масимально независимыми от бизнес-логики.
Идеально, если при сохранении чего-то смотреть изменившиеся поля и понимать что именно надо выполнить по бизнес-процессу, если, например изменился только статус объекта, однако пока что это невозможно. Однако мы хотели сделать возможность подписываться на изменения объектов, чтобы не добавлять логику оповещения внутренних подсистем предприятия прямо в код страниц META.
Пример: на карточке клиента при сохранении списка главных пользователей клиента надо записывать изменения в стороннюю CRM. В простейшем случае все тривиально - в код страницы вставляется вызов апи CRM, например даже через какое-то наше внутреннее API. Однако список таких систем может расти и вам просто сложно будет за этим всем следить, а так же параллелить и перезапускать при недоступности внешних API, код будет ухудшаться и не будет соблюдаться принцип единой ответственности (SRP).
Кроме того, если вы или с вами будут делиться модулями меты, вы вряд ли захотите менять эти страницы, чтобы уведомить какие-то части своей подсистемы.
Как работает
На основе Starter в качестве backend для распределенного запуска скриптов. Он же обеспечивает перезапуск при падениях, ограничение очереди выполнения, логи и пр. вещи нужные для обработки очередей
META Страницы
В коде страниц редактирования карточек объектов в принципе всегда есть похожий код, который занимается журналированием изменений:
<script type="meta/js" elem="hidden" states="save">
function main(pvm, vm, env, ObjectLogService) {
// тут обычное сохранение вашего объекта или списка объектов
// Это стандартная запись в журнал изменений по объекту
ObjectLogService.logValue(env.entityId, objectId, isNew ? 'ADD' : 'SET', env.sp.obj);
// или с пятым параметром, который укажет на некую "форму", которая меняла объект
// например тут придумано название main_user_list, которое именно у нас в приложении будет означать форму со списком главных пользователей
ObjectLogService.logValue(env.entityId, objectId, isNew ? 'ADD' : 'SET', env.sp.obj, 'main_user_list');
}
</script>
Dispatchers
Содержится в параметре dispatcher
Будет использоваться для шины событий как клиент, который продуцирует полезную работу: запускает фоновые процессы, записывает логи, генерирует события в шину
Примеры:
- meta.{META_APP_ALIAS} - мета-приложение
- appscript.{SERVICE_ID} - фоновый скрипт (включает в себя обработчики шины событий)
- apiservice.{SERVICE_ID} - http апи сервисы
Python worker (Listener)
from metasdk import MetaApp
from metasdk.event_bus import Event
META = MetaApp()
log = META.log
# в декораторе вы должны указать на какие типу уведомлений подписываетесь и начать обработку события
@META.event_bus.listener(entity_id="2830", code=["ADD", "SET"], form_pattern=["*"], dispatcher_pattern=["meta.garpun_feeds"])
def on_upsert_ex_access(event: Event):
print(u"on_upsert_ex_access = %s" % str(event))
print(u"event.value = %s" % str(event.value)) # для получения value нужно делать receive_value=True, но не делайте это всегда, часто это не нужно, но создает большую доп. нагрузку
print(u"event.object_id = %s" % str(event.object_id))
import time
print("sleep...")
time.sleep(60)
# Это пример для тестового локального запуска, прямо как в типичных фоновых скриптах
META.worker.debug_tasks = [{
"data": {
"event": {
"userId": 10191,
"entityId": 2830,
"objectId": "42",
"code": "SET",
"form": "test",
"value": {
"a": 1
}
}
}
}]
# Бойлерплейт, который запускает обработку
@META.worker.single_task
def main(task):
META.event_bus.accept(task)
Python Client
Если вам надо спровоцировать запись в журнал с записью в Шину Событий. Вы должны вызвать журналирование, а в app-content вы должны иметь настроенные обработчики (listeners)
from metasdk import MetaApp
META = MetaApp()
META.ObjectLogService.log({
"userId": 10191,
"entityId": 2671,
"objectId": 53,
"code": "SET",
"form": "my_form", # optional
"value": { # optional
"foo": "bar",
"subFoo": {
"foo2": [1, 2, 3]
}
}
})
Добавление нового обработчика
Выбираете нужный бандл. Обычно бандл уже есть у страниц, для которых делается обработчик,
но в целом это не обязательно делать именно в нем. Например в случае, если вам надо обработать
событие изменения пользователя для какой-то бизнес фичи вы вряд ли должны добавлять обработчик в пакет io.devision.meta
.
Скорее вам надо добавлять обработчик в пакет вашего приложения или крупной бизнес функциональности.
В корень папки бандла добавьте папку event_listener
если ее нет.
В папку event_listener
добавьте папку с названием своего обработчика, например myfirst_hanlder
.
В папку обработчика (Например myfirst_hanlder
) добавьте app.py
и requirements.txt
.
В requirements.txt
обязательно должна быть зависимость metasdk
.
Ограничения
- Звездочка в паттерне формы "*" может стоять или в начале, или в конце, но НЕ в середине выражения
- Если нет подписчиков события, событие будет записано в журнал, но не уйдет в Starter
- По-умолчанию value лога не прокидывается в таски для шины. Для того, чтобы получать его делайте receive_value=True
Отказоустойчивость
Мы стараемся делать все надежно, однако все ломается (например падение сети или выключение электричества) и событие может не уйти и вы должны иметь механизмы перепроверки. Желательно подумать о них прямо при проектировании своей подсистемы. Работать они могут сильно реже - например раз в час перепроверяют, что все сработало как надо и если что-то не так - делают нужные задачи. Мы будем улучшать надежность Шины, однако все же надо помнить, что только вы знаете как правильно отреагировать на серьезные ошибки в вашем продукте.
ROADMAP
- попытаться очень быстро обрабатывать события с минимальными накладными расходами на запуск питона, например
- убрать оповещение юзеров и какие-то уведомления туда же
- автоматические уведомления popup от ObjectLogService через PubSub
- понятные ограничения обработки очередей event-роутеров