Подготовить skeleton лабораторной 6 по мессенджеру

This commit is contained in:
Mikhail Verkovykh
2026-09-10 18:01:36 +03:00
commit 470d9d6f1e
38 changed files with 5013 additions and 0 deletions
+61
View File
@@ -0,0 +1,61 @@
# Семантика API
Этот текст дополняет [openapi.json](openapi.json). Обязательный API соответствует уровню 3 текущей лабораторной. Факультативный crop или rate limiting не становятся обязательными только из-за наличия допустимого имени варианта или кода ошибки в схеме.
## Формат и ошибки
- JSON — UTF-8, `Content-Type: application/json`. UUID — строка; время — RFC 3339 UTC с `Z`, назначается сервером. Часы не считаются уникальным ID.
- Каждый ответ API, включая ошибки и health, содержит непустой `X-Request-Id`. Тело ошибки: `{"error":{"code":"invalid_request","message":"...","request_id":"..."}}`; `request_id` совпадает с заголовком. Сообщение ошибки не раскрывает stack trace, SQL, credentials или наличие чужого объекта.
- Обязательные строки не состоят только из пробельных символов. `username` строго `[a-z0-9_]{3,32}`, уникален; `display_name` — 1100 Unicode code points. JSON с лишними полями или неверными типами — `400`; malformed JSON также `400`.
- `400 invalid_request` — неверные поля, UUID, cursor, limit; `401 unauthorized` — нет действительной идентификации/сессии; `404 not_found` — объекта нет или он чужой; `409 conflict` — username занят, повторный ключ с другим сообщением, объект ещё не готов; `413 payload_too_large`; `415 unsupported_media_type`; `429 rate_limited`; `503 unavailable`. Неожиданное исключение — `500 internal_error` без внутренних деталей.
- Сначала аутентификация, затем доступ к конкретному ресурсу. Любой аутентифицированный участник чата может читать его историю и вложения и писать в него; посторонний получает `404`. Списки содержат только доступные объекты. Не используйте `403` для раскрытия существования чужого чата.
- `GET /health/live` проверяет только жизнь процесса, возвращает `200 {"status":"ok"}`. `GET /health/ready` возвращает 200, если приложение может безопасно обслуживать обязательный синхронный путь; иначе 503 в общем формате ошибки. Какие зависимости критичны, зафиксируйте в отчёте; недоступность Grafana не должна останавливать API.
## Пользователи, чаты и сообщения
- `POST /users` публичен; возвращает `201 User`. Пароль/его hash и сессионные данные никогда не входят в User.
- `POST /chats` принимает title и явный список существующих `member_ids`, включая вызывающего. Дубликаты, неизвестные участники и отсутствие вызывающего — 400. Чат создаётся атомарно. GET списка чатов сортируется по `(created_at, id)` по возрастанию.
- Отправитель сообщения выводится из текущей идентификации, поле `sender_id` в запросе не принимается. Текст — до 4000 Unicode code points. Нельзя отправить пустой/пробельный текст без вложений.
- История использует keyset cursor: `(created_at, id)`, по возрастанию; `limit` — целое 1–100, по умолчанию 50. Ответ `{"items":[...],"next_cursor":null}`. Непустой cursor — непрозрачная для клиента строка; последний элемент предыдущей страницы не повторяется. Cursor привязан к ресурсу/запросу: от другого чата — 400. Изменять `limit` при продолжении можно.
- `next_cursor` равен null, если на момент чтения следующей записи нет. Гарантируется отсутствие повторов при обходе неизменной истории; snapshot изоляция между несколькими HTTP-запросами не требуется. При конкурентных вставках опишите выбранную семантику и ограничения часов.
- Подтверждённое сообщение видно следующему чтению через любую API-реплику. Допускается кэш, но он не должен возвращать устаревшую историю после успешной записи. Удаление и изменение участников в обязательный API не входят.
## Идентификация и сессии
**Lab1:** защищённые endpoint требуют `X-User-Id: <UUID существующего пользователя>`; отсутствие/неизвестный ID — 401. Это учебный выбор пользователя: любой клиент может выдать себя за другого.
**Lab27:** `POST /auth/sessions` принимает только `Authorization: Basic <base64(username:password)>`, без JSON body. Username ограничен ASCII; пароль кодируется UTF-8, 12128 code points, двоеточие в пароле допустимо. Неверные credentials — одинаковый 401 и `WWW-Authenticate: Basic realm="messenger", charset="UTF-8"`.
Вход возвращает `201 {"token":"...","token_type":"Bearer","expires_at":"...Z","user":{...}}`. Токен — непрозрачное случайное значение, не username/UUID и не обязательный JWT; энтропия не менее 256 бит. Храните серверную сессию с expiry; TTL задаётся `SESSION_TTL_SECONDS`, по умолчанию 3600 (в тесте истечения можно 2). В обычном smoke TTL должен быть не менее 300.
Все защищённые endpoint принимают только `Authorization: Bearer <token>`. Истёкший/отозванный/неизвестный токен — 401 с `WWW-Authenticate: Bearer realm="messenger"`. Basic на бизнес-endpoint — 401. `X-User-Id` с lab2 игнорируется: он не меняет владельца сессии и сам по себе не даёт доступ. `DELETE /auth/sessions/current` отзывает текущий токен и возвращает 204 без тела; повтор с отозванным токеном — 401.
Пользователи, чаты, сообщения и ещё действительные сессии переживают перезапуск приложения и используемых хранилищ с сохранёнными volumes. TTL не продлевается из-за рестарта. Пароли уже с lab2 хешируются библиотечным password KDF (Argon2id, scrypt или bcrypt с учётом ограничений библиотеки), не SHA-256 и не plaintext. Если bcrypt не поддерживает весь диапазон UTF-8 паролей без усечения, выберите другую библиотечную схему. В lab7 обязателен Argon2id.
Basic — это кодирование, не шифрование. HTTP lab1–6 допускается только для локального изолированного стенда с синтетическими данными; для удалённой демонстрации нужен TLS. Lab7 использует HTTPS обязательно. См. [RFC 7617](https://www.rfc-editor.org/info/rfc7617/).
## Балансировка и повторная отправка (с lab3)
- Каждый ответ, созданный API, содержит `X-Instance-Id`; значение стабильно для жизни процесса. Ошибки соединения, созданные самим LB, могут не иметь этого заголовка. В lab7 используйте случайный alias реплики без раскрытия hostname/IP.
- Для POST сообщения обязателен `Idempotency-Key` (ASCII `[A-Za-z0-9._:-]{1,128}`). Нет/неверный ключ — 400. Область уникальности: текущий пользователь + HTTP-метод + путь чата + ключ.
- При повторе того же валидного JSON в течение 24 часов сервер возвращает тот же код 201 и то же Message. Порядок ключей JSON и отсутствующий `attachment_ids` вместо `[]` семантически равны. Другой body при том же ключе — 409. Разные пользователи/чаты могут использовать одинаковый ключ независимо.
- Одновременные повторы через разные реплики создают ровно одно сообщение. Повтор снова проверяет действительность сессии и доступ. Транзиентные 5xx не фиксируются как окончательный результат; побочный эффект и запись результата должны быть атомарны.
- Ключ обязателен только для сообщений. Самостоятельный retry загрузки файла или создания чата может создать ещё один объект; не добавляйте прозрачные повторы этих POST без расширения контракта.
## Вложения (с lab4)
- `POST /chats/{chat_id}/attachments?filename=photo.png`: сырые bytes одного PNG/JPEG; `Content-Type: image/png` или `image/jpeg`. Не multipart. Максимум **10 MiB = 10 485 760 байт**. MIME проверяется по содержимому; заведомо повреждённые/чужие форматы — 415, превышение bytes — 413. Не более 40 миллионов декодированных пикселей (превышение — 413). Устанавливайте лимит до полной распаковки изображения.
- `filename` — отображаемое имя (1–255 code points). Не используйте его как путь/ключ S3. Имя с `/`, `\`, NUL или управляющими символами — 400. Bucket приватен; object key генерирует сервер. Оригинал возвращается побайтно, его lowercase SHA-256 и размер соответствуют исходному файлу.
- Lab4 возвращает `201 Attachment` со `status=ready`, `variants=[]`, `error_code=null` только после сохранения оригинала и метаданных. Lab5–7 возвращают `202 Attachment` со снимком `status=queued`; готовность определяется последующим GET метаданных. Быстрый worker может закончить до первого GET — это нормально.
- Файл заранее привязан к чату. В POST сообщения добавляется `attachment_ids` (0–10 уникальных ID, по умолчанию []). Можно отправить `text=""` с хотя бы одним вложением. Все вложения должны принадлежать этому чату; чужое/неизвестное — 404. Любой участник может ссылаться на доступное вложение чата. Failed-вложение прикреплять нельзя (409); queued/processing с lab5 можно.
- `GET /attachments/{id}` и `/content` доступны только участникам чата. Download возвращает байты с истинным Content-Type, через авторизованный API, **без redirect** в обязательном контракте. Приватный bucket сам по себе не заменяет эту проверку. `variant` по умолчанию original. Для queued/processing content — 409; для failed — 409. Отсутствующий вариант ready-объекта — 404.
- В ready-метаданных `error_code=null`. В failed — краткий машинный код (например `invalid_image`, `processing_failed`, `retry_exhausted`), без stack trace. `size_bytes`, `sha256`, `content_type` всегда описывают оригинал; у вариантов отдельные поля. Публичный API не раскрывает ключи бакета, внутренние адреса и credentials.
- Прямой upload/download по временной подписи — факультативное расширение. Его дополняют ограничение срока/размера, уникальный ключ и подтверждение реально загруженного объекта; он не заменяет стандартный endpoint. Особенности подписанных URL: [S3](https://docs.aws.amazon.com/AmazonS3/latest/userguide/using-presigned-url.html).
## Фоновая обработка (с lab5)
До 202 все байты приняты в **устойчивый staging**, а намерение обработать файл надёжно сохранено. HTTP-передача клиента не исчезает и не становится мгновенной. Staging — приватный S3-prefix либо общий persistent volume, доступный worker; локальная память/API ephemeral disk не подходит. Worker асинхронно переносит/публикует оригинал в конечный S3-prefix и создаёт варианты; если staging уже в S3, опишите эту границу честно.
Обязательный thumbnail: JPEG, изображение вписано в 256×256 с сохранением пропорций, без увеличения маленьких исходников, EXIF orientation применяется; alpha компонуется на белый. Размеры округляются вниз, минимум 1 px. Fixture 320×200 должен дать 256×160. Статус ready ставится после сохранения оригинала и thumbnail. Crop на 4: JPEG, центрированный квадрат 128×128 (для маленьких исходников допускается увеличение). Optimized на 4: WebP, вписан в 1280×1280 без увеличения, параметры качества и удаления EXIF документируются; не обещайте уменьшение каждого маленького файла.
Допустимые состояния и события описаны в `jobs.schema.json` и `attachment-states.md`. Доставка — at least once; обработка идемпотентна, повторы не размножают варианты. Финальные ошибки наблюдаемы, бесконечный немой retry недопустим. Обязательный smoke ждёт ready до 60 секунд; это бюджет локального smoke для одного файла при свободном worker, не универсальный production SLA. В очередь идут ссылки/ID, не бинарное содержимое.
+21
View File
@@ -0,0 +1,21 @@
# Состояния вложения и договор между producer/worker
Сообщение broker соответствует [jobs.schema.json](jobs.schema.json). Можно применять native headers брокера для trace context, но семантика carrier сохраняется; для проверки покажите отображение полей. `job_id` идентифицирует одну логическую работу и не меняется при redelivery; `attempt` начинается с 1 и отражает управляемую попытку обработки (broker redelivery сам по себе не создаёт новый job_id). Ключ эффекта — `(attachment_id, pipeline_version, variant)`.
| Переход | Условие | Что устойчиво сохранено |
| --- | --- | --- |
| нет → queued | Принимаем upload; только после durable handoff отвечаем 202 | Полный проверенный оригинал в staging, метаданные, намерение обработки |
| queued → processing | Worker захватил/арендовал работу | Попытка, lease/deadline или эквивалентная защита |
| processing → queued | Временная ошибка и остались попытки | Причина без секретов, время следующей попытки |
| processing → ready | Все обязательные объекты записаны и результат атомарно опубликован | Оригинал, thumbnail (на 4 ещё crop/optimized), метаданные/checksums |
| processing → failed | Ошибка постоянная или попытки исчерпаны | error_code и запись, доступная оператору |
| processing → queued | Истёк lease после смерти worker | Восстановленная работа, без дублирования эффектов |
| failed → queued | Явный операторский replay, доступен на 4 | Audit, новая попытка, прежний логический эффект защищён от дубля |
Повторная доставка уже ready-задачи — no-op с ack после проверки завершённого эффекта. Конкурентные попытки не перезаписывают более новый pipeline_version. Основной курс использует pipeline_version=1; версионирование нескольких pipeline — трек 5. Заявляйте готовность только когда все обещанные уровнем варианты доступны. Нельзя откатывать ready в processing из-за старого дубля.
**Окна отказа для защиты:** после staging до записи задания; после COMMIT до publish; после publish до confirm; после записи S3 до COMMIT результата; после COMMIT результата до ack. На 3 покажите сохранность успешно принятой работы и повторную доставку; на 4 автоматическое восстановление разрыва DB/broker (outbox/эквивалент) и зависшего processing.
Staging не удаляется до безопасной публикации результатов. Неуспешные/непривязанные исходники имеют документированный retention и cleanup; он не удаляет текущую работу. Broker принимает только ID/служебные данные, не произвольный URL для скачивания и не путь из клиентского filename. Worker берёт доверенные storage metadata из БД.
Ack подтверждает устойчивую обработку, publisher confirm — приём брокером; это разные границы. См. [RabbitMQ reliability](https://www.rabbitmq.com/docs/reliability). At-least-once + идемпотентный эффект не означает exactly-once доставку.
+60
View File
@@ -0,0 +1,60 @@
{
"CreateUser": {
"username": "alice",
"display_name": "Алиса",
"password": "Example-only-not-a-real-secret-123"
},
"User": {
"id": "11111111-1111-4111-8111-111111111111",
"username": "alice",
"display_name": "Алиса",
"created_at": "2026-09-01T12:00:00Z"
},
"CreateChat": {
"title": "Проект по ВНП",
"member_ids": [
"11111111-1111-4111-8111-111111111111",
"22222222-2222-4222-8222-222222222222"
]
},
"Chat": {
"title": "Проект по ВНП",
"member_ids": [
"11111111-1111-4111-8111-111111111111",
"22222222-2222-4222-8222-222222222222"
],
"id": "33333333-3333-4333-8333-333333333333",
"created_at": "2026-09-01T12:00:00Z"
},
"CreateMessage": {
"text": "Привет, Боб!"
},
"Message": {
"id": "44444444-4444-4444-8444-444444444444",
"chat_id": "33333333-3333-4333-8333-333333333333",
"sender_id": "11111111-1111-4111-8111-111111111111",
"text": "Привет, Боб!",
"created_at": "2026-09-01T12:00:00Z",
"attachment_ids": []
},
"MessagePage": {
"items": [
{
"id": "44444444-4444-4444-8444-444444444444",
"chat_id": "33333333-3333-4333-8333-333333333333",
"sender_id": "11111111-1111-4111-8111-111111111111",
"text": "Привет, Боб!",
"created_at": "2026-09-01T12:00:00Z",
"attachment_ids": []
}
],
"next_cursor": null
},
"Error": {
"error": {
"code": "not_found",
"message": "Объект не найден",
"request_id": "example-request-id"
}
}
}
+88
View File
@@ -0,0 +1,88 @@
{
"lab": 6,
"kind": "requirements, not runnable deployment",
"required_for_grade_3": [
{
"role": "api",
"count_min": 2,
"student_implements": true,
"persistent_local_state": false
},
{
"role": "postgres",
"purpose": "users/chats/membership/messages/metadata",
"persistent_volume": true
},
{
"role": "load_balancer",
"public_entrypoint": true,
"backend_readiness": true
},
{
"role": "s3",
"choose_one": [
"MinIO",
"SeaweedFS",
"RustFS"
],
"private_bucket": true,
"persistent_volume": true
},
{
"role": "broker",
"choose_one": [
"RabbitMQ",
"Redis Streams",
"Valkey Streams",
"NATS JetStream",
"documented durable equivalent"
],
"persistent_volume": true,
"delivery": "at-least-once"
},
{
"role": "worker",
"separate_process": true
},
{
"role": "otel_collector",
"signals": [
"metrics",
"logs",
"traces"
]
},
{
"role": "telemetry_storage",
"signals": [
"metrics",
"logs",
"traces"
],
"possible_combination": [
"Prometheus",
"Loki",
"Tempo"
],
"persistent_volume": true
},
{
"role": "grafana",
"provisioning_in_git": true
}
],
"optional_components": [
{
"role": "redis_or_valkey",
"required_for_lab2_grade4": true,
"note": "Choose one; if only store of sessions, configure durable persistence."
}
],
"rules": [
"Choose Compose or Swarm for lab3+; do not need both.",
"Never publish internal API replica ports in bypass of LB from lab3.",
"Versions/digests must be fixed in implementation.",
"Do not remove volumes in restart/failover checks.",
"Document service names, healthchecks, bootstrap/migrations and credentials."
]
}
+63
View File
@@ -0,0 +1,63 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "urn:mtusi:messenger:attachment-job:v1",
"title": "Durable attachment processing job v1",
"type": "object",
"additionalProperties": false,
"required": [
"schema_version",
"job_id",
"type",
"attachment_id",
"pipeline_version",
"attempt",
"created_at"
],
"properties": {
"schema_version": {
"const": 1
},
"job_id": {
"type": "string",
"format": "uuid"
},
"type": {
"const": "attachment.process.v1"
},
"attachment_id": {
"type": "string",
"format": "uuid"
},
"pipeline_version": {
"type": "integer",
"minimum": 1
},
"attempt": {
"type": "integer",
"minimum": 1
},
"created_at": {
"type": "string",
"format": "date-time"
},
"traceparent": {
"type": "string",
"pattern": "^00-[0-9a-f]{32}-[0-9a-f]{16}-[0-9a-f]{2}$"
},
"tracestate": {
"type": "string",
"maxLength": 512
}
},
"examples": [
{
"schema_version": 1,
"job_id": "11111111-1111-4111-8111-111111111111",
"type": "attachment.process.v1",
"attachment_id": "22222222-2222-4222-8222-222222222222",
"pipeline_version": 1,
"attempt": 1,
"created_at": "2026-09-01T12:00:00Z"
}
]
}
+28
View File
@@ -0,0 +1,28 @@
# Контракт наблюдаемости
Ниже логические сигналы. Допустимы стандартные semantic conventions выбранного SDK и их native metric names: зафиксируйте соответствие в `REPORT.md`, dashboard обязан ссылаться на реально существующие имена/единицы. Переименование SDK не повод дублировать одну метрику двумя инструментами. Версии SDK, semantic conventions, Collector distribution и backend закрепляются в решении.
| Сигнал | Минимальные поля/измерения | Уровень |
| --- | --- | --- |
| HTTP requests | method, route template, status class; counter | 3 |
| HTTP duration | histogram в секундах или документированное преобразование из ms | 3 |
| Jobs processed / failed | job type, outcome, counter | 3 |
| Job processing duration | job type, outcome, histogram | 3 |
| API и worker logs | timestamp, severity, service.name, event, trace_id, span_id при активном span | 3 |
| API и worker traces | реальные server/consumer spans, duration, outcome; не искусственный статический trace | 3 |
| Queue depth / oldest job age | queue name, количество / секунды | 4 |
| Pool/saturation | DB pool used/waiting, worker busy/capacity, process/container CPU/RAM | 4 |
| Сквозная связь upload → publish → consume → S3/DB | W3C traceparent/tracestate; parent-child или link по выбранной семантике | 4 |
| End-to-end time to ready | от durable acceptance до публикации ready, histogram | 4 |
Resource attributes: `service.name` (messenger-api / messenger-worker), `service.version`, `service.instance.id`, `deployment.environment.name=lab`. Список имён может отличаться, если явно сопоставлен. `X-Request-Id` коррелирует HTTP-ошибку с логом; trace_id — не замена всех request IDs.
**Не labels метрик:** user_id, chat_id, message_id, attachment_id, request_id, raw URL, filename, текст. Для маршрута используйте `/api/v1/chats/{chat_id}/messages`, не реальный UUID. В логах/трассах допустимы ограниченные технические ID для учебных синтетических данных, если обоснована необходимость; пароли, Authorization, session token, body сообщения, картинка, private key и signed URL запрещены.
Для HTTP error ratio заранее определите numerator/denominator: например 5xx/все бизнес-запросы, исключая health; не смешивайте неверный пароль клиента с отказом БД. p95 агрегируется из histogram buckets всех нужных реплик, а не средних p95. Границы buckets должны соответствовать ожидаемым задержкам. Backlog — не queue processing latency.
В Git находятся Collector config, backend configs, datasources/dashboard provisioning, dashboard JSON и на 4 alert rules. У alerts есть условие, окно, порог, `for` при необходимости, описание и локальный runbook. Отправка сообщений в почту/мессенджер не требуется.
Минимальные контролируемые инциденты для 4: (1) остановить worker и увидеть рост возраста задач; (2) сделать DB медленной/недоступной и отличить её от медленного HTTP handler по trace/метрикам. После восстановления backlog уменьшается, alerts resolved. Третий сценарий — недоступность backend telemetry: SDK/Collector ограничивают RAM/очередь, API сохраняет работу, потери telemetry считаются и объясняются.
Collector — получатель/обработчик/экспортёр, не долговременная БД. Grafana — интерфейс и запросы к источникам. Логи можно отправлять OTLP в совместимый backend; не закладывайте удалённый/устаревший exporter без проверки текущей сборки. См. [Collector configuration](https://opentelemetry.io/docs/collector/configuration/), [Trace Context](https://www.w3.org/TR/trace-context/) и [histograms](https://prometheus.io/docs/practices/histograms/).
File diff suppressed because it is too large Load Diff