Add homework 01 guarantees assignment

This commit is contained in:
2026-09-10 13:22:31 +03:00
parent 16a7a0e469
commit e29c7eb39e
17 changed files with 5613 additions and 1 deletions
+130
View File
@@ -0,0 +1,130 @@
# Гарантии доставки
В этом задании вам предстоит реализовать различные гарантии доставки сообщений в распределённой системе.
В нашей системе есть два узла, и нам требуется организовать одностороннюю передачу текстовых сообщений между выполняющимися на узлах процессами. Процесс _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](solution/guarantees.py). Вам надо доработать реализации классов `...Sender` и `...Receiver` для каждой гарантии так, чтобы они проходили все тесты.
Ваши реализации процессов AnySystem должны соблюдать правило [изоляции процессов](../readme.md#изоляция-процессов-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](https://docs.docker.com/engine/install/) (можно использовать [Docker Desktop](https://docs.docker.com/desktop/)). Для запуска тестов выполните команду:
```commandline
docker run --pull always --rm -t -v ./solution:/solution distsys.ru/course/guarantees:latest [ЗДЕСЬ МОЖНО УКАЗАТЬ ОПЦИИ]
```
Для запуска полного набора тестов с теми же параметрами, что и в тестирующей системе, выполните команду:
```commandline
docker run --pull always --rm -t -v ./solution:/solution distsys.ru/course/guarantees:latest -m 100 -c -o
```
Вы также можете запустить тесты, скомпилировав их локально с помощью компилятора Rust. Такой вариант может быть удобен, если вы хотите лучше изучить или доработать тесты. Убедитесь, что на вашей машине установлен [Rust](https://www.rust-lang.org/tools/install). Скомпилируйте тесты с помощью команды `cargo install --locked --path tests`. Для запуска тестов выполните команду:
```commandline
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 балл.
### Проверка в тестирующей системе
Отправьте ваше решение в тестирующую систему следуя [инструкции](../readme.md) и дождитесь результатов.
## ЧаВо
**Как измеряется потребление памяти в тестах на overhead и что в него входит?**
См. [здесь](https://github.com/osukhoroslov/anysystem/blob/main/src/python/mod.rs#L282). Данной функции передаётся объект _Sender_ или _Receiver_. Измеряется потребление памяти структурами данных (атрибутами) внутри этих объектов. Функция вызывается периодически по ходу выполнения теста, и запоминается максимальное полученное значение, которое выводится в конце теста. Затраты на глобальные переменные и таймеры не учитываются.
**Что можно и что нельзя использовать для хранения состояния процесса?**
Можно и нужно использовать атрибуты объектов _Sender_ и _Receiver_.
Если вы претендуете на баллы за оптимизацию потребляемых ресурсов, также нельзя:
- хранить полный набор идентификаторов сообщений в виде битовых значений в числе или битовом наборе (bitset); тесты проверяют наличие в коде операторов битового сдвига,
- хранить содержимое сообщений в именах таймеров (допускается хранить номера сообщений),
- отправлять из процесса самому себе сообщения с его состоянием.
Эти способы будут рассматриваться как попытка обойти измерение ресурсов, и пункт с оптимизацией потребляемых ресурсов засчитан не будет. Авторское решение не использует подобных ухищрений и хранит всё состояние в атрибутах процессов.
Для прохождения тестов на overhead не требуются сторонние библиотеки типа numpy, сжатие и распаковка данных, специальные бинарные форматы и т.п. Авторское решение не использует ничего из перечисленного.
Если вы не понимаете, как пройти тесты без вышеописанных хаков, перечитайте условие, там есть намёк.
**Допускается ли падение тестов на overhead с другим random seed?**
Да. Пороги в тестах указаны не вообще для всех возможных выполнений (где потребление ресурсов может довольно сильно отличаться), а только для запусков с дефолтным seed.
@@ -0,0 +1,177 @@
from anysystem import Context, Message, Process
# AT MOST ONCE ---------------------------------------------------------------------------------------------------------
class AtMostOnceSender(Process):
def __init__(self, proc_id: str, receiver_id: str):
self._id = proc_id
self._receiver = receiver_id
def on_local_message(self, msg: Message, ctx: Context):
# receive message for delivery from local user
pass
def on_start(self, ctx: Context):
pass
def on_message(self, msg: Message, sender: str, ctx: Context):
# process messages from receiver here
pass
def on_timer(self, timer_name: str, ctx: Context):
# process fired timers here
pass
class AtMostOnceReceiver(Process):
def __init__(self, proc_id: str):
self._id = proc_id
def on_local_message(self, msg: Message, ctx: Context):
# not used in this task
pass
def on_start(self, ctx: Context):
pass
def on_message(self, msg: Message, sender: str, ctx: Context):
# process messages from receiver
# deliver message to local user with ctx.send_local()
pass
def on_timer(self, timer_name: str, ctx: Context):
# process fired timers here
pass
# AT LEAST ONCE --------------------------------------------------------------------------------------------------------
class AtLeastOnceSender(Process):
def __init__(self, proc_id: str, receiver_id: str):
self._id = proc_id
self._receiver = receiver_id
def on_local_message(self, msg: Message, ctx: Context):
# receive message for delivery from local user
pass
def on_start(self, ctx: Context):
pass
def on_message(self, msg: Message, sender: str, ctx: Context):
# process messages from receiver here
pass
def on_timer(self, timer_name: str, ctx: Context):
# process fired timers here
pass
class AtLeastOnceReceiver(Process):
def __init__(self, proc_id: str):
self._id = proc_id
def on_local_message(self, msg: Message, ctx: Context):
# not used in this task
pass
def on_start(self, ctx: Context):
pass
def on_message(self, msg: Message, sender: str, ctx: Context):
# process messages from receiver
# deliver message to local user with ctx.send_local()
pass
def on_timer(self, timer_name: str, ctx: Context):
# process fired timers here
pass
# EXACTLY ONCE ---------------------------------------------------------------------------------------------------------
class ExactlyOnceSender(Process):
def __init__(self, proc_id: str, receiver_id: str):
self._id = proc_id
self._receiver = receiver_id
def on_local_message(self, msg: Message, ctx: Context):
# receive message for delivery from local user
pass
def on_start(self, ctx: Context):
pass
def on_message(self, msg: Message, sender: str, ctx: Context):
# process messages from receiver here
pass
def on_timer(self, timer_name: str, ctx: Context):
# process fired timers here
pass
class ExactlyOnceReceiver(Process):
def __init__(self, proc_id: str):
self._id = proc_id
def on_local_message(self, msg: Message, ctx: Context):
# not used in this task
pass
def on_start(self, ctx: Context):
pass
def on_message(self, msg: Message, sender: str, ctx: Context):
# process messages from receiver
# deliver message to local user with ctx.send_local()
pass
def on_timer(self, timer_name: str, ctx: Context):
# process fired timers here
pass
# EXACTLY ONCE + ORDERED -----------------------------------------------------------------------------------------------
class ExactlyOnceOrderedSender(Process):
def __init__(self, proc_id: str, receiver_id: str):
self._id = proc_id
self._receiver = receiver_id
def on_local_message(self, msg: Message, ctx: Context):
# receive message for delivery from local user
pass
def on_start(self, ctx: Context):
pass
def on_message(self, msg: Message, sender: str, ctx: Context):
# process messages from receiver here
pass
def on_timer(self, timer_name: str, ctx: Context):
# process fired timers here
pass
class ExactlyOnceOrderedReceiver(Process):
def __init__(self, proc_id: str):
self._id = proc_id
def on_local_message(self, msg: Message, ctx: Context):
# not used in this task
pass
def on_start(self, ctx: Context):
pass
def on_message(self, msg: Message, sender: str, ctx: Context):
# process messages from receiver
# deliver message to local user with ctx.send_local()
pass
def on_timer(self, timer_name: str, ctx: Context):
# process fired timers here
pass
@@ -0,0 +1,5 @@
target
**/target
**/__pycache__
**/.pytest_cache
*.pyc
+990
View File
@@ -0,0 +1,990 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "aho-corasick"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
dependencies = [
"memchr",
]
[[package]]
name = "anysystem"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e01badd3bc0d042e4399f8f685c394a79a72e7f233b3d3b5eabb3b94734f4dbe"
dependencies = [
"colored",
"downcast-rs",
"dyn-clone",
"indexmap 2.14.0",
"lazy_static",
"log",
"ordered-float",
"pyo3",
"rand",
"rand_pcg",
"regex",
"rstest",
"serde",
"serde_json",
"simcore",
"sugars",
]
[[package]]
name = "assertables"
version = "3.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d84b91d63c67e6c0b1a74728dad0057c4a6124b74e671ff91b7fe9f8bbd671aa"
[[package]]
name = "atty"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
dependencies = [
"hermit-abi",
"libc",
"winapi",
]
[[package]]
name = "autocfg"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "cfg-if"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9"
[[package]]
name = "clap"
version = "3.2.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123"
dependencies = [
"atty",
"bitflags",
"clap_derive",
"clap_lex",
"indexmap 1.9.3",
"once_cell",
"strsim",
"termcolor",
"textwrap",
]
[[package]]
name = "clap_derive"
version = "3.2.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae6371b8bdc8b7d3959e9cf7b22d4435ef3e79e138688421ec654acf8c81b008"
dependencies = [
"heck 0.4.1",
"proc-macro-error",
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "clap_lex"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5"
dependencies = [
"os_str_bytes",
]
[[package]]
name = "colored"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c"
dependencies = [
"lazy_static",
"windows-sys 0.59.0",
]
[[package]]
name = "distsys-guarantees"
version = "0.1.0"
dependencies = [
"anysystem",
"assertables",
"clap",
"env_logger",
"indexmap 2.14.0",
"log",
"pyo3",
"rand",
"rand_pcg",
"sugars",
]
[[package]]
name = "downcast-rs"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2"
[[package]]
name = "dyn-clone"
version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
[[package]]
name = "env_logger"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a12e6657c4c97ebab115a42dcee77225f7f482cdd841cf7088c657a42e9e00e7"
dependencies = [
"atty",
"humantime",
"log",
"regex",
"termcolor",
]
[[package]]
name = "equivalent"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "erased-serde"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e004d887f51fcb9fef17317a2f3525c887d8aa3f4f50fed920816a688284a5b7"
dependencies = [
"serde",
"typeid",
]
[[package]]
name = "futures"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
dependencies = [
"futures-channel",
"futures-core",
"futures-executor",
"futures-io",
"futures-sink",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-channel"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
dependencies = [
"futures-core",
"futures-sink",
]
[[package]]
name = "futures-core"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
[[package]]
name = "futures-executor"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f"
dependencies = [
"futures-core",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-io"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
[[package]]
name = "futures-macro"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.106",
]
[[package]]
name = "futures-sink"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7"
[[package]]
name = "futures-task"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
[[package]]
name = "futures-timer"
version = "3.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24"
[[package]]
name = "futures-util"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
dependencies = [
"futures-channel",
"futures-core",
"futures-io",
"futures-macro",
"futures-sink",
"futures-task",
"memchr",
"pin-project-lite",
"pin-utils",
"slab",
]
[[package]]
name = "getrandom"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
dependencies = [
"cfg-if",
"libc",
"wasi",
]
[[package]]
name = "glob"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
[[package]]
name = "hashbrown"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
[[package]]
name = "hashbrown"
version = "0.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
[[package]]
name = "heck"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "hermit-abi"
version = "0.1.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
dependencies = [
"libc",
]
[[package]]
name = "humantime"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424"
[[package]]
name = "indexmap"
version = "1.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
dependencies = [
"autocfg",
"hashbrown 0.12.3",
]
[[package]]
name = "indexmap"
version = "2.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
dependencies = [
"equivalent",
"hashbrown 0.17.1",
]
[[package]]
name = "itoa"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
[[package]]
name = "lazy_static"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "libc"
version = "0.2.175"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543"
[[package]]
name = "log"
version = "0.4.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432"
[[package]]
name = "memchr"
version = "2.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0"
[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
]
[[package]]
name = "once_cell"
version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]]
name = "ordered-float"
version = "4.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951"
dependencies = [
"num-traits",
"rand",
"serde",
]
[[package]]
name = "os_str_bytes"
version = "6.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2355d85b9a3786f481747ced0e0ff2ba35213a1f9bd406ed906554d7af805a1"
[[package]]
name = "pin-project-lite"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
[[package]]
name = "pin-utils"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "portable-atomic"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85"
[[package]]
name = "ppv-lite86"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
dependencies = [
"zerocopy",
]
[[package]]
name = "proc-macro-crate"
version = "3.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35"
dependencies = [
"toml_edit",
]
[[package]]
name = "proc-macro-error"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
dependencies = [
"proc-macro-error-attr",
"proc-macro2",
"quote",
"syn 1.0.109",
"version_check",
]
[[package]]
name = "proc-macro-error-attr"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
dependencies = [
"proc-macro2",
"quote",
"version_check",
]
[[package]]
name = "proc-macro2"
version = "1.0.101"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de"
dependencies = [
"unicode-ident",
]
[[package]]
name = "pyo3"
version = "0.29.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4688ddedf473e32662b9b067670129a8afb8c18e351482c70d62ba4a88171e8b"
dependencies = [
"libc",
"once_cell",
"portable-atomic",
"pyo3-build-config",
"pyo3-ffi",
"pyo3-macros",
]
[[package]]
name = "pyo3-build-config"
version = "0.29.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f41027e41b4bd03f6e60f9f417fe24a6341a6bb744edd62b6f709f2a52ea30e9"
dependencies = [
"target-lexicon",
]
[[package]]
name = "pyo3-ffi"
version = "0.29.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e591a95526fead067432c3b3a33fc74770b87b1e04e73671090d9c2055a2b327"
dependencies = [
"libc",
"pyo3-build-config",
]
[[package]]
name = "pyo3-macros"
version = "0.29.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73225868fc1cd84eef2c3c230ddb91273bf1de46aeb8a4248da76d32a0924a1c"
dependencies = [
"proc-macro2",
"pyo3-macros-backend",
"quote",
"syn 2.0.106",
]
[[package]]
name = "pyo3-macros-backend"
version = "0.29.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "571575aa3749fa6216757dd47d2a3e7ef360f329a40f0666a9fbd14889024952"
dependencies = [
"heck 0.5.0",
"proc-macro2",
"quote",
"syn 2.0.106",
]
[[package]]
name = "quote"
version = "1.0.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
dependencies = [
"proc-macro2",
]
[[package]]
name = "rand"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [
"libc",
"rand_chacha",
"rand_core",
"serde",
]
[[package]]
name = "rand_chacha"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
"rand_core",
]
[[package]]
name = "rand_core"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
"getrandom",
"serde",
]
[[package]]
name = "rand_pcg"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59cad018caf63deb318e5a4586d99a24424a364f40f1e5778c29aca23f4fc73e"
dependencies = [
"rand_core",
]
[[package]]
name = "regex"
version = "1.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.4.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b9458fa0bfeeac22b5ca447c63aaf45f28439a709ccd244698632f9aa6394d6"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001"
[[package]]
name = "relative-path"
version = "1.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2"
[[package]]
name = "rstest"
version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9afd55a67069d6e434a95161415f5beeada95a01c7b815508a82dcb0e1593682"
dependencies = [
"futures",
"futures-timer",
"rstest_macros",
"rustc_version",
]
[[package]]
name = "rstest_macros"
version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4165dfae59a39dd41d8dec720d3cbfbc71f69744efb480a3920f5d4e0cc6798d"
dependencies = [
"cfg-if",
"glob",
"proc-macro-crate",
"proc-macro2",
"quote",
"regex",
"relative-path",
"rustc_version",
"syn 2.0.106",
"unicode-ident",
]
[[package]]
name = "rustc-hash"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
[[package]]
name = "rustc_version"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
dependencies = [
"semver",
]
[[package]]
name = "ryu"
version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
[[package]]
name = "semver"
version = "1.0.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0"
[[package]]
name = "serde"
version = "1.0.219"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.219"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.106",
]
[[package]]
name = "serde_json"
version = "1.0.143"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a"
dependencies = [
"indexmap 2.14.0",
"itoa",
"memchr",
"ryu",
"serde",
]
[[package]]
name = "serde_type_name"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92acc4cd6ae20767c54d6cf1a075624e7f4d9e99d7ebc685398ff243144d8714"
dependencies = [
"serde",
]
[[package]]
name = "simcore"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a20dfec85e94e569fa5b810b2d992c68b361f31bfba471447427536488c9c292"
dependencies = [
"colored",
"downcast-rs",
"dyn-clone",
"erased-serde",
"futures",
"log",
"rand",
"rand_pcg",
"rustc-hash",
"serde",
"serde_json",
"serde_type_name",
]
[[package]]
name = "slab"
version = "0.4.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589"
[[package]]
name = "strsim"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]]
name = "sugars"
version = "3.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc0db74f9ee706e039d031a560bd7d110c7022f016051b3d33eeff9583e3e67a"
[[package]]
name = "syn"
version = "1.0.109"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "syn"
version = "2.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "target-lexicon"
version = "0.13.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca"
[[package]]
name = "termcolor"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755"
dependencies = [
"winapi-util",
]
[[package]]
name = "textwrap"
version = "0.16.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057"
[[package]]
name = "toml_datetime"
version = "0.6.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
[[package]]
name = "toml_edit"
version = "0.22.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
dependencies = [
"indexmap 2.14.0",
"toml_datetime",
"winnow",
]
[[package]]
name = "typeid"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c"
[[package]]
name = "unicode-ident"
version = "1.0.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d"
[[package]]
name = "version_check"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "wasi"
version = "0.11.1+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-util"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
"windows-sys 0.61.0",
]
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows-link"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65"
[[package]]
name = "windows-sys"
version = "0.59.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-sys"
version = "0.61.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e201184e40b2ede64bc2ea34968b28e33622acdbbf37104f0e4a33f7abe657aa"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-targets"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_gnullvm",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "winnow"
version = "0.7.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf"
dependencies = [
"memchr",
]
[[package]]
name = "zerocopy"
version = "0.8.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.106",
]
+17
View File
@@ -0,0 +1,17 @@
[package]
name = "distsys-guarantees"
version = "0.1.0"
edition = "2021"
resolver = "3"
[dependencies]
anysystem = "=0.3.0"
indexmap = "2.12"
assertables = "3.2.2"
clap = { version = "3.1.17", features = ["cargo", "derive"] }
env_logger = "0.9.0"
log = "0.4.14"
pyo3 = { version = "=0.29.2", features = ["auto-initialize"] }
rand = "0.8.5"
rand_pcg = "0.3.1"
sugars = "3.0.0"
+19
View File
@@ -0,0 +1,19 @@
# syntax=docker/dockerfile:1
FROM rust:1.97.1-bookworm@sha256:0e2bcaef56d041a486784e54104a81aebe0da44bd03019bd70bc0401e42e4a97 AS builder
RUN apt-get update && apt-get install -y build-essential python3-dev
WORKDIR /tests
COPY . .
RUN --mount=type=cache,id=distsys-course-cargo-registry,target=/usr/local/cargo/registry,sharing=locked \
--mount=type=cache,id=distsys-course-cargo-target-rust-1-97-1,target=/tests/target,sharing=locked \
cargo install --locked --path . --root /opt/course
FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y python3-dev && rm -rf /var/lib/apt/lists/*
COPY --from=builder /opt/course/bin/distsys-guarantees /usr/local/bin/distsys-guarantees
WORKDIR /solution
ENTRYPOINT ["timeout", "-k", "10", "300", "distsys-guarantees", "-i", "guarantees.py"]
@@ -0,0 +1,2 @@
max_width = 120
newline_style = "Unix"
File diff suppressed because it is too large Load Diff
+285
View File
@@ -0,0 +1,285 @@
use std::collections::HashMap;
use assertables::{assume, assume_eq};
use sugars::boxed;
use anysystem::python::PyProcessFactory;
use anysystem::test::TestResult;
use anysystem::{Message, System};
#[derive(Copy, Clone)]
pub struct TestConfig<'a> {
pub impl_path: &'a str,
pub sender_class: &'a str,
pub receiver_class: &'a str,
pub seed: u64,
pub monkeys: u32,
pub reliable: bool,
pub once: bool,
pub ordered: bool,
}
pub fn build_system(config: &TestConfig, measure_max_size: bool) -> System {
let mut sys = System::new(config.seed);
sys.add_node("sender-node");
sys.add_node("receiver-node");
let sender_f = PyProcessFactory::new(config.impl_path, config.sender_class);
let mut sender = sender_f.build(("sender", "receiver"), config.seed);
if measure_max_size {
sender.set_max_size_freq(100);
}
sys.add_process("sender", boxed!(sender), "sender-node");
let receiver_f = PyProcessFactory::new(config.impl_path, config.receiver_class);
let mut receiver = receiver_f.build(("receiver",), config.seed);
if measure_max_size {
receiver.set_max_size_freq(100);
}
sys.add_process("receiver", boxed!(receiver), "receiver-node");
sys
}
pub fn generate_message_texts(sys: &mut System, message_count: usize) -> Vec<String> {
if message_count == 5 {
["distributed", "systems", "need", "some", "guarantees"]
.map(String::from)
.to_vec()
} else {
let mut messages = Vec::new();
for _i in 0..message_count {
let msg = if message_count == 10 {
format!("{}C", sys.gen_range(20..30))
} else {
sys.random_string(100)
};
messages.push(msg);
}
messages
}
}
pub fn send_messages(sys: &mut System, message_count: usize) -> Vec<Message> {
let texts = generate_message_texts(sys, message_count);
let mut messages = Vec::new();
for text in texts {
let msg = Message::new("MESSAGE", &format!(r#"{{"text": "{text}"}}"#));
sys.send_local_message("sender", msg.clone());
if message_count <= 50 {
let steps = sys.gen_range(0..2);
if steps > 0 {
sys.steps(steps);
}
} else {
let duration = sys.gen_range(0.0..2.0);
sys.step_for_duration(duration);
};
messages.push(msg);
}
messages
}
pub fn check_delivered_messages(
delivered: &[Message],
expected_msg_count: &HashMap<String, i32>,
expected_tip: &String,
) -> Result<HashMap<String, i32>, String> {
assert!(!expected_msg_count.is_empty());
let mut delivered_msg_count = HashMap::default();
for msg in delivered.iter() {
// assuming all messages have the same type
assume_eq!(msg.tip, *expected_tip, format!("Wrong message type {}", msg.tip))?;
assume!(
expected_msg_count.contains_key(&msg.data),
format!("Wrong message data: {}", msg.data)
)?;
*delivered_msg_count.entry(msg.data.clone()).or_insert(0) += 1;
}
Ok(delivered_msg_count)
}
pub fn check_message_delivery_reliable(
delivered_msg_count: &HashMap<String, i32>,
expected_msg_count: &HashMap<String, i32>,
) -> TestResult {
for (data, expected_count) in expected_msg_count {
let delivered_count = delivered_msg_count.get(data).unwrap_or(&0);
assume!(
delivered_count >= expected_count,
format!(
"Message {} is not delivered (observed count {} < expected count {})",
data, delivered_count, expected_count
)
)?;
}
Ok(true)
}
pub fn check_message_delivery_once(
delivered_msg_count: &HashMap<String, i32>,
expected_msg_count: &HashMap<String, i32>,
) -> TestResult {
for (data, delivered_count) in delivered_msg_count {
if expected_msg_count.contains_key(data) {
let expected_count = expected_msg_count[data];
assume!(
*delivered_count <= expected_count,
format!(
"Message {} is delivered more than once (observed count {} > expected count {})",
data, delivered_count, expected_count
)
)?;
}
}
Ok(true)
}
pub fn check_message_delivery_ordered(delivered: &[Message], sent: &[Message]) -> TestResult {
let mut next_idx = 0;
for i in 0..delivered.len() {
let msg = &delivered[i];
let mut matched = false;
while !matched && next_idx < sent.len() {
if msg.data == sent[next_idx].data {
matched = true;
} else {
next_idx += 1;
}
}
assume!(
matched,
format!("Order violation: {} after {}", msg.data, &delivered[i - 1].data)
)?;
}
Ok(true)
}
pub fn check_guarantees(sys: &mut System, sent: &[Message], config: &TestConfig) -> TestResult {
let mut expected_msg_count = HashMap::new();
for msg in sent {
*expected_msg_count.entry(msg.data.clone()).or_insert(0) += 1;
}
let delivered = sys.read_local_messages("receiver");
// check that delivered messages have expected type and data
let delivered_msg_count = check_delivered_messages(&delivered, &expected_msg_count, &sent[0].tip)?;
// check delivered message count according to expected guarantees
if config.reliable {
check_message_delivery_reliable(&delivered_msg_count, &expected_msg_count)?;
}
if config.once {
check_message_delivery_once(&delivered_msg_count, &expected_msg_count)?;
}
if config.ordered {
check_message_delivery_ordered(&delivered, sent)?;
}
Ok(true)
}
#[allow(clippy::too_many_arguments)]
pub fn check_overhead(
guarantee: &str,
faulty: bool,
message_count: usize,
sender_mem: u64,
receiver_mem: u64,
net_message_count: u64,
net_traffic: u64,
throughput: f64,
) -> TestResult {
let (sender_mem_limit, receiver_mem_limit, net_message_count_limit, net_traffic_limit, throughput_limit) =
match guarantee {
"AMO" => match message_count {
100 => {
if !faulty {
(800, 1500, 100, 20000, 0.6)
} else {
(800, 3500, 100, 20000, 0.6)
}
}
1000 => {
if !faulty {
(800, 1500, 1000, 200000, 0.6)
} else {
(800, 30000, 1000, 200000, 0.6)
}
}
_ => (u64::MAX, u64::MAX, u64::MAX, u64::MAX, 0.),
},
"ALO" => match message_count {
100 => {
if !faulty {
(1700, 600, 200, 20000, 0.6)
} else {
(5000, 600, 500, 40000, 0.6)
}
}
1000 => {
if !faulty {
(2200, 600, 2000, 200000, 0.6)
} else {
(6000, 600, 5000, 400000, 0.6)
}
}
_ => (u64::MAX, u64::MAX, u64::MAX, u64::MAX, 0.),
},
"EO" => match message_count {
100 => {
if !faulty {
(1700, 1500, 200, 20000, 0.6)
} else {
(5000, 2200, 500, 40000, 0.6)
}
}
1000 => {
if !faulty {
(2200, 1500, 2000, 200000, 0.6)
} else {
(6000, 2200, 5000, 400000, 0.6)
}
}
_ => (u64::MAX, u64::MAX, u64::MAX, u64::MAX, 0.),
},
"EOO" => match message_count {
100 => {
if !faulty {
(2900, 1200, 200, 25000, 0.4)
} else {
(9000, 2500, 500, 45000, 0.4)
}
}
1000 => {
if !faulty {
(3400, 1200, 2000, 250000, 0.4)
} else {
(55000, 4000, 5000, 450000, 0.4)
}
}
_ => (u64::MAX, u64::MAX, u64::MAX, u64::MAX, 0.),
},
_ => (u64::MAX, u64::MAX, u64::MAX, u64::MAX, 0.),
};
assume!(
sender_mem <= sender_mem_limit,
format!("Sender memory > {}", sender_mem_limit)
)?;
assume!(
receiver_mem <= receiver_mem_limit,
format!("Receiver memory > {}", receiver_mem_limit)
)?;
assume!(
net_message_count <= net_message_count_limit,
format!("Message count > {}", net_message_count_limit)
)?;
assume!(
net_traffic <= net_traffic_limit,
format!("Traffic > {}", net_traffic_limit)
)?;
assume!(
throughput >= throughput_limit,
format!("Throughput < {}", throughput_limit)
)?;
Ok(true)
}
+310
View File
@@ -0,0 +1,310 @@
mod common;
mod shared_state;
mod tests;
mod tests_mc;
use indexmap::IndexMap;
use std::collections::HashSet;
use std::env;
use std::io::Write;
use clap::Parser;
use env_logger::Builder;
use log::LevelFilter;
use anysystem::test::{TestResult, TestSuite};
use crate::common::TestConfig;
use crate::tests::*;
use crate::tests_mc::*;
/// Guarantees Homework Tests
#[derive(Parser, Debug)]
#[clap(about, long_about = None)]
struct Args {
/// Path to Python file with solution
#[clap(long = "impl", short = 'i', default_value = "solution/guarantees.py")]
solution_path: String,
/// Test to run (optional)
#[clap(long = "test", short)]
test: Option<String>,
/// Print execution trace
#[clap(long, short)]
debug: bool,
/// Guarantee to check
#[clap(long, short, possible_values = ["AMO", "ALO", "EO", "EOO"])]
guarantee: Option<String>,
/// Random seed used in tests
#[clap(long, short, default_value = "123")]
seed: u64,
/// Number of chaos monkey runs
#[clap(long, short, default_value = "0")]
monkeys: u32,
/// Run overhead tests
#[clap(long, short)]
overhead: bool,
/// Run model checking tests
#[clap(long, short = 'c')]
model_checking: bool,
}
fn main() {
let args = Args::parse();
if !shared_state::validate_or_report(
&args.solution_path,
&[
"AtMostOnceSender",
"AtMostOnceReceiver",
"AtLeastOnceSender",
"AtLeastOnceReceiver",
"ExactlyOnceSender",
"ExactlyOnceReceiver",
"ExactlyOnceOrderedSender",
"ExactlyOnceOrderedReceiver",
],
) {
return;
}
if args.debug {
Builder::new()
.filter(Some("anysystem"), LevelFilter::Debug)
.format(|buf, record| writeln!(buf, "{}", record.args()))
.init();
}
let guarantee = args.guarantee.as_deref();
env::set_var("PYTHONHASHSEED", args.seed.to_string());
let mut config = TestConfig {
impl_path: &args.solution_path,
sender_class: "",
receiver_class: "",
seed: args.seed,
monkeys: args.monkeys,
reliable: false,
once: false,
ordered: false,
};
let mut tests = TestSuite::new();
// At most once
if guarantee.is_none() || guarantee == Some("AMO") {
config.sender_class = "AtMostOnceSender";
config.receiver_class = "AtMostOnceReceiver";
config.once = true;
// without drops should be reliable
config.reliable = true;
tests.add("[AT MOST ONCE] NORMAL", test_normal, config);
tests.add("[AT MOST ONCE] NORMAL NON-UNIQUE", test_normal_non_unique, config);
tests.add("[AT MOST ONCE] DELAYED", test_delayed, config);
tests.add("[AT MOST ONCE] DUPLICATED", test_duplicated, config);
tests.add("[AT MOST ONCE] DELAYED+DUPLICATED", test_delayed_duplicated, config);
tests.add("[AT MOST ONCE] OLD DUPLICATE", test_old_duplicate, config);
// with drops is not reliable
config.reliable = false;
tests.add("[AT MOST ONCE] DROPPED", test_dropped, config);
if args.monkeys > 0 {
tests.add("[AT MOST ONCE] CHAOS MONKEY", test_chaos_monkey, config);
}
if args.overhead {
config.reliable = true;
tests.add(
"[AT MOST ONCE] OVERHEAD NORMAL",
|x| test_overhead(x, "AMO", false),
config,
);
config.reliable = false;
tests.add(
"[AT MOST ONCE] OVERHEAD FAULTY",
|x| test_overhead(x, "AMO", true),
config,
);
}
if args.model_checking {
tests.add("[AT MOST ONCE] MODEL CHECKING", test_mc_reliable_network, config);
tests.add(
"[AT MOST ONCE] MODEL CHECKING MESSAGE DROPS",
test_mc_message_drops,
config,
);
tests.add(
"[AT MOST ONCE] MODEL CHECKING UNSTABLE NETWORK",
test_mc_unstable_network,
config,
);
}
}
// At least once
if guarantee.is_none() || guarantee == Some("ALO") {
config.sender_class = "AtLeastOnceSender";
config.receiver_class = "AtLeastOnceReceiver";
config.reliable = true;
config.once = false;
tests.add("[AT LEAST ONCE] NORMAL", test_normal, config);
tests.add("[AT LEAST ONCE] NORMAL NON-UNIQUE", test_normal_non_unique, config);
tests.add("[AT LEAST ONCE] DELAYED", test_delayed, config);
tests.add("[AT LEAST ONCE] DUPLICATED", test_duplicated, config);
tests.add("[AT LEAST ONCE] DELAYED+DUPLICATED", test_delayed_duplicated, config);
tests.add("[AT LEAST ONCE] DROPPED", test_dropped, config);
if args.monkeys > 0 {
tests.add("[AT LEAST ONCE] CHAOS MONKEY", test_chaos_monkey, config);
}
if args.overhead {
tests.add(
"[AT LEAST ONCE] OVERHEAD NORMAL",
|x| test_overhead(x, "ALO", false),
config,
);
tests.add(
"[AT LEAST ONCE] OVERHEAD FAULTY",
|x| test_overhead(x, "ALO", true),
config,
);
}
if args.model_checking {
tests.add("[AT LEAST ONCE] MODEL CHECKING", test_mc_reliable_network, config);
tests.add(
"[AT LEAST ONCE] MODEL CHECKING MESSAGE DROPS",
test_mc_message_drops,
config,
);
tests.add(
"[AT LEAST ONCE] MODEL CHECKING UNSTABLE NETWORK",
test_mc_unstable_network,
config,
);
}
}
// Exactly once
if guarantee.is_none() || guarantee == Some("EO") {
config.sender_class = "ExactlyOnceSender";
config.receiver_class = "ExactlyOnceReceiver";
config.reliable = true;
config.once = true;
tests.add("[EXACTLY ONCE] NORMAL", test_normal, config);
tests.add("[EXACTLY ONCE] NORMAL NON-UNIQUE", test_normal_non_unique, config);
tests.add("[EXACTLY ONCE] DELAYED", test_delayed, config);
tests.add("[EXACTLY ONCE] DUPLICATED", test_duplicated, config);
tests.add("[EXACTLY ONCE] DELAYED+DUPLICATED", test_delayed_duplicated, config);
tests.add("[EXACTLY ONCE] DROPPED", test_dropped, config);
if args.monkeys > 0 {
tests.add("[EXACTLY ONCE] CHAOS MONKEY", test_chaos_monkey, config);
}
if args.overhead {
tests.add(
"[EXACTLY ONCE] OVERHEAD NORMAL",
|x| test_overhead(x, "EO", false),
config,
);
tests.add(
"[EXACTLY ONCE] OVERHEAD FAULTY",
|x| test_overhead(x, "EO", true),
config,
);
}
if args.model_checking {
tests.add("[EXACTLY ONCE] MODEL CHECKING", test_mc_reliable_network, config);
tests.add(
"[EXACTLY ONCE] MODEL CHECKING MESSAGE DROPS",
test_mc_message_drops,
config,
);
tests.add(
"[EXACTLY ONCE] MODEL CHECKING UNSTABLE NETWORK",
test_mc_unstable_network,
config,
);
}
}
// EXACTLY ONCE ORDERED
if guarantee.is_none() || guarantee == Some("EOO") {
config.sender_class = "ExactlyOnceOrderedSender";
config.receiver_class = "ExactlyOnceOrderedReceiver";
config.reliable = true;
config.once = true;
config.ordered = true;
tests.add("[EXACTLY ONCE ORDERED] NORMAL", test_normal, config);
tests.add(
"[EXACTLY ONCE ORDERED] NORMAL NON-UNIQUE",
test_normal_non_unique,
config,
);
tests.add("[EXACTLY ONCE ORDERED] DELAYED", test_delayed, config);
tests.add("[EXACTLY ONCE ORDERED] DUPLICATED", test_duplicated, config);
tests.add(
"[EXACTLY ONCE ORDERED] DELAYED+DUPLICATED",
test_delayed_duplicated,
config,
);
tests.add("[EXACTLY ONCE ORDERED] DROPPED", test_dropped, config);
if args.monkeys > 0 {
tests.add("[EXACTLY ONCE ORDERED] CHAOS MONKEY", test_chaos_monkey, config);
}
if args.overhead {
tests.add(
"[EXACTLY ONCE ORDERED] OVERHEAD NORMAL",
|x| test_overhead(x, "EOO", false),
config,
);
tests.add(
"[EXACTLY ONCE ORDERED] OVERHEAD FAULTY",
|x| test_overhead(x, "EOO", true),
config,
);
}
if args.model_checking {
tests.add(
"[EXACTLY ONCE ORDERED] MODEL CHECKING",
test_mc_reliable_network,
config,
);
tests.add(
"[EXACTLY ONCE ORDERED] MODEL CHECKING MESSAGE DROPS",
test_mc_message_drops,
config,
);
tests.add(
"[EXACTLY ONCE ORDERED] MODEL CHECKING UNSTABLE NETWORK",
test_mc_unstable_network,
config,
);
}
}
if let Some(test) = &args.test {
tests.run_test(test);
} else {
let (_, results) = tests.run();
let score = score(results);
println!("SCORE: {score}\n");
}
}
fn score(results: IndexMap<String, TestResult>) -> f32 {
let guarantees = HashSet::from(["AT MOST ONCE", "AT LEAST ONCE", "EXACTLY ONCE", "EXACTLY ONCE ORDERED"]);
let mut failed_guarantees: HashSet<&str> = HashSet::new();
let mut failed_overheads: HashSet<&str> = HashSet::new();
for (test, result) in results {
if result.is_err() {
for guarantee in guarantees.iter() {
if test.contains(format!("[{guarantee}]").as_str()) {
if test.contains("OVERHEAD") {
failed_overheads.insert(guarantee);
} else {
failed_guarantees.insert(guarantee);
}
}
}
}
}
9. - failed_guarantees.len() as f32 * 2. - f32::from(!failed_overheads.is_empty())
}
@@ -0,0 +1,636 @@
use std::ffi::CString;
use pyo3::prelude::*;
use pyo3::types::{PyList, PyModule};
const VALIDATOR_CODE: &str = include_str!("../shared_state_validator.py");
fn validator_module<'py>(py: Python<'py>) -> PyResult<Bound<'py, PyModule>> {
let code = CString::new(VALIDATOR_CODE).expect("validator source contains a null byte");
PyModule::from_code(py, &code, c"shared_state_validator.py", c"shared_state_validator")
}
#[cfg(test)]
fn validate_source(source: &str, filename: &str, class_names: &[&str]) -> Result<Vec<String>, String> {
Python::attach(|py| {
let module = validator_module(py).map_err(|error| error.to_string())?;
let class_names = PyList::new(py, class_names).map_err(|error| error.to_string())?;
module
.getattr("validate_source")
.and_then(|function| function.call1((source, filename, class_names)))
.and_then(|result| result.extract::<Vec<String>>())
.map_err(|error| error.to_string())
})
}
fn validate_solution(path: &str, class_names: &[&str]) -> Result<Vec<String>, String> {
Python::attach(|py| {
let module = validator_module(py).map_err(|error| error.to_string())?;
let class_names = PyList::new(py, class_names).map_err(|error| error.to_string())?;
module
.getattr("validate_solution")
.and_then(|function| function.call1((path, class_names)))
.and_then(|result| result.extract::<Vec<String>>())
.map_err(|error| error.to_string())
})
}
pub fn validate_or_report(path: &str, class_names: &[&str]) -> bool {
match validate_solution(path, class_names) {
Ok(violations) if violations.is_empty() => true,
Ok(violations) => {
println!("ERROR: shared state between AnySystem processes is forbidden");
for violation in violations {
println!("{violation}");
}
println!("\nSCORE: 0\n");
false
}
Err(error) => panic!("failed to validate solution for shared state: {error}"),
}
}
#[cfg(test)]
mod tests {
use super::{validate_solution, validate_source};
const PROCESS: &[&str] = &["ProcessImpl"];
fn violations(source: &str) -> Vec<String> {
validate_source(source, "solution.py", PROCESS).unwrap()
}
#[test]
fn accepts_imports_helpers_constants_and_instance_state() {
let source = r#"
import random
from dataclasses import dataclass, field
from enum import Enum, auto
from types import MappingProxyType
LIMIT = 2 ** 10
NAMES = ("a", "b")
LOOKUP = MappingProxyType({"a": 1, "b": 2})
@dataclass
class Helper:
values: list[str] = field(default_factory=list)
@dataclass(frozen=True)
class Status:
ok: bool
class Kind(Enum):
READY = auto()
class ProcessImpl:
RETRIES = 3
READY = Status(True)
def __init__(self):
self.values = []
self.helper = Helper()
def choose(self):
return random.choice(NAMES)
"#;
assert_eq!(violations(source), Vec::<String>::new());
}
#[test]
fn checks_local_imports_without_executing_them() {
let suffix = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos();
let directory = std::env::temp_dir().join(format!(
"distsys-shared-state-validator-{}-{suffix}",
std::process::id()
));
std::fs::create_dir_all(&directory).unwrap();
let entrypoint = directory.join("solution.py");
std::fs::write(&entrypoint, "import helper\n\nclass ProcessImpl:\n pass\n").unwrap();
std::fs::write(
directory.join("helper.py"),
"class Shared:\n data = {}\n\nraise RuntimeError('must not execute')\n",
)
.unwrap();
let result = validate_solution(entrypoint.to_str().unwrap(), PROCESS);
std::fs::remove_dir_all(directory).unwrap();
let violations = result.unwrap();
assert_eq!(violations.len(), 1);
assert!(violations[0].contains("Shared.data"));
}
#[test]
fn rejects_shared_state_hidden_behind_helper_class() {
let source = r#"
from enum import Enum
class Shared:
data = {}
@classmethod
def data_ref(cls):
return cls.data
class UnsafeKind(Enum):
VALUES = []
class ProcessImpl:
def write(self):
Shared.data_ref()["key"] = "value"
"#;
let violations = violations(source);
assert_eq!(violations.len(), 2);
assert!(violations.iter().any(|item| item.contains("Shared.data")));
assert!(violations.iter().any(|item| item.contains("UnsafeKind.VALUES")));
}
#[test]
fn rejects_module_and_process_class_state() {
let source = r#"
CACHE = {}
from dataclasses import dataclass, field
@dataclass(frozen=True)
class FrozenButMutable:
values: list[int] = field(default_factory=list)
BROKEN = FrozenButMutable()
class Base:
pending = set()
class ProcessImpl(Base):
values = []
"#;
let violations = violations(source);
assert_eq!(violations.len(), 4);
assert!(violations.iter().any(|message| message.contains("CACHE")));
assert!(violations.iter().any(|message| message.contains("BROKEN")));
assert!(violations.iter().any(|message| message.contains("Base.pending")));
assert!(violations.iter().any(|message| message.contains("ProcessImpl.values")));
}
#[test]
fn rejects_module_level_dynamic_shared_state() {
let source = r#"
def helper():
pass
class ProcessImpl:
COUNT = 0
alias = ProcessImpl
alias.cache = {}
helper.cache = []
setattr(ProcessImpl, "pending", set())
"#;
let violations = violations(source);
assert_eq!(violations.len(), 3);
assert!(violations
.iter()
.all(|message| message.contains("module-level shared state")));
}
#[test]
fn rejects_mutable_defaults_and_scope_state() {
let source = r#"
class ProcessImpl:
def on_message(self, message, cache={}):
global counter
def next_value():
nonlocal message
return message
"#;
let violations = violations(source);
assert_eq!(violations.len(), 2);
}
#[test]
fn rejects_shared_but_accepts_local_closure_state() {
let source = r#"
def make_counter():
count = 0
def next_value():
nonlocal count
count += 1
return count
return next_value
SHARED_COUNTER = make_counter()
class ProcessImpl:
def local_counter(self):
count = 0
def next_value():
nonlocal count
count += 1
return count
return next_value()
"#;
let violations = violations(source);
assert_eq!(violations.len(), 2);
assert!(violations.iter().any(|message| message.contains("shared closure")));
}
#[test]
fn rejects_getattr_eval_and_shared_cache_decorator() {
let source = r#"
from functools import lru_cache
@lru_cache
def cached_value(key):
return key
class ProcessImpl:
cache = ()
def on_message(self, message):
getattr(type(self), "cache").clear()
eval("globals()")
"#;
let violations = violations(source);
assert_eq!(violations.len(), 3);
assert!(violations.iter().any(|message| message.contains("lru_cache")));
assert!(violations.iter().any(|message| message.contains("clear")));
assert!(violations.iter().any(|message| message.contains("eval")));
}
#[test]
fn rejects_direct_aliased_and_reflective_mutation() {
let source = r#"
class ProcessImpl:
COUNT = 0
def on_message(self, message):
cls = type(self)
state = cls.__dict__
cls.COUNT += 1
state.update({"x": 1})
setattr(cls, "other", [])
globals()["hidden"] = {}
exec("hidden = {}")
"#;
let violations = violations(source);
assert!(violations.len() >= 5);
assert!(violations.iter().any(|message| message.contains("assignment mutates")));
assert!(violations.iter().any(|message| message.contains("setattr")));
assert!(violations.iter().any(|message| message.contains("globals")));
assert!(violations.iter().any(|message| message.contains("exec")));
}
#[test]
fn rejects_spoofed_safe_names_and_aliased_cache_decorator() {
let source = r#"
def tuple():
return []
def MappingProxyType(value):
return value
def dataclass(*args, **kwargs):
def wrap(cls):
return cls
return wrap
SHARED_TUPLE = tuple()
SHARED_MAPPING = MappingProxyType({})
@dataclass(frozen=True)
class Mutable:
pass
SHARED_OBJECT = Mutable()
from functools import lru_cache as memo
@memo
def cached(value):
return value
class ProcessImpl:
pass
"#;
let violations = violations(source);
assert!(violations.iter().any(|message| message.contains("SHARED_TUPLE")));
assert!(violations.iter().any(|message| message.contains("SHARED_MAPPING")));
assert!(violations.iter().any(|message| message.contains("SHARED_OBJECT")));
assert!(violations.iter().any(|message| message.contains("lru_cache")));
}
#[test]
fn rejects_imported_and_function_object_state() {
let source = r#"
import math
import sys
SHARED = sys.modules
holder = lambda: None
module_alias = math
math.shared = {}
holder.shared = {}
module_alias.other = {}
class ProcessImpl:
def __init__(self):
self.shared = sys.modules
def write(self):
local_alias = math
local_alias.more = {}
self.shared["hidden"] = {}
"#;
let violations = violations(source);
assert!(violations.iter().any(|message| message.contains("SHARED")));
assert!(violations.len() >= 6);
}
#[test]
fn rejects_unknown_constructor_as_mutable_default() {
let source = r#"
class Box:
def __init__(self):
self.values = {}
class ProcessImpl:
def on_message(self, message, box=Box()):
pass
"#;
let violations = violations(source);
assert!(violations
.iter()
.any(|message| message.contains("mutable default argument")));
}
#[test]
fn rejects_dunder_and_class_namespace_reflection() {
let source = r#"
from dataclasses import dataclass
@dataclass(frozen=True)
class Frozen:
value: int
SHARED = Frozen(1)
class ProcessImpl:
locals()["shared"] = {}
def on_message(self, message):
type.__setattr__(ProcessImpl, "cache", {})
object.__setattr__(SHARED, "value", 2)
"#;
let violations = violations(source);
assert!(violations
.iter()
.any(|message| message.contains("class-level shared state")));
assert!(violations.iter().any(|message| message.contains("__setattr__")));
assert!(violations.len() >= 3);
}
#[test]
fn rejects_mutating_frozen_dataclass_methods() {
let source = r#"
from dataclasses import dataclass
@dataclass(frozen=True)
class Frozen:
value: int = 1
def __post_init__(self):
object.__setattr__(self, "state", {})
def put(self, key, value):
self.state[key] = value
GLOBAL = Frozen()
class ProcessImpl:
def write(self):
GLOBAL.put("x", 1)
"#;
let violations = violations(source);
assert!(violations.iter().any(|message| message.contains("GLOBAL")));
}
#[test]
fn rejects_dunder_and_operator_item_mutation() {
let source = r#"
import math
import operator
class ProcessImpl:
def write(self):
math.__dict__.__setitem__("shared", {})
operator.setitem(math.__dict__, "other", {})
"#;
let violations = violations(source);
assert!(violations.len() >= 2);
assert!(violations.iter().any(|message| message.contains("__setitem__")));
assert!(violations.iter().any(|message| message.contains("operator")));
}
#[test]
fn rejects_class_local_and_star_cache_decorators() {
let source = r#"
from functools import *
@lru_cache
def module_cached():
return []
class ProcessImpl:
from functools import lru_cache as memo
@staticmethod
@memo
def shared():
return []
"#;
let violations = violations(source);
assert_eq!(
violations
.iter()
.filter(|message| message.contains("lru_cache"))
.count(),
2
);
}
#[test]
fn checks_literal_dynamic_local_imports() {
let suffix = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos();
let directory = std::env::temp_dir().join(format!(
"distsys-dynamic-import-validator-{}-{suffix}",
std::process::id()
));
std::fs::create_dir_all(&directory).unwrap();
let entrypoint = directory.join("solution.py");
std::fs::write(
&entrypoint,
"from importlib import import_module as load\n\nPREFIX = 'hel'\nMODULE = PREFIX + 'per'\n\nclass ProcessImpl:\n def __init__(self):\n self.shared = load(MODULE).CACHE\n",
)
.unwrap();
std::fs::write(directory.join("helper.py"), "CACHE = {}\n").unwrap();
let result = validate_solution(entrypoint.to_str().unwrap(), PROCESS);
std::fs::remove_dir_all(directory).unwrap();
let violations = result.unwrap();
assert_eq!(violations.len(), 1);
assert!(violations[0].contains("CACHE"));
}
#[test]
fn rejects_shared_values_returned_by_wrappers_and_wrapped_cache() {
let source = r#"
import sys
from functools import lru_cache
def get_shared():
return sys.modules
def memo(function):
return lru_cache(function)
@memo
def shared():
return []
class ProcessImpl:
def __init__(self):
self.shared = get_shared()
def write(self):
self.shared["key"] = shared()
"#;
let violations = violations(source);
assert!(violations.iter().any(|message| message.contains("assignment mutates")));
assert!(violations.iter().any(|message| message.contains("memo")));
}
#[test]
fn rejects_aliased_mutator_callables() {
let source = r#"
import math
import operator
class ProcessImpl:
def write(self):
mutate = operator.setitem
mutate(math.__dict__, "first", {})
bound_mutate = math.__dict__.__setitem__
bound_mutate("second", {})
"#;
let violations = violations(source);
assert_eq!(violations.len(), 2);
assert!(violations.iter().any(|message| message.contains("operator")));
assert!(violations.iter().any(|message| message.contains("aliased")));
}
#[test]
fn rejects_unresolved_dynamic_local_imports() {
let suffix = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos();
let directory = std::env::temp_dir().join(format!(
"distsys-unresolved-import-validator-{}-{suffix}",
std::process::id()
));
std::fs::create_dir_all(&directory).unwrap();
let entrypoint = directory.join("solution.py");
std::fs::write(
&entrypoint,
"from importlib import import_module as load\n\nclass ProcessImpl:\n def write(self):\n name = ''.join(['helper'])\n state = load(name).CACHE\n state['key'] = 1\n",
)
.unwrap();
std::fs::write(directory.join("helper.py"), "CACHE = {}\n").unwrap();
let result = validate_solution(entrypoint.to_str().unwrap(), PROCESS);
std::fs::remove_dir_all(directory).unwrap();
let violations = result.unwrap();
assert_eq!(violations.len(), 1);
assert!(violations[0].contains("cannot be resolved statically"));
}
#[test]
fn rejects_stateful_imported_api_but_allows_reviewed_calls() {
let source = r#"
import random
from random import seed as direct_seed
random.seed(1)
class ProcessImpl:
def write(self, value):
direct_seed(value)
reseed = random.seed
reseed(value)
def read(self):
return random.getstate()
def choose(self):
return random.choice((1, 2))
"#;
let violations = violations(source);
assert_eq!(violations.len(), 4);
assert!(violations.iter().any(|message| message.contains("random.seed")));
assert!(violations.iter().any(|message| message.contains("random.getstate")));
assert!(!violations.iter().any(|message| message.contains("random.choice")));
}
#[test]
fn rejects_passthrough_and_lambda_shared_references() {
let source = r#"
import sys
def identity(value):
return value
get_shared = lambda: sys.modules
lambda_identity = lambda value: value
class ProcessImpl:
def __init__(self):
self.first = identity(sys.modules)
self.second = get_shared()
self.third = lambda_identity(sys.modules)
def write(self):
self.first["first"] = {}
self.second["second"] = {}
self.third["third"] = {}
"#;
let violations = violations(source);
assert_eq!(violations.len(), 3);
assert!(violations.iter().all(|message| message.contains("assignment mutates")));
}
#[test]
fn rejects_getattr_hidden_imported_api_and_mutator() {
let source = r#"
import math
import random
class ProcessImpl:
def write(self, value):
getattr(random, "seed")(value)
getattr(math.__dict__, "__setitem__")("shared", {})
"#;
let violations = violations(source);
assert_eq!(violations.len(), 2);
assert!(violations.iter().any(|message| message.contains("random.seed")));
assert!(violations.iter().any(|message| message.contains("__setitem__")));
}
}
+158
View File
@@ -0,0 +1,158 @@
use std::fs;
use assertables::assume;
use rand::prelude::*;
use rand_pcg::Pcg64;
use anysystem::test::TestResult;
use anysystem::Message;
use crate::common::{build_system, check_guarantees, check_overhead, send_messages, TestConfig};
pub fn test_normal(config: &TestConfig) -> TestResult {
let mut sys = build_system(config, false);
let messages = send_messages(&mut sys, 5);
sys.step_until_no_events();
check_guarantees(&mut sys, &messages, config)?;
// We expect no more than 5 messages from sender in normal network conditions
let sent_count = sys.sent_message_count("sender");
assume!(
sent_count <= 5,
format!("Sender sent {} messages, expected at most 5", sent_count)
)
}
pub fn test_normal_non_unique(config: &TestConfig) -> TestResult {
let mut sys = build_system(config, false);
let messages = send_messages(&mut sys, 10);
sys.step_until_no_events();
check_guarantees(&mut sys, &messages, config)?;
// We expect no more than 10 messages from sender in normal network conditions (stable delay, no loss).
// If solution sends multiple messages without or with too small (<RTT) delay, this results in extra redundant
// traffic. We want to avoid wasting network resources, assuming that normal conditions happen most of the time.
let sent_count = sys.sent_message_count("sender");
assume!(
sent_count <= 10,
format!("Sender sent {} messages, expected at most 10", sent_count)
)
}
pub fn test_delayed(config: &TestConfig) -> TestResult {
let mut sys = build_system(config, false);
sys.network().set_delays(1., 3.);
let messages = send_messages(&mut sys, 5);
sys.step_until_no_events();
check_guarantees(&mut sys, &messages, config)
}
pub fn test_duplicated(config: &TestConfig) -> TestResult {
let mut sys = build_system(config, false);
sys.network().set_dupl_rate(0.3);
let messages = send_messages(&mut sys, 5);
sys.step_until_no_events();
check_guarantees(&mut sys, &messages, config)
}
pub fn test_delayed_duplicated(config: &TestConfig) -> TestResult {
let mut sys = build_system(config, false);
sys.network().set_delays(1., 3.);
sys.network().set_dupl_rate(0.3);
let messages = send_messages(&mut sys, 5);
sys.step_until_no_events();
check_guarantees(&mut sys, &messages, config)
}
pub fn test_old_duplicate(config: &TestConfig) -> TestResult {
let mut sys = build_system(config, false);
sys.network().set_delays(1., 3.);
sys.network().set_dupl_rate(1.);
let first = Message::new("MESSAGE", r#"{"text": "first"}"#);
let mut messages = vec![first.clone()];
sys.send_local_message("sender", first);
// Stop after the first copy is delivered.
while sys.local_outbox("receiver").is_empty() {
if !sys.step() {
return Err("The first message was not delivered".to_string());
}
}
// Keep the old duplicate pending while newer messages are delivered first.
// Zero-delay messages are processed before the delayed copy.
sys.network().set_dupl_rate(0.);
sys.network().set_delay(0.);
for i in 0..50 {
let msg = Message::new("MESSAGE", &format!(r#"{{"text": "message-{i}"}}"#));
sys.send_local_message("sender", msg.clone());
messages.push(msg);
}
sys.step_until_no_events();
check_guarantees(&mut sys, &messages, config)
}
pub fn test_dropped(config: &TestConfig) -> TestResult {
let mut sys = build_system(config, false);
sys.network().set_drop_rate(0.3);
let messages = send_messages(&mut sys, 5);
sys.step_until_no_events();
check_guarantees(&mut sys, &messages, config)
}
pub fn test_chaos_monkey(config: &TestConfig) -> TestResult {
let mut rand = Pcg64::seed_from_u64(config.seed);
for i in 1..=config.monkeys {
let mut run_config = *config;
run_config.seed = rand.next_u64();
println!("Run {} (seed: {})", i, run_config.seed);
let mut sys = build_system(&run_config, false);
sys.network().set_delays(1., 3.);
sys.network().set_dupl_rate(0.3);
sys.network().set_drop_rate(0.3);
let messages = send_messages(&mut sys, 50);
sys.step_until_no_events();
let res = check_guarantees(&mut sys, &messages, &run_config);
res.as_ref()?;
}
Ok(true)
}
pub fn test_overhead(config: &TestConfig, guarantee: &str, faulty: bool) -> TestResult {
for message_count in [100, 500, 1000] {
let mut sys = build_system(config, true);
if faulty {
sys.network().set_delays(1., 3.);
sys.network().set_dupl_rate(0.3);
sys.network().set_drop_rate(0.3);
}
let messages = send_messages(&mut sys, message_count);
sys.step_until_no_events();
let res = check_guarantees(&mut sys, &messages, config);
res.as_ref()?;
let sender_mem = sys.max_size("sender");
let receiver_mem = sys.max_size("receiver");
let net_message_count = sys.network().network_message_count();
let net_traffic = sys.network().traffic();
let throughput = message_count as f64 / sys.time();
println!(
"{message_count:<6} Send Mem: {sender_mem:<8} Recv Mem: {receiver_mem:<8} Messages: {net_message_count:<8} Traffic: {net_traffic:<8} Throughput: {throughput:.3}"
);
check_overhead(
guarantee,
faulty,
message_count,
sender_mem,
receiver_mem,
net_message_count,
net_traffic,
throughput,
)?;
}
let impl_code = fs::read_to_string(config.impl_path).unwrap();
assume!(
!impl_code.contains("<<") && !impl_code.contains(">>"),
"Implementation contains bitwise shift operators"
)?;
Ok(true)
}
@@ -0,0 +1,148 @@
use std::collections::HashMap;
use std::time::Duration;
use sugars::boxed;
use anysystem::logger::LogEntry;
use anysystem::mc::{
predicates::{goals, invariants, prunes},
strategies::Bfs,
InvariantFn, ModelChecker, StrategyConfig,
};
use anysystem::test::TestResult;
use anysystem::Message;
use crate::common::{
build_system, check_delivered_messages, check_message_delivery_once, check_message_delivery_ordered,
check_message_delivery_reliable, generate_message_texts, TestConfig,
};
fn mc_invariant_guarantees(messages_expected: Vec<Message>, config: TestConfig) -> InvariantFn {
boxed!(move |state| {
let mut expected_msg_count = HashMap::new();
for msg in &messages_expected {
*expected_msg_count.entry(msg.data.clone()).or_insert(0) += 1;
}
let delivered = &state.node_states["receiver-node"].proc_states["receiver"].local_outbox;
// check that delivered messages have expected type and data
let delivered_msg_count = check_delivered_messages(delivered, &expected_msg_count, &messages_expected[0].tip)?;
// check delivered message count according to expected guarantees
if config.reliable && state.events.is_empty() {
check_message_delivery_reliable(&delivered_msg_count, &expected_msg_count)?;
}
if config.once {
check_message_delivery_once(&delivered_msg_count, &expected_msg_count)?;
}
if config.ordered {
check_message_delivery_ordered(delivered, &messages_expected)?;
}
Ok(())
})
}
pub fn test_mc_reliable_network(config: &TestConfig) -> TestResult {
let mut sys = build_system(config, false);
let messages: Vec<Message> = generate_message_texts(&mut sys, 2)
.into_iter()
.map(|text| Message::new("MESSAGE", &format!(r#"{{"text": "{text}"}}"#)))
.collect();
let strategy_config = StrategyConfig::default()
.prune(prunes::sent_messages_limit(4))
.goal(goals::got_n_local_messages("receiver-node", "receiver", 2))
.invariant(invariants::all_invariants(vec![
invariants::state_depth(20),
mc_invariant_guarantees(messages.clone(), *config),
]));
let mut mc = ModelChecker::new(&sys);
let res = mc.run_with_change::<Bfs>(strategy_config, move |sys| {
for message in messages {
sys.send_local_message("sender-node", "sender", message);
}
});
if let Err(e) = res {
e.print_trace();
Err(e.message())
} else {
Ok(true)
}
}
pub fn test_mc_message_drops(config: &TestConfig) -> TestResult {
let mut sys = build_system(config, false);
sys.network().set_drop_rate(0.1);
let messages: Vec<Message> = generate_message_texts(&mut sys, 2)
.into_iter()
.map(|text| Message::new("MESSAGE", &format!(r#"{{"text": "{text}"}}"#)))
.collect();
let strategy_config = StrategyConfig::default()
.prune(prunes::state_depth(7))
.goal(goals::any_goal(vec![
goals::got_n_local_messages("receiver-node", "receiver", 2),
goals::no_events(),
]))
.invariant(mc_invariant_guarantees(messages.clone(), *config));
let mut mc = ModelChecker::new(&sys);
let res = mc.run_with_change::<Bfs>(strategy_config, move |sys| {
for message in messages {
sys.send_local_message("sender-node", "sender", message);
}
});
if let Err(e) = res {
e.print_trace();
Err(e.message())
} else {
Ok(true)
}
}
pub fn test_mc_unstable_network(config: &TestConfig) -> TestResult {
let mut sys = build_system(config, false);
sys.network().set_drop_rate(0.1);
sys.network().set_dupl_rate(0.1);
let msg_count = if config.ordered { 3 } else { 2 };
let messages: Vec<Message> = generate_message_texts(&mut sys, msg_count)
.into_iter()
.map(|text| Message::new("MESSAGE", &format!(r#"{{"text": "{text}"}}"#)))
.collect();
let num_drops_allowed = 1;
let num_duplication_allowed = 1;
let goal = if config.reliable && config.once {
goals::all_goals(vec![
goals::got_n_local_messages("receiver-node", "receiver", msg_count),
goals::no_events(),
])
} else {
goals::no_events()
};
let mut invariants = vec![
invariants::state_depth(20),
mc_invariant_guarantees(messages.clone(), *config),
];
if config.ordered {
invariants.push(invariants::time_limit(Duration::from_secs(80)))
};
let strategy_config = StrategyConfig::default()
.prune(prunes::any_prune(vec![
prunes::events_limit(LogEntry::is_mc_message_dropped, num_drops_allowed),
prunes::events_limit(LogEntry::is_mc_message_duplicated, num_duplication_allowed),
prunes::events_limit(LogEntry::is_mc_timer_fired, 1),
prunes::events_limit(LogEntry::is_mc_message_received, msg_count + num_drops_allowed),
]))
.goal(goal)
.invariant(invariants::all_invariants(invariants));
let mut mc = ModelChecker::new(&sys);
let res = mc.run_with_change::<Bfs>(strategy_config, |sys| {
for msg in messages {
sys.send_local_message("sender-node", "sender", msg.clone());
}
});
if let Err(e) = res {
e.print_trace();
Err(e.message())
} else {
Ok(true)
}
}