Перейти к основному содержимому

Consumer: News Events → Notifications (notifications-news-events)

Назначение

Группа notifications-news-events слушает события news (публикация постов, комментарии) и уведомляет подписчиков автора о новой публикации, а автора поста — о новом комментарии (NotificationEntity). Список подписчиков не приходит в событии: consumer запрашивает аудиторию у news (internal API подписок) на enqueue. persist-then-send; кросс-контекстной записи нет. Общий алгоритм enqueue — как в crm-events.md.

Owning-сервис и триггер

ПолеЗначение
Сервис/контекстnotifications (Pin.Notifications.Background)
Consumer groupnotifications-news-events
Обрабатываемые событиятопики pin.news.*.v1 (таблица ниже)
Producernews
ContractVersionожидается 1; несовпадение — warn-and-proceed
Эффект (событие → что меняется)создаются NotificationEntity (Pending) для подписчиков/автора

Топики и маппинг

ТопикСобытие → NotificationKindПолучателиUrgency
pin.news.post-published.v1PostPublishedподписчики автора (резолв через internal API news)Low
pin.news.comment-added.v1CommentAddedавтор поста (+ участники треда)Low

Дом полей payload — news/events/post-published.md, comment-added.md; поля уведомления — NewsNotificationPayload (notification.md).

Контекст события

  • Из метаданных события доступны MessageId, CreatedAt, Version, AggregateId; actor = system; tenant по получателю.
  • Идемпотентность: consumer-level ключ notifications-news:{EventType}:{MessageId}; business-level — unique (SourceMessageId, RecipientUserId, Channel, Kind) (INV-NTF-1).

Входные данные / payload события

Поле payloadТипОбяз.Как используется
PostId/AuthorUserId/CommentIdGuidДаid для NewsNotificationPayload + резолв аудитории
Title/AuthorNameстрокаДаPost { Id, Title }, Author { Id, Name }
CommentPreviewstring?commentCommentPreview (усечённый текст)

Алгоритм

Общая логика enqueue (резолв каналов/шаблона/адреса, материализация, согласованность) — как в crm-events.md; дельта источника — резолв аудитории (шаг 3).

  1. Контекст события. Восстановить метаданные и сверить ContractVersion (несовпадение — warn-and-proceed); consumer-идемпотентность по notifications-news:{EventType}:{MessageId} (дубль → skipped-duplicate, ack). Определить Kind по типу события.
  2. Проверка входа. Обязательные идентификаторы непусты; иначе → NTF_INVALID_PAYLOAD (DLQ).
  3. Резолв получателей. Подписчики автора (запрос аудитории у news, пагинация, timeout 3с) или автор поста; дедуп; при CommentAdded от самого автора — исключить его; пусто → no-recipients (ack). Недоступность источника аудитории → NTF_AUDIENCE_UNAVAILABLE (retriable, backoff).
  4. Резолв доставки и материализация. По каждому получателю — каналы для Kind/Urgency = Low (лента — сдержанные каналы по дефолту); шаблон (Kind, Locale, Channel) (нет активного → NTF_TEMPLATE_NOT_RESOLVED, DLQ); адрес/локаль — снапшот из контактной реплики recipient-contact.md (INV-RC-4/5, graceful); тихие часы (Low всегда откладывается по interruptive-каналам); создаётся Notification(Pending) с рендером батчем по подписчикам (dedup INV-NTF-1). Отправку выполняет dispatcher; собственных интеграционных событий consumer не публикует.
  5. Согласованность и результат. Батч уведомлений и отметка идемпотентности фиксируются в одном согласованном изменении, offset — после фиксации. Успех/no-recipients → ack; retriable → retry; non-retriable → DLQ.

Диаграмма

Идемпотентность и replay

  • Consumer-level по MessageId; business-level — dedup-unique (INV-NTF-1). Replay: да (dedup + повторный резолв аудитории идемпотентен).

Retry и DLQ

СбойКодRetryDLQ
Инфраструктурныйbackoff + пауза партицииНет
Аудитория news недоступнаNTF_AUDIENCE_UNAVAILABLEДа (backoff)После исчерпания
Нарушение payloadNTF_INVALID_PAYLOADНетДа + алерт
Шаблон не резолвитсяNTF_TEMPLATE_NOT_RESOLVEDНетДа + алерт

Наблюдаемость и безопасность

  • Logs: consumed/skipped-duplicate/no-recipients/enqueued (fan-out size)/failed.
  • Metrics: notifications_enqueued_total{kind,channel}, notifications_fanout_size, notifications_consumer_lag_seconds{source=news}.
  • Безопасность: system-actor; крупный fan-out (подписчики) — батчинг + throttle; m2m ACL Kafka.

Кросс-сервисные контракты

Ожидает от news: (1) событийный контракт pin.news.*.v1 — at-least-once, устойчивый MessageId, партиционирование по AggregateId поста, ContractVersion, непустые PostId/AuthorUserId (+ CommentId для комментария) и display-поля Title/AuthorName; (2) internal API аудитории — список подписчиков автора с пагинацией, timeout 3с; список возвращается на момент запроса (snapshot-at-consume), гарантий согласованности с моментом публикации нет.

Предоставляет: набор NotificationEntity(Pending) на пару (подписчик × канал) / (автор × канал) (dedup INV-NTF-1); обратной записи в news нет; исходящее событие — только dispatched (dispatcher).

Порядок, версии и неизвестные события

  • Snapshot аудитории. Подписчик, добавившийся после публикации, но до consume — попадёт в рассылку; отписавшийся до consume — не попадёт. Это допустимо (лента, Urgency = Low), сверки с моментом публикации нет.
  • Частичный отказ fan-out. Если аудитория тянется страницами и источник упал на середине → NTF_AUDIENCE_UNAVAILABLE (retriable, backoff); всё событие ретраится целиком. Уже созданные на первой попытке уведомления при повторе не дублируются (INV-NTF-1 по (SourceMessageId, Recipient, Channel, Kind)).
  • Само-исключение. При CommentAdded от самого автора поста — автор из получателей исключается.
  • Неизвестный тип события на pin.news.*.v1ignored-event (ack); версия ниже/выше 1 — warn-and-proceed по известным полям, отсутствие обязательного известного поля → NTF_INVALID_PAYLOAD (DLQ).

Постусловие

  • Для каждого получателя (подписчик/автор) по каждому разрешённому каналу создан ровно один NotificationEntity(Pending); размер fan-out залогирован; отметка идемпотентности и уведомления — в одном согласованном изменении; offset подтверждён после фиксации.
  • Urgency = Low — interruptive-каналы отложены до окончания тихих часов (ScheduledAt); in-app доставляется сразу realtime.
  • Обратной записи в news нет.

Acceptance criteria

  • Given PostPublished с N подписчиками автора, When consumer резолвит аудиторию и enqueue, Then создаётся ≤ N×(разрешённые каналы) уведомлений, каждому подписчику по его настройкам.
  • Given источник аудитории недоступен, When резолв подписчиков, Then NTF_AUDIENCE_UNAVAILABLE, событие ретраится (backoff), после исчерпания — DLQ.
  • Given PostPublished доставлен повторно после частичной обработки, When повторный проход, Then уже созданные уведомления не дублируются (INV-NTF-1).
  • Given CommentAdded, инициированный автором поста, When резолв получателей, Then автор исключён из адресатов.
  • Given interruptive-канал подписчика и активные тихие часы, When материализация, Then уведомление создаётся с ScheduledAt на окончание окна (Low всегда откладывается по interruptive-каналам).

Обратные ссылки

Автоссылки: где используется этот документ.

Связанные документы