улучшить observability и документацию outbox
- обновить README с текущим Kafka, DB, outbox и scaling flow - расширить логи listener, ingestion, publisher и outbox worker - не логировать raw payload и outbox payload - закрыть test controller явным property - добавить Micrometer counters для ingestion и outbox outcomes - сохранить listener flow, payload contract и delivery semantics без изменений
This commit is contained in:
@@ -1,35 +1,92 @@
|
||||
# route-passport-kafka-consumer
|
||||
# pcp-route-processing-service
|
||||
|
||||
Минимальный сервис на Kotlin + Spring Boot, который:
|
||||
- слушает Kafka topic;
|
||||
- получает в сообщении JSON паспорта маршрута;
|
||||
- парсит JSON в DTO;
|
||||
- больше ничего не делает.
|
||||
## Назначение сервиса
|
||||
|
||||
## Что внутри
|
||||
`pcp-route-processing-service` принимает route passport из Kafka, парсит входной JSON, нормализует GeoJSON geometry в WKT, сохраняет паспорт маршрута в БД и создаёт downstream events в outbox.
|
||||
|
||||
- `RoutePassportConsumerApplication` — точка входа Spring Boot.
|
||||
- `RoutePassportMessageListener` — Kafka listener.
|
||||
- `RoutePassportJsonParserService` — сервис парсинга входного JSON в DTO.
|
||||
- `RoutePassportDto` — итоговый DTO из `pcp-types-lib`.
|
||||
- `RawPassportEnvelopeDto` и связанные `Raw*` DTO — модель входного JSON из `pcp-types-lib`.
|
||||
Публикация downstream events выполняется не из Kafka listener, а через scheduled outbox worker:
|
||||
|
||||
## Как запустить
|
||||
1. `RoutePassportMessageListener` читает input Kafka record.
|
||||
2. `RoutePassportJsonParserService` валидирует и парсит payload.
|
||||
3. `RoutePassportIngestionService` в одной DB transaction сохраняет `route_passports` и создаёт `route_passport_outbox`.
|
||||
4. Listener подтверждает offset только после успешного ingestion.
|
||||
5. `RoutePassportOutboxWorker` claim'ит due outbox rows и публикует их через `RoutePassportKafkaPublisherService`.
|
||||
|
||||
1. Поднять Kafka.
|
||||
2. Указать адрес брокера в `application.yml` или через переменную окружения `KAFKA_BOOTSTRAP_SERVERS`.
|
||||
3. При необходимости указать topic через `APP_KAFKA_TOPIC`.
|
||||
4. Запустить приложение:
|
||||
## Kafka topics
|
||||
|
||||
```bash
|
||||
./gradlew bootRun
|
||||
Input topic:
|
||||
- `pcp.request.survey-georeference.v1`
|
||||
|
||||
DLQ topic:
|
||||
- `pcp.request.survey-georeference-dlq.v1`
|
||||
|
||||
Output topics:
|
||||
- `pcp.route.in.v1` - событие для mission-planning flow.
|
||||
- `pcp.route.georeference.v1` - событие для request-service matching flow.
|
||||
|
||||
## DB tables
|
||||
|
||||
- `route_passports` - сохранённые паспорта маршрутов и source metadata входного Kafka record.
|
||||
- `route_passport_outbox` - pending/published/failed downstream events для Kafka publication.
|
||||
|
||||
## Horizontal scaling
|
||||
|
||||
Несколько pod'ов сервиса могут безопасно запускать outbox worker одновременно.
|
||||
|
||||
Outbox claim использует DB-level concurrency:
|
||||
- `SELECT ... FOR UPDATE SKIP LOCKED` выбирает due `PENDING` rows.
|
||||
- `lockedUntil` выставляется в той же transaction, что и claim.
|
||||
- После commit другие instances не claim'ят row, пока `lockedUntil` в будущем.
|
||||
- Если instance умер после claim, abandoned row снова станет доступна после истечения `lockedUntil`.
|
||||
- In-memory locks, leader election и single-instance assumptions не используются.
|
||||
|
||||
## Delivery semantics
|
||||
|
||||
Delivery semantics: at-least-once.
|
||||
|
||||
Exactly-once не обещается. Kafka transactions не используются. Downstream consumers должны быть идемпотентны к повторной доставке одного и того же события.
|
||||
|
||||
## Error handling
|
||||
|
||||
Input Kafka flow:
|
||||
- contract/parsing errors не подтверждают offset и обрабатываются configured retry/DLQ policy;
|
||||
- после исчерпания retry record публикуется в `pcp.request.survey-georeference-dlq.v1`;
|
||||
- временные ошибки ingestion retry'ятся bounded policy.
|
||||
|
||||
Output Kafka flow:
|
||||
- downstream events сначала сохраняются в `route_passport_outbox`;
|
||||
- output Kafka errors не теряются, а переводят row обратно в `PENDING` с bounded exponential backoff;
|
||||
- после исчерпания `maxAttempts` из outbox row событие получает статус `FAILED`.
|
||||
|
||||
## Observability
|
||||
|
||||
Логи содержат operational identifiers без полного payload:
|
||||
- source topic, partition, offset, key и payload size для input ingestion;
|
||||
- `routeId`, `routeNameFull`, `traceId`, `correlationId`;
|
||||
- outbox id, event type, target topic, message key, attempts, status, `nextAttemptAt`, `lockedUntil`, `publishedAt`;
|
||||
- exception class/message для failure path.
|
||||
|
||||
Доступные Micrometer counters:
|
||||
- `route_passport_ingest_success_total`
|
||||
- `route_passport_ingest_failure_total`
|
||||
- `route_passport_outbox_published_total`
|
||||
- `route_passport_outbox_retry_total`
|
||||
- `route_passport_outbox_failed_total`
|
||||
|
||||
## Operational notes
|
||||
|
||||
Test controller is disabled by default and must not be exposed in production unless explicitly enabled with:
|
||||
|
||||
```yaml
|
||||
app:
|
||||
route-processing:
|
||||
test-controller:
|
||||
enabled: true
|
||||
```
|
||||
|
||||
## Сообщение, которое ожидает сервис
|
||||
## Current business limitations
|
||||
|
||||
Сервис ожидает, что value Kafka-сообщения — это JSON того же формата, что и `src/test/resources/passport_example.json`.
|
||||
Поле `geometry` должно приходить строкой в формате WKT.
|
||||
|
||||
## Примечание
|
||||
|
||||
Поскольку в запросе был указан просто «слушает очередь», реализация сделана через Kafka topic как наиболее типичный вариант для стека Kotlin + Spring и Kafka-ориентированного сервиса.
|
||||
- `routeStatus` сейчас остаётся `PROCESSED`.
|
||||
- Сервис не делает request matching.
|
||||
- Сервис не считает coverage.
|
||||
- Сервис не вызывает request-service или mission-service по HTTP.
|
||||
|
||||
Reference in New Issue
Block a user