Add HW 4
This commit is contained in:
@@ -0,0 +1,126 @@
|
|||||||
|
# Надежная упорядоченная рассылка
|
||||||
|
|
||||||
|
В этом задании вам предстоит реализовать механизм рассылки сообщений с описанными далее гарантиями по надежности и порядку доставки. Выполнение и тестирование решения будет происходить в фреймворке AnySystem, уже знакомом вам по первому заданию.
|
||||||
|
|
||||||
|
Пусть имеется распределенное приложение типа чата, пользователи которого общаются путем обмена сообщениями. Для простоты будем считать, что число пользователей фиксировано, чат один, и сообщения в нём доставляются всем пользователям, включая отправителя. С каждым пользователем связан процесс, который принимает его сообщения, рассылает их остальным процессам и доставляет сообщения чата своему пользователю. Процессы выполняются на разных узлах системы, связанных сетью. Как обычно, взаимодействие с пользователем (прием и доставка сообщений) реализуется в AnySystem с помощью локальных сообщений.
|
||||||
|
|
||||||
|
Процессы взаимодействуют путем обмена сообщениями по сети. Изначально вам доступна только отправка сообщения одному процессу (unicast), поверх чего вам надо реализовать рассылку сообщения всем процессам (broadcast).
|
||||||
|
|
||||||
|
Узлы и процессы на них могут внезапно остановиться, например во время рассылки. Отказавший процесс останавливается навсегда и не возвращается в систему. Процесс называется _корректным_, если он не отказал за всё рассматриваемое выполнение системы. Процесс, который сейчас работает, но позднее упадёт, корректным не является. Это определение используется для анализа выполнения: алгоритм не знает заранее, какие процессы откажут. Корректные процессы всегда составляют _большинство_: например, из пяти процессов могут отказать не более двух. (Подсказка: это требуется для реализации свойства 4 ниже.)
|
||||||
|
|
||||||
|
Вам предоставлен надежный транспорт: каждое сетевое сообщение между корректными процессами в конце концов будет получено ровно один раз (exactly once). Транспорт не создаёт и не дублирует сообщения, но порядок их получения не гарантируется. Однако если отправитель упал до получения сообщения адресатом, доставка этого сообщения уже _не гарантируется_, даже если оно было отправлено задолго до отказа. Обеспечивать надежность отдельных сетевых передач в решении не нужно. При этом ваш алгоритм рассылки может использовать собственные подтверждения и другие служебные сообщения.
|
||||||
|
|
||||||
|
Различайте три события: пользователь отправляет сообщение своему процессу (`SEND`), процесс получает сетевое сообщение от другого процесса, процесс доставляет сообщение своему пользователю (`DELIVER`). Получение по сети само по себе не является доставкой пользователю. В свойствах ниже под доставкой понимается именно `DELIVER`.
|
||||||
|
|
||||||
|
Ваша реализация рассылки должна удовлетворять следующим свойствам (см. лекцию 4):
|
||||||
|
|
||||||
|
1. **No Duplication:** Сообщения доставляются пользователю не более одного раза (нет повторов).
|
||||||
|
2. **No Creation:** Если пользователю доставлено сообщение _m_ от пользователя _u_, то _m_ было ранее отправлено _u_.
|
||||||
|
3. **Validity:** Если пользователь корректного процесса _n_ отправил сообщение _m_, то _n_ должен в конце концов доставить _m_ пользователю.
|
||||||
|
4. **Uniform Agreement:** Если сообщение _m_ было доставлено некоторым (необязательно корректным) процессом, то _m_ будет в конце концов доставлено каждым корректным процессом.
|
||||||
|
5. **Causal Order:** Сообщения доставляются с сохранением причинного порядка - если процесс _n_ доставил сообщение _m_ от пользователя _u_, то перед этим _n_ должен доставить все сообщения, которые могли повлиять на _m_ (сообщения, отправленные или доставленные _u_ до отправки им _m_).
|
||||||
|
|
||||||
|
Дополнительная часть задания на 9-10 баллов посвящена масштабируемой рассылке.
|
||||||
|
|
||||||
|
## Реализация
|
||||||
|
|
||||||
|
Для реализации и тестирования решения используется фреймворк AnySystem (см. материалы первого семинара). В папке `solution` размещена заготовка для решения [broadcast.py](solution/broadcast.py). Доработайте класс `BroadcastProcess`, чтобы рассылка удовлетворяла перечисленным свойствам.
|
||||||
|
|
||||||
|
Ваши реализации процессов AnySystem должны соблюдать правило [изоляции процессов](../readme.md#изоляция-процессов-anysystem): изменяемое состояние каждого процесса должно храниться только в `self`, а общая изменяемая память между процессами запрещена. Все взаимодействия между процессами должны происходить через сообщения, как и в реальной распределённой системе.
|
||||||
|
|
||||||
|
При инициализации процессу передается его уникальный id (он же является id локального пользователя), а также список id всех процессов в системе. Отправка сообщения пользователем реализована с помощью локального сообщения `SEND` со строковым полем `text`. Все пользовательские сообщения в пределах одного выполнения системы имеют разные значения `text`. Для доставки сообщения пользователю используйте локальное сообщение `DELIVER` с исходным значением `text` (см. заготовку). Для взаимодействия между процессами вы можете использовать любые собственные типы сообщений.
|
||||||
|
|
||||||
|
Основная реализация не должна делать предположений о максимальных временах доставки сообщений или пытаться определить жив ли некоторый процесс или упал. Позже в курсе мы научимся реализовывать детектор отказов, в основной части этого задания вам надо обойтись без него. Не используйте бесконечно повторяющиеся таймеры: обычные тесты ожидают опустошения очереди событий. Разовые таймеры допустимы, но для решения этой задачи они не нужны.
|
||||||
|
|
||||||
|
## Дополнительная часть: масштабируемая рассылка
|
||||||
|
|
||||||
|
Реализуйте отдельный вариант рассылки из лекции 4: например, gossip, рассылку по дереву или flooding по разреженному графу. Для дерева обязателен механизм восстановления доставки при отказе посредника. Можно предложить другую сопоставимую схему. Новизна идеи не требуется. Экономия памяти или уменьшение константы без изменения схемы распространения сами по себе эту часть не заменяют.
|
||||||
|
|
||||||
|
Сдайте дополнительный вариант в `solution/broadcast_scalable.py`: класс `BroadcastProcess` с теми же аргументами конструктора и сообщениями `SEND`/`DELIVER`, что и в основном решении. Основное решение остаётся в `solution/broadcast.py`, отчёт для обеих версий - в `solution/readme.md`.
|
||||||
|
|
||||||
|
В дополнительном варианте можно ослабить гарантии или использовать дополнительные предположения, включая тайм-ауты и периодические таймеры. В отчёте опишите механизм рассылки и восстановления, сохраняемые и ослабленные свойства, условия применимости и ограничения. Изоляция процессов обязательна для обеих версий. Ошибка, противоречащая заявленным свойствам протокола, не считается допустимым компромиссом.
|
||||||
|
|
||||||
|
Первый дополнительный балл даётся за работающий масштабируемый протокол с таким описанием. Второй - за воспроизводимое сравнение с основным решением на предоставленном тесте `OPTIMIZATION`: приведите команды и параметры запуска, таблицу или график результатов и объясните, как рост системы, отказы и задержки влияют на затраты и доставку. Сокращение трафика рассматривайте вместе с охватом: меньше отправленных сообщений за счёт недоставленных сообщений ещё не означает улучшение. Единого порога выигрыша в процентах нет.
|
||||||
|
|
||||||
|
## Оценивание
|
||||||
|
|
||||||
|
| Компонент | Баллы |
|
||||||
|
| --- | ---: |
|
||||||
|
| Основная реализация удовлетворяет свойствам 1–4 | 5 |
|
||||||
|
| Дополнительно обеспечен Causal Order | до +2 |
|
||||||
|
| В отчёте обосновано достижение заявленных свойств | +1 |
|
||||||
|
| Реализован масштабируемый вариант рассылки | +1 |
|
||||||
|
| Проведено экспериментальное сравнение и объяснены компромиссы | +1 |
|
||||||
|
|
||||||
|
За Causal Order начисляются 2 балла при прохождении всех тестов. Если нарушений не найдено, но `MODEL CHECKING CAUSAL ORDER` не завершён из-за ресурсного лимита (`INCOMPLETE`), начисляется 1 балл из 2. Обнаруженное нарушение Causal Order в любом тесте означает 0 баллов за этот компонент. Эти баллы доступны при выполнении свойств 1–4.
|
||||||
|
|
||||||
|
Отчёт с описанием вашего решения в `solution/readme.md` обязателен, см. [общие правила](../readme.md#отчёт). Для балла за обоснование отдельно объясните в отчёте, почему алгоритм обеспечивает каждое заявленное свойство. Строгих доказательств не требуется, достаточно понятного и убедительного объяснения, связанного с вашим кодом и учитывающего отказы процессов.
|
||||||
|
|
||||||
|
Полное основное решение с обоснованием получает 8 баллов. Баллы за дополнительную часть доступны только после получения всех 8 основных; балл за эксперимент требует реализации масштабируемого варианта. Автоматический `SCORE` равен 0, 5, 6 или 7 и учитывает только основную реализацию. Остальные баллы выставляются по коду и отчёту.
|
||||||
|
|
||||||
|
Бонусы за пробелы в тестах начисляются по [общим правилам](../readme.md#бонусы-за-пробелы-в-тестах).
|
||||||
|
|
||||||
|
## Тестирование
|
||||||
|
|
||||||
|
### Локальное тестирование
|
||||||
|
|
||||||
|
Тесты находятся в папке `tests`. Есть два варианта их запуска.
|
||||||
|
|
||||||
|
Рекомендуемый вариант запуска тестов - через готовый Docker-образ. В этом случае используемое окружение будет аналогично тестирующей системе. Для запуска тестов выполните команду:
|
||||||
|
|
||||||
|
```commandline
|
||||||
|
docker run --pull always --rm -t -v ./solution:/solution distsys.ru/course/broadcast:latest [ЗДЕСЬ МОЖНО УКАЗАТЬ ОПЦИИ]
|
||||||
|
```
|
||||||
|
|
||||||
|
Для запуска полного набора тестов с теми же параметрами, что и в тестирующей системе, выполните команду:
|
||||||
|
|
||||||
|
```commandline
|
||||||
|
docker run --pull always --rm -t -v ./solution:/solution distsys.ru/course/broadcast:latest -m 100
|
||||||
|
```
|
||||||
|
|
||||||
|
Вы также можете запустить тесты, скомпилировав их локально с помощью компилятора Rust. Такой вариант может быть удобен, если вы хотите лучше изучить или доработать тесты. Скомпилируйте тесты с помощью команды `cargo install --locked --path tests`. Для запуска тестов выполните команду:
|
||||||
|
|
||||||
|
```commandline
|
||||||
|
distsys-broadcast [ЗДЕСЬ МОЖНО УКАЗАТЬ ОПЦИИ]
|
||||||
|
```
|
||||||
|
|
||||||
|
Тестер выводит пояснения к сценариям, найденные нарушения и финальную сводку. Подробную трассу событий и истории `SEND`/`DELIVER` можно включить флагом `-d`, например: `distsys-broadcast -t "TWO CRASHES" -d`. Все доступные опции показаны в справке `-h`. Основные тесты по умолчанию запускаются на системе из пяти процессов; это число можно изменить опцией `-p`. При тестировании в системе число прогонов `CHAOS MONKEY` увеличено до 100 (опция `-m 100`).
|
||||||
|
|
||||||
|
Если найдено нарушение свойств 1–4 или превышен ресурсный лимит до причинного model checking, основной результат уже равен нулю: тестер прекращает проверку и помечает оставшиеся тесты как `SKIPPED`. При нарушении только Causal Order тестер продолжает проверять остальные свойства, а причинный MC пропускает. Опция `--keep-going` позволяет собрать больше ошибок, в том числе запустить причинный MC после найденного нарушения. Превышение лимита или аварийное завершение процесса останавливает проверку и с этой опцией.
|
||||||
|
|
||||||
|
Обычный тест ограничен 15 секундами, вся серия `CHAOS MONKEY` - 30 секундами, `SCALABILITY` с системами до 50 процессов - 60 секундами. На основные тесты без `OPTIMIZATION` отводится суммарно 300 секунд. Общий лимит запуска в Docker - 420 секунд. Лимиты защищают от бесконечных обработчиков, таймеров и обмена сообщениями.
|
||||||
|
|
||||||
|
В тестах сообщения процессов, которые позднее упадут, могут теряться ещё до их остановки. Так моделируется, например, ситуация, когда сообщение потерялось в сети, а отправитель упал до повторной передачи на уровне транспорта. До отказа отправитель может продолжать работать, а другие его сообщения - доходить. Реализовывать транспортные повторные передачи в решении не нужно.
|
||||||
|
|
||||||
|
В `CHAOS MONKEY` перед каждым прогоном указаны два процесса, которые позднее упадут: потери исходящих сообщений включаются только для них. Для повторения прогона с номером _k_ используйте указанный базовый seed через `-s` и запустите первые _k_ прогонов через `-m k`.
|
||||||
|
|
||||||
|
В тесте `TWO CRASHES` процессы 0 и 1 могут общаться друг с другом, но изолированы от остальных; связь внутри оставшейся группы сохранена. Процесс 0 начинает рассылку, затем 0 и 1 падают. Если никто не доставил сообщение пользователю, свойства не нарушены: отправитель отказал, а предпосылка Uniform Agreement не выполнена. Если же 0 или 1 доставил сообщение, все корректные процессы тоже должны его доставить. Поэтому ошибка Uniform Agreement показывает обе стороны нарушения: кто уже доставил сообщение и какие корректные процессы его не доставили.
|
||||||
|
|
||||||
|
Тест `QUORUM` проверяет системы из 7 и 10 процессов в двух независимых сценариях. В первом отправитель находится в изолированном меньшинстве: процессы этой группы могут общаться только между собой, а затем падают. Тест проверяет, что до отказа ни один из них не доставляет сообщение пользователю. Во втором меньшинство падает до начала рассылки, а корректный отправитель и остальные процессы должны завершить доставку. Нарушения требуемых свойств в этом тесте учитываются при оценивании.
|
||||||
|
|
||||||
|
Тесты `MODEL CHECKING ...` используют три процесса независимо от `-p`. В `MODEL CHECKING CAUSAL ORDER` два пользователя последовательно отправляют по одному сообщению. Тест перебирает возможные порядки доставки сообщений и останавливается, если при этом требуется проверить больше 100 000 состояний системы. Этот лимит общий для обоих этапов теста. На каждый этап поиска отводится до 55 секунд, на весь тест - до 60 секунд. Превышение одного из этих пределов помечается `INCOMPLETE`: причинная проверка не завершилась, но контрпример не найден. Если предыдущие тесты прошли, автоматический результат равен 6 баллам: 5 за свойства 1–4 и 1 из 2 за Causal Order.
|
||||||
|
|
||||||
|
Частая причина `INCOMPLETE` - избыточный сетевой обмен: model checking перебирает разные порядки сообщений, и число состояний быстро растёт. Проверьте, не повторяет ли инициатор свою первоначальную рассылку после получения сетевой копии и не вызывает ли каждая новая копия сообщения повторную рассылку без новой информации. Сократите лишний обмен, сохранив требуемые свойства рассылки. Для ориентира: известные нам решения проходят этот тест за несколько десятков секунд или быстрее.
|
||||||
|
|
||||||
|
Тест `SCALABILITY` проверяет корректность основной реализации при разных размерах системы и измеряет число сетевых сообщений. Это число само по себе не влияет на оценку. Нарушения требуемых свойств учитываются так же, как в остальных обычных тестах.
|
||||||
|
|
||||||
|
Время выполнения тестов при локальном запуске зависит от компьютера и способа запуска. Подробнее см. в [общей инструкции](../readme.md#локальное-тестирование-решения).
|
||||||
|
|
||||||
|
### Сравнение масштабируемого варианта
|
||||||
|
|
||||||
|
При полном запуске тестер автоматически выполняет `OPTIMIZATION`, если основной результат равен `SCORE: 7` и рядом с основным файлом есть `broadcast_scalable.py`. На сравнение обеих версий отводится общий бюджет 90 секунд. Результаты сохраняются в журнале тестирующей системы. Ошибка или превышение лимита в измерениях не меняет основной `SCORE`. Для отдельного запуска используйте:
|
||||||
|
|
||||||
|
```commandline
|
||||||
|
distsys-broadcast -t OPTIMIZATION
|
||||||
|
docker run --pull always --rm -t -v ./solution:/solution distsys.ru/course/broadcast:latest -t OPTIMIZATION
|
||||||
|
```
|
||||||
|
|
||||||
|
Основной файл можно указать через `-i`, дополнительный - через `--optimized-impl`. Тест сравнивает версии на одинаковой нагрузке при разных размерах системы: без отказов, с отказами и с большими сетевыми задержками. Рядом выводятся число и объём сетевых сообщений, включая служебные, максимальное число отправок одним процессом, охват корректных процессов, задержки и ошибки доставки.
|
||||||
|
|
||||||
|
Наблюдение ограничено по модельному времени, поэтому периодические таймеры допустимы. При сравнении задержек и сетевых затрат учитывайте долю доставленных сообщений: быстрая доставка части сообщений с небольшими затратами может сопровождаться недоставкой остальных. При этом сообщения, не доставленные за время теста, могут быть доставлены позже — тест этого не проверяет. Превышение лимитов помечается как `INCOMPLETE`, невыполненные измерения - `NOT MEASURED`.
|
||||||
|
|
||||||
|
Параметры сценариев и расчёт метрик можно изучить в [коде теста](tests/src/optimization.rs), опции запуска - в справке `-h`. Результаты используются для анализа в отчёте; автоматического порога для получения дополнительных баллов нет.
|
||||||
|
|
||||||
|
### Проверка в тестирующей системе
|
||||||
|
|
||||||
|
Отправьте ваше решение в тестирующую систему следуя [инструкции](../readme.md) и дождитесь результатов.
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
from anysystem import Context, Message, Process
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
|
||||||
|
class BroadcastProcess(Process):
|
||||||
|
def __init__(self, proc_id: str, processes: List[str]):
|
||||||
|
self._id = proc_id
|
||||||
|
self._processes = processes
|
||||||
|
|
||||||
|
def on_local_message(self, msg: Message, ctx: Context):
|
||||||
|
if msg.type == 'SEND':
|
||||||
|
bcast_msg = Message('BCAST', {
|
||||||
|
'text': msg['text']
|
||||||
|
})
|
||||||
|
# best-effort broadcast
|
||||||
|
for proc in self._processes:
|
||||||
|
ctx.send(bcast_msg, proc)
|
||||||
|
|
||||||
|
def on_start(self, ctx: Context):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def on_message(self, msg: Message, sender: str, ctx: Context):
|
||||||
|
if msg.type == 'BCAST':
|
||||||
|
# deliver message to the local user
|
||||||
|
deliver_msg = Message('DELIVER', {
|
||||||
|
'text': msg['text']
|
||||||
|
})
|
||||||
|
ctx.send_local(deliver_msg)
|
||||||
|
|
||||||
|
def on_timer(self, timer_name: str, ctx: Context):
|
||||||
|
pass
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
target
|
||||||
|
**/target
|
||||||
|
**/__pycache__
|
||||||
|
**/.pytest_cache
|
||||||
|
*.pyc
|
||||||
Generated
+985
@@ -0,0 +1,985 @@
|
|||||||
|
# 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 = "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-broadcast"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"anysystem",
|
||||||
|
"clap",
|
||||||
|
"env_logger",
|
||||||
|
"indexmap 2.14.0",
|
||||||
|
"log",
|
||||||
|
"pyo3",
|
||||||
|
"rand",
|
||||||
|
"rand_pcg",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"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",
|
||||||
|
]
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
[package]
|
||||||
|
name = "distsys-broadcast"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
resolver = "3"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anysystem = "=0.3.0"
|
||||||
|
indexmap = "2.12"
|
||||||
|
clap = { version = "3.2.17", features = ["cargo", "derive"] }
|
||||||
|
env_logger = "0.9.0"
|
||||||
|
log = "0.4.17"
|
||||||
|
pyo3 = { version = "=0.29.2", features = ["auto-initialize"] }
|
||||||
|
rand = "0.8.5"
|
||||||
|
rand_pcg = "0.3.1"
|
||||||
|
serde = { version = "1.0.142", features = ["derive"] }
|
||||||
|
serde_json = "1.0.83"
|
||||||
|
sugars = "3.0.1"
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# 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-broadcast /usr/local/bin/distsys-broadcast
|
||||||
|
|
||||||
|
WORKDIR /solution
|
||||||
|
# 300s for correctness tests, 90s shared by both measurements, plus cleanup.
|
||||||
|
ENTRYPOINT ["timeout", "-k", "10", "420", "distsys-broadcast", "-i", "broadcast.py"]
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,36 @@
|
|||||||
|
use serde::Serialize;
|
||||||
|
use sugars::boxed;
|
||||||
|
|
||||||
|
use anysystem::python::PyProcessFactory;
|
||||||
|
use anysystem::System;
|
||||||
|
|
||||||
|
#[derive(Copy, Clone)]
|
||||||
|
pub struct TestConfig<'a> {
|
||||||
|
pub proc_factory: &'a PyProcessFactory,
|
||||||
|
pub proc_count: u64,
|
||||||
|
pub seed: u64,
|
||||||
|
pub monkeys: u32,
|
||||||
|
pub debug: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct BroadcastMessage<'a> {
|
||||||
|
pub text: &'a str,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build_system(config: &TestConfig) -> System {
|
||||||
|
let mut sys = System::new(config.seed);
|
||||||
|
let mut proc_names = Vec::new();
|
||||||
|
for n in 0..config.proc_count {
|
||||||
|
proc_names.push(format!("{n}"));
|
||||||
|
}
|
||||||
|
for proc_name in &proc_names {
|
||||||
|
let proc = config
|
||||||
|
.proc_factory
|
||||||
|
.build((proc_name, proc_names.clone()), config.seed);
|
||||||
|
// process and node on which it runs have the same name
|
||||||
|
sys.add_node(proc_name);
|
||||||
|
sys.add_process(proc_name, boxed!(proc), proc_name);
|
||||||
|
}
|
||||||
|
sys
|
||||||
|
}
|
||||||
@@ -0,0 +1,219 @@
|
|||||||
|
mod common;
|
||||||
|
mod optimization;
|
||||||
|
mod runner;
|
||||||
|
mod shared_state;
|
||||||
|
mod tests;
|
||||||
|
mod tests_mc;
|
||||||
|
|
||||||
|
use anysystem::test::TestResult;
|
||||||
|
use clap::Parser;
|
||||||
|
use indexmap::IndexMap;
|
||||||
|
use std::collections::HashSet;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
/// Broadcast 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/broadcast.py")]
|
||||||
|
solution_path: String,
|
||||||
|
|
||||||
|
/// Test to run (optional)
|
||||||
|
#[clap(long = "test", short)]
|
||||||
|
test: Option<String>,
|
||||||
|
|
||||||
|
/// Print execution trace
|
||||||
|
#[clap(long, short)]
|
||||||
|
debug: bool,
|
||||||
|
|
||||||
|
/// Random seed used in tests
|
||||||
|
#[clap(long, short, default_value = "2023")]
|
||||||
|
seed: u64,
|
||||||
|
|
||||||
|
/// Number of processes
|
||||||
|
#[clap(long, short, default_value = "5")]
|
||||||
|
proc_count: u64,
|
||||||
|
|
||||||
|
/// Number of chaos monkey runs
|
||||||
|
#[clap(long, short, default_value = "10")]
|
||||||
|
monkeys: u32,
|
||||||
|
|
||||||
|
/// Do not run model checking tests
|
||||||
|
#[clap(long)]
|
||||||
|
disable_mc_tests: bool,
|
||||||
|
|
||||||
|
/// Optional scalable solution (defaults to broadcast_scalable.py beside --impl)
|
||||||
|
#[clap(long)]
|
||||||
|
optimized_impl: Option<String>,
|
||||||
|
|
||||||
|
/// Repetitions per OPTIMIZATION scenario and system size
|
||||||
|
#[clap(long, default_value = "10", value_parser = clap::value_parser!(u32).range(1..=100))]
|
||||||
|
optimization_runs: u32,
|
||||||
|
|
||||||
|
/// Shared wall-clock budget in seconds for both implementations in OPTIMIZATION
|
||||||
|
#[clap(long, default_value = "90", value_parser = clap::value_parser!(u64).range(1..=90))]
|
||||||
|
optimization_timeout: u64,
|
||||||
|
|
||||||
|
#[clap(long, hide = true)]
|
||||||
|
optimization_worker: bool,
|
||||||
|
|
||||||
|
/// Wall-clock limit in seconds per ordinary test
|
||||||
|
#[clap(long, default_value = "15", value_parser = clap::value_parser!(u64).range(1..=300))]
|
||||||
|
test_timeout: u64,
|
||||||
|
|
||||||
|
/// Wall-clock limit in seconds for SCALABILITY (up to 50 processes)
|
||||||
|
#[clap(long, default_value = "60", value_parser = clap::value_parser!(u64).range(1..=300))]
|
||||||
|
scalability_timeout: u64,
|
||||||
|
|
||||||
|
/// Wall-clock limit in seconds for all CHAOS MONKEY runs
|
||||||
|
#[clap(long, default_value = "30", value_parser = clap::value_parser!(u64).range(1..=300))]
|
||||||
|
chaos_timeout: u64,
|
||||||
|
|
||||||
|
/// External MC test limit (NORMAL DELIVERY is capped at 110s, CAUSAL ORDER at 60s)
|
||||||
|
#[clap(long, default_value = "210", value_parser = clap::value_parser!(u64).range(1..=210))]
|
||||||
|
mc_timeout: u64,
|
||||||
|
|
||||||
|
/// Total wall-clock budget in seconds for correctness tests
|
||||||
|
#[clap(long, default_value = "300", value_parser = clap::value_parser!(u64).range(1..=300))]
|
||||||
|
suite_timeout: u64,
|
||||||
|
|
||||||
|
/// Continue after failures for local diagnostics; resource limits and crashes still stop
|
||||||
|
#[clap(long)]
|
||||||
|
keep_going: bool,
|
||||||
|
|
||||||
|
#[clap(long, hide = true, requires = "test-result")]
|
||||||
|
test_worker: Option<String>,
|
||||||
|
|
||||||
|
#[clap(long, hide = true)]
|
||||||
|
test_result: Option<PathBuf>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let args = Args::parse();
|
||||||
|
if args.test_worker.is_some() {
|
||||||
|
runner::worker(&args);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if args.optimization_worker {
|
||||||
|
optimization::worker(&args);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if args.test.as_deref() == Some("OPTIMIZATION") {
|
||||||
|
optimization::run(&args);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if !shared_state::validate_or_report(&args.solution_path, &["BroadcastProcess"]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let results = runner::run(&args);
|
||||||
|
if args.test.is_none() {
|
||||||
|
let score = score(results);
|
||||||
|
optimization::run_if_present(&args, score);
|
||||||
|
println!("SCORE: {score}\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn score(results: IndexMap<String, TestResult>) -> f32 {
|
||||||
|
let mut violated = HashSet::new();
|
||||||
|
let mut causal_incomplete = false;
|
||||||
|
for (name, result) in results {
|
||||||
|
if let Err(e) = result {
|
||||||
|
if e.starts_with("Violated") {
|
||||||
|
for prop in e.replace("Violated ", "").split(", ") {
|
||||||
|
violated.insert(prop.to_string());
|
||||||
|
}
|
||||||
|
} else if runner::causal_mc_incomplete(&name, &e) {
|
||||||
|
// Keep partial credit separate from a discovered violation.
|
||||||
|
causal_incomplete = true;
|
||||||
|
} else {
|
||||||
|
return 0.;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let violated: HashSet<&str> = violated.iter().map(|s| s.as_str()).collect();
|
||||||
|
let base_props = HashSet::from([
|
||||||
|
"NO DUPLICATION",
|
||||||
|
"NO CREATION",
|
||||||
|
"VALIDITY",
|
||||||
|
"UNIFORM AGREEMENT",
|
||||||
|
]);
|
||||||
|
if violated.is_disjoint(&base_props) {
|
||||||
|
if !violated.is_empty() {
|
||||||
|
5.
|
||||||
|
} else if causal_incomplete {
|
||||||
|
6.
|
||||||
|
} else {
|
||||||
|
7.
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
0.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod score_tests {
|
||||||
|
use super::score;
|
||||||
|
use indexmap::IndexMap;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn incomplete_causal_mc_gets_partial_credit() {
|
||||||
|
for error in [
|
||||||
|
"time limit of 55s exceeded",
|
||||||
|
"test time limit of 60s exceeded",
|
||||||
|
"suite time limit of 300s exceeded",
|
||||||
|
"causal model-checking state limit of 100000 exceeded",
|
||||||
|
] {
|
||||||
|
let mut results = IndexMap::new();
|
||||||
|
results.insert("MODEL CHECKING CAUSAL ORDER".to_string(), Err(error.into()));
|
||||||
|
assert_eq!(score(results), 6., "{error}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn discovered_violation_takes_precedence_over_incomplete_search() {
|
||||||
|
for (error, expected) in [
|
||||||
|
("Violated CAUSAL ORDER", 5.),
|
||||||
|
("Violated VALIDITY", 0.),
|
||||||
|
("Violated VALIDITY, CAUSAL ORDER", 0.),
|
||||||
|
] {
|
||||||
|
for incomplete_first in [false, true] {
|
||||||
|
let mut entries = vec![
|
||||||
|
("CAUSAL ORDER", error),
|
||||||
|
("MODEL CHECKING CAUSAL ORDER", "time limit of 55s exceeded"),
|
||||||
|
];
|
||||||
|
if incomplete_first {
|
||||||
|
entries.reverse();
|
||||||
|
}
|
||||||
|
let results = entries
|
||||||
|
.into_iter()
|
||||||
|
.map(|(name, error)| (name.to_string(), Err(error.to_string())))
|
||||||
|
.collect();
|
||||||
|
assert_eq!(score(results), expected, "{error}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn other_failures_do_not_get_partial_credit() {
|
||||||
|
for (name, error) in [
|
||||||
|
("NORMAL", "test time limit of 15s exceeded"),
|
||||||
|
(
|
||||||
|
"MODEL CHECKING NORMAL DELIVERY",
|
||||||
|
"time limit of 100s exceeded",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"MODEL CHECKING CAUSAL ORDER",
|
||||||
|
"test worker exited with exit code: 1",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"MODEL CHECKING CAUSAL ORDER",
|
||||||
|
"nothing left to do to reach the goal",
|
||||||
|
),
|
||||||
|
] {
|
||||||
|
let mut results = IndexMap::new();
|
||||||
|
results.insert(name.to_string(), Err(error.to_string()));
|
||||||
|
assert_eq!(score(results), 0., "{name}: {error}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,516 @@
|
|||||||
|
//! Bounded measurements, deliberately separate from correctness tests and SCORE.
|
||||||
|
use std::cell::Cell;
|
||||||
|
use std::collections::{BTreeMap, BTreeSet};
|
||||||
|
use std::io::{Read, Write};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::process::{Command, Stdio};
|
||||||
|
use std::rc::Rc;
|
||||||
|
use std::sync::{
|
||||||
|
atomic::{AtomicBool, Ordering},
|
||||||
|
Arc,
|
||||||
|
};
|
||||||
|
use std::thread;
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
use anysystem::logger::LogEntry;
|
||||||
|
use anysystem::python::PyProcessFactory;
|
||||||
|
use anysystem::{Context, Message, Process, System};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::common::{BroadcastMessage, TestConfig};
|
||||||
|
use crate::{shared_state, Args};
|
||||||
|
|
||||||
|
const SIZES: [u64; 4] = [5, 10, 20, 50];
|
||||||
|
const SCENARIOS: [&str; 3] = ["NORMAL", "CRASHES", "DELAYED"];
|
||||||
|
const MESSAGES: usize = 10;
|
||||||
|
const HORIZON: f64 = 109.; // 100 model-time units after the last SEND at t=9.
|
||||||
|
const EVENT_LIMIT: u64 = 100_000;
|
||||||
|
const OUTPUT_LIMIT: u64 = 2 * 1024 * 1024;
|
||||||
|
const RESULT_PREFIX: &str = "OPTIMIZATION RESULT ";
|
||||||
|
|
||||||
|
// Coverage counts first valid DELIVERs to correct processes, including the
|
||||||
|
// sender. Missing deliveries at the horizon do not prove a liveness violation.
|
||||||
|
// Latencies use model time and only those deliveries. Network counters include
|
||||||
|
// service traffic; duplicate/unexpected/malformed outputs count all processes.
|
||||||
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
|
struct Measurement {
|
||||||
|
scenario: String,
|
||||||
|
n: u64,
|
||||||
|
seed: u64,
|
||||||
|
status: String,
|
||||||
|
observed_until: f64,
|
||||||
|
sent: usize,
|
||||||
|
messages: u64,
|
||||||
|
bytes: u64,
|
||||||
|
max_sent: u64,
|
||||||
|
delivered: usize,
|
||||||
|
expected: usize,
|
||||||
|
latency_mean: Option<f64>,
|
||||||
|
latency_max: Option<f64>,
|
||||||
|
duplicates: usize,
|
||||||
|
unexpected: usize,
|
||||||
|
malformed: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
// A native test driver schedules external actions independently of the student's
|
||||||
|
// traffic. It has no network traffic and is excluded from all measurements.
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct Workload {
|
||||||
|
action: Rc<Cell<usize>>,
|
||||||
|
crashes: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Process for Workload {
|
||||||
|
fn on_local_message(&mut self, _: Message, ctx: &mut Context) -> Result<(), String> {
|
||||||
|
for i in 1..MESSAGES {
|
||||||
|
ctx.set_timer(&i.to_string(), i as f64);
|
||||||
|
}
|
||||||
|
if self.crashes {
|
||||||
|
ctx.set_timer("10", 4.5);
|
||||||
|
}
|
||||||
|
ctx.set_timer("11", HORIZON);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_message(&mut self, _: Message, _: String, _: &mut Context) -> Result<(), String> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_timer(&mut self, timer: String, _: &mut Context) -> Result<(), String> {
|
||||||
|
self.action.set(timer.parse().unwrap());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn send(sys: &mut System, index: usize) {
|
||||||
|
let text = format!("{index:02}:{}", "x".repeat(61)); // fixed 64-byte payload
|
||||||
|
sys.send_local_message(
|
||||||
|
"0",
|
||||||
|
Message::json("SEND", &BroadcastMessage { text: &text }),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn measure(config: &TestConfig, scenario: &str) -> Measurement {
|
||||||
|
let mut sys = System::new(config.seed);
|
||||||
|
if scenario == "DELAYED" {
|
||||||
|
sys.network().set_delays(1., 10.);
|
||||||
|
} else {
|
||||||
|
sys.network().set_delays(1., 3.);
|
||||||
|
}
|
||||||
|
let processes: Vec<String> = (0..config.proc_count).map(|i| i.to_string()).collect();
|
||||||
|
for proc in &processes {
|
||||||
|
let implementation = config
|
||||||
|
.proc_factory
|
||||||
|
.build((proc, processes.clone()), config.seed);
|
||||||
|
sys.add_node(proc);
|
||||||
|
sys.add_process(proc, Box::new(implementation), proc);
|
||||||
|
}
|
||||||
|
let action = Rc::new(Cell::new(0));
|
||||||
|
sys.add_node("_workload");
|
||||||
|
sys.add_process(
|
||||||
|
"_workload",
|
||||||
|
Box::new(Workload {
|
||||||
|
action: action.clone(),
|
||||||
|
crashes: scenario == "CRASHES",
|
||||||
|
}),
|
||||||
|
"_workload",
|
||||||
|
);
|
||||||
|
sys.send_local_message("_workload", Message::new("START", "{}"));
|
||||||
|
send(&mut sys, 0);
|
||||||
|
let mut status = "event limit";
|
||||||
|
for _ in 0..EVENT_LIMIT {
|
||||||
|
if !sys.step() {
|
||||||
|
status = "queue exhausted";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
match action.replace(0) {
|
||||||
|
0 => {}
|
||||||
|
index @ 1..=9 => send(&mut sys, index),
|
||||||
|
10 => {
|
||||||
|
sys.crash_node("1");
|
||||||
|
sys.crash_node("2");
|
||||||
|
}
|
||||||
|
11 => {
|
||||||
|
status = "horizon";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
summarize(&sys, config, scenario, status)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn summarize(sys: &System, config: &TestConfig, scenario: &str, status: &str) -> Measurement {
|
||||||
|
let mut latencies = Vec::new();
|
||||||
|
let mut duplicates = 0;
|
||||||
|
let mut unexpected = 0;
|
||||||
|
let mut malformed = 0;
|
||||||
|
let mut network_sends = BTreeMap::<String, u64>::new();
|
||||||
|
let mut sent = BTreeMap::new();
|
||||||
|
let mut seen = BTreeSet::new();
|
||||||
|
// Trace order matters: a guessed DELIVER before SEND is unexpected even
|
||||||
|
// when the two events have the same model timestamp.
|
||||||
|
for entry in sys.logger().trace() {
|
||||||
|
match entry {
|
||||||
|
LogEntry::MessageSent {
|
||||||
|
src_proc, dst_proc, ..
|
||||||
|
} if src_proc != dst_proc => {
|
||||||
|
*network_sends.entry(src_proc.clone()).or_default() += 1;
|
||||||
|
}
|
||||||
|
LogEntry::LocalMessageReceived {
|
||||||
|
time, proc, msg, ..
|
||||||
|
} if proc == "0" && msg.tip == "SEND" => {
|
||||||
|
let data: serde_json::Value = serde_json::from_str(&msg.data).unwrap();
|
||||||
|
sent.insert(data["text"].as_str().unwrap().to_string(), *time);
|
||||||
|
}
|
||||||
|
LogEntry::LocalMessageSent {
|
||||||
|
time, proc, msg, ..
|
||||||
|
} => {
|
||||||
|
let data = serde_json::from_str::<serde_json::Value>(&msg.data).ok();
|
||||||
|
let text = data.as_ref().and_then(|v| v["text"].as_str());
|
||||||
|
if msg.tip != "DELIVER" || text.is_none() {
|
||||||
|
malformed += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let text = text.unwrap();
|
||||||
|
if !seen.insert((proc.clone(), text.to_string())) {
|
||||||
|
duplicates += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if let Some(sent_at) = sent.get(text) {
|
||||||
|
if scenario != "CRASHES" || (proc != "1" && proc != "2") {
|
||||||
|
latencies.push(time - sent_at);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
unexpected += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let correct_count = config.proc_count - if scenario == "CRASHES" { 2 } else { 0 };
|
||||||
|
let network = sys.network();
|
||||||
|
Measurement {
|
||||||
|
scenario: scenario.to_string(),
|
||||||
|
n: config.proc_count,
|
||||||
|
seed: config.seed,
|
||||||
|
status: status.to_string(),
|
||||||
|
observed_until: sys.time(),
|
||||||
|
sent: sent.len(),
|
||||||
|
messages: network.network_message_count(),
|
||||||
|
bytes: network.traffic(),
|
||||||
|
max_sent: network_sends.into_values().max().unwrap_or(0),
|
||||||
|
delivered: latencies.len(),
|
||||||
|
expected: MESSAGES * correct_count as usize,
|
||||||
|
latency_mean: (!latencies.is_empty())
|
||||||
|
.then(|| latencies.iter().sum::<f64>() / latencies.len() as f64),
|
||||||
|
latency_max: latencies.into_iter().reduce(f64::max),
|
||||||
|
duplicates,
|
||||||
|
unexpected,
|
||||||
|
malformed,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn worker(args: &Args) {
|
||||||
|
// This process may panic, hang in Python, or print SCORE from the validator.
|
||||||
|
// The supervisor captures its output and never forwards it verbatim.
|
||||||
|
if !shared_state::validate_or_report(&args.solution_path, &["BroadcastProcess"]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let factory = PyProcessFactory::new(&args.solution_path, "BroadcastProcess");
|
||||||
|
let mut rows = Vec::new();
|
||||||
|
'measurements: for scenario in SCENARIOS {
|
||||||
|
for n in SIZES {
|
||||||
|
for run in 0..args.optimization_runs {
|
||||||
|
let config = TestConfig {
|
||||||
|
proc_factory: &factory,
|
||||||
|
proc_count: n,
|
||||||
|
seed: args.seed.wrapping_add(run as u64),
|
||||||
|
monkeys: 0,
|
||||||
|
debug: false,
|
||||||
|
};
|
||||||
|
let result = measure(&config, scenario);
|
||||||
|
let incomplete = result.status == "event limit";
|
||||||
|
rows.push(result);
|
||||||
|
if incomplete {
|
||||||
|
break 'measurements;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
drop(factory);
|
||||||
|
// Emit one final record only after all simulator callbacks and cleanup.
|
||||||
|
// Earlier stdout belongs to the solution and is diagnostic text, even if it
|
||||||
|
// resembles this record. This framing is not a sandbox for hostile Python.
|
||||||
|
println!("\n{RESULT_PREFIX}{}", serde_json::to_string(&rows).unwrap());
|
||||||
|
std::io::stdout().flush().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
struct WorkerResult {
|
||||||
|
rows: Vec<Measurement>,
|
||||||
|
problem: Option<String>,
|
||||||
|
diagnostics: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_bounded(
|
||||||
|
reader: impl Read + Send + 'static,
|
||||||
|
exceeded: Arc<AtomicBool>,
|
||||||
|
) -> thread::JoinHandle<Vec<u8>> {
|
||||||
|
thread::spawn(move || {
|
||||||
|
let mut bytes = Vec::new();
|
||||||
|
let _ = reader.take(OUTPUT_LIMIT + 1).read_to_end(&mut bytes);
|
||||||
|
if bytes.len() as u64 > OUTPUT_LIMIT {
|
||||||
|
exceeded.store(true, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
bytes
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_worker(args: &Args, path: &str, deadline: Instant) -> std::io::Result<WorkerResult> {
|
||||||
|
if Instant::now() >= deadline {
|
||||||
|
return Ok(WorkerResult {
|
||||||
|
rows: Vec::new(),
|
||||||
|
problem: Some("shared wall-clock budget exhausted".to_string()),
|
||||||
|
diagnostics: Vec::new(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let mut child = Command::new(std::env::current_exe()?)
|
||||||
|
.args([
|
||||||
|
"--optimization-worker",
|
||||||
|
"-i",
|
||||||
|
path,
|
||||||
|
"-s",
|
||||||
|
&args.seed.to_string(),
|
||||||
|
"--optimization-runs",
|
||||||
|
&args.optimization_runs.to_string(),
|
||||||
|
])
|
||||||
|
.env("PYTHONUNBUFFERED", "1")
|
||||||
|
// The validator also initializes Python, so set the seed before spawn.
|
||||||
|
.env("PYTHONHASHSEED", args.seed.to_string())
|
||||||
|
.stdout(Stdio::piped())
|
||||||
|
.stderr(Stdio::piped())
|
||||||
|
.spawn()?;
|
||||||
|
let exceeded = Arc::new(AtomicBool::new(false));
|
||||||
|
let out = read_bounded(child.stdout.take().unwrap(), exceeded.clone());
|
||||||
|
let err = read_bounded(child.stderr.take().unwrap(), exceeded.clone());
|
||||||
|
let mut problem = None;
|
||||||
|
loop {
|
||||||
|
match child.try_wait() {
|
||||||
|
Ok(Some(status)) => {
|
||||||
|
if !status.success() {
|
||||||
|
problem = Some(format!("worker exited with {status}"));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Ok(None) => {}
|
||||||
|
Err(error) => {
|
||||||
|
problem = Some(error.to_string());
|
||||||
|
let _ = child.kill();
|
||||||
|
let _ = child.wait();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if exceeded.load(Ordering::Relaxed) {
|
||||||
|
problem = Some("output limit".to_string());
|
||||||
|
let _ = child.kill();
|
||||||
|
let _ = child.wait();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if Instant::now() >= deadline {
|
||||||
|
problem = Some(format!(
|
||||||
|
"shared wall-clock limit ({}s)",
|
||||||
|
args.optimization_timeout
|
||||||
|
));
|
||||||
|
let _ = child.kill();
|
||||||
|
let _ = child.wait();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
thread::sleep(Duration::from_millis(10));
|
||||||
|
}
|
||||||
|
let stdout = out.join().unwrap_or_default();
|
||||||
|
let stderr = err.join().unwrap_or_default();
|
||||||
|
if stdout.len() as u64 > OUTPUT_LIMIT || stderr.len() as u64 > OUTPUT_LIMIT {
|
||||||
|
problem = Some("output limit".to_string());
|
||||||
|
}
|
||||||
|
let output = String::from_utf8_lossy(&stdout);
|
||||||
|
let (diagnostic_output, result_line) = output
|
||||||
|
.strip_suffix('\n')
|
||||||
|
.unwrap_or(&output)
|
||||||
|
.rsplit_once('\n')
|
||||||
|
.unwrap_or(("", output.as_ref()));
|
||||||
|
let mut rows = Vec::new();
|
||||||
|
if problem.is_none() {
|
||||||
|
match parse_measurements(result_line.trim_end_matches('\r'), args) {
|
||||||
|
Ok(measurements) => rows = measurements,
|
||||||
|
Err(error) => problem = Some(error),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut diagnostics = Vec::new();
|
||||||
|
for line in diagnostic_output.lines() {
|
||||||
|
if !line.is_empty() && diagnostics.len() < 4 {
|
||||||
|
diagnostics.push(line.chars().take(240).collect());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// A missing or malformed final record is also useful diagnostic output.
|
||||||
|
if rows.is_empty() && !result_line.is_empty() && diagnostics.len() < 4 {
|
||||||
|
diagnostics.push(result_line.chars().take(240).collect());
|
||||||
|
}
|
||||||
|
for line in String::from_utf8_lossy(&stderr).lines().take(4) {
|
||||||
|
diagnostics.push(line.chars().take(240).collect());
|
||||||
|
}
|
||||||
|
Ok(WorkerResult {
|
||||||
|
rows,
|
||||||
|
problem,
|
||||||
|
diagnostics,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_measurements(line: &str, args: &Args) -> Result<Vec<Measurement>, String> {
|
||||||
|
let json = line
|
||||||
|
.strip_prefix(RESULT_PREFIX)
|
||||||
|
.ok_or("worker did not return measurements")?;
|
||||||
|
let rows: Vec<Measurement> =
|
||||||
|
serde_json::from_str(json).map_err(|e| format!("invalid measurements: {e}"))?;
|
||||||
|
let expected = SCENARIOS.iter().flat_map(|scenario| {
|
||||||
|
SIZES.iter().flat_map(move |n| {
|
||||||
|
(0..args.optimization_runs)
|
||||||
|
.map(move |run| (*scenario, *n, args.seed.wrapping_add(run as u64)))
|
||||||
|
})
|
||||||
|
});
|
||||||
|
let total = SCENARIOS.len() * SIZES.len() * args.optimization_runs as usize;
|
||||||
|
if rows.is_empty()
|
||||||
|
|| rows.len() > total
|
||||||
|
|| rows.iter().zip(expected).any(|(row, (scenario, n, seed))| {
|
||||||
|
row.scenario != scenario || row.n != n || row.seed != seed
|
||||||
|
})
|
||||||
|
|| rows[..rows.len() - 1]
|
||||||
|
.iter()
|
||||||
|
.any(|row| row.status == "event limit")
|
||||||
|
|| (rows.len() < total && rows.last().unwrap().status != "event limit")
|
||||||
|
{
|
||||||
|
return Err("worker returned an incomplete or unexpected measurement sequence".to_string());
|
||||||
|
}
|
||||||
|
Ok(rows)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn optimized_path(args: &Args) -> PathBuf {
|
||||||
|
args.optimized_impl
|
||||||
|
.as_ref()
|
||||||
|
.map(PathBuf::from)
|
||||||
|
.unwrap_or_else(|| {
|
||||||
|
PathBuf::from(&args.solution_path).with_file_name("broadcast_scalable.py")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run_if_present(args: &Args, score: f32) {
|
||||||
|
if args.optimized_impl.is_some() || optimized_path(args).exists() {
|
||||||
|
if score == 7. {
|
||||||
|
run(args);
|
||||||
|
} else {
|
||||||
|
println!("SKIPPED: OPTIMIZATION (requires main SCORE: 7)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cell(row: Option<&Measurement>) -> String {
|
||||||
|
let Some(r) = row else {
|
||||||
|
return "NOT MEASURED".to_string();
|
||||||
|
};
|
||||||
|
let latency = |value: Option<f64>| {
|
||||||
|
value
|
||||||
|
.map(|v| format!("{v:.3}"))
|
||||||
|
.unwrap_or_else(|| "n/a".to_string())
|
||||||
|
};
|
||||||
|
format!(
|
||||||
|
"{} {} {} {}/{} {} {} {} {} {} [{}; t={:.3}; sent={}]",
|
||||||
|
r.messages,
|
||||||
|
r.bytes,
|
||||||
|
r.max_sent,
|
||||||
|
r.delivered,
|
||||||
|
r.expected,
|
||||||
|
latency(r.latency_mean),
|
||||||
|
latency(r.latency_max),
|
||||||
|
r.duplicates,
|
||||||
|
r.unexpected,
|
||||||
|
r.malformed,
|
||||||
|
if r.status == "event limit" {
|
||||||
|
"INCOMPLETE: event limit"
|
||||||
|
} else {
|
||||||
|
&r.status
|
||||||
|
},
|
||||||
|
r.observed_until,
|
||||||
|
r.sent
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run(args: &Args) {
|
||||||
|
println!("\n--- OPTIMIZATION ---\n");
|
||||||
|
let optimized = optimized_path(args);
|
||||||
|
if !optimized.is_file() {
|
||||||
|
println!(
|
||||||
|
"SKIPPED: scalable implementation not found: {}",
|
||||||
|
optimized.display()
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Both implementations receive the same external workload: ten unique
|
||||||
|
// 64-byte messages from process 0 at t=0..9, observed until HORIZON.
|
||||||
|
// Run indices start at zero; internal traffic depends on the implementation.
|
||||||
|
// Measurements have no performance threshold or automatic optimization score.
|
||||||
|
// Event/time/output limits mark incomplete measurements without changing SCORE.
|
||||||
|
println!("Workload: N={SIZES:?}, messages={MESSAGES} x 64B, sender=0, send_t=0..9, horizon={HORIZON}");
|
||||||
|
println!("Delays: NORMAL/CRASHES=[1,3], DELAYED=[1,10]; CRASHES: [1,2] at t=4.5");
|
||||||
|
println!("Runs={}, base_seed={}; limits: {EVENT_LIMIT} events/case, {}s/comparison, {OUTPUT_LIMIT} bytes/stream", args.optimization_runs, args.seed, args.optimization_timeout);
|
||||||
|
let deadline = Instant::now() + Duration::from_secs(args.optimization_timeout);
|
||||||
|
let mut results = Vec::new();
|
||||||
|
for (label, path) in [
|
||||||
|
("BASE", args.solution_path.as_str()),
|
||||||
|
("SCALABLE", optimized.to_str().unwrap_or("")),
|
||||||
|
] {
|
||||||
|
if label == "SCALABLE"
|
||||||
|
&& results.first().is_some_and(|r: &WorkerResult| {
|
||||||
|
r.problem.is_some() || r.rows.iter().any(|row| row.status == "event limit")
|
||||||
|
})
|
||||||
|
{
|
||||||
|
println!("SKIPPED: SCALABLE (BASE measurement incomplete)");
|
||||||
|
results.push(WorkerResult {
|
||||||
|
rows: Vec::new(),
|
||||||
|
problem: None,
|
||||||
|
diagnostics: Vec::new(),
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
println!("Measuring {label}: {path}");
|
||||||
|
let result = run_worker(args, path, deadline).unwrap_or_else(|e| WorkerResult {
|
||||||
|
rows: Vec::new(),
|
||||||
|
problem: Some(e.to_string()),
|
||||||
|
diagnostics: Vec::new(),
|
||||||
|
});
|
||||||
|
if let Some(problem) = &result.problem {
|
||||||
|
println!("{label}: INCOMPLETE: {problem}");
|
||||||
|
}
|
||||||
|
for line in &result.diagnostics {
|
||||||
|
println!("{label} diagnostic: {line}");
|
||||||
|
}
|
||||||
|
results.push(result);
|
||||||
|
}
|
||||||
|
println!("scenario N seed | BASE: messages bytes max_sent delivered/expected latency_mean latency_max dup unexpected malformed | SCALABLE: same columns");
|
||||||
|
for scenario in SCENARIOS {
|
||||||
|
for n in SIZES {
|
||||||
|
for run in 0..args.optimization_runs {
|
||||||
|
let seed = args.seed.wrapping_add(run as u64);
|
||||||
|
let find = |side: usize| {
|
||||||
|
results[side]
|
||||||
|
.rows
|
||||||
|
.iter()
|
||||||
|
.find(|r| r.scenario == scenario && r.n == n && r.seed == seed)
|
||||||
|
};
|
||||||
|
println!(
|
||||||
|
"{scenario} {n} {seed} | {} | {}",
|
||||||
|
cell(find(0)),
|
||||||
|
cell(find(1))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,336 @@
|
|||||||
|
//! Run each correctness test in a killable process. Student callbacks can block
|
||||||
|
//! inside Python, so an in-simulator deadline alone cannot enforce wall time.
|
||||||
|
use std::fs::{self, OpenOptions};
|
||||||
|
use std::io::Write;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::process::Command;
|
||||||
|
use std::thread;
|
||||||
|
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
|
use anysystem::python::PyProcessFactory;
|
||||||
|
use anysystem::test::TestResult;
|
||||||
|
use env_logger::Builder;
|
||||||
|
use indexmap::IndexMap;
|
||||||
|
use log::LevelFilter;
|
||||||
|
|
||||||
|
use crate::common::TestConfig;
|
||||||
|
use crate::tests::*;
|
||||||
|
use crate::tests_mc::*;
|
||||||
|
use crate::Args;
|
||||||
|
|
||||||
|
struct Test {
|
||||||
|
name: &'static str,
|
||||||
|
run: fn(&TestConfig) -> TestResult,
|
||||||
|
}
|
||||||
|
|
||||||
|
const TESTS: &[Test] = &[
|
||||||
|
Test {
|
||||||
|
name: "NORMAL",
|
||||||
|
run: test_normal,
|
||||||
|
},
|
||||||
|
Test {
|
||||||
|
name: "SENDER CRASH",
|
||||||
|
run: test_sender_crash,
|
||||||
|
},
|
||||||
|
Test {
|
||||||
|
name: "SENDER CRASH 2",
|
||||||
|
run: test_sender_crash2,
|
||||||
|
},
|
||||||
|
Test {
|
||||||
|
name: "TWO CRASHES",
|
||||||
|
run: test_two_crashes,
|
||||||
|
},
|
||||||
|
Test {
|
||||||
|
name: "TWO CRASHES 2",
|
||||||
|
run: test_two_crashes2,
|
||||||
|
},
|
||||||
|
Test {
|
||||||
|
name: "TWO CRASHES AFTER DELIVERY",
|
||||||
|
run: test_two_crashes_after_delivery,
|
||||||
|
},
|
||||||
|
Test {
|
||||||
|
name: "CAUSAL ORDER",
|
||||||
|
run: test_causal_order,
|
||||||
|
},
|
||||||
|
Test {
|
||||||
|
name: "QUORUM",
|
||||||
|
run: test_quorum,
|
||||||
|
},
|
||||||
|
Test {
|
||||||
|
name: "CHAOS MONKEY",
|
||||||
|
run: test_chaos_monkey,
|
||||||
|
},
|
||||||
|
Test {
|
||||||
|
name: "SCALABILITY",
|
||||||
|
run: test_scalability,
|
||||||
|
},
|
||||||
|
Test {
|
||||||
|
name: "MODEL CHECKING NORMAL DELIVERY",
|
||||||
|
run: test_mc_normal_delivery,
|
||||||
|
},
|
||||||
|
Test {
|
||||||
|
name: "MODEL CHECKING SENDER CRASH",
|
||||||
|
run: test_mc_sender_crash,
|
||||||
|
},
|
||||||
|
Test {
|
||||||
|
name: "MODEL CHECKING CAUSAL ORDER",
|
||||||
|
run: test_mc_causal_order,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
pub fn worker(args: &Args) {
|
||||||
|
let test = TESTS
|
||||||
|
.iter()
|
||||||
|
.find(|test| Some(test.name) == args.test_worker.as_deref())
|
||||||
|
.unwrap();
|
||||||
|
if args.debug {
|
||||||
|
Builder::new()
|
||||||
|
.filter(Some("anysystem"), LevelFilter::Debug)
|
||||||
|
.format(|buf, record| writeln!(buf, "{}", record.args()))
|
||||||
|
.init();
|
||||||
|
}
|
||||||
|
let factory = PyProcessFactory::new(&args.solution_path, "BroadcastProcess");
|
||||||
|
let config = TestConfig {
|
||||||
|
proc_factory: &factory,
|
||||||
|
proc_count: if test.name.starts_with("MODEL CHECKING") {
|
||||||
|
3
|
||||||
|
} else {
|
||||||
|
args.proc_count
|
||||||
|
},
|
||||||
|
seed: args.seed,
|
||||||
|
monkeys: args.monkeys,
|
||||||
|
debug: args.debug,
|
||||||
|
};
|
||||||
|
let result = (test.run)(&config);
|
||||||
|
// Keep the structured result separate from student prints and debug traces.
|
||||||
|
fs::write(
|
||||||
|
args.test_result.as_ref().unwrap(),
|
||||||
|
serde_json::to_vec(&result).unwrap(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ResultFile(PathBuf);
|
||||||
|
|
||||||
|
impl ResultFile {
|
||||||
|
fn new() -> std::io::Result<Self> {
|
||||||
|
let nonce = SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.unwrap()
|
||||||
|
.as_nanos();
|
||||||
|
let path = std::env::temp_dir().join(format!(
|
||||||
|
"broadcast-test-{}-{nonce}.json",
|
||||||
|
std::process::id()
|
||||||
|
));
|
||||||
|
OpenOptions::new()
|
||||||
|
.write(true)
|
||||||
|
.create_new(true)
|
||||||
|
.open(&path)?;
|
||||||
|
Ok(Self(path))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for ResultFile {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
let _ = fs::remove_file(&self.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_one(args: &Args, test: &Test, suite_deadline: Instant) -> TestResult {
|
||||||
|
let seconds = if test.name == "CHAOS MONKEY" {
|
||||||
|
args.chaos_timeout
|
||||||
|
} else if test.name == "SCALABILITY" {
|
||||||
|
args.scalability_timeout
|
||||||
|
} else if test.name.starts_with("MODEL CHECKING") {
|
||||||
|
// The process deadline also catches a callback hanging inside the MC.
|
||||||
|
args.mc_timeout
|
||||||
|
.min(if test.name == "MODEL CHECKING NORMAL DELIVERY" {
|
||||||
|
110
|
||||||
|
} else if test.name == "MODEL CHECKING CAUSAL ORDER" {
|
||||||
|
60
|
||||||
|
} else {
|
||||||
|
210
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
args.test_timeout
|
||||||
|
};
|
||||||
|
let test_deadline = Instant::now() + Duration::from_secs(seconds);
|
||||||
|
let deadline = test_deadline.min(suite_deadline);
|
||||||
|
let timeout = if suite_deadline <= test_deadline {
|
||||||
|
format!("suite time limit of {}s exceeded", args.suite_timeout)
|
||||||
|
} else {
|
||||||
|
format!("test time limit of {seconds}s exceeded")
|
||||||
|
};
|
||||||
|
if Instant::now() >= deadline {
|
||||||
|
return Err(timeout);
|
||||||
|
}
|
||||||
|
let result_file = ResultFile::new().map_err(|e| format!("test worker error: {e}"))?;
|
||||||
|
let mut command = Command::new(std::env::current_exe().map_err(|e| e.to_string())?);
|
||||||
|
command
|
||||||
|
.args([
|
||||||
|
"--test-worker",
|
||||||
|
test.name,
|
||||||
|
"-i",
|
||||||
|
&args.solution_path,
|
||||||
|
"-s",
|
||||||
|
&args.seed.to_string(),
|
||||||
|
"-p",
|
||||||
|
&args.proc_count.to_string(),
|
||||||
|
"-m",
|
||||||
|
&args.monkeys.to_string(),
|
||||||
|
"--test-result",
|
||||||
|
])
|
||||||
|
.arg(&result_file.0)
|
||||||
|
.env("PYTHONUNBUFFERED", "1")
|
||||||
|
// Python must see the seed before interpreter initialization.
|
||||||
|
.env("PYTHONHASHSEED", args.seed.to_string());
|
||||||
|
if args.debug {
|
||||||
|
command.arg("-d");
|
||||||
|
}
|
||||||
|
let mut child = command
|
||||||
|
.spawn()
|
||||||
|
.map_err(|e| format!("test worker error: {e}"))?;
|
||||||
|
loop {
|
||||||
|
match child.try_wait() {
|
||||||
|
Ok(Some(status)) => {
|
||||||
|
if !status.success() {
|
||||||
|
return Err(format!("test worker exited with {status}"));
|
||||||
|
}
|
||||||
|
return serde_json::from_slice(
|
||||||
|
&fs::read(&result_file.0).map_err(|e| format!("test worker result: {e}"))?,
|
||||||
|
)
|
||||||
|
.map_err(|e| format!("test worker result: {e}"))?;
|
||||||
|
}
|
||||||
|
Ok(None) => {}
|
||||||
|
Err(error) => {
|
||||||
|
let _ = child.kill();
|
||||||
|
let _ = child.wait();
|
||||||
|
return Err(format!("test worker error: {error}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if Instant::now() >= deadline {
|
||||||
|
let _ = child.kill();
|
||||||
|
let _ = child.wait();
|
||||||
|
return Err(timeout);
|
||||||
|
}
|
||||||
|
thread::sleep(Duration::from_millis(10));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn must_stop(result: &TestResult, keep_going: bool) -> bool {
|
||||||
|
let Err(error) = result else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
// Resource exhaustion and process failures always stop, including in local
|
||||||
|
// diagnostic mode. A pure causal violation must not hide a later base error.
|
||||||
|
if error.contains("time limit")
|
||||||
|
|| error.starts_with("causal model-checking ")
|
||||||
|
|| error.starts_with("test worker")
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if keep_going {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
!error.strip_prefix("Violated ").is_some_and(|properties| {
|
||||||
|
properties
|
||||||
|
.split(", ")
|
||||||
|
.all(|property| property == "CAUSAL ORDER")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn causal_mc_incomplete(name: &str, error: &str) -> bool {
|
||||||
|
name == "MODEL CHECKING CAUSAL ORDER"
|
||||||
|
&& (error.contains("time limit") || error.starts_with("causal model-checking "))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run(args: &Args) -> IndexMap<String, TestResult> {
|
||||||
|
let tests: Vec<_> = TESTS
|
||||||
|
.iter()
|
||||||
|
.filter(|test| {
|
||||||
|
if let Some(name) = &args.test {
|
||||||
|
test.name == name
|
||||||
|
} else {
|
||||||
|
!args.disable_mc_tests || !test.name.starts_with("MODEL CHECKING")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let deadline = Instant::now() + Duration::from_secs(args.suite_timeout);
|
||||||
|
let mut results = IndexMap::new();
|
||||||
|
let mut stopped = None;
|
||||||
|
let mut passed = 0;
|
||||||
|
let mut causal_violated = false;
|
||||||
|
for test in &tests {
|
||||||
|
if let Some(failed_test) = stopped {
|
||||||
|
println!("SKIPPED: {} (stopped after {failed_test})", test.name);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if test.name == "MODEL CHECKING CAUSAL ORDER" && causal_violated && !args.keep_going {
|
||||||
|
println!("SKIPPED: {} (CAUSAL ORDER already violated)", test.name);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
println!("\n--- {} ---\n", test.name);
|
||||||
|
std::io::stdout().flush().unwrap();
|
||||||
|
let result = run_one(args, test, deadline);
|
||||||
|
match &result {
|
||||||
|
Ok(_) => {
|
||||||
|
println!("\nPASSED\n");
|
||||||
|
passed += 1;
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
causal_violated |= error.strip_prefix("Violated ").is_some_and(|properties| {
|
||||||
|
properties
|
||||||
|
.split(", ")
|
||||||
|
.any(|property| property == "CAUSAL ORDER")
|
||||||
|
});
|
||||||
|
println!("\nFAILED: {error}");
|
||||||
|
if causal_mc_incomplete(test.name, error) {
|
||||||
|
println!("INCOMPLETE: causal model checking did not finish within its resource budget");
|
||||||
|
}
|
||||||
|
println!();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if must_stop(&result, args.keep_going) {
|
||||||
|
stopped = Some(test.name);
|
||||||
|
}
|
||||||
|
results.insert(test.name.to_string(), result);
|
||||||
|
}
|
||||||
|
if let Some(name) = &args.test {
|
||||||
|
if tests.is_empty() {
|
||||||
|
println!("FAILED: unknown test {name}");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
println!("-------------------------------------------------------------------------------");
|
||||||
|
println!("\nPassed {passed} from {} tests\n", tests.len());
|
||||||
|
if results.len() != tests.len() {
|
||||||
|
println!("Skipped {} tests\n", tests.len() - results.len());
|
||||||
|
}
|
||||||
|
if results.values().any(Result::is_err) {
|
||||||
|
println!("Failed tests:");
|
||||||
|
for (name, result) in &results {
|
||||||
|
if let Err(error) = result {
|
||||||
|
println!("- {name}: {error}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
println!();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
results
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::must_stop;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mc_timeout_always_stops_but_causal_violation_does_not() {
|
||||||
|
assert!(must_stop(&Err("time limit of 100s exceeded".into()), false));
|
||||||
|
assert!(must_stop(&Err("time limit of 100s exceeded".into()), true));
|
||||||
|
assert!(!must_stop(&Err("Violated CAUSAL ORDER".into()), false));
|
||||||
|
assert!(must_stop(
|
||||||
|
&Err("Violated VALIDITY, CAUSAL ORDER".into()),
|
||||||
|
false
|
||||||
|
));
|
||||||
|
assert!(!must_stop(&Err("Violated VALIDITY".into()), true));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
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_source;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn embeds_validator_and_rejects_shared_state() {
|
||||||
|
let violations = validate_source(
|
||||||
|
"class ProcessImpl:\n shared = {}\n",
|
||||||
|
"solution.py",
|
||||||
|
&["ProcessImpl"],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(violations.len(), 1);
|
||||||
|
assert!(violations[0].contains("ProcessImpl.shared"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,495 @@
|
|||||||
|
use std::collections::{BTreeMap, BTreeSet, HashSet};
|
||||||
|
|
||||||
|
use rand::prelude::*;
|
||||||
|
use rand_pcg::Pcg64;
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
|
use anysystem::test::TestResult;
|
||||||
|
use anysystem::{Message, ProcessEvent, System};
|
||||||
|
|
||||||
|
use crate::common::{build_system, BroadcastMessage, TestConfig};
|
||||||
|
|
||||||
|
pub fn test_normal(config: &TestConfig) -> TestResult {
|
||||||
|
let mut sys = build_system(config);
|
||||||
|
let text = "0:Hello";
|
||||||
|
sys.send_local_message("0", Message::json("SEND", &BroadcastMessage { text }));
|
||||||
|
sys.step_until_no_events();
|
||||||
|
check(&sys, config, HashSet::from([text.to_string()]))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn test_sender_crash(config: &TestConfig) -> TestResult {
|
||||||
|
println!("Process 0 will crash after another process receives a network message.");
|
||||||
|
let mut sys = build_system(config);
|
||||||
|
let text = "0:Hello";
|
||||||
|
sys.send_local_message("0", Message::json("SEND", &BroadcastMessage { text }));
|
||||||
|
// run until the message is received by one other process
|
||||||
|
let mut received = false;
|
||||||
|
while !received {
|
||||||
|
if !sys.step() {
|
||||||
|
return Err("no events left before another process received the message".to_string());
|
||||||
|
}
|
||||||
|
for n in 1..config.proc_count {
|
||||||
|
if sys.received_message_count(&n.to_string()) == 1 {
|
||||||
|
received = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// crash source node
|
||||||
|
sys.crash_node("0");
|
||||||
|
sys.step_until_no_events();
|
||||||
|
check(&sys, config, HashSet::from([text.to_string()]))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn test_sender_crash2(config: &TestConfig) -> TestResult {
|
||||||
|
println!("Process 0 is isolated and will crash; its messages cannot reach other processes.");
|
||||||
|
let mut sys = build_system(config);
|
||||||
|
let text = "0:Hello";
|
||||||
|
// let the message to be received only by the sender itself by disconnecting it
|
||||||
|
sys.network().disconnect_node("0");
|
||||||
|
sys.send_local_message("0", Message::json("SEND", &BroadcastMessage { text }));
|
||||||
|
sys.step();
|
||||||
|
sys.crash_node("0");
|
||||||
|
sys.step_until_no_events();
|
||||||
|
check(&sys, config, HashSet::from([text.to_string()]))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn test_two_crashes(config: &TestConfig) -> TestResult {
|
||||||
|
if config.proc_count < 5 {
|
||||||
|
return Err(
|
||||||
|
"There are 2 crashes in this test, so at least 5 processes are required.".to_string(),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
let mut sys = build_system(config);
|
||||||
|
let survivors: Vec<String> = (2..config.proc_count).map(|n| n.to_string()).collect();
|
||||||
|
println!(
|
||||||
|
"Processes [0, 1] are isolated from [{}] and will crash; links within each group remain enabled.",
|
||||||
|
survivors.join(", ")
|
||||||
|
);
|
||||||
|
sys.network().make_partition(
|
||||||
|
&["0", "1"],
|
||||||
|
&survivors.iter().map(String::as_str).collect::<Vec<_>>(),
|
||||||
|
);
|
||||||
|
let text = "0:Hello";
|
||||||
|
sys.send_local_message("0", Message::json("SEND", &BroadcastMessage { text }));
|
||||||
|
sys.steps(config.proc_count.pow(2));
|
||||||
|
sys.crash_node("0");
|
||||||
|
sys.crash_node("1");
|
||||||
|
sys.step_until_no_events();
|
||||||
|
check(&sys, config, HashSet::from([text.to_string()]))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn test_two_crashes2(config: &TestConfig) -> TestResult {
|
||||||
|
if config.proc_count < 5 {
|
||||||
|
return Err(
|
||||||
|
"There are 2 crashes in this test, so at least 5 processes are required.".to_string(),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
let mut sys = build_system(config);
|
||||||
|
println!(
|
||||||
|
"Outgoing messages from processes [1, 2] are dropped; both processes will later crash."
|
||||||
|
);
|
||||||
|
// Model messages that remain undelivered until their senders crash.
|
||||||
|
sys.network().drop_outgoing("1");
|
||||||
|
sys.network().drop_outgoing("2");
|
||||||
|
let text = "0:Hello";
|
||||||
|
sys.send_local_message("0", Message::json("SEND", &BroadcastMessage { text }));
|
||||||
|
sys.steps(config.proc_count.pow(2));
|
||||||
|
sys.crash_node("1");
|
||||||
|
sys.crash_node("2");
|
||||||
|
sys.step_until_no_events();
|
||||||
|
check(&sys, config, HashSet::from([text.to_string()]))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn test_two_crashes_after_delivery(config: &TestConfig) -> TestResult {
|
||||||
|
if config.proc_count < 5 {
|
||||||
|
return Err(
|
||||||
|
"There are 2 crashes in this test, so at least 5 processes are required.".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
println!("Process 0 and the first other process to deliver to its local user will crash immediately after that delivery.");
|
||||||
|
let mut sys = build_system(config);
|
||||||
|
let text = "0:Hello";
|
||||||
|
sys.send_local_message("0", Message::json("SEND", &BroadcastMessage { text }));
|
||||||
|
// Crash the sender and the first other process to deliver to its user.
|
||||||
|
// Uniform agreement must still hold even though that recipient has crashed.
|
||||||
|
while sys.step() {
|
||||||
|
let recipient = (1..config.proc_count)
|
||||||
|
.map(|n| n.to_string())
|
||||||
|
.find(|proc| !sys.local_outbox(proc).is_empty());
|
||||||
|
if let Some(recipient) = recipient {
|
||||||
|
println!("Process {recipient} delivered to its local user; crashing processes [0, {recipient}].");
|
||||||
|
sys.crash_node("0");
|
||||||
|
sys.crash_node(&recipient);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sys.step_until_no_events();
|
||||||
|
check(&sys, config, HashSet::from([text.to_string()]))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn test_quorum(config: &TestConfig) -> TestResult {
|
||||||
|
for node_count in [7, 10] {
|
||||||
|
let mut run_config = *config;
|
||||||
|
run_config.proc_count = node_count;
|
||||||
|
let failures = (node_count - 1) / 2;
|
||||||
|
let text = "0:Hello";
|
||||||
|
|
||||||
|
println!("- N={node_count}: {failures} isolated processes crash");
|
||||||
|
let mut sys = build_system(&run_config);
|
||||||
|
let minority: Vec<String> = (0..failures).map(|n| n.to_string()).collect();
|
||||||
|
let majority: Vec<String> = (failures..node_count).map(|n| n.to_string()).collect();
|
||||||
|
println!(
|
||||||
|
"Processes [{}] are isolated from [{}] and will crash; links within each group remain enabled.",
|
||||||
|
minority.join(", "),
|
||||||
|
majority.join(", ")
|
||||||
|
);
|
||||||
|
sys.network().make_partition(
|
||||||
|
&minority.iter().map(String::as_str).collect::<Vec<_>>(),
|
||||||
|
&majority.iter().map(String::as_str).collect::<Vec<_>>(),
|
||||||
|
);
|
||||||
|
sys.send_local_message("0", Message::json("SEND", &BroadcastMessage { text }));
|
||||||
|
sys.step_until_no_events();
|
||||||
|
for n in 0..failures {
|
||||||
|
sys.crash_node(&n.to_string());
|
||||||
|
}
|
||||||
|
sys.step_until_no_events();
|
||||||
|
check(&sys, &run_config, HashSet::from([text.to_string()]))?;
|
||||||
|
|
||||||
|
println!("- N={node_count}: {failures} processes crash before broadcast");
|
||||||
|
println!(
|
||||||
|
"Processes [{}] will crash; the remaining majority must deliver to their local users.",
|
||||||
|
((node_count - failures)..node_count)
|
||||||
|
.map(|n| n.to_string())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(", ")
|
||||||
|
);
|
||||||
|
let mut sys = build_system(&run_config);
|
||||||
|
// The surviving majority, including the sender, must complete delivery.
|
||||||
|
for n in (node_count - failures)..node_count {
|
||||||
|
sys.crash_node(&n.to_string());
|
||||||
|
}
|
||||||
|
sys.send_local_message("0", Message::json("SEND", &BroadcastMessage { text }));
|
||||||
|
sys.step_until_no_events();
|
||||||
|
check(&sys, &run_config, HashSet::from([text.to_string()]))?;
|
||||||
|
}
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn test_causal_order(config: &TestConfig) -> TestResult {
|
||||||
|
let mut sys = build_system(config);
|
||||||
|
sys.network().set_delays(100., 200.);
|
||||||
|
let texts = ["0:Hello", "1:How?", "0:Fine!"];
|
||||||
|
sys.send_local_message(
|
||||||
|
"0",
|
||||||
|
Message::json("SEND", &BroadcastMessage { text: texts[0] }),
|
||||||
|
);
|
||||||
|
while sys.event_log("1").is_empty() {
|
||||||
|
if !sys.step() {
|
||||||
|
return Err("no events left before the second SEND".to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sys.network().set_delays(10., 20.);
|
||||||
|
sys.send_local_message(
|
||||||
|
"1",
|
||||||
|
Message::json("SEND", &BroadcastMessage { text: texts[1] }),
|
||||||
|
);
|
||||||
|
while sys.event_log("0").len() < 3 {
|
||||||
|
if !sys.step() {
|
||||||
|
return Err("no events left before the third SEND".to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sys.network().set_delay(1.);
|
||||||
|
sys.send_local_message(
|
||||||
|
"0",
|
||||||
|
Message::json("SEND", &BroadcastMessage { text: texts[2] }),
|
||||||
|
);
|
||||||
|
sys.step_until_no_events();
|
||||||
|
let sent_messages = HashSet::from_iter(texts.into_iter().map(String::from));
|
||||||
|
check(&sys, config, sent_messages)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn test_chaos_monkey(config: &TestConfig) -> TestResult {
|
||||||
|
if config.proc_count < 5 {
|
||||||
|
return Err(
|
||||||
|
"There are 2 crashes in this test, so at least 5 processes are required.".to_string(),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
let mut rand = Pcg64::seed_from_u64(config.seed);
|
||||||
|
println!("Only outgoing messages from processes that will later crash may be dropped.");
|
||||||
|
for i in 1..=config.monkeys {
|
||||||
|
// Preserve the random sequence: this draw was previously printed as an unused seed.
|
||||||
|
let _ = rand.next_u64();
|
||||||
|
let mut sys = build_system(config);
|
||||||
|
let victim1 = format!("{}", rand.gen_range(0..config.proc_count));
|
||||||
|
let mut victim2 = format!("{}", rand.gen_range(0..config.proc_count));
|
||||||
|
while victim2 == victim1 {
|
||||||
|
victim2 = format!("{}", rand.gen_range(0..config.proc_count));
|
||||||
|
}
|
||||||
|
println!(
|
||||||
|
"- Run {i} (base seed: {}; processes that will crash: [{victim1}, {victim2}])",
|
||||||
|
config.seed
|
||||||
|
);
|
||||||
|
let mut sent_messages = HashSet::new();
|
||||||
|
for i in 0..10 {
|
||||||
|
let sender = format!("{}", rand.gen_range(0..config.proc_count));
|
||||||
|
let text = format!("{sender}:{i}");
|
||||||
|
sent_messages.insert(text.clone());
|
||||||
|
if i % 2 == 0 {
|
||||||
|
sys.network().set_delays(10., 20.);
|
||||||
|
} else {
|
||||||
|
sys.network().set_delays(1., 2.);
|
||||||
|
}
|
||||||
|
for j in 0..8 {
|
||||||
|
if rand.gen_range(0.0..1.0) > 0.3 {
|
||||||
|
sys.network().drop_outgoing(&victim1);
|
||||||
|
} else {
|
||||||
|
sys.network().pass_outgoing(&victim1);
|
||||||
|
}
|
||||||
|
if rand.gen_range(0.0..1.0) > 0.3 {
|
||||||
|
sys.network().drop_outgoing(&victim2);
|
||||||
|
} else {
|
||||||
|
sys.network().pass_outgoing(&victim2);
|
||||||
|
}
|
||||||
|
if j == 0 {
|
||||||
|
sys.send_local_message(
|
||||||
|
&sender,
|
||||||
|
Message::json("SEND", &BroadcastMessage { text: &text }),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
sys.step();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sys.crash_node(&victim1);
|
||||||
|
sys.crash_node(&victim2);
|
||||||
|
sys.step_until_no_events();
|
||||||
|
check(&sys, config, sent_messages)?;
|
||||||
|
}
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn test_scalability(config: &TestConfig) -> TestResult {
|
||||||
|
let sys_sizes = [
|
||||||
|
config.proc_count,
|
||||||
|
config.proc_count * 2,
|
||||||
|
config.proc_count * 4,
|
||||||
|
config.proc_count * 10,
|
||||||
|
];
|
||||||
|
let mut msg_counts = Vec::new();
|
||||||
|
for node_count in sys_sizes {
|
||||||
|
let mut run_config = *config;
|
||||||
|
run_config.proc_count = node_count;
|
||||||
|
let mut sys = build_system(&run_config);
|
||||||
|
let text = "0:Hello!";
|
||||||
|
sys.send_local_message("0", Message::json("SEND", &BroadcastMessage { text }));
|
||||||
|
sys.step_until_no_events();
|
||||||
|
check(&sys, &run_config, HashSet::from([text.to_string()]))?;
|
||||||
|
msg_counts.push(sys.network().network_message_count());
|
||||||
|
}
|
||||||
|
println!("\nMessage count:");
|
||||||
|
for i in 0..sys_sizes.len() {
|
||||||
|
let baseline = sys_sizes[i] * (sys_sizes[i] - 1);
|
||||||
|
println!(
|
||||||
|
"- N={}: {} (baseline {})",
|
||||||
|
sys_sizes[i], msg_counts[i], baseline
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check(sys: &System, config: &TestConfig, all_sent_messages: HashSet<String>) -> TestResult {
|
||||||
|
let mut sent = BTreeMap::new();
|
||||||
|
let mut delivered = BTreeMap::new();
|
||||||
|
let mut all_delivered = BTreeSet::new();
|
||||||
|
let mut histories = BTreeMap::new();
|
||||||
|
let mut debug_histories = BTreeMap::new();
|
||||||
|
for proc in sys.process_names() {
|
||||||
|
let mut history = Vec::new();
|
||||||
|
let mut debug_history = Vec::new();
|
||||||
|
let mut sent_msgs = Vec::new();
|
||||||
|
let mut delivered_msgs = Vec::new();
|
||||||
|
for e in sys.event_log(&proc) {
|
||||||
|
match e.event {
|
||||||
|
ProcessEvent::LocalMessageReceived { msg: m } => {
|
||||||
|
let data: Value = serde_json::from_str(&m.data).unwrap();
|
||||||
|
let message = data["text"].as_str().unwrap().to_string();
|
||||||
|
sent_msgs.push(message.clone());
|
||||||
|
if config.debug {
|
||||||
|
debug_history.push(format!("SEND {message:?}"));
|
||||||
|
}
|
||||||
|
history.push(message);
|
||||||
|
}
|
||||||
|
ProcessEvent::LocalMessageSent { msg: m } => {
|
||||||
|
let data: Value = serde_json::from_str(&m.data).unwrap();
|
||||||
|
let message = data["text"].as_str().unwrap().to_string();
|
||||||
|
delivered_msgs.push(message.clone());
|
||||||
|
all_delivered.insert(message.clone());
|
||||||
|
if config.debug {
|
||||||
|
debug_history.push(format!("DELIVER {message:?}"));
|
||||||
|
}
|
||||||
|
history.push(message);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sent.insert(proc.clone(), sent_msgs);
|
||||||
|
delivered.insert(proc.clone(), delivered_msgs);
|
||||||
|
debug_histories.insert(proc.clone(), debug_history);
|
||||||
|
histories.insert(proc, history);
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.debug {
|
||||||
|
println!(
|
||||||
|
"Messages sent across network: {}",
|
||||||
|
sys.network().network_message_count()
|
||||||
|
);
|
||||||
|
println!("Process histories:");
|
||||||
|
for proc in sys.process_names() {
|
||||||
|
println!(
|
||||||
|
"- [{}] {}",
|
||||||
|
proc,
|
||||||
|
debug_histories.get(&proc).unwrap().join(", ")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NO DUPLICATION
|
||||||
|
let mut no_duplication = true;
|
||||||
|
for (proc, delivered_msgs) in &delivered {
|
||||||
|
let mut uniq = HashSet::new();
|
||||||
|
for msg in delivered_msgs {
|
||||||
|
if uniq.contains(msg) {
|
||||||
|
println!("NO DUPLICATION: process {proc} delivered message {msg:?} to its local user more than once.");
|
||||||
|
no_duplication = false;
|
||||||
|
};
|
||||||
|
uniq.insert(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NO CREATION
|
||||||
|
let mut no_creation = true;
|
||||||
|
for (proc, delivered_msgs) in &delivered {
|
||||||
|
for msg in delivered_msgs {
|
||||||
|
if !all_sent_messages.contains(msg) {
|
||||||
|
println!("NO CREATION: process {proc} delivered message {msg:?} to its local user, but no user sent it.");
|
||||||
|
no_creation = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// VALIDITY
|
||||||
|
let mut validity = true;
|
||||||
|
for (proc, sent_msgs) in &sent {
|
||||||
|
if sys.proc_node_is_crashed(proc) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let delivered_msgs = delivered.get(proc).unwrap();
|
||||||
|
for msg in sent_msgs {
|
||||||
|
if !delivered_msgs.contains(msg) {
|
||||||
|
println!("VALIDITY: correct process {proc} received SEND {msg:?} from its local user, but did not deliver it back with DELIVER.");
|
||||||
|
validity = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UNIFORM AGREEMENT
|
||||||
|
let mut uniform_agreement = true;
|
||||||
|
for msg in all_delivered.iter() {
|
||||||
|
let mut delivered_by = Vec::new();
|
||||||
|
let mut missing = Vec::new();
|
||||||
|
for (proc, delivered_msgs) in &delivered {
|
||||||
|
if delivered_msgs.contains(msg) {
|
||||||
|
delivered_by.push(if sys.proc_node_is_crashed(proc) {
|
||||||
|
format!("{proc} (later crashed)")
|
||||||
|
} else {
|
||||||
|
proc.clone()
|
||||||
|
});
|
||||||
|
} else if !sys.proc_node_is_crashed(proc) {
|
||||||
|
missing.push(proc.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !missing.is_empty() {
|
||||||
|
println!("UNIFORM AGREEMENT violated for message {msg:?}:");
|
||||||
|
println!(
|
||||||
|
" Delivered to local users by processes: {}.",
|
||||||
|
delivered_by.join(", ")
|
||||||
|
);
|
||||||
|
println!(
|
||||||
|
" Not delivered by correct processes: {}.",
|
||||||
|
missing.join(", ")
|
||||||
|
);
|
||||||
|
uniform_agreement = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CAUSAL ORDER
|
||||||
|
let mut causal_order = true;
|
||||||
|
for (src, sent_msgs) in &sent {
|
||||||
|
for msg in sent_msgs.iter() {
|
||||||
|
if !all_delivered.contains(msg) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// build sender past for send message event
|
||||||
|
let mut src_past = HashSet::new();
|
||||||
|
for e in histories.get(src).unwrap() {
|
||||||
|
if e != msg {
|
||||||
|
src_past.insert(e.clone());
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// check that other correct processes have delivered all past events before delivering the message
|
||||||
|
for (dst, delivered_msgs) in &delivered {
|
||||||
|
if sys.proc_node_is_crashed(dst) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let mut dst_past = HashSet::new();
|
||||||
|
for e in delivered_msgs {
|
||||||
|
if e != msg {
|
||||||
|
dst_past.insert(e.clone());
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !dst_past.is_superset(&src_past) {
|
||||||
|
let mut missing = src_past
|
||||||
|
.difference(&dst_past)
|
||||||
|
.cloned()
|
||||||
|
.collect::<Vec<String>>();
|
||||||
|
missing.sort();
|
||||||
|
println!(
|
||||||
|
"CAUSAL ORDER: process {} did not deliver messages {:?} before message {:?} from process {}.",
|
||||||
|
dst,
|
||||||
|
missing,
|
||||||
|
msg,
|
||||||
|
src
|
||||||
|
);
|
||||||
|
causal_order = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if no_duplication & no_creation & validity & uniform_agreement & causal_order {
|
||||||
|
Ok(true)
|
||||||
|
} else {
|
||||||
|
let mut violated = Vec::new();
|
||||||
|
if !no_duplication {
|
||||||
|
violated.push("NO DUPLICATION")
|
||||||
|
}
|
||||||
|
if !no_creation {
|
||||||
|
violated.push("NO CREATION")
|
||||||
|
}
|
||||||
|
if !validity {
|
||||||
|
violated.push("VALIDITY")
|
||||||
|
}
|
||||||
|
if !uniform_agreement {
|
||||||
|
violated.push("UNIFORM AGREEMENT")
|
||||||
|
}
|
||||||
|
if !causal_order {
|
||||||
|
violated.push("CAUSAL ORDER")
|
||||||
|
}
|
||||||
|
Err(format!("Violated {}", violated.join(", ")))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,289 @@
|
|||||||
|
use std::cell::Cell;
|
||||||
|
use std::collections::HashSet;
|
||||||
|
use std::rc::Rc;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use serde_json::Value;
|
||||||
|
use sugars::boxed;
|
||||||
|
|
||||||
|
use anysystem::logger::LogEntry;
|
||||||
|
use anysystem::test::TestResult;
|
||||||
|
use anysystem::Message;
|
||||||
|
|
||||||
|
use anysystem::mc::predicates::{collects, goals, invariants, prunes};
|
||||||
|
use anysystem::mc::strategies::Bfs;
|
||||||
|
use anysystem::mc::strategy::{GoalFn, InvariantFn, PruneFn, StrategyConfig};
|
||||||
|
use anysystem::mc::{McState, ModelChecker};
|
||||||
|
|
||||||
|
use crate::common::{build_system, BroadcastMessage, TestConfig};
|
||||||
|
|
||||||
|
// The causal test explores two broadcasts on three processes. The state budget
|
||||||
|
// is checked as an invariant, so exhausting it cannot silently pass a pruned search.
|
||||||
|
const CAUSAL_MC_STATE_LIMIT: usize = 100_000;
|
||||||
|
|
||||||
|
pub fn test_mc_normal_delivery(config: &TestConfig) -> TestResult {
|
||||||
|
let mut sys = build_system(config);
|
||||||
|
let proc_names = sys.process_names();
|
||||||
|
let text = "0:Hello";
|
||||||
|
sys.send_local_message(
|
||||||
|
proc_names[0].as_str(),
|
||||||
|
Message::json("SEND", &BroadcastMessage { text }),
|
||||||
|
);
|
||||||
|
let goal = goals::all_goals(
|
||||||
|
proc_names
|
||||||
|
.iter()
|
||||||
|
.map(|name| goals::got_n_local_messages(name, name, 1))
|
||||||
|
.collect::<Vec<GoalFn>>(),
|
||||||
|
);
|
||||||
|
let strategy_config = StrategyConfig::default()
|
||||||
|
.goal(goal)
|
||||||
|
.prune(prunes::any_prune(vec![
|
||||||
|
prunes::state_depth(10),
|
||||||
|
mc_prune_proc_permutations(&proc_names[1..]),
|
||||||
|
// Prune states with more than 2 messages received from any process
|
||||||
|
mc_prune_msg_per_proc_limit(&proc_names, 2),
|
||||||
|
]))
|
||||||
|
.invariant(invariants::all_invariants(vec![
|
||||||
|
mc_invariant(proc_names.clone(), text.to_string()),
|
||||||
|
invariants::time_limit(Duration::from_secs(100)),
|
||||||
|
]));
|
||||||
|
let mut mc = ModelChecker::new(&sys);
|
||||||
|
let res = mc.run::<Bfs>(strategy_config);
|
||||||
|
if let Err(err) = res {
|
||||||
|
err.print_trace();
|
||||||
|
Err(err.message())
|
||||||
|
} else {
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn test_mc_sender_crash(config: &TestConfig) -> TestResult {
|
||||||
|
let mut sys = build_system(config);
|
||||||
|
let proc_names = sys.process_names();
|
||||||
|
let text = "0:Hello";
|
||||||
|
sys.send_local_message(
|
||||||
|
proc_names[0].as_str(),
|
||||||
|
Message::json("SEND", &BroadcastMessage { text }),
|
||||||
|
);
|
||||||
|
let goal = goals::all_goals(
|
||||||
|
proc_names
|
||||||
|
.iter()
|
||||||
|
.map(|name| goals::got_n_local_messages(name, name, 1))
|
||||||
|
.collect::<Vec<GoalFn>>(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let strategy_config = StrategyConfig::default()
|
||||||
|
.prune(prunes::any_prune(vec![
|
||||||
|
prunes::state_depth(4),
|
||||||
|
mc_prune_proc_permutations(&proc_names[1..]),
|
||||||
|
]))
|
||||||
|
.goal(goal)
|
||||||
|
.invariant(invariants::all_invariants(vec![
|
||||||
|
mc_invariant(proc_names.clone(), text.to_string()),
|
||||||
|
invariants::time_limit(Duration::from_secs(100)),
|
||||||
|
]))
|
||||||
|
.collect(collects::any_collect(
|
||||||
|
proc_names[1..]
|
||||||
|
.iter()
|
||||||
|
.map(|proc| collects::got_n_local_messages(proc, proc, 1))
|
||||||
|
.collect(),
|
||||||
|
));
|
||||||
|
|
||||||
|
let mut mc = ModelChecker::new(&sys);
|
||||||
|
let res = mc.run::<Bfs>(strategy_config);
|
||||||
|
let intermediate_states = res
|
||||||
|
.map_err(|err| {
|
||||||
|
err.print_trace();
|
||||||
|
err.message()
|
||||||
|
})?
|
||||||
|
.collected_states;
|
||||||
|
if intermediate_states.is_empty() {
|
||||||
|
return Err("no states collected after first stage".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Crash first node in the list
|
||||||
|
let left_proc_names = proc_names[1..].to_vec();
|
||||||
|
let goal = goals::all_goals(
|
||||||
|
left_proc_names
|
||||||
|
.iter()
|
||||||
|
.map(|name| goals::got_n_local_messages(name, name, 1))
|
||||||
|
.collect::<Vec<GoalFn>>(),
|
||||||
|
);
|
||||||
|
let strategy_config = StrategyConfig::default()
|
||||||
|
.goal(goal)
|
||||||
|
.invariant(invariants::all_invariants(vec![
|
||||||
|
mc_invariant(left_proc_names, text.to_string()),
|
||||||
|
invariants::time_limit(Duration::from_secs(100)),
|
||||||
|
]))
|
||||||
|
.prune(prunes::any_prune(vec![
|
||||||
|
prunes::state_depth(6),
|
||||||
|
mc_prune_proc_permutations(&proc_names[1..]),
|
||||||
|
// Prune states with more than 4 messages received from any process
|
||||||
|
mc_prune_msg_per_proc_limit(&proc_names, 4),
|
||||||
|
]));
|
||||||
|
let res = mc.run_from_states_with_change::<Bfs>(strategy_config, intermediate_states, |sys| {
|
||||||
|
sys.crash_node(proc_names[0].clone());
|
||||||
|
});
|
||||||
|
if let Err(err) = res {
|
||||||
|
err.print_trace();
|
||||||
|
Err(err.message())
|
||||||
|
} else {
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send "a" from P2, then send "b" from P1 after P1 delivers "a",
|
||||||
|
// which gives the causal order "a" -> "b", then check that all processes
|
||||||
|
// deliver both messages and deliver "a" before "b".
|
||||||
|
pub fn test_mc_causal_order(config: &TestConfig) -> TestResult {
|
||||||
|
let mut sys = build_system(config);
|
||||||
|
sys.send_local_message("2", Message::json("SEND", &BroadcastMessage { text: "a" }));
|
||||||
|
let proc_names = sys.process_names();
|
||||||
|
let report_error = |err: anysystem::mc::McError| {
|
||||||
|
err.print_trace();
|
||||||
|
err.message()
|
||||||
|
};
|
||||||
|
let checked = Rc::new(Cell::new(0));
|
||||||
|
let make_strategy = |texts: Vec<&'static str>| {
|
||||||
|
let goal_procs = proc_names.clone();
|
||||||
|
let invariant_procs = proc_names.clone();
|
||||||
|
let checked = checked.clone();
|
||||||
|
StrategyConfig::default()
|
||||||
|
// Stop this branch when everyone has delivered the messages
|
||||||
|
// sent in this stage. If events end before that, the test fails.
|
||||||
|
.goal(boxed!(move |state: &McState| {
|
||||||
|
goal_procs
|
||||||
|
.iter()
|
||||||
|
.all(|proc| {
|
||||||
|
texts
|
||||||
|
.iter()
|
||||||
|
.all(|text| mc_delivery_index(state, proc, text).is_some())
|
||||||
|
})
|
||||||
|
.then(|| "all messages have been delivered by every process".to_string())
|
||||||
|
}))
|
||||||
|
// Since P1 sends "b" after it delivers "a",
|
||||||
|
// every process must also deliver "a" before "b".
|
||||||
|
.invariant(invariants::all_invariants(vec![
|
||||||
|
boxed!(move |state: &McState| {
|
||||||
|
for proc in &invariant_procs {
|
||||||
|
if let Some(b) = mc_delivery_index(state, proc, "b") {
|
||||||
|
if !mc_delivery_index(state, proc, "a").is_some_and(|a| a < b) {
|
||||||
|
return Err("Violated CAUSAL ORDER".to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}),
|
||||||
|
boxed!(move |_: &McState| {
|
||||||
|
let next = checked.get() + 1;
|
||||||
|
checked.set(next);
|
||||||
|
if next > CAUSAL_MC_STATE_LIMIT {
|
||||||
|
Err(format!(
|
||||||
|
"causal model-checking state limit of {CAUSAL_MC_STATE_LIMIT} exceeded"
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
invariants::time_limit(Duration::from_secs(55)),
|
||||||
|
]))
|
||||||
|
};
|
||||||
|
let mut mc = ModelChecker::new(&sys);
|
||||||
|
|
||||||
|
// Explore all schedules of "a" before P1 sends "b". The first stage
|
||||||
|
// checks delivery of "a" on every branch and retains every state where
|
||||||
|
// P1 can send "b" after delivering "a".
|
||||||
|
let states = mc
|
||||||
|
.run::<Bfs>(make_strategy(vec!["a"]).collect(boxed!(|state: &McState| {
|
||||||
|
mc_delivery_index(state, "1", "a").is_some()
|
||||||
|
})))
|
||||||
|
.map_err(&report_error)?
|
||||||
|
.collected_states;
|
||||||
|
if states.is_empty() {
|
||||||
|
return Err("No state with delivery of a on process 1 was reached".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Explore every continuation after sending "b" from each collected state.
|
||||||
|
// Resource exhaustion is reported as incomplete rather than pruning paths.
|
||||||
|
mc.run_from_states_with_change::<Bfs>(make_strategy(vec!["a", "b"]), states, |sys| {
|
||||||
|
sys.send_local_message(
|
||||||
|
"1",
|
||||||
|
"1",
|
||||||
|
Message::json("SEND", &BroadcastMessage { text: "b" }),
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.map(|_| true)
|
||||||
|
.map_err(report_error)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mc_delivery_index(state: &McState, proc: &str, text: &str) -> Option<usize> {
|
||||||
|
state.node_states[proc].proc_states[proc]
|
||||||
|
.local_outbox
|
||||||
|
.iter()
|
||||||
|
.position(|msg| {
|
||||||
|
msg.tip == "DELIVER"
|
||||||
|
&& serde_json::from_str::<Value>(&msg.data).unwrap()["text"] == text
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mc_prune_proc_permutations(equivalent_procs: &[String]) -> PruneFn {
|
||||||
|
let equivalent_procs = equivalent_procs.to_vec();
|
||||||
|
boxed!(move |state| {
|
||||||
|
let proc_names = HashSet::<String>::from_iter(equivalent_procs.clone().into_iter());
|
||||||
|
let mut used_proc_names = HashSet::<String>::new();
|
||||||
|
let mut waiting_for_proc = 0;
|
||||||
|
for entry in &state.trace {
|
||||||
|
match entry {
|
||||||
|
LogEntry::McMessageReceived { src: proc, .. }
|
||||||
|
| LogEntry::McTimerFired { proc, .. } => {
|
||||||
|
if used_proc_names.contains(proc) || !proc_names.contains(proc) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if equivalent_procs[waiting_for_proc] != *proc {
|
||||||
|
return Some(
|
||||||
|
"state is the same as another state with renumerated processes"
|
||||||
|
.to_owned(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
used_proc_names.insert(proc.clone());
|
||||||
|
waiting_for_proc += 1;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mc_prune_msg_per_proc_limit(proc_names: &[String], limit: usize) -> PruneFn {
|
||||||
|
prunes::events_limit_per_proc(
|
||||||
|
|entry: &LogEntry, proc: &String| match entry {
|
||||||
|
LogEntry::McMessageReceived { src, .. } => src == proc,
|
||||||
|
_ => false,
|
||||||
|
},
|
||||||
|
proc_names.to_owned(),
|
||||||
|
limit,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mc_invariant(proc_names: Vec<String>, sent_message: String) -> InvariantFn {
|
||||||
|
boxed!(move |state: &McState| {
|
||||||
|
for name in &proc_names {
|
||||||
|
let outbox = &state.node_states[name].proc_states[name].local_outbox;
|
||||||
|
let mut message_data = HashSet::new();
|
||||||
|
for message in outbox {
|
||||||
|
let data: Value = serde_json::from_str(&message.data).unwrap();
|
||||||
|
let message = data["text"].as_str().unwrap().to_string();
|
||||||
|
|
||||||
|
if message_data.contains(&message) {
|
||||||
|
return Err("Violated NO DUPLICATION".to_owned());
|
||||||
|
}
|
||||||
|
message_data.insert(message.clone());
|
||||||
|
if message != sent_message {
|
||||||
|
return Err("Violated NO CREATION".to_owned());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
}
|
||||||
+17
-3
@@ -28,7 +28,7 @@
|
|||||||
|
|
||||||
В AnySystem 0.3.0 Python-модуль встроен в Rust-тестер: при запуске тестов локально или в Docker отдельный `anysystem.py` и настройка `PYTHONPATH` не требуются. Запускайте тесты из папки задания, чтобы относительные пути к решениям были корректны. Стаб служит только для IDE.
|
В AnySystem 0.3.0 Python-модуль встроен в Rust-тестер: при запуске тестов локально или в Docker отдельный `anysystem.py` и настройка `PYTHONPATH` не требуются. Запускайте тесты из папки задания, чтобы относительные пути к решениям были корректны. Стаб служит только для IDE.
|
||||||
|
|
||||||
### Регистрация
|
## Регистрация
|
||||||
|
|
||||||
(Пропустите эту часть, если вы вольнослушатель.)
|
(Пропустите эту часть, если вы вольнослушатель.)
|
||||||
|
|
||||||
@@ -71,6 +71,22 @@ docker run --rm -t -v ./solution:/solution distsys-tests-local [ЗДЕСЬ МО
|
|||||||
|
|
||||||
Тестирующая система используется для итоговой проверки и служит эталоном для временных лимитов, но не должна заменять локальное тестирование после каждой правки.
|
Тестирующая система используется для итоговой проверки и служит эталоном для временных лимитов, но не должна заменять локальное тестирование после каждой правки.
|
||||||
|
|
||||||
|
## Бонусы за пробелы в тестах
|
||||||
|
|
||||||
|
Если вы найдёте ошибку в тестах или требование задания, нарушение которого тесты не обнаруживают, опишите проблему в отчёте; при необходимости приведите пример ошибочного решения. За подтверждённую проблему можно получить 1 бонусный балл. За тест, который выявляет её, или описание его логики можно получить ещё 1 балл. Бонусы начисляются отдельно, независимо от баллов за выполнение задания.
|
||||||
|
|
||||||
|
## Отчёт
|
||||||
|
|
||||||
|
К каждому домашнему заданию приложите краткий отчёт в файле `solution/readme.md`. Сдайте его вместе с решением до дедлайна. Без отчёта автоматические тесты запускаются, но защита не проводится и **решение не засчитывается**.
|
||||||
|
|
||||||
|
В отчёте объясните, как устроено ваше решение и как работают его основные части. Дополнительные требования к содержимому отчёта могут быть указаны в условиях соответствующего ДЗ.
|
||||||
|
|
||||||
|
Пожалуйста, пишите кратко и по существу. Перед сдачей перечитайте отчёт: проверьте, что он описывает именно вашу реализацию, и уберите повторы, общие рассуждения и ненужные подробности. Проверяющему должно быть понятно, как работает решение и почему вы выбрали такой подход.
|
||||||
|
|
||||||
|
Оценка за ДЗ может быть снижена на **1 балл**, если изложение заметно затрудняет понимание решения. В исключительных случаях снижение может составить **2 балла**, если по отчёту практически невозможно понять, как работает решение: например, существенные объяснения отсутствуют или теряются среди большого объёма лишнего текста.
|
||||||
|
|
||||||
|
При снижении оценки проверяющий укажет конкретные недостатки отчёта. Отдельные стилистические шероховатости и само по себе использование LLM основанием для снижения не являются.
|
||||||
|
|
||||||
## Сдача решения
|
## Сдача решения
|
||||||
|
|
||||||
После локального тестирования откройте папку задания, например `homework/01-guarantees`, и выполните команду:
|
После локального тестирования откройте папку задания, например `homework/01-guarantees`, и выполните команду:
|
||||||
@@ -81,8 +97,6 @@ python3 ../cli.py submit
|
|||||||
|
|
||||||
Утилита определит идентификатор задания по имени текущей папки и отправит содержимое её поддиректории `solution`. Она выведет идентификатор и официальное время приёма решения, а также ссылку на запуск workflow. Откройте эту ссылку в Gitea, чтобы следить за проверкой и прочитать её журнал.
|
Утилита определит идентификатор задания по имени текущей папки и отправит содержимое её поддиректории `solution`. Она выведет идентификатор и официальное время приёма решения, а также ссылку на запуск workflow. Откройте эту ссылку в Gitea, чтобы следить за проверкой и прочитать её журнал.
|
||||||
|
|
||||||
В состав решения для каждого задания должен входить краткий отчёт в файле `solution/readme.md`. Отчёт необходимо сдать вместе с остальными файлами решения до дедлайна задания. При его отсутствии автоматические тесты всё равно запускаются, но защита не проводится и **решение не засчитывается**. Требования к содержанию отчёта приведены в условии соответствующего задания.
|
|
||||||
|
|
||||||
## Изоляция процессов AnySystem
|
## Изоляция процессов AnySystem
|
||||||
|
|
||||||
В заданиях на AnySystem каждый экземпляр `Process` моделирует отдельный процесс распределённой системы. Его изменяемое состояние должно храниться только в атрибутах экземпляра (`self`). Все взаимодействия между процессами должны происходить через сообщения, как и в реальной распределённой системе.
|
В заданиях на AnySystem каждый экземпляр `Process` моделирует отдельный процесс распределённой системы. Его изменяемое состояние должно храниться только в атрибутах экземпляра (`self`). Все взаимодействия между процессами должны происходить через сообщения, как и в реальной распределённой системе.
|
||||||
|
|||||||
Reference in New Issue
Block a user