# Практика с RabbitMQ В этом задании вы научитесь работать с брокером сообщений [RabbitMQ](https://rabbitmq.com/getstarted.html). Вам предстоит реализовать сервис, который принимает URL изображения, асинхронно генерирует описание, сохраняет его в файл и возвращает по запросу. Генерация описания имитируется готовой функцией из заготовки. ## Архитектура и интерфейс сервиса Очередь сообщений позволяет передать длительную задачу на асинхронную обработку. Она помогает распределять работу между несколькими воркерами и сглаживать всплески нагрузки. В этом задании вам также предстоит обеспечить обработку задач при временных отказах. Архитектура сервиса 1. Сервер с REST API принимает запросы пользователей и передаёт задачи на генерацию описаний через RabbitMQ. 2. Воркеры получают задачи, генерируют описания и записывают их в общее хранилище, откуда сервер читает результаты. В задании хранилище — это [Docker volume](https://docs.docker.com/engine/storage/volumes/), см. [docker-compose.yml](docker-compose.yml). Версия RabbitMQ для проверки задана в [docker-compose.yml](docker-compose.yml). Используйте это окружение для локального тестирования. ### REST API Тела запросов и успешных ответов передаются в JSON. Успешный запрос возвращает код `200`; формат тела ответа при ошибке не регламентируется. Идентификатор изображения может быть строкой или целым числом; в ответах POST и GET он должен иметь одинаковый тип. ``` POST /api/v1.0/images Принимает URL изображения для обработки и возвращает id изображения. Каждый запрос получает новый уникальный id, даже при повторной отправке того же URL. Если JSON некорректен или image_url отсутствует, пуст или не является строкой, верните код 400. Тело запроса: { "image_url": str } Тело ответа: { "image_id": str | int } ``` ``` GET /api/v1.0/images Возвращает id всех обработанных изображений без повторов. Порядок не важен. Тело ответа: { "image_ids": List[str | int] } ``` ``` GET /api/v1.0/images/ Возвращает описание для данного изображения, если оно уже было обработано, и код 404, если id неизвестен или обработка ещё не завершена. Тело ответа: { "caption": str } ``` ## Компоненты задания и оценивание За выполнение задания можно получить 10 баллов: | Группа тестов | Баллы | Что проверяется | | --- | ---: | --- | | `BASIC_API` | 1 | Пустой список результатов, отклонение некорректных запросов и ответ `404` для неизвестного ID. | | `BASIC_PROCESSING` | 3 | Передача задач через RabbitMQ, обработка воркерами и получение результатов через REST API без отказов. | | `CALLBACKS` | 2 | Уведомления о завершении обработки через RabbitMQ: сервер отвечает на `GET .../images` без чтения списка файлов в общей директории. | | `FAULT_TOLERANCE_1` | 1 | Работа сервиса после простоя соединений с брокером. | | `FAULT_TOLERANCE_2` | 1 | Сохранность запросов при недоступности и перезапуске брокера во время отправки задач. | | `FAULT_TOLERANCE_3` | 1 | Завершение задач после отказа одного или обоих воркеров. | | `FAULT_TOLERANCE_4` | 1 | Восстановление после отказов брокера и воркеров, сохранность уведомлений и обработка повторных доставок. | Баллы за каждую группу начисляются при прохождении всех её тестов. Провал одной группы не обнуляет баллы за остальные. Группа `FAULT_TOLERANCE_4` предполагает реализацию уведомлений через RabbitMQ; передавать в них сами описания изображений не требуется. При проверке отказоустойчивости уже запущенный REST API должен принимать корректные запросы с кодом `200` и возвращать готовые результаты, даже когда брокер или воркеры недоступны. Запрос считается принятым, если POST вернул `200`. После восстановления брокера, связи и хотя бы одного воркера каждый принятый запрос должен быть обработан, а его описание — доступно через API. Таймаут каждого HTTP-запроса в тестах — 3 секунды. Сервер и общее хранилище в этих сценариях не отказывают: восстановление после их перезапуска или потери данных не требуется. Повторная доставка задач и уведомлений возможна. Повторная обработка задачи допустима, но её ID должен появляться в списке результатов только один раз. Приложите отчёт `solution/readme.md` с описанием устройства решения и выполненных компонентов. Для отказоустойчивости обоснуйте, почему принятые запросы не теряются. На защите нужно объяснить работу реализованных компонентов и подтвердить это обоснование. Общие требования к отчёту приведены в [правилах курса](../readme.md#отчёт). ## Заготовки для решения В папке `solution/server` находится заготовка для сервера с реализацией REST API на Flask. В папке `solution/worker` содержится заготовка для воркера. Для генерации описания используйте функцию `produce_image_caption`, передавая ей строку `image_url`. Скачивать и сохранять само изображение не нужно: функция имитирует его обработку. В файл сохраняется только полученное описание в кодировке UTF-8. Очередь задач должна называться `task_queue`, а описания должны сохраняться в файлы `/data/{image_id}.txt`. Эти соглашения используются в тестах. Для взаимодействия с RabbitMQ на Python предлагается использовать библиотеку [Pika](https://github.com/pika/pika) (см. семинар 5). Весь код решения должен размещаться в папке `solution`. При сдаче решения в тестирующую систему отправляется только эта папка, изменения вне неё учитываться не будут. ## Порядок выполнения задания Начните с [материалов семинара 5](../../materials/05-indirect-comm/seminar/) и примера [work_queues](../../materials/05-indirect-comm/seminar/work_queues/). Дополнительно можно обращаться к официальным [туториалам RabbitMQ](https://www.rabbitmq.com/tutorials). 1. Реализуйте передачу задач от REST API через RabbitMQ, обработку воркерами и получение результатов. Проверьте группы `BASIC_API` и `BASIC_PROCESSING`. При необходимости измените Dockerfile сервера и воркера. 2. Добавьте уведомления сервера о завершении обработки и проверьте группу `CALLBACKS`. Полезный пример обмена запросами и ответами есть в [туториале по RPC](https://www.rabbitmq.com/tutorials/tutorial-six-python). При использовании Pika учитывайте [ограничения потокобезопасности](https://pika.github.io/pika/latest/faq/). 3. Работу над отказоустойчивостью начните с `test_heartbeats_timeout` (`FAULT_TOLERANCE_1`). Изучите [документацию по heartbeat](https://www.rabbitmq.com/docs/heartbeats) и настройки в [rabbitmq.conf](tests/rabbitmq.conf). Для `BlockingConnection` полезно [обсуждение работы соединения при редкой отправке сообщений](https://github.com/pika/pika/discussions/1382). 4. Подумайте, в каких случаях ваше решение может потерять принятый запрос. Изучите [подтверждения доставки в RabbitMQ](https://www.rabbitmq.com/docs/confirms); для Pika также полезен [пример асинхронного отправителя](https://github.com/pika/pika/blob/main/examples/asynchronous_publisher_example.py). Проверьте отдельно отказы брокера при отправке задач (`FAULT_TOLERANCE_2`) и отказы воркеров (`FAULT_TOLERANCE_3`). 5. Перейдите к комплексным сценариям `FAULT_TOLERANCE_4`: перезапуску брокера, сохранности уведомлений и повторной доставке. Сопоставьте поведение решения с [руководством RabbitMQ по надёжности](https://www.rabbitmq.com/docs/reliability). Подумайте, какие моменты отказа ещё стоит проверить самостоятельно. 6. Подготовьте отчёт с обоснованием отказоустойчивости реализованных компонентов, запустите все тесты и сдайте решение с отчётом в тестирующую систему. При отладке сопоставляйте логи компонентов с состоянием очередей в веб-интерфейсе RabbitMQ: `http://localhost:15672`, логин и пароль — `guest`. Как на семинаре, обращайте внимание на сообщения Ready и Unacked. ## Тестирование решения Тесты, проверяющие решение, находятся в папке `tests`. Бонусы за пробелы в тестах начисляются по [общим правилам](../readme.md#бонусы-за-пробелы-в-тестах). ### Локальное тестирование Команды ниже выполняйте из папки задания. Для локального запуска тестов установите зависимости из `tests/requirements.txt`. Перед запуском тестов соберите образы: ``` docker compose build ``` Запуск всех тестов выполняется с помощью команды: ``` python3 tests/main.py ``` Отдельный тест можно запустить так: ``` pytest -vs --tb=short tests/test_server.py::test_single_image ``` Для тестирования в окружении, аналогичном тестирующей системе, выполните из папки задания: ```bash docker run --privileged --pull always --rm -v ./solution:/hw/solution distsys.ru/course/message-queue:latest ``` ### Проверка в тестирующей системе Отправьте ваше решение в тестирующую систему следуя [инструкции](../readme.md) и дождитесь результатов. ## ЧаВо **Можно ли реализовать решение не на Python?** Да. Замените заготовки и Dockerfile сервера и воркера. Реализуйте аналог функции-заглушки, возвращающий строку по `image_url`; точное совпадение результата с Python-версией не требуется. **Можно ли обрабатывать изображения локально на сервере?** Нет. Описания должны генерироваться только воркерами. Решение, которое генерирует их на сервере, не засчитывается (0 баллов).