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 group | notifications-news-events |
| Обрабатываемые события | топики pin.news.*.v1 (таблица ниже) |
| Producer | news |
| ContractVersion | ожидается 1; несовпадение — warn-and-proceed |
| Эффект (событие → что меняется) | создаются NotificationEntity (Pending) для подписчиков/автора |
Топики и маппинг
| Топик | Событие → NotificationKind | Получатели | Urgency |
|---|---|---|---|
pin.news.post-published.v1 | PostPublished | подписчики автора (резолв через internal API news) | Low |
pin.news.comment-added.v1 | CommentAdded | автор поста (+ участники треда) | 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/CommentId | Guid | Да | id для NewsNotificationPayload + резолв аудитории |
Title/AuthorName | строка | Да | → Post { Id, Title }, Author { Id, Name } |
CommentPreview | string? | comment | → CommentPreview (усечённый текст) |
Алгоритм
Общая логика enqueue (резолв каналов/шаблона/адреса, материализация, согласованность) — как в crm-events.md; дельта источника — резолв аудитории (шаг 3).
- Контекст события. Восстановить метаданные и сверить
ContractVersion(несовпадение — warn-and-proceed); consumer-идемпотентность поnotifications-news:{EventType}:{MessageId}(дубль →skipped-duplicate, ack). ОпределитьKindпо типу события. - Проверка входа. Обязательные идентификаторы непусты; иначе →
NTF_INVALID_PAYLOAD(DLQ). - Резолв получателей. Подписчики автора (запрос аудитории у
news, пагинация, timeout 3с) или автор поста; дедуп; приCommentAddedот самого автора — исключить его; пусто →no-recipients(ack). Недоступность источника аудитории →NTF_AUDIENCE_UNAVAILABLE(retriable, backoff). - Резолв доставки и материализация. По каждому получателю — каналы для
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 не публикует. - Согласованность и результат. Батч уведомлений и отметка идемпотентности фиксируются в одном
согласованном изменении, offset — после фиксации. Успех/
no-recipients→ ack; retriable → retry; non-retriable → DLQ.
Диаграмма
Идемпотентность и replay
- Consumer-level по
MessageId; business-level — dedup-unique (INV-NTF-1). Replay: да (dedup + повторный резолв аудитории идемпотентен).
Retry и DLQ
| Сбой | Код | Retry | DLQ |
|---|---|---|---|
| Инфраструктурный | — | backoff + пауза партиции | Нет |
Аудитория news недоступна | NTF_AUDIENCE_UNAVAILABLE | Да (backoff) | После исчерпания |
| Нарушение payload | NTF_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.*.v1→ignored-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-каналам).
Обратные ссылки
Автоссылки: где используется этот документ.
- События (2): События сервиса news: карта топиков и отклонённые события/консюмеры, Event Contract: pin.news.post-published (PostPublishedKafkaEvent)
- index.md (1): Notifications Service
Связанные документы
- Целевой агрегат: notification.md; общий алгоритм — crm-events.md; входные контракты — news/events/; карта — README контекста.
- ADR: 0052, 0056.