К основному содержимому
Программа курса

Лабораторная 01: Сеть ненадёжна

Постройте API сервиса задач. Добавьте тайм-ауты, повторные запросы и ключи идемпотентности. Смоделируйте потерю ответа.

Код стенда, установка и шаблон отчёта

Лабораторная 1. Потерянный ответ не означает потерянную операцию

Создайте надёжный HTTP-контракт одного сервиса задач. После занятия вы должны уметь отличать повтор запроса от новой операции, обосновывать атомарную границу и воспроизводить неопределённый результат. Предварительно: HTTP, Python, Docker, лекции о моделях отказов и взаимодействии сервисов. Рабочая длительность — около 90 минут плюс самостоятельное завершение; это предложение автора, не регламент ВШЭ.

Подготовка и запуск

Нужны Python 3.12+, Docker и Compose. Общая инструкция описывает установку, контракт, изоляцию и cleanup. Команды выполняются из корня репозитория:

python3 -m venv /tmp/ds-course-venv
/tmp/ds-course-venv/bin/python -m pip install -r labs/distributed-systems/requirements.txt
PYTHONDONTWRITEBYTECODE=1 /tmp/ds-course-venv/bin/python labs/distributed-systems/runner.py --lab 1 --solution starter --report /tmp/lab01.json

Starter запускает реальный API и настоящий кластер etcd, но намеренно не проходит атомарность. Исправляйте starter/protocol.py; повторяйте ту же команду. Эталон доступен преподавателю через --solution reference. Проверки в tests/scenarios.py общие; их ослабление не является решением.

Задание

  1. Зафиксируйте область Idempotency-Key: один namespace запуска, 1–128 ASCII-символов, хранение на весь прогон. Тело — {"amount": 1} с необязательным label; детали в README. У одинакового ключа и одинакового JSON должна быть одна идентичность. Изменённое содержание — 409.
  2. Замените check-then-write на условную etcd-транзакцию. Task, idempotency и outbox должны получить одну ревизию: намерение публикации нужно сохранить уже сейчас для совместимости с Lab3. Брокер пока не требуется для завершения Lab1.
  3. Опишите, что клиент делает после timeout: использует тот же ключ и не выводит «запись не произошла». GET /tasks/<id> читает linearizable состояние.

Три сценария

  • Ответ потерян после commit. Runner отправляет X-Lab-Drop-Response: true; API фиксирует данные и закрывает настоящее TCP-соединение. Первый ответ недоступен. Повтор возвращает 200 и исходный task_id; изменённый amount даёт 409.
  • Конкурентные повторы. Двенадцать потоков одновременно посылают одинаковый запрос. Ровно один получает 201, одиннадцать — 200; task_id один. Два конкурентных запроса с одним ключом и разными amount дают один 201 и один 409.
  • Падение API. После подтверждённой записи процесс API получает SIGKILL и запускается заново. Повтор возвращает прежний task_id; задача читается из etcd, а не из памяти процесса.

Проверяемый инвариант: для одного ключа за срок хранения существует не более одной принятой операции; её task, idempotency и outbox фиксируются атомарно. Подтверждённая задача переживает падение API в заявленной модели. Это ещё не гарантия завершения worker и не глобальное «exactly once».

Что сдавать

  • Diff starter с объяснением точки линеаризации и судьбы конкурента, проигравшего compare.
  • /tmp/lab01.json и минимальный фрагмент JSONL с task_id/trace_id; укажите версию, команду и число попыток.
  • Короткую последовательность «commit → потеря ответа → повтор» и ответ, почему 201 нельзя вернуть двум конкурентам.
  • Отчёт: сохраните и неудачный прогон, затем сравните с исправленным. В Git не кладите venv, container volumes и полные диагностические логи.

Разбор и чтение

Сопоставьте границу операции с Lamport, Time, Clocks, and the Ordering of Events, и границу локальной атомарности с Helland, Life beyond Distributed Transactions. Исследовательские деки преподавателя по Lamport и Helland поддерживают упражнение; публичная методичка ссылается на первоисточники. Точный используемый механизм: etcd transactions.

На защите объясните, что изменится при удалении idempotency через час, при другой сериализации JSON и при замене локального эффекта внешним платёжным API. Расширение: смоделируйте два ключа для одного бизнес-намерения и покажите, почему транспортная идемпотентность не распознает их автоматически.

Восстановление и ограничения

Runner сам завершает только свой уникальный Compose project, включая volumes. Ctrl+C также запускает cleanup; после SIGKILL используйте сохранённый compose.env и инструкцию README. Все порты — loopback. Не используйте глобальный prune. Потеря всего компьютера, дисков большинства и Byzantine-сбои не проверяются. Timeout может означать неизвестный исход, а не отказ записи.