refactor chat delivery to graphql + hatchet services
This commit is contained in:
@@ -1,104 +1,93 @@
|
||||
# ADR-0001: Разделение Chat Platform на 3 сервиса
|
||||
# ADR-0001: Chat Platform Boundaries (GraphQL + Hatchet)
|
||||
|
||||
Дата: 2026-02-21
|
||||
Дата: 2026-03-08
|
||||
Статус: accepted
|
||||
|
||||
## Контекст
|
||||
|
||||
Сейчас delivery уже вынесен отдельно, но часть omni-интеграции остается в приложении `frontend`.
|
||||
Нужна архитектура, где входящие вебхуки, доменная логика чатов и исходящая доставка развиваются независимо и не ломают друг друга.
|
||||
Нужна минимальная и предсказуемая схема из 5 сервисов:
|
||||
|
||||
Критичные требования:
|
||||
- `frontend`
|
||||
- `backend`
|
||||
- `telegram_backend`
|
||||
- `telegram_worker`
|
||||
- `hatchet`
|
||||
|
||||
- входящие webhook-события не теряются при рестартах;
|
||||
- delivery управляет retry/rate-limit централизованно;
|
||||
- omni_chat остается единственным местом доменной логики и хранения состояния диалогов;
|
||||
- сервисы можно обновлять независимо.
|
||||
Ключевые ограничения:
|
||||
|
||||
- основная Prisma/доменная БД только в `backend`;
|
||||
- `telegram_backend` и `telegram_worker` не содержат CRM-домен и не пишут в основную БД;
|
||||
- взаимодействие между сервисами только через GraphQL;
|
||||
- асинхронность и ретраи централизованы в Hatchet.
|
||||
|
||||
## Решение
|
||||
|
||||
Принимаем разделение на 3 сервиса:
|
||||
Принимаем архитектуру:
|
||||
|
||||
1. `omni_inbound`
|
||||
- Принимает вебхуки провайдеров.
|
||||
- Валидирует подпись/секрет.
|
||||
- Нормализует событие в универсальный envelope.
|
||||
- Пишет событие в durable queue (`receiver.flow`) с идемпотентным `jobId`.
|
||||
- Возвращает `200` только после успешной durable enqueue.
|
||||
- Не содержит бизнес-логики CRM.
|
||||
1. `backend`
|
||||
- владеет доменной моделью чатов и единственной основной Prisma-базой;
|
||||
- принимает inbound события от `telegram_worker` через GraphQL (`ingestTelegramInbound`);
|
||||
- создает outbound задачи в `telegram_backend` через GraphQL (`requestTelegramOutbound`);
|
||||
- принимает delivery-отчеты от `telegram_worker` через GraphQL (`reportTelegramOutbound`).
|
||||
|
||||
2. `omni_chat`
|
||||
- Потребляет входящие события из `receiver.flow`.
|
||||
- Разрешает идентичности и треды.
|
||||
- Создает/обновляет `OmniMessage`, `OmniThread`, статусы и доменные эффекты.
|
||||
- Формирует исходящие команды и кладет их в `sender.flow`.
|
||||
2. `telegram_backend`
|
||||
- принимает webhook Telegram;
|
||||
- нормализует payload в `OmniInboundEnvelopeV1`;
|
||||
- ставит задачи в Hatchet (`process-telegram-inbound`, `process-telegram-outbound`);
|
||||
- предоставляет GraphQL API для enqueue и отправки в Telegram API.
|
||||
|
||||
3. `omni_outbound`
|
||||
- Потребляет `sender.flow`.
|
||||
- Выполняет отправку в провайдеров (Telegram Business и др.).
|
||||
- Управляет retry/backoff/failover, DLQ и статусами доставки.
|
||||
- Не содержит UI и доменной логики чатов.
|
||||
3. `telegram_worker`
|
||||
- исполняет задачи Hatchet;
|
||||
- для inbound вызывает `backend /graphql`;
|
||||
- для outbound вызывает `telegram_backend /graphql` (`sendTelegramMessage`), затем `backend /graphql` (`reportTelegramOutbound`);
|
||||
- не имеет собственной Prisma-базы.
|
||||
|
||||
## Почему webhook и delivery разделены
|
||||
4. `hatchet`
|
||||
- единый оркестратор задач, ретраев и backoff-политик.
|
||||
|
||||
- Входящий контур должен отвечать быстро и предсказуемо.
|
||||
- Исходящий контур живет с долгими retry и ограничениями провайдера.
|
||||
- Сбой внешнего API не должен блокировать прием входящих сообщений.
|
||||
## Потоки
|
||||
|
||||
### Inbound (Telegram -> CRM)
|
||||
|
||||
1. Telegram webhook приходит в `telegram_backend`.
|
||||
2. `telegram_backend` нормализует событие и enqueue в Hatchet `process-telegram-inbound`.
|
||||
3. `telegram_worker` исполняет задачу и вызывает `backend.ingestTelegramInbound`.
|
||||
4. `backend` сохраняет доменные изменения в своей БД.
|
||||
|
||||
### Outbound (CRM -> Telegram)
|
||||
|
||||
1. `backend` инициирует отправку (`requestTelegramOutbound`) в `telegram_backend`.
|
||||
2. `telegram_backend` enqueue в Hatchet `process-telegram-outbound`.
|
||||
3. `telegram_worker` вызывает `telegram_backend.sendTelegramMessage`.
|
||||
4. `telegram_worker` репортит итог в `backend.reportTelegramOutbound`.
|
||||
|
||||
## Границы ответственности
|
||||
|
||||
`omni_inbound`:
|
||||
`backend`:
|
||||
- можно: вся бизнес-логика и состояние;
|
||||
- нельзя: прямой вызов Telegram API.
|
||||
|
||||
- можно: auth, валидация, нормализация, дедуп, enqueue;
|
||||
- нельзя: запись доменных сущностей CRM, принятие продуктовых решений.
|
||||
`telegram_backend`:
|
||||
- можно: webhook ingress, нормализация, enqueue, адаптер Telegram API;
|
||||
- нельзя: доменные записи CRM.
|
||||
|
||||
`omni_chat`:
|
||||
`telegram_worker`:
|
||||
- можно: исполнение задач, ретраи, orchestration шагов;
|
||||
- нельзя: хранение CRM-состояния и прямой доступ к основной БД.
|
||||
|
||||
- можно: вся доменная модель чатов, orchestration, бизнес-правила;
|
||||
- нельзя: прямые вызовы провайдеров из sync API-контекста.
|
||||
## Надежность
|
||||
|
||||
`omni_outbound`:
|
||||
|
||||
- можно: провайдерные адаптеры, retry, rate limits;
|
||||
- нельзя: резолвинг бизнес-правил и маршрутизации диалога.
|
||||
|
||||
## Универсальный протокол событий
|
||||
|
||||
Внутренний контракт входящих событий: `docs/contracts/omni-inbound-envelope.v1.json`.
|
||||
|
||||
Обязательные поля:
|
||||
|
||||
- `version`
|
||||
- `idempotencyKey`
|
||||
- `provider`, `channel`, `direction`
|
||||
- `providerEventId`, `providerMessageId`
|
||||
- `eventType`, `occurredAt`, `receivedAt`
|
||||
- `payloadRaw`, `payloadNormalized`
|
||||
|
||||
## Идемпотентность и надежность
|
||||
|
||||
- `jobId` в очереди строится из `idempotencyKey`.
|
||||
- Дубликаты входящих webhook событий безопасны и возвращают `200`.
|
||||
- `200` от `omni_inbound` отдается только после успешного добавления в Redis/BullMQ.
|
||||
- При ошибке durable enqueue `omni_inbound` возвращает `5xx`, провайдер выполняет повторную доставку.
|
||||
- Базовые рабочие очереди: `receiver.flow` и `sender.flow`; технические очереди для эскалации: `receiver.retry`, `sender.retry`, `receiver.dlq`, `sender.dlq`.
|
||||
- webhook отвечает `200` только после успешной постановки задачи в Hatchet;
|
||||
- при недоступности сервисов задача ретраится Hatchet;
|
||||
- inbound обработка идемпотентна через `idempotencyKey` и provider identifiers в `backend`.
|
||||
|
||||
## Последствия
|
||||
|
||||
Плюсы:
|
||||
|
||||
- независимые релизы и масштабирование по ролям;
|
||||
- меньше blast radius при инцидентах;
|
||||
- проще подключать новые каналы поверх общего контракта.
|
||||
- меньше сервисов и меньше скрытых связей;
|
||||
- изоляция доменной БД в `backend`;
|
||||
- единая точка ретраев/оркестрации (Hatchet).
|
||||
|
||||
Минусы:
|
||||
|
||||
- больше инфраструктурных компонентов (очереди, мониторинг, трассировка);
|
||||
- требуется дисциплина по контрактам между сервисами.
|
||||
|
||||
## План внедрения
|
||||
|
||||
1. Вводим `omni_inbound` как отдельный сервис для Telegram Business.
|
||||
2. Потребление `receiver.flow` реализуем в `omni_chat`.
|
||||
3. Текущее исходящее API оставляем за `omni_outbound`.
|
||||
4. После стабилизации выносим оставшиеся omni endpoint'ы из `frontend` в `omni_chat`/`omni_inbound`.
|
||||
- выше требования к стабильности GraphQL-контрактов между сервисами;
|
||||
- нужна наблюдаемость по цепочке `telegram_backend -> hatchet -> telegram_worker -> backend`.
|
||||
|
||||
Reference in New Issue
Block a user