feat(outbox): attempt cap → DLQ + admin endpoint + 6 unit-тестов

После N (default 1000) неудачных попыток публикации событие маркируется
dlq_at и исключается из polling. Без cap poller бесконечно ретраил мёртвые
сообщения (orphan topic, ACL deny), забивая логи и lag-метрику.

- 0011-outbox-dlq.xml: dlq_at column + перестроенный idx_outbox_unpublished
  (исключает DLQ) + idx_outbox_dlq для admin-листинга.
- OutboxPoller: ordinis.outbox.attempt-max (default 1000), markDlq() при
  достижении cap, новый counter ordinis_outbox_dlq_total.
- OutboxLagGauge: ordinis_outbox_dlq_size gauge — алерт на > 0.
- OutboxEventRepository: countPending() (без DLQ), findByDlqAtIsNotNull(Pageable).
- OutboxAdminController: GET /admin/outbox/{stats,dlq} для UI.
- 6 unit-тестов через mock-maker-subclass (JDK 25 ломает Mockito inline).
This commit is contained in:
Zimin A.N.
2026-05-04 15:43:56 +03:00
parent f7efb23e09
commit e182b30c58
10 changed files with 391 additions and 9 deletions
@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.27.xsd">
<!--
Outbox DLQ: после N неудачных попыток публикации (ordinis.outbox.attempt-max,
default 1000) события маркируются dlq_at и исключаются из поллинга. Без cap
poller бесконечно ретраит мёртвые сообщения (orphan topic, тупик ACL),
забивая лог и Prometheus alert ordinis_outbox_pending_events.
-->
<changeSet id="0011-1-outbox-dlq-column" author="ordinis">
<addColumn tableName="outbox_events">
<column name="dlq_at" type="TIMESTAMPTZ"/>
</addColumn>
</changeSet>
<changeSet id="0011-2-outbox-rebuild-pending-index" author="ordinis">
<comment>
Pending index должен исключать dlq_at — иначе poller продолжит "видеть"
DLQ-записи и ordinis_outbox_pending_events будет показывать их в lag.
</comment>
<sql>
DROP INDEX IF EXISTS idx_outbox_unpublished;
CREATE INDEX idx_outbox_unpublished ON outbox_events (created_at)
WHERE published_at IS NULL AND dlq_at IS NULL;
</sql>
<rollback>
<sql>
DROP INDEX IF EXISTS idx_outbox_unpublished;
CREATE INDEX idx_outbox_unpublished ON outbox_events (created_at)
WHERE published_at IS NULL;
</sql>
</rollback>
</changeSet>
<changeSet id="0011-3-outbox-dlq-index" author="ordinis">
<comment>Партиальный индекс для быстрого листинга DLQ в admin UI.</comment>
<sql>
CREATE INDEX idx_outbox_dlq ON outbox_events (dlq_at DESC)
WHERE dlq_at IS NOT NULL;
</sql>
<rollback>
<sql>DROP INDEX IF EXISTS idx_outbox_dlq;</sql>
</rollback>
</changeSet>
</databaseChangeLog>
@@ -15,5 +15,6 @@
<include file="changes/0008-fix-scope-check-uppercase.xml"/>
<include file="changes/0009-fix-records-scope-check.xml"/>
<include file="changes/0010-envers-tables.xml"/>
<include file="changes/0011-outbox-dlq.xml"/>
</databaseChangeLog>