Skip to content

Repository files navigation

Booking Service

Сервис записи на встречи. Клиент создаёт бронь через REST API — она сразу сохраняется со статусом pending, а в очередь уходит фоновая задача. Воркер подтверждает бронь (имитируя поход во внешний сервис, который иногда падает), переводит её в confirmed или failed и пишет mock-уведомление в лог.

Что под капотом

  • FastAPI — асинхронный API с валидацией на Pydantic и Swagger из коробки.
  • SQLAlchemy 2.0 + Alembic — работа с БД и миграции.
  • PostgreSQL в проде, SQLite в тестах.
  • Celery + Redis — фоновая обработка (Redis заодно и broker, и backend).
  • structlog — логи в JSON.
  • slowapi — лимит запросов на создание брони.

API сделан асинхронным (драйвер asyncpg), чтобы не блокировать event loop на запросах к БД. А вот воркер синхронный: задачи Celery по своей природе синхронны, тащить туда event loop смысла нет — проще и надёжнее взять psycopg2. Поэтому в конфиге две строки подключения: DATABASE_URL для API и SYNC_DATABASE_URL для воркера и Alembic.

Эндпоинты

Метод Путь Что делает
POST /bookings создать бронь (name, datetime, service_type)
GET /bookings/{id} посмотреть статус
GET /bookings список, фильтр ?status=, пагинация ?limit=&offset=
DELETE /bookings/{id} отменить — только пока бронь pending

service_type: consultation, meeting, support. status: pendingconfirmed или failed.

Swagger / OpenAPI

Когда сервис запущен, есть интерактивная документация — там же можно дёргать эндпоинты прямо из браузера:

Swagger UI

Модель данных и индексы

Одна таблица bookings:

Поле Тип Заметки
id VARCHAR(36) UUID, первичный ключ
name VARCHAR(255) имя
datetime TIMESTAMPTZ время встречи
service_type VARCHAR(32) consultation / meeting / support
status VARCHAR(16) pending / confirmed / failed, есть индекс
created_at, updated_at TIMESTAMPTZ проставляются БД

На status повешен индекс (ix_bookings_status) — это единственное поле, по которому идёт фильтрация в GET /bookings?status=..., и список по статусу хочется отдавать без полного скана таблицы.

Запуск

cp .env.example .env
docker-compose up --build

Поднимутся Postgres, Redis, API и воркер. Миграции накатываются сами при старте API. Дальше API живёт на http://localhost:8000, документация — на /docs.

Примеры запросов и ответов

Создать бронь — сразу возвращается pending, обработка уходит в очередь:

curl -X POST localhost:8000/bookings \
  -H 'Content-Type: application/json' \
  -d '{"name":"Pavel","datetime":"2026-07-01T10:00:00Z","service_type":"meeting"}'
{
  "id": "da7693e0-c330-4596-8e50-4e4ba7bd1df7",
  "name": "Pavel",
  "datetime": "2026-07-01T10:00:00Z",
  "service_type": "meeting",
  "status": "pending",
  "created_at": "2026-06-17T14:19:11.752391Z",
  "updated_at": "2026-06-17T14:19:11.752391Z"
}

Невалидное тело — 422 с понятной ошибкой:

curl -X POST localhost:8000/bookings \
  -H 'Content-Type: application/json' \
  -d '{"name":"Pavel","datetime":"2026-07-01T10:00:00Z","service_type":"spaceship"}'
{
  "detail": [
    {
      "type": "enum",
      "loc": ["body", "service_type"],
      "msg": "Input should be 'consultation', 'meeting' or 'support'",
      "input": "spaceship"
    }
  ]
}

Статус брони — через секунду-другую воркер переводит её в confirmed (или в failed с шансом ~15%):

curl localhost:8000/bookings/da7693e0-c330-4596-8e50-4e4ba7bd1df7
{
  "id": "da7693e0-c330-4596-8e50-4e4ba7bd1df7",
  "name": "Pavel",
  "datetime": "2026-07-01T10:00:00Z",
  "service_type": "meeting",
  "status": "confirmed",
  "created_at": "2026-06-17T14:19:11.752391Z",
  "updated_at": "2026-06-17T14:19:11.860709Z"
}

Несуществующий id — 404:

{ "detail": "Booking not found" }

Список с фильтром по статусу и пагинацией:

curl "localhost:8000/bookings?status=confirmed&limit=20&offset=0"
{
  "items": [
    {
      "id": "da7693e0-c330-4596-8e50-4e4ba7bd1df7",
      "name": "Pavel",
      "datetime": "2026-07-01T10:00:00Z",
      "service_type": "meeting",
      "status": "confirmed",
      "created_at": "2026-06-17T14:19:11.752391Z",
      "updated_at": "2026-06-17T14:19:11.860709Z"
    }
  ],
  "total": 1,
  "limit": 20,
  "offset": 0
}

Отмена — работает только пока бронь pending (тогда 204 без тела). Если бронь уже confirmed/failed409:

curl -X DELETE localhost:8000/bookings/da7693e0-c330-4596-8e50-4e4ba7bd1df7
{ "detail": "Only pending bookings can be cancelled" }

В логах воркера на успешной брони видно mock-уведомление:

{"booking_id": "da7693e0-...", "channel": "mock", "event": "notification_sent", "level": "info", "timestamp": "2026-06-17T14:19:11.860Z"}

Тесты

Docker для них не нужен — всё на SQLite in-memory и Celery в eager-режиме:

python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
pytest

Покрыты все эндпоинты (включая 422/404/409, фильтр и пагинацию) и логика воркера: успех, провал, идемпотентность, отсутствующая бронь. Или просто make test.

Решения, которые стоит пояснить

Идемпотентность. Первое, что делает задача — читает бронь и смотрит на статус. Если брони уже нет или она не pending, задача молча выходит. За счёт этого повторная доставка из очереди или ретрай с тем же booking_id ничего не ломают и не шлют второе уведомление — оно уходит ровно один раз, на переходе pending → confirmed.

Задача ставится после коммита. Сначала бронь фиксируется в БД, и только потом уходит в очередь. Иначе воркер мог бы схватить задачу раньше, чем строка появилась в базе.

Сбои и ретраи. Внешний вызов с вероятностью FAILURE_RATE бросает исключение. На ошибке задача переотправляется с экспоненциальным backoff, а в failed переходит только когда MAX_RETRIES исчерпан — разовый сбой не должен хоронить бронь навсегда.

UUID как id. Не светим автоинкремент наружу, удобно для идемпотентности. Храним строкой на 36 символов, чтобы одинаково работало и в Postgres, и в SQLite.

Раздельные БД в тестах. API и воркер тестируются на своих in-memory базах: гонять одно in-memory-соединение разом через async- и sync-движок — путь к плавающим багам. Внешний вызов в тестах всегда замокан, на рандом не полагаемся.

Структура

app/
  main.py            FastAPI: роутеры, rate-limit, lifespan
  config.py          настройки (pydantic-settings)
  database.py        async-сессия для API
  models.py          модель Booking, статусы, типы услуг
  schemas.py         Pydantic-схемы
  crud.py            операции с БД
  logging_config.py  structlog → JSON
  rate_limit.py      лимитер для POST
  api/
    bookings.py      эндпоинты
    dispatch.py      постановка задачи (мокается в тестах)
  worker/
    celery_app.py    инстанс Celery
    db.py            sync-сессия
    external.py      мок внешнего сервиса
    tasks.py         process_booking
alembic/             миграции
docker/entrypoint.sh миграции + запуск API/воркера
tests/               pytest
docs/img/            скриншоты для README
docker-compose.yml   db + redis + api + worker

Переменные окружения

Шаблон — в .env.example:

Переменная Зачем
DATABASE_URL подключение API (async)
SYNC_DATABASE_URL подключение воркера и Alembic (sync)
CELERY_BROKER_URL / CELERY_RESULT_BACKEND Redis для Celery
FAILURE_RATE вероятность сбоя внешнего сервиса (0..1)
RATE_LIMIT лимит на POST (синтаксис slowapi)
MAX_RETRIES сколько раз ретраить упавшую задачу
LOG_LEVEL уровень логов

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages