slot-booking/README.md
Dmitriy 16f3f851b1 feat: slot booking API with hot cache and oversell protection
GET /slots/availability, POST /slots/{id}/hold, POST /holds/{id}/confirm
and DELETE /holds/{id} on Laravel 12 and MySQL 8.

- availability cached for 10s behind a single-flight lock with a stale
  copy, invalidated after commit on confirm and cancel
- oversell prevented by a conditional decrement, verified by the number
  of affected rows rather than a preceding select
- idempotency enforced by a unique index on holds.idempotency_key
- active holds counted with an expires_at filter, so no background
  cleanup is required for correctness
- 27 feature tests against MySQL, service documentation, Postman collection

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 14:47:18 +03:00

185 lines
6.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Slot Booking API
API бронирования слотов: горячий кеш доступности, временные холды с идемпотентностью, защита от оверсела.
**Стек:** Laravel 12 (PHP 8.2+), MySQL 8+. Внешних сервисов не требуется — кеш и блокировки хранятся в той же базе.
Описание устройства сервиса — [docs/README.md](docs/README.md). Коллекция Postman — [docs/postman](docs/postman/slot-booking-api.postman_collection.json).
## Быстрый старт
```bash
docker run -d --name stl-mysql \
-e MYSQL_ROOT_PASSWORD=secret -e MYSQL_DATABASE=slots \
-p 3307:3306 mysql:8
```
```bash
cp .env.example .env && composer install && php artisan key:generate
```
```bash
php artisan migrate --seed
```
```bash
php artisan serve --port=8001
```
Сидер создаёт три слота с вместимостью 10, 5 и 1. Слот с одним местом позволяет воспроизвести конфликт оверсела двумя запросами.
## Тесты
Тесты выполняются против MySQL (см. `phpunit.xml`), поэтому нужна отдельная база:
```bash
docker exec stl-mysql mysql -uroot -psecret -e "CREATE DATABASE IF NOT EXISTS slots_test"
```
```bash
php artisan test
```
27 тестов, 90 проверок: кеш и его инвалидация, поведение при потере гонки за пересчёт, идемпотентность, истечение холдов, защита от оверсела при конкурентном подтверждении.
## Эндпоинты
| Метод | Путь | Ответ |
|---|---|---|
| `GET` | `/slots/availability` | `200` — массив слотов |
| `POST` | `/slots/{id}/hold` | `201` создан / `200` повтор по тому же ключу / `409` мест нет / `422` некорректный `Idempotency-Key` / `404` |
| `POST` | `/holds/{id}/confirm` | `200` / `409` мест нет либо холд неактивен / `404` |
| `DELETE` | `/holds/{id}` | `200` / `404` |
Ошибки приходят в виде `{"message": "...", "reason": "<код>"}`. Коды: `capacity_exhausted`, `hold_cancelled`, `hold_expired`, `idempotency_key_reuse`, `not_found`.
## Примеры запросов
Прогон против запущенного сервера. Слот `3` имеет `capacity = 1`.
### Доступность
```bash
curl -s http://127.0.0.1:8001/slots/availability
```
```json
[{"slot_id":1,"capacity":10,"remaining":10},{"slot_id":2,"capacity":5,"remaining":5},{"slot_id":3,"capacity":1,"remaining":1}]
```
### Создание холда
```bash
curl -si -X POST http://127.0.0.1:8001/slots/3/hold -H 'Idempotency-Key: 6e517e8e-1470-4a78-bdbd-e2b50776a6a0'
```
```
HTTP/1.1 201 Created
{"hold_id":1,"slot_id":3,"status":"held","expires_at":"2026-07-29T18:19:56+00:00","confirmed_at":null,"cancelled_at":null}
```
### Повтор с тем же ключом — тот же холд, `200` вместо `201`
```bash
curl -si -X POST http://127.0.0.1:8001/slots/3/hold -H 'Idempotency-Key: 6e517e8e-1470-4a78-bdbd-e2b50776a6a0'
```
```
HTTP/1.1 200 OK
{"hold_id":1,"slot_id":3,"status":"held","expires_at":"2026-07-29T18:19:56+00:00","confirmed_at":null,"cancelled_at":null}
```
Второй строки в `holds` не появляется.
### Конфликт при оверселе
```bash
curl -si -X POST http://127.0.0.1:8001/slots/3/hold -H 'Idempotency-Key: bab7c4be-3184-40d6-ab90-b4118c40bcb3'
```
```
HTTP/1.1 409 Conflict
{"message":"Slot 3 has no remaining capacity.","reason":"capacity_exhausted"}
```
### Переиспользование ключа для другого слота
```bash
curl -si -X POST http://127.0.0.1:8001/slots/1/hold -H 'Idempotency-Key: 6e517e8e-1470-4a78-bdbd-e2b50776a6a0'
```
```
HTTP/1.1 422 Unprocessable Content
{"message":"Idempotency-Key 6e517e8e-... was already used for a different slot than 1.","reason":"idempotency_key_reuse"}
```
### Подтверждение
```bash
curl -si -X POST http://127.0.0.1:8001/holds/1/confirm
```
```
HTTP/1.1 200 OK
{"hold_id":1,"slot_id":3,"status":"confirmed","expires_at":"2026-07-29T18:19:56+00:00","confirmed_at":"2026-07-29T18:14:57+00:00","cancelled_at":null}
```
Кеш инвалидирован, остаток слота `3` обновился сразу:
```bash
curl -s http://127.0.0.1:8001/slots/availability
```
```json
[{"slot_id":1,"capacity":10,"remaining":10},{"slot_id":2,"capacity":5,"remaining":5},{"slot_id":3,"capacity":1,"remaining":0}]
```
Повторный `confirm` того же холда возвращает `200` и тот же результат — место списывается один раз.
### Отмена
```bash
curl -si -X DELETE http://127.0.0.1:8001/holds/1
```
```
HTTP/1.1 200 OK
{"hold_id":1,"slot_id":3,"status":"cancelled","expires_at":"2026-07-29T18:19:56+00:00","confirmed_at":"2026-07-29T18:14:57+00:00","cancelled_at":"2026-07-29T18:14:57+00:00"}
```
```bash
curl -s http://127.0.0.1:8001/slots/availability
```
```json
[{"slot_id":1,"capacity":10,"remaining":10},{"slot_id":2,"capacity":5,"remaining":5},{"slot_id":3,"capacity":1,"remaining":1}]
```
Подтвердить отменённый холд уже нельзя:
```bash
curl -si -X POST http://127.0.0.1:8001/holds/1/confirm
```
```
HTTP/1.1 409 Conflict
{"message":"Hold 1 is no longer active and cannot be confirmed.","reason":"hold_cancelled"}
```
## Структура
```
app/Enums/HoldStatus.php held | confirmed | cancelled
app/Exceptions/SlotBookingException.php доменные отказы и их HTTP-статус
app/Http/Controllers/AvailabilityController.php
app/Http/Controllers/HoldController.php
app/Http/Requests/StoreHoldRequest.php валидация заголовка Idempotency-Key
app/Http/Resources/HoldResource.php
app/Models/Slot.php
app/Models/Hold.php scopeActive() — определение активного холда
app/Services/SlotService.php транзакции, кеш, идемпотентность
config/slots.php TTL кеша и TTL холда
routes/api.php
docs/README.md устройство сервиса
docs/postman/ коллекция Postman
```