Files
hse-2026/homework/01-guarantees/readme.md
T

24 KiB
Raw Blame History

Гарантии доставки

В этом задании вам предстоит реализовать различные гарантии доставки сообщений в распределённой системе.

В нашей системе есть два узла, и нам требуется организовать одностороннюю передачу текстовых сообщений между выполняющимися на узлах процессами. Процесс Sender будет принимать сообщения от локального пользователя S и отправлять их по сети процессу Receiver. Процесс Receiver будет принимать сообщения от Sender и доставлять их локальному пользователю R. Под доставкой подразумевается отправка локального сообщения, идентичного исходному сообщению от S.

Вам необходимо написать четыре реализации Sender и Receiver, обеспечивающие следующие гарантии доставки сообщений:

  1. Не более одного раза (at most once). Каждое сообщение от S должно быть или доставлено R ровно один раз или не доставлено вовсе. Иными словами, нельзя допускать повторные доставки сообщений.
  2. Не менее одного раза (at least once). Каждое сообщение от S должно быть доставлено R, при этом допускаются повторы.
  3. Ровно один раз (exactly once). Каждое сообщение от S должно быть доставлено R ровно один раз, то есть повторы не допускаются.
  4. Ровно один раз и с сохранением порядка (exactly once + ordered). Каждое сообщение от S должно быть доставлено R ровно один раз и в порядке их отправки S.

Процессы могут взаимодействовать друг с другом путём обмена сообщениями через сетевой транспорт со следующими характеристиками: отдельное сообщение может быть потеряно, доставленные сообщения не искажаются, сообщения могут дублироваться, порядок их получения не гарантируется, все получаемые сообщения были кем-то отправлены (нет сообщений «из воздуха»). При этом сеть обладает свойством fair-loss: если отправитель неограниченно повторяет передачу одного и того же сообщения, хотя бы одна его копия рано или поздно достигнет получателя. Это предположение действует для передачи в обоих направлениях и соблюдается в тестах. Отказы узлов в данном задании отсутствуют. Все указанные гарантии рассматриваются только в рамках описанной модели.

Независимо от типа гарантий, если сеть ведёт себя надёжно (нет потерь сообщений), все сообщения от S должны доставляться R. Это исключает, например, тривиальную реализацию гарантии 1, которая не отправляет ничего по сети.

Ваша реализация не должна делать предположений об уникальности содержимого доставляемых сообщений. Например, в разные моменты времени могут быть отправлены два сообщения с идентичным текстом. В этом случае для гарантии 2 требуется доставить эти сообщения пользователю не менее двух раз, а для гарантий 3 и 4 — ровно два раза. Также не следует делать предположений о размере сообщений — он может быть произвольным.

Несложно заметить, что реализация последней, самой сильной гарантии покрывает первые три гарантии. Тем не менее мы просим вас реализовать каждую гарантию отдельно. Так вы наглядно увидите, какие дополнительные ресурсы и накладные расходы требуются для поддержки той или иной гарантии. На практике не всегда требуются самые сильные гарантии: например, порядок доставки может быть неважен и «платить» за него будет расточительно. Поэтому, если вы реализуете только последнюю гарантию, мы не зачтём вам остальные.

На максимальный балл требуется также оптимизировать ресурсы, потребляемые для обеспечения каждой из гарантий. Во-первых, это объём памяти, используемой процессами, то есть размер хранимого ими состояния. Overhead-тесты сравнивают потребление памяти с заданными порогами: если бессрочно хранить все когда-либо обработанные сообщения или их идентификаторы, решение в эти пороги не уложится. При этом актуальные данные, например неподтверждённые сообщения, хранить можно и в некоторых гарантиях необходимо. Постарайтесь удалять лишнюю или неактуальную информацию (например, если доставлены все 100 предыдущих сообщений, то так ли необходимо хранить все 100 идентификаторов сообщений?). При оптимизации памяти учитывайте не только количество хранимых элементов, но и накладные расходы выбранного представления данных. Во-вторых, это число и суммарный объём сообщений, передаваемых по сети между Sender и Receiver. Постарайтесь не передавать по сети лишние данные и не отправлять слишком много сообщений, особенно если они могут быть отброшены получателем, то есть переданы по сети зря. В процессе оптимизации вы должны осознать возникающие при такой оптимизации компромиссы между потреблением памяти, нагрузкой на сеть и скоростью доставки сообщений.

Реализация

Для реализации и тестирования решения используется учебный фреймворк AnySystem (см. материалы первого семинара). В папке solution размещена заготовка для решения guarantees.py. Вам надо доработать реализации классов ...Sender и ...Receiver для каждой гарантии так, чтобы они проходили все тесты.

Ваши реализации процессов AnySystem должны соблюдать правило изоляции процессов: изменяемое состояние каждого процесса должно храниться только в self, а общая изменяемая память между процессами запрещена. Все взаимодействия между процессами должны происходить через сообщения, как и в реальной распределённой системе.

Sender

Сообщения от S передаются процессу с помощью локальных сообщений, см. метод on_local_message(). Все сообщения имеют тип MESSAGE и одинаковую структуру: в единственном поле text содержится строка с текстом сообщения. Для взаимодействия с Receiver вы можете использовать сообщения произвольного типа и структуры. Приходящие от Receiver сообщения следует обрабатывать в методе on_message(). Также вы можете устанавливать таймеры в любом из методов и обрабатывать их наступление в on_timer().

Receiver

Данный процесс не принимает локальные сообщения, поэтому метод on_local_message() не используется. Сетевые сообщения следует обрабатывать в методе on_message(). Также вы можете устанавливать таймеры в любом из методов и обрабатывать их наступление в on_timer().

Важно правильно реализовать доставку сообщений локальному пользователю R, иначе тесты не будут проходить. Для этого вы должны отправить локальное сообщение с помощью метода ctx.send_local(). Сообщение должно быть полностью идентично исходному сообщению, принятому Sender от пользователя S, то есть иметь тот же тип MESSAGE и поле text с тем же значением. Других полей в сообщении быть не должно.

Оценивание

Компоненты задачи и их вклад в оценку:

  • Корректная реализация гарантии at most once - 2 балла
    • проходят все тесты на эту гарантию без "OVERHEAD..."
  • Корректная реализация гарантии at least once - 2 балла
    • проходят все тесты на эту гарантию без "OVERHEAD..."
  • Корректная реализация гарантии exactly once - 2 балла
    • проходят все тесты на эту гарантию без "OVERHEAD..."
  • Корректная реализация гарантии exactly once + ordered - 2 балла
    • проходят все тесты на эту гарантию без "OVERHEAD..."
  • Оптимизация потребляемых ресурсов - 2 балла*
    • проходят тесты "OVERHEAD..." для всех гарантий (1 балл)
    • в отчёте описаны возникающие при оптимизации компромиссы и обоснованы используемые подходы (1 балл)
    • *засчитывается только при успешном выполнении всех предыдущих компонентов

Краткий отчёт с описанием решения в solution/readme.md обязателен и сдаётся вместе с решением до дедлайна. Без отчёта автоматические тесты запускаются, но защита не проводится и решение не засчитывается.

Автоматический SCORE, который выводят тесты, составляет не более 9 баллов. Ещё 1 балл за описание и обоснование оптимизаций выставляется отдельно по отчёту.

Штрафы:

  • Для гарантии A используется реализация более сильной гарантии B - минус 1 балл за гарантию A

Тестирование

Локальное тестирование

Тесты находятся в папке tests. Есть два варианта их запуска.

Рекомендуемый вариант запуска тестов - через готовый Docker-образ. В этом случае используемое окружение будет аналогично тестирующей системе. Убедитесь, что на вашей машине установлен Docker Engine (можно использовать Docker Desktop). Для запуска тестов выполните команду:

docker run --pull always --rm -t -v ./solution:/solution distsys.ru/course/guarantees:latest [ЗДЕСЬ МОЖНО УКАЗАТЬ ОПЦИИ]

Для запуска полного набора тестов с теми же параметрами, что и в тестирующей системе, выполните команду:

docker run --pull always --rm -t -v ./solution:/solution distsys.ru/course/guarantees:latest -m 100 -c -o

Вы также можете запустить тесты, скомпилировав их локально с помощью компилятора Rust. Такой вариант может быть удобен, если вы хотите лучше изучить или доработать тесты. Убедитесь, что на вашей машине установлен Rust. Скомпилируйте тесты с помощью команды cargo install --locked --path tests. Для запуска тестов выполните команду:

distsys-guarantees [ЗДЕСЬ МОЖНО УКАЗАТЬ ОПЦИИ]

Доступные опции тестов можно посмотреть с помощью флага -h. Опишем наиболее важные из них:

  • Флаг -d включает вывод трасс - последовательностей событий во время выполнения каждого из тестов. Его рекомендуется использовать при отладке решений.
  • Опция -m задает количество запусков рандомизированных тестов (chaos monkey). Значение по умолчанию - 0. Как только ваше решение будет проходить основные тесты, установите значение в 10 и убедитесь, что эти тесты проходят. Далее можно проверить решение на 100 запусках (-d лучше убрать для скорости) - такое значение используется в тестирующей системе. (Обратите внимание, что эти тесты хоть и рандомизированные, но детерминированные - при одном значении seed результат будет всегда одинаковый. Так что не стоит пытаться заново тестировать то же самое решение, надеясь что оно вдруг пройдет.)
  • Флаг -c включает тесты на model checking (см. первый семинар), по умолчанию они выключены. Как только ваше решение будет проходить основные тесты, добавьте этот флаг и убедитесь, что эти тесты также проходят.
  • Флаг -o включает тесты на потребление ресурсов (памяти и сети), по умолчанию они выключены. В этих тестах измеряются и выводятся максимальное потребление памяти объектами Sender и Receiver, число переданных по сети сообщений, их суммарный объем (traffic) и отношение числа исходных сообщений к времени работы вашей реализации (throughput). Полученные значения сравниваются с пороговыми значениями, в которые укладывается с запасом авторское решение. Как только ваше решение будет проходить основные тесты, chaos monkey и model checking, включите эти тесты и при необходимости займитесь оптимизацией решения.
  • Опция -t позволяет прогнать только один конкретный тест, указав его имя (в точности как оно выводится в консоли, например [AT MOST ONCE] NORMAL).
  • Опция -g позволяет прогнать только тесты для одной из гарантий, указав её сокращение (AMO, ALO, EO, EOO).
  • Опция -s позволяет изменить используемый random seed (см. первый семинар). Можно использовать для дополнительной проверки вашего решения. В тестирующей системе используется значение по умолчанию (123).

Во время проверки решения в тестирующей системе используются опции -m 100 -c -o с лимитом времени в 5 минут. На авторском решении выполнение всех тестов с этими опциями занимает около 10 секунд.

Код тестов открыт и находится в tests/src. Вы можете обращаться к нему и использовать в своём решении информацию об условиях тестирования, например о минимальной и максимальной задержках в сети. При этом корректность решения не должна зависеть от конкретных значений задержек, вероятностей потери и дублирования сообщений: гарантии должны выполняться при любом поведении сети, соответствующем описанной выше модели.

Если вы найдете ошибки или требования из условий, которые не покрывают наши тесты, то вы можете получить за это бонусные баллы. Для этого надо включить в отчёт описание ситуации, которую не ловят тесты, добавив при необходимости пример решения с ошибкой. За это полагается 1 балл. Если вы также реализуете тесты, которые ловят найденную проблему, или хотя бы опишите их логику, то получите еще 1 балл.

Проверка в тестирующей системе

Отправьте ваше решение в тестирующую систему следуя инструкции и дождитесь результатов.

ЧаВо

Как измеряется потребление памяти в тестах на overhead и что в него входит?

См. здесь. Данной функции передаётся объект Sender или Receiver. Измеряется потребление памяти структурами данных (атрибутами) внутри этих объектов. Функция вызывается периодически по ходу выполнения теста, и запоминается максимальное полученное значение, которое выводится в конце теста. Затраты на глобальные переменные и таймеры не учитываются.

Что можно и что нельзя использовать для хранения состояния процесса?

Можно и нужно использовать атрибуты объектов Sender и Receiver.

Если вы претендуете на баллы за оптимизацию потребляемых ресурсов, также нельзя:

  • хранить полный набор идентификаторов сообщений в виде битовых значений в числе или битовом наборе (bitset); тесты проверяют наличие в коде операторов битового сдвига,
  • хранить содержимое сообщений в именах таймеров (допускается хранить номера сообщений),
  • отправлять из процесса самому себе сообщения с его состоянием.

Эти способы будут рассматриваться как попытка обойти измерение ресурсов, и пункт с оптимизацией потребляемых ресурсов засчитан не будет. Авторское решение не использует подобных ухищрений и хранит всё состояние в атрибутах процессов.

Для прохождения тестов на overhead не требуются сторонние библиотеки типа numpy, сжатие и распаковка данных, специальные бинарные форматы и т.п. Авторское решение не использует ничего из перечисленного.

Если вы не понимаете, как пройти тесты без вышеописанных хаков, перечитайте условие, там есть намёк.

Допускается ли падение тестов на overhead с другим random seed?

Да. Пороги в тестах указаны не вообще для всех возможных выполнений (где потребление ресурсов может довольно сильно отличаться), а только для запусков с дефолтным seed.