Микросервис асинхронной обработки платежей: API → Outbox → RabbitMQ → Consumer → Webhook.
История изменений: CHANGELOG.md
- FastAPI + Pydantic v2
- SQLAlchemy 2.0 (async) + PostgreSQL
- RabbitMQ + FastStream
- Alembic, Docker Compose
- OpenTelemetry (traces + metrics via OTLP), Jaeger, Prometheus (local stack)
При OTEL_ENABLED=true все процессы экспортируют traces и metrics через OTLP в otel-collector.
| Инструмент | URL (local compose) |
|---|---|
| Jaeger UI | http://localhost:16686 |
| Prometheus | http://localhost:9090 |
Сквозной distributed trace: POST /payments → outbox → RabbitMQ → consumer → webhook dispatcher → HTTP webhook.
Переменные окружения:
| Env | Default | Описание |
|---|---|---|
OTEL_ENABLED |
false |
Включить traces + metrics |
OTEL_EXPORTER_OTLP_ENDPOINT |
http://localhost:4317 |
OTLP gRPC endpoint |
OTEL_METRIC_EXPORT_INTERVAL_MS |
10000 |
Интервал экспорта метрик |
Локальный стек observability поднимается вместе с docker compose -f docker-compose.yaml -f docker-compose.local.yaml up.
Все команды проекта — через make. Справка по умолчанию:
make # то же, что make help
make helpВывод сгруппирован по блокам:
| Блок | Примеры |
|---|---|
| General | env, install |
| PostgreSQL (pg) | pg-up, pg-down, pg-logs |
| API | api-dev, api-prod, gen-openapi, smoke |
| RabbitMQ (rabbit) | rabbit-up, rabbit-down |
| Consumer | consumer-dev, consumer-up, nginx-up |
| Database migrations (db) | db-migrate, db-migrate-docker, db-revision |
| Code quality | lint, format, test, test-cov |
| Full stack | up, up-prod, down, logs |
make env
make install
make pg-up && make rabbit-up
make db-migrate
# Терминал 1 — API (publisher встроен в lifespan)
make api-dev
# Терминал 2 — consumer
make consumer-dev
# Smoke-тест
make smokemake env
make up # postgres → migrate → api, publisher, consumer
curl http://localhost:8000/api/v1/health
make smokeСервис migrate запускается автоматически при make up / make up-prod и выполняет alembic upgrade head. api, publisher и consumer стартуют только после успешного завершения миграций.
Повторно прогнать миграции (например, после git pull с новыми ревизиями):
make db-migrate-docker # local
make db-migrate-prod # prodProd (internal network + nginx):
make up-prod
curl http://localhost/api/v1/health| Сервис | Назначение |
|---|---|
postgres |
База данных |
rabbitmq |
Брокер сообщений |
migrate |
Alembic migrations (one-shot, автоматически при up) |
api |
HTTP API (gunicorn в production) |
publisher |
Публикация outbox → payments.new |
consumer |
Обработка платежей + webhook |
nginx |
Reverse proxy (только prod overlay) |
Почему
publisherотдельно? В production API (APP_ENV=production) outbox publisher в lifespan выключен — публикация идёт отдельным процессом. Это часть Outbox pattern.
Спецификация: docs/openapi.yaml
X-API-Key— обязателен для/api/v1/payments/*/api/v1/health— без ключа (для Docker/K8s health probes)
export API_KEY=change-me-in-production
export IDEM_KEY="order-$(date +%s)"
# Health
curl -s http://localhost:8000/api/v1/health
curl -s http://localhost:8000/api/v1/health/ready
# Создать платёж
curl -s -X POST http://localhost:8000/api/v1/payments \
-H "Content-Type: application/json" \
-H "X-API-Key: $API_KEY" \
-H "Idempotency-Key: $IDEM_KEY" \
-d '{
"amount": "100.50",
"currency": "RUB",
"description": "Test payment",
"metadata": {"order_id": "1"},
"webhook_url": "https://example.com/webhook"
}'
# Получить платёж
curl -s -H "X-API-Key: $API_KEY" \
http://localhost:8000/api/v1/payments/<payment_id>Или: make smoke
webhook_url задаёт клиент, поэтому:
- SSRF-защита — URL проверяется при создании платежа (схема/порт/credentials) и повторно резолвится перед доставкой с блокировкой приватных адресов.
- HMAC-подпись — при заданном
WEBHOOK_SIGNING_SECRETзапросы подписываются HMAC-SHA256 (заголовкиX-Webhook-Signature,X-Webhook-Timestamp).
Детали, альтернативы и trade-offs: ADR 0008. Переменные — в .env.example.
| Этап | Попытки | Механизм |
|---|---|---|
| Outbox → RabbitMQ | 3 | tenacity, exponential backoff |
| Webhook HTTP | 3 | tenacity, exponential backoff |
| Consumer | 3 | x-retry-count header + nack/requeue → DLQ |
DLQ: очередь payments.new.dlq (см. RABBITMQ_PAYMENTS_NEW_DLQ).
Границы системы проговорены явно — детали решений в ADR.
- At-least-once на всех этапах:
outbox → RabbitMQ(ADR 0001),RabbitMQ → consumer(ADR 0004),webhook_deliveries → HTTP(ADR 0003). Exactly-once нет → получатель вебхука обязан быть идемпотентным поpayment_id. - Дубли вебхуков возможны при падении между успешным HTTP-ответом и
mark_delivered: запись останетсяPENDINGи будет доставлена повторно. - Порядок событий не гарантируется — параллельная обработка и ретраи могут переставлять доставки.
- Идемпотентность создания платежа гарантируется UNIQUE-ключом + обработкой гонки
IntegrityError(ADR 0005).
publisher,consumer,webhook-dispatcher— stateless и горизонтально масштабируются. Конкурентные поллеры безопасны за счётSELECT ... FOR UPDATE SKIP LOCKED; повторный enqueue вебхука — за счётON CONFLICT (payment_id) DO NOTHING.- Состояние живёт в БД: при рестарте процессов ничего не теряется — незавершённые записи подхватываются на следующем поллинге.
Rate-limiting, ротация API-ключей, listing/пагинация платежей, exactly-once, архивация/очистка outbox, пин на резолвнутый IP для вебхуков.
- DNS-rebinding между валидацией/резолвом и TCP-коннектом закрыт не полностью (ADR 0008).
- Requeue без задержки на транзиентных ошибках consumer'а — попытки исчерпываются быстро (ADR 0004).
- Один вебхук на платёж (
webhook_deliveries.payment_idUNIQUE): повторная нотификация о том же платеже по дизайну не создаётся.
См. .env.example. Профиль APP_ENV=local|production задаётся в Makefile targets.
make pg-up && make db-migrate # PostgreSQL нужен для integration-тестов
make test
make test-cov # 100% coverage по app/
make lintPOST /payments → DB (payment + outbox)
↓
Outbox Publisher
↓
payments.new (RabbitMQ)
↓
Consumer
gateway (2-5s, 90%) → update status
↓
webhook (retry ×3)
Почему приняты ключевые решения — в docs/adr/: Outbox pattern, отдельные процессы publisher/dispatcher, DLQ-стратегия, идемпотентность, observability, distributed tracing и безопасность вебхуков.