# Модель распределенной системы и фреймворк AnySystem

> After you have the experience of building your first distributed system on top of a simulator that induces partitions / delays etc… you will forever consider writing them without a simulator to be like driving drunk and blindfolded. ([sled simulation guide](https://sled.rs/simulation.html))

## Учебные задания и тестирование распределенных систем

Для закрепления знаний, полученных на курсе, и развития практических навыков вы будете выполнять домашние задания. Задания на курсе будут двух типов. Первый тип — реализация распределенных систем/приложений с использованием реальных технологий, например gRPC. Второй тип — реализация распределенных систем в рамках фреймворка [AnySystem](https://github.com/systems-group/anysystem), имитирующего сеть и отказы. Заданий второго типа будет больше, поэтому сегодня мы поговорим о том, почему так сделано и как устроен этот фреймворк.

Мотивация заданий первого типа понятна — это опыт создания реальных приложений и использования технологий, которые применяются в индустрии. Однако проверить корректность работы таких приложений очень сложно в силу недетерминированного характера распределенных систем. От одного выполнения нашей системы к другому могут меняться порядок прихода сообщений, возникающие отказы, внешние воздействия и т.д. Запустив приложение один раз и получив корректный результат мы не можем быть уверены в том, что так будет всегда. Ошибка в реализации может проявляться в крайне редко возникающих, но, тем не менее, возможных ситуациях. Смоделировать все такие ситуации на реальной системе крайне сложно. А поймав ошибку во время работы приложения, сложно воспроизвести потом приведшее к ней выполнение системы. 

Мы еще вернемся к тому, как тестировать распределенные приложения. Пока главное осознать, что это крайне непростая задача, требующая поддержки со стороны используемой программной инфраструктуры, и во многих реальных системах тесты не гарантируют корректность реализации.

Одна из главных целей нашего курса — научить вас писать корректно работающие распределенные системы, осознавая при этом все возможные риски и ошибки. Для этого важно исчерпывающим и воспроизводимым образом тестировать ваши решения, что в случае заданий первого типа, как мы выяснили, проблематично. Поэтому эти задания мы будем использовать для знакомства и практики с реальными технологиями. А более сложные с точки зрения требований к корректности задания мы будем писать на базе фреймворка, позволяющего моделировать и воспроизводить при тестировании все "интересные" ситуации. Что-то вроде тренажера-симулятора, на котором отрабатывают навыки пилоты самолётов.

## Модель распределенной системы

Начать знакомство с фреймворком стоит с используемой в нём модели распределенной системы. Как и в любой модели в ней оставлены наиболее важные для нас особенности изучаемых систем (например, отказы сети), а несущественные и слишком низкоуровневые детали (например, передача пакетов в сети) отброшены.

Мы будем использовать довольно простую, естественную и часто используемую модель распределенной системы. Система моделируется как набор _узлов (node)_, связанных сетью. На узлах выполняются _процессы_, взаимодействующие друг с другом путём обмена _сообщениями_. Процессы обрабатывают входящие сообщения в соответствии с описанной программистом логикой. Во время обработки сообщения процесс может изменять свое состояние, отправлять сообщения другим процессам и выполнять другие описанные далее действия. Обработка сообщений процессом происходит строго последовательно в порядке их получения. Нет потоков и связанных с ними проблем, код процесса по определению потокобезопасен, прямого доступа к его внутреннему состоянию у других процессов нет. Описанная модель процесса близка к [модели акторов](https://en.wikipedia.org/wiki/Actor_model), наиболее известным примером реализации которой для распределенных систем является [Erlang/OTP](https://en.wikipedia.org/wiki/Erlang_(programming_language)).

Сообщения между процессами, находящимися на разных узлах, передаются через сеть. Модель сети, в зависимости от её настроек, может вносить _задержку_ (фиксированную или случайную) при передаче сообщений и реализовывать основные виды _сетевых отказов_ — терять сообщения, искажать, переупорядочивать или дублировать их. Также можно моделировать более сложные виды отказов: полное отключение некоторого узла от сети, недоступность сети между парой узлов или в некотором направлении, разделение сети на несколько изолированных компонент (network partition).

Помимо отказов сети важно также моделировать _отказы узлов_. Отказ или "падение" узла моделируется путем отключения узла от сети и остановки выполнявшихся на нем процессов. Восстановление узла после отказа моделируется путем перезапуска процессов "с нуля" (состояние процессов на момент падения теряется) и подключения узла к сети.

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

Также процессы могут отправлять себе напоминания в виде _таймеров_. При создании таймера указывается его имя и промежуток времени, через который он должен сработать. По истечении заданного времени процессу направляется уведомление о срабатывании таймера с его именем. Таймер срабатывает только один раз. Также таймер можно отменить до его срабатывания.

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

## Симуляция

Выполнение распределенной системы в AnySystem реализовано как пошаговая _симуляция_. Каждый шаг соответствует наступлению и обработке некоторого _события_ (например, получения сообщения или срабатывания таймера). С каждым событием связано глобальное время его наступления. Время получения сообщения вычисляется как время отправки + задержка сети, в том числе случайная. Время срабатывания таймера определяется заданной задержкой. События обрабатываются по очереди в порядке их времен путем установки глобального времени на время события и вызова кода процесса-получателя. При обработке событий процессы могут выполнять действия (отправлять сообщения, ставить таймеры), приводящие к генерации новых событий. Например, в ответ на полученное сообщение процесс может отправить новое сообщение, что приведет к созданию нового события получения сообщения или отбрасыванию сообщения, если сеть ненадежна. В нашей модели предполагается, что обработка событий процессами происходит мгновенно. В общем случае описанный подход называют [дискретно-событийным моделированием](https://en.wikipedia.org/wiki/Discrete-event_simulation).

Важно, что порядок событий однозначно определяется настройками симуляции — параметрами сети, random seed и т.д. Иными словами, в отличие от реальной системы выполнение в нашем симуляторе является _детерминированным_ — оно не будет меняться от запуска к запуску, мы можем контролировать и воспроизводить его. Путем задержек и отбрасывания событий мы можем реализовать произвольное возможное выполнение распределенной системы в рамках описанной модели, в том числе такое, которое сложно "поймать" в реальной системе.

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

## Пример

Рассмотрим пример, на котором разберем как пишутся и тестируются распределенные системы в AnySystem. У нас будет очень простая система, состоящая из двух узлов и выполняющихся на них процессов — _клиента_ и _сервера_. Клиент отправляет сообщения PING на сервер, а сервер отвечает на них сообщениями PONG. Сообщения содержат в себе строковое поле `value`. Сервер вставляет в ответ значение `value` из сообщения клиента.   

В папке `ping-pong` есть несколько реализаций. Для начала откроем `impl_basic.py` и посмотрим как устроены реализации `PingClient` и `PingServer`. Они реализуют интерфейс `Process`, описанный вместе с классами `Message` и `Context` в [стабе для IDE](../../../typings/anysystem/__init__.pyi). Стаб содержит объявления и документацию методов; их реализация находится в [исходнике AnySystem 0.3.0](https://github.com/osukhoroslov/anysystem/blob/v0.3.0/python/anysystem.py). Изучим эти классы и методы.

Для автодополнения в VS Code с Pylance откройте корень репозитория: стаб подключается через [настройки проекта](../../../pyrightconfig.json). Для другой IDE укажите папку `typings` в корне репозитория как каталог стабов согласно её документации. При запуске через Rust-тестер или Docker модуль `anysystem` загружается из AnySystem автоматически, без отдельного файла и настройки `PYTHONPATH`.

### Интерфейс процесса

Метод `on_start()` вызывается при запуске процесса на узле. Он обязателен для каждой реализации `Process`; если при запуске ничего делать не требуется, метод должен иметь пустое тело (`pass`).

Методы `on_local_message()` и `on_message()` предназначены для приема и обработки процессом локальных и сетевых сообщений соответственно. Сообщения описываются классом `Message` — у сообщения есть тип и содержимое в виде словаря со строковыми ключами, с которым можно работать через методы `Message`. В реализации системы можно использовать сообщения произвольных типов и структур. В заданиях часть сообщений будет специфицирована, а часть надо будет определять самому. 

Метод `on_timer()` предназначен для обработки таймеров, он вызывается в момент срабатывания ранее установленного таймера. 

Во все методы `Process` передается объект типа `Context`, он предназначен для взаимодействия с симулируемым окружением. Через контекст можно узнать текущее локальное время, отправить сообщение, установить или отменить таймер. 

Процессы в системе идентифицируются с помощью уникальных строковых id, назначаемых при добавлении процесса в систему (см. далее). Эти идентификаторы передаются при отправке (в `ctx.send()`) и приеме (в `on_message()`) сообщения. Без знания id процесса отправить ему сообщение нельзя.

### Реализация процессов

Далее посмотрим как реализованы процессы `PingClient` и `PingServer` в `impl_basic.py`. 

Инициализация процессов (содержимое конструкторов) определяется в зависимости от системы. В данном случае мы хотим, чтобы и клиенту и серверу передавались их id (скорее для порядка, в реализации они нигде не используются) и клиенту передавался id сервера, чтобы клиент мог отправлять ему сообщения. 

В `on_local_message()` клиент реализует обработку локальных сообщений — при получении сообщения PING он направляет его серверу. Это будет наш способ инициировать выполнение системы. 

В `on_message()` клиент реализует обработку сообщений от сервера — при получении ответного сообщения PONG он пересылает его локально. Это будет наш способ проверить, что выполнение завершено корректно (ответ получен). 

Сервер принимает только сообщения из сети, в методе `on_message()` при получении сообщения PING он создает сообщение PONG и отправляет его обратно клиенту. Видно, что при этом сервер копирует поле `value` из исходного сообщения. 

Таймеры в этой реализации не используются.

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

Теперь разберемся как запускать и тестировать нашу систему. Тесты и сам фреймворк написаны на языке Rust, а Python мы будем использовать только для описания логики процессов. Поэтому необходимо [установить Rust](https://www.rust-lang.org/tools/install) и, разумеется, [Python](https://www.python.org/downloads). В случае работы на Windows не рекомендуется устанавливать Python из Microsoft Store – это может привести к ошибкам, поэтому лучше использовать [официальный сайт](https://www.python.org/downloads). Вместо установки Rust и компиляции тестов можно воспользоваться готовым Docker-образом, но в таком случае вы не сможете изменять код тестов.

Примеры тестов находятся в папке `tests`. В случае установки Rust, скомпилируйте тесты с помощью команды `cargo install --locked --path tests` и попробуйте запустить `distsys-ping-pong -h`. В случае использования Docker-образа, здесь и далее замените `distsys-ping-pong` на `docker run --rm -t -v ./:/solution distsys.ru/course/ping-pong:latest`. Вы должны увидеть справку по запуску.

> Возможно, вы увидите ошибку о том, что библиотека исполнения Python-кода не нашла интерпретатор: `dyld[55004]: Library not loaded: @rpath/Python3.framework/Versions/3.9/Python3`. В этом случае попробуйте указать путь до интерпретатора через переменную окружения, например так:
>
> ```bash
> $ which python3.9
> /opt/homebrew/bin/python3.9
>
> $ PYO3_PYTHON='/opt/homebrew/bin/python3.9' cargo install --locked --path tests
> ```

Запустим простейший тест, который создает систему и инициирует выполнение, передав клиенту локальное сообщение: `distsys-ping-pong -i impl_basic.py -t run`. Тест выводит _трассу_ (англ. trace) выполнения системы в виде списка событий в порядке их наступления, с указанием времени и описания события.

Для сдачи заданий по курсу вам не потребуется изучать Rust. Достаточно только базового понимания синтаксиса и основных конструкций языка (см. полезные ссылки в конце), а также как реализуются тесты на AnySystem. Самим писать тесты в заданиях не требуется (однако если вы увидите, что наших тестов недостаточно, то вы можете придумать и описать в произвольной форме сценарии, которые не покрывают наши тесты, и получить за это бонусные баллы).

#### Устройство тестов

Посмотрим как устроены тесты в нашем примере, для этого перейдем в папку `tests/src`. В `main.rs` происходит считывание параметров командной строки, настройка и запуск тестов. В `common.rs` находится вспомогательный код, используемый тестами. Структура `TestConfig` используется для описания параметров теста, а функция `build_system()` для создания нашей системы из клиента и сервера. В `tests.rs` находятся сами тесты в отдельных функциях. 

Мы запускали тест из `test_run()`, в нём собирается система и клиенту отправляется локальное сообщение PING, после чего с помощью метода `step_until_no_events()` запускается пошаговое выполнение системы до тех пор, пока есть необработанные события. После обмена сообщениями между клиентом и сервером, события закончатся и выполнение завершится. В данном тесте мы ничего не проверяем и всегда возвращаем `Ok(true)`.

Теперь рассмотрим тест в `test_result()`. Он похож на предыдущий, но после выполнения системы мы также проверяем, вернул ли клиент локально ответное сообщение от сервера. Для этого мы читаем все локальные сообщения, которые отправил клиент, (см. `read_local_messages()`) и проверяем, что сообщение одно и оно имеет ожидаемый тип и содержимое (см. `check()`). Проверки выполняются с помощью макроса `assume!`, который возвращает результат типа `Result<bool, String>` — по сути или `true` (проверка пройдена) или строку с указанным сообщением об ошибке. Оператор `?` в конце позволяет досрочно выйти из функции в случае ошибки, вернув сообщение о ней в `TestResult`. Если же все проверки прошли, то мы дойдем до конца теста и вернем `Ok(true)`.

Запустим этот тест: `distsys-ping-pong -i impl_basic.py -t result`. Наша реализация проходит его. Попробуйте изменить код реализации так, чтобы тест не проходил.

#### Зависимость результатов от random seed

По умолчанию сеть в тестах надежная, то есть все отправленные сообщения доходят до адресатов. Рассмотрим теперь тест с ненадежной сетью в `test_result_unreliable()`. Чтобы сделать сеть ненадежной, в начале теста мы вызываем `sys.network().set_drop_rate(0.5)`. Эта функция задает вероятность потери сообщения сетью во время доставки (по умолчанию используется значение 0).

Запустим тест с ненадежной сетью: `distsys-ping-pong -i impl_basic.py -t result_unreliable`. Реализация проходит тест. Но значит ли это, что в ней нет проблем? 

Попробуем запустить тест, изменив random seed: `distsys-ping-pong -i impl_basic.py -t result_unreliable -s 12345`. Теперь тест не проходит из-за того, что PING теряется и не доходит до сервера. 

Какое ещё выполнение может привести к ошибке в этом тесте? (Чтобы увидеть его, измените seed на `12345678`.)

Рассматриваемый тест выглядит ненадежным — он сильно зависит от значения seed и может пропускать интересующие нас ситуации и ошибки, а подбирать seed руками неудобно. Один из вариантов повысить вероятность поймать ошибку — смоделировать обработку не одного, а нескольких запросов. См. тест `test_10_results_unreliable()`. 

Запустим этот тест: `distsys-ping-pong -i impl_basic.py -t 10_results_unreliable`. Ошибку удалось поймать уже со второй попытки. В общем случае при большом количестве попыток такой подход неплохо работает, но 100% гарантии поймать все проблемы он не дает. Посмотрим как гарантированно воспроизвести нужные нам ситуации.

#### Воспроизведение нужных ситуаций в тестах

Для гарантированной потери сообщения PING мы можем в начале сделать сеть полностью ненадежной (drop rate = 1), а через некоторое время, чтобы дать системе возможность завершить выполнение, сделаем её надежной. См. тест в `test_drop_ping()`, запустить его можно передав `-t drop_ping`. Перед тем, как сделать сеть надежной, мы делаем 10 шагов в симуляторе с помощью `sys.steps(10)`.

Для гарантированной потери сообщения PONG надо сделать сеть полностью ненадежной только после того, как PING дошел до сервера. Для этого мы сначала отправляем локальное сообщение и только после этого устанавливаем drop rate = 1. См. тест в `test_drop_pong()`, запустить его можно передав `-t drop_pong`. Здесь мы пользуемся тем, что при вызове `send_local_message()` фреймворк сразу же вызывает соответствующий процесс и тут же обрабатывает его действия. В том числе генерируется событие получения сообщения PING сервером. А вызов `set_drop_rate()` не влияет на существующие события — только на новые отправки сообщений. 

Аналогично предыдущим двум тестам, желаемый эффект можно также достичь временно отключив сеть между клиентом и сервером в одном из направлений. Как это сделать показано в `test_drop_ping_2()` и `test_drop_pong_2()`. Запустим эти версии тестов и проверим, что их выполнение аналогично версиям с изменением drop rate.

#### Использование таймеров

Реализация из `impl_basic.py` не работает в ненадежной сети. Попробуем исправить её с помощью таймеров — после отправки PING клиент будет устанавливать таймер, при срабатывании которого проверять получил ли он ответ от сервера, и если нет — повторно отправлять PING. Если ответ получен, то клиент будет отменять таймер. Данная реализация находится в `impl_retry.py`. Проверим, что она проходит все ранее рассмотренные тесты: `distsys-ping-pong -i impl_retry.py`. Три других теста не проходят, мы их рассмотрим далее.

Обратите внимание, что таймер срабатывает только один раз, после чего, в случае необходимости, его надо установить заново. Можно устанавливать несколько таймеров. Главное, чтобы имена активных (установленных, но пока не сработавших) таймеров были уникальными. В наших тестах клиент получает и обрабатывает PING сообщения последовательно, поэтому это условие соблюдается. В общем случае, если в вызове `set_timer()` передается имя активного таймера, то его задержка переопределяется новым значением. Чтобы избежать подобных коллизий, можно использовать метод `set_timer_once()`, который игнорирует вызовы с именем активного таймера.

#### Тест с уникальными запросами

Рассмотрим теперь тест `test_10_unique_results()`. В нём используется еще одна настройка модели сети, позволяющая смоделировать случайную задержку (и, тем самым, переупорядочивание) сообщений. В вызове метода `sys.network().set_delays()` мы передаем минимальное и максимальное значения задержки (по умолчанию они равны 1). Далее в тесте мы отправляем клиенту несколько запросов, но теперь с уникальным содержимым `value`. На каждый запрос PING мы ожидаем получить ответ PONG с таким же содержимым. 

Запустим тест: `distsys-ping-pong -i impl_retry.py -t 10_unique_results`. Реализация не проходит его, хотя сеть надежна. В чем причина? Запустим тест с ненадежной сетью: `distsys-ping-pong -i impl_retry.py -t 10_unique_results_unreliable`. Почему этот тест проходит, если закомментировать в нем `sys.network().set_delays()`? Как можно изменить реализацию, чтобы она проходила тест в обоих случаях?

### Тесты на основе model checking

Все тесты, которые мы запускали ранее, основаны на симуляции некоторой _трассы выполнения системы_, определяемой последовательностью событий. В идеальных условиях, когда в системе нет источников недетерминизма вроде случайных задержек и потерь сообщений, состав и порядок событий всегда будут одинаковыми, независимо от значения seed (см. первые два теста). Однако нас, конечно же, больше интересуют условия, приближенные к реальным, когда недетерминизм есть, и существует множество возможных трасс выполнения системы. Например, случайная задержка сети приводит к различным возможным порядкам доставки сообщений. Именно такие условия приводят к самым интересным и нетривиальным ошибкам в РС.

Рассмотренные ранее тесты по сути пытаются "руками" завести систему на трассу, в которой возможно проявление той или иной ошибки. Однако, как мы видели, трасса и результат теста могут зависеть от seed, и в общем случае такие тесты не ловят ошибки гарантированно. Мы также видели, что вероятность обнаружения ошибки можно повысить путем увеличения количества испытаний или даже сделать 100%, если грамотно реализовать тест. Но все равно остается риск, что какие-то трассы и ошибки мы нашими тестами не покрыли. 

Почему бы тогда не проверить все возможные трассы? [Model checking](https://en.wikipedia.org/wiki/Model_checking) — это подход к тестированию распределенных (и не только) систем, который как раз основан на этой идее. По сути данный подход состоит в обходе графа возможных состояний системы, начиная от исходного в начале теста. _Состояние системы_ определяется состоянием узлов, процессов и сети, а также списком необработанных событий. Дуги в графе соответствуют переходам между состояниями в результате наступления одного из таких событий. Трассы выполнения системы можно представить в виде путей в графе состояний системы. (Упражнение: Попробуйте нарисовать граф состояний для `impl_basic.py` на одном из рассмотренных тестов. Можно ли нарисовать такой же граф для `impl_retry.py`?)

Model checking может использоваться для поиска состояний, в которых нарушаются требуемые свойства системы. Например, состояние в котором процесс доставил два одинаковых сообщения может нарушать требование, что каждое сообщения должно доставляться не более одного раза. Такие свойства называют [safety properties](https://en.wikipedia.org/wiki/Safety_and_liveness_properties#Safety) или инвариантами. Также с помощью model checking можно проверять, что система всегда приходит в желаемое состояние. Например, что процесс в конце концов доставит требуемое сообщение. Такие свойства называют [liveness properties](https://en.wikipedia.org/wiki/Safety_and_liveness_properties#Liveness).

В теории model checking позволяет исчерпывающим образом протестировать систему и поймать все ошибки. Однако, применимость этого подхода на практике сильно ограничена из-за огромных размеров графов состояний систем, которые растут экспоненциально с количеством процессов и событий. Поэтому за разумное время можно протестировать только трассы с небольшим числом процессов и событий, иногда прибегая при этом к дополнительным ограничениям на рассматриваемые состояния. 

Рассмотрим model checking тесты для нашего примера ping-pong. Они находятся в `tests_mc.rs`.

#### Тест mc_reliable_network

Начальным состоянием в этом тесте является состояние после отправки локального сообщения PING клиенту. В `strategy_config` описываются настройки model checking:
- Мы ограничиваем рассматриваемые состояния только теми, в которых каждый процесс отправил не более 4 сообщений (т.н. prune). 
- Мы ожидаем, что конечным состоянием будет то, в котором событий не осталось и клиент отправил 1 локальное сообщение (т.н. goal). Если условие goal не выполнено, а событий больше нет, то найдена ошибка. 
- В процессе обхода для каждого состояния мы также проверяем условие, что доставленное клиентом сообщение (если оно уже есть) совпадает с ожидаемым и нет других, а также что глубина обхода (длина трассы) не более 20 (т.н. invariant). Если условие invariant нарушено, то также выдается ошибка.

Как видно, мы вводим некоторые ограничения на решение (не более 4 сообщений, не более 20 шагов), чтобы ограничить размер графа состояний. Это довольно естественные предположения о том, как должно работать разумное решение. Аналогичным образом будут устроены model checking тесты для домашних заданий. В том числе они могут вводить похожие ограничения.

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

#### Тест mc_unreliable_network

В этом тесте сеть ненадежная и может терять сообщения. В терминах model checking это значит, что для каждого события отправки сообщения нужно изучить два случая — когда сообщение будет доставлено и когда оно будет потеряно.

Начальное состояние аналогично прошлому тесту, но есть некоторые отличия в настройках model checking. Мы ограничиваем глубину обхода 7 событиями и убираем ограничение по глубине из инварианта. Это означает, что мы не будем рассматривать трассы с более 7 событиями, то есть проверка уже не будет исчерпывающей. Но все более короткие трассы мы проверим полностью.

Запустите тест на двух вариантах реализации и убедитесь, что он проходит только для `impl_retry.py`. Обратите внимание, что для `impl_basic.py` тест выводит найденную трассу с ошибкой, аналогичную тесту `drop_ping`. Единственное отличие — в выводе теперь нет времени, поскольку model checking его не использует.

#### Тест mc_limited_message_drops

В этом тесте сеть также ненадежная, но используются другие настройки model checking. Мы рассматриваем только трассы с не более 5 отправками и 3 потерями сообщений. Для всех таких трасс мы проверяем, что требуемое конечное состояние будет достигнуто. Запустите тест на двух вариантах реализации и убедитесь, что он проходит только для `impl_retry.py`.

#### Тест mc_consecutive_messages

Несмотря на наличие надежной сети, этот тест посложнее, потому что в нём отправляется два локальных сообщения PING и используется техника сбора промежуточных состояний. В общих чертах суть теста состоит в том, чтобы сначала собрать промежуточные состояния, в которые может попасть система после отправки первого сообщения. Затем из каждого полученного состояния запускается отдельный model checking с отправкой второго сообщения и проверяется, что будут доставлены оба сообщения, в ограничениях похожих на тест `mc_reliable_network`.

Запустите тест на двух вариантах реализации и убедитесь, что он проходит только для `impl_basic.py`. Почему так получается? Сравните вывод теста для `impl_retry.py` с выводом теста `10_unique_results`. Видно, что новый тест смог найти самую короткую трассу, приводящую к ошибке.

## Тестирование реальных систем

Для тестирования распределенных систем наиболее часто применяется подход, основанный на запусках тестируемой реализации в контролируемом окружении с внедрением разных отказов (fault injection), например с помощью фреймворка [Jepsen](https://github.com/jepsen-io/jepsen). Для проверки различных трасс выполнения системы делается много рандомизированных запусков. В качестве окружения обычно выступает реальный кластер из нескольких (а то и большого числа) машин, что приводит к следующей проблеме — тестирование в таких условиях сложно, дорого и медленно.

Мы рассмотрели два альтернативных подхода к тестированию распределенных систем — симуляция и model checking. За счёт замены частей реального окружения их моделями они позволяют ускорить и повысить качество тестирования в сравнении с тестами в реальном окружении. Хотя использованный нами фреймворк является учебным и не рассчитан на разработку распределенных систем, данные подходы вполне можно использовать и для тестирования реальных систем. Однако это требует от разработчиков дополнительных усилий — система должна изначально разрабатываться в рамках некоторого фреймворка с интерфейсами, поддерживающими легкую подмену компонентов, работающих с часами, сетью, дисками и т.д., на их модели, без необходимости менять код приложений. По этой причине данные подходы пока используются не так часто, но есть несколько успешных примеров их внедрения (см. ссылки в конце). Пионером в этом направлении стала база данных FoundationDB. Симуляция также активно используется разработчиками Amazon Web Services:

> ...we picked an approach that is in wide use at Amazon Web Services, which we would like to see broadly adopted: build a test harness which abstracts networking, performance, and other systems concepts (we call it a _simworld_). The goal of this approach is to allow developers to write distributed systems tests, including tests that simulate packet loss, server failures, corruption, and other failure cases, as unit tests in the same language as the system itself. In this case, these unit tests run inside the developer’s IDE (or with junit at build time), with no need for test clusters or other infrastructure. A typical test which tests correctness under packet loss can be implemented in less than 10 lines of Java code, and executes in less than 100ms. The Physalia team have written hundreds of such tests, far exceeding the coverage that would be practical in any cluster-based or container-based approach.

В будущем можно ожидать, что все больше систем будут использовать эти подходы на практике.

## Полезные ссылки

### Тестирование и симуляция распределенных систем

- [FoundationDB: Simulation and Testing](https://apple.github.io/foundationdb/testing.html)
- [Testing Distributed Systems w/ Deterministic Simulation](https://www.youtube.com/watch?v=4fFDFbi3toc)
- [FoundationDB or: How I Learned to Stop Worrying and Trust the Database](https://www.youtube.com/watch?v=OJb8A6h9jQQ)
- [sled simulation guide (jepsen-proof engineering)](https://sled.rs/simulation.html)
- [Teaching Rigorous Distributed Systems With Efficient Model Checking](https://syslab.cs.washington.edu/papers/dslabs-eurosys19.pdf)

### Rust

- [17 Resources to Help You Learn Rust in 2023](https://serokell.io/blog/learn-rust)
- [Rust For Systems Programmers](https://github.com/nrc/r4cppp)
