diff --git a/materials/04-group-comm/04-group-comm.pdf b/materials/04-group-comm/04-group-comm.pdf new file mode 100644 index 0000000..bf816a6 Binary files /dev/null and b/materials/04-group-comm/04-group-comm.pdf differ diff --git a/materials/04-group-comm/readme.md b/materials/04-group-comm/readme.md new file mode 100644 index 0000000..7c5111d --- /dev/null +++ b/materials/04-group-comm/readme.md @@ -0,0 +1,10 @@ +## 4. Групповые взаимодействия, рассылка и распространение информации. + +### Лекция + +- [Презентация](04-group-comm.pdf) +- См. также материалы в конце + +### Семинар + +- [Материалы семинара](seminar/readme.md) diff --git a/materials/04-group-comm/seminar/gossip/pull.py b/materials/04-group-comm/seminar/gossip/pull.py new file mode 100644 index 0000000..18bb4e6 --- /dev/null +++ b/materials/04-group-comm/seminar/gossip/pull.py @@ -0,0 +1,42 @@ +import random + +from anysystem import Context, Message, Process + + +class Peer(Process): + def __init__(self, proc_id: int, proc_count: int, fanout: int): + self._id = proc_id + self._proc_count = proc_count + self._peers = [id for id in range(0, self._proc_count) if id != self._id] + self._fanout = fanout + self._info = None + + def on_local_message(self, msg: Message, ctx: Context): + if msg.type == 'START': + ctx.set_timer("gossip", 1) + elif msg.type == 'BROADCAST': + self.got_info(msg['info'], ctx) + + def on_start(self, ctx: Context): + pass + + def on_message(self, msg: Message, sender: str, ctx: Context): + if msg.type == 'GOSSIP_REQ' and self._info is not None: + ctx.send(Message('GOSSIP_RESP', {'info': self._info}), sender) + elif msg.type == 'GOSSIP_RESP' and self._info is None: + self.got_info(msg['info'], ctx) + + def on_timer(self, timer_name: str, ctx: Context): + if self._info is None: + self.gossip(ctx) + ctx.set_timer("gossip", 1) + + def got_info(self, info, ctx): + self._info = info + ctx.send_local(Message('DELIVER', {'info': self._info})) + ctx.cancel_timer("gossip") + ctx.send_local(Message('STOPPED', {})) + + def gossip(self, ctx): + for peer in random.sample(self._peers, self._fanout): + ctx.send(Message('GOSSIP_REQ', {}), str(peer)) diff --git a/materials/04-group-comm/seminar/gossip/push.py b/materials/04-group-comm/seminar/gossip/push.py new file mode 100644 index 0000000..35f2b14 --- /dev/null +++ b/materials/04-group-comm/seminar/gossip/push.py @@ -0,0 +1,38 @@ +import random + +from anysystem import Context, Message, Process + + +class Peer(Process): + def __init__(self, proc_id: int, proc_count: int, fanout: int): + self._id = proc_id + self._proc_count = proc_count + self._peers = [id for id in range(0, self._proc_count) if id != self._id] + self._fanout = fanout + self._info = None + + def on_local_message(self, msg: Message, ctx: Context): + if msg.type == 'START': + ctx.set_timer("gossip", 1) + elif msg.type == 'BROADCAST': + self.got_info(msg['info'], ctx) + + def on_start(self, ctx: Context): + pass + + def on_message(self, msg: Message, sender: str, ctx: Context): + if msg.type == 'GOSSIP' and self._info is None: + self.got_info(msg['info'], ctx) + + def on_timer(self, timer_name: str, ctx: Context): + if self._info is not None: + self.gossip(ctx) + ctx.set_timer("gossip", 1) + + def got_info(self, info, ctx): + self._info = info + ctx.send_local(Message('DELIVER', {'info': self._info})) + + def gossip(self, ctx): + for peer in random.sample(self._peers, self._fanout): + ctx.send(Message('GOSSIP', {'info': self._info}), str(peer)) diff --git a/materials/04-group-comm/seminar/gossip/push_pull.py b/materials/04-group-comm/seminar/gossip/push_pull.py new file mode 100644 index 0000000..97f6737 --- /dev/null +++ b/materials/04-group-comm/seminar/gossip/push_pull.py @@ -0,0 +1,43 @@ +import random + +from anysystem import Context, Message, Process + + +class Peer(Process): + def __init__(self, proc_id: int, proc_count: int, fanout: int): + self._id = proc_id + self._proc_count = proc_count + self._peers = [id for id in range(0, self._proc_count) if id != self._id] + self._fanout = fanout + self._info = None + + def on_local_message(self, msg: Message, ctx: Context): + if msg.type == 'START': + ctx.set_timer("gossip", 1) + elif msg.type == 'BROADCAST': + self.got_info(msg['info'], ctx) + + def on_start(self, ctx: Context): + pass + + def on_message(self, msg: Message, sender: str, ctx: Context): + if msg.type == 'GOSSIP_REQ': + if self._info is not None: + ctx.send(Message('GOSSIP_RESP', {'info': self._info}), sender) + else: + if msg['info'] is not None: + self.got_info(msg['info'], ctx) + elif msg.type == 'GOSSIP_RESP' and self._info is None: + self.got_info(msg['info'], ctx) + + def on_timer(self, timer_name: str, ctx: Context): + self.gossip(ctx) + ctx.set_timer("gossip", 1) + + def got_info(self, info, ctx): + self._info = info + ctx.send_local(Message('DELIVER', {'info': self._info})) + + def gossip(self, ctx): + for peer in random.sample(self._peers, self._fanout): + ctx.send(Message('GOSSIP_REQ', {'info': self._info}), str(peer)) diff --git a/materials/04-group-comm/seminar/gossip/push_pull_stop.py b/materials/04-group-comm/seminar/gossip/push_pull_stop.py new file mode 100644 index 0000000..e218da4 --- /dev/null +++ b/materials/04-group-comm/seminar/gossip/push_pull_stop.py @@ -0,0 +1,54 @@ +import random + +from anysystem import Context, Message, Process + + +class Peer(Process): + def __init__(self, proc_id: int, proc_count: int, fanout: int): + self._id = proc_id + self._proc_count = proc_count + self._peers = [id for id in range(0, self._proc_count) if id != self._id] + self._fanout = fanout + self._info = None + self._stop_prob = 0.8 + self._stopped = False + + def on_local_message(self, msg: Message, ctx: Context): + if msg.type == 'START': + ctx.set_timer("gossip", 1) + elif msg.type == 'BROADCAST': + self.got_info(msg['info'], ctx) + + def on_start(self, ctx: Context): + pass + + def on_message(self, msg: Message, sender: str, ctx: Context): + if msg.type == 'GOSSIP_REQ': + if self._info is not None: + ctx.send(Message('GOSSIP_RESP', {'info': self._info}), sender) + elif msg['info'] is not None: + self.got_info(msg['info'], ctx) + elif msg.type == 'GOSSIP_RESP': + self.got_info(msg['info'], ctx) + + def on_timer(self, timer_name: str, ctx: Context): + self.gossip(ctx) + ctx.set_timer("gossip", 1) + + def got_info(self, info, ctx): + if self._info is None: + self._info = info + ctx.send_local(Message('DELIVER', {'info': self._info})) + else: + self.try_stop(ctx) + + def gossip(self, ctx): + for peer in random.sample(self._peers, self._fanout): + ctx.send(Message('GOSSIP_REQ', {'info': self._info}), str(peer)) + + def try_stop(self, ctx): + if not self._stopped and random.uniform(0, 1) < self._stop_prob: + self._stopped = True + # Stop initiating exchanges; keep answering incoming requests. + ctx.cancel_timer("gossip") + ctx.send_local(Message('STOPPED', {})) diff --git a/materials/04-group-comm/seminar/gossip/readme.md b/materials/04-group-comm/seminar/gossip/readme.md new file mode 100644 index 0000000..2826371 --- /dev/null +++ b/materials/04-group-comm/seminar/gossip/readme.md @@ -0,0 +1,129 @@ +# Знакомство с gossip + +В этой работе сравним push, pull и push-pull в симуляторе на [AnySystem](https://github.com/systems-group/anysystem). Перед каждым экспериментом запишите прогноз, затем сравните его с результатом и объясните различия. + +## Подготовка + +Откройте корень репозитория курса в IDE. Для VS Code с Pylance [стаб AnySystem](../../../../typings/anysystem/__init__.pyi) подключается через [настройки проекта](../../../../pyrightconfig.json). Для другой IDE укажите папку `typings` в корне репозитория как каталог стабов. Стаб содержит объявления и документацию API; реализация доступна в [исходнике AnySystem 0.3.0](https://github.com/osukhoroslov/anysystem/blob/v0.3.0/python/anysystem.py). + +При запуске симулятора локально или в Docker Python-модуль встроен в AnySystem. Отдельный `anysystem.py` и настройка `PYTHONPATH` не нужны. + +Из корня репозитория перейдите в каталог лабораторной: + +```sh +cd materials/04-group-comm/seminar/gossip +``` + +Если установлен Rust, соберите текущую версию симулятора: + +```sh +cargo install --locked --path simulator +distsys-gossip -h +``` + +Для запуска в Docker соберите образ из текущего кода: + +```sh +docker build -t distsys-gossip-seminar simulator +docker run --rm -t -v ./:/impl distsys-gossip-seminar -h +``` + +Далее заменяйте `distsys-gossip` на `docker run --rm -t -v ./:/impl distsys-gossip-seminar`. Команды выполняются из каталога `gossip`. Если оболочка не передаёт относительный путь монтирования, в PowerShell используйте `--mount "type=bind,source=$($PWD.Path),target=/impl"` вместо `-v ./:/impl`. + +## Модель и метрики + +Распространяется одно сообщение, изначально известное процессу `0`. Все узлы знают состав группы и выбирают случайных соседей, исключая себя. Сетевая задержка равна 0,1 единицы модельного времени, периодический таймер срабатывает через 1. В этих экспериментах узлы не отказывают; можно задавать вероятность потери сетевого сообщения. + +Основные параметры: + +| Параметр | Значение | +| --- | --- | +| `-i` | Файл реализации | +| `-n` | Число узлов, не меньше 2 | +| `-f` | Число соседей для одного обмена, от 1 до `n - 1` | +| `-d` | Вероятность потери сообщения, от 0 до 1 | +| `-s` | Seed для воспроизведения запуска | +| `-t` | Положительный лимит модельного времени | +| `-q` | Завершить симуляцию при обнаружении полного охвата | + +В таблице вывода `time` — модельное время, `delivered` — число узлов, доставивших сообщение приложению, `stopped` — число узлов, прекративших периодические инициативные обмены, `messages` — накопленное число сетевых отправлений, включая запросы и ответы. `STOPPED` не означает выключение узла: он сохраняет информацию и может отвечать на входящие запросы. + +Симулятор выводит статистику после продвижения времени интервалами по 1. В итоговой строке `First full-coverage sample` указаны время первой напечатанной строки с `delivered = n` и число отправлений к ней. Обозначим их `t_all` и `M_all`. Это показатели с дискретностью вывода, а не точные мгновение последней доставки и число отправлений в это мгновение. + +Строка `Finished` объясняет завершение: + +- `full coverage (--quick-mode)` — сработало условие `-q`; +- `no pending events` — в симуляции не осталось событий; +- `time limit reached` — достигнут лимит времени. + +Если условия совпали, отсутствие событий проверяется первым, затем полный охват с `-q`, затем лимит. Строка `Final state` всегда показывает фактический охват и трафик к завершению. Если полного охвата не было, выводится `Full coverage: not reached`. Завершение по лимиту не означает, что алгоритм сам остановился. + +## 1. Сравнение push, pull и push-pull + +Изучите [push.py](push.py), [pull.py](pull.py) и [push_pull.py](push_pull.py). В push информированный узел посылает данные соседям. В pull неинформированный узел запрашивает их; после получения перестаёт инициировать запросы, но продолжает отвечать другим. В push-pull запрос сам может нести информацию, а информированный получатель отвечает своими данными. + +До запуска ответьте: + +1. Какой подход быстрее распространит информацию в начале, когда источник один? +2. Какой подход быстрее найдёт последних неинформированных участников? +3. Должен ли самый быстрый вариант отправить меньше всего сообщений? + +Запустите алгоритмы с одинаковыми параметрами: + +```sh +distsys-gossip -i push.py -n 1000 -f 2 -s 123 -d 0 -t 60 -q +distsys-gossip -i pull.py -n 1000 -f 2 -s 123 -d 0 -t 60 -q +distsys-gossip -i push_pull.py -n 1000 -f 2 -s 123 -d 0 -t 60 -q +``` + +Заполните таблицу. Если полного охвата нет, укажите достигнутый охват и число отправлений к лимиту вместо `t_all` и `M_all`. + +| Алгоритм | `n` | `f` | `d` | seed | `t_all` | `M_all` | Причина завершения | +| --- | --- | --- | --- | --- | --- | --- | --- | +| push | 1000 | 2 | 0 | 123 | | | | +| pull | 1000 | 2 | 0 | 123 | | | | +| push-pull | 1000 | 2 | 0 | 123 | | | | + +Сравните начальный рост и доставку последним узлам. Объясните результат через действия отправителей. Флаг `-q` использует глобальную статистику симулятора: сами процессы не узнают, что сообщение уже получили все. В push и push-pull периодические обмены без этого ограничения продолжаются. + +## 2. Изменение одного параметра + +Выберите один алгоритм и сравните каждый следующий запуск с его базовым результатом. В командах замените `push.py` на выбранный файл. + +Сначала увеличьте fanout с 2 до 4: + +```sh +distsys-gossip -i push.py -n 1000 -f 4 -s 123 -d 0 -t 60 -q +``` + +Затем верните fanout 2 и добавьте вероятность потери 0,2: + +```sh +distsys-gossip -i push.py -n 1000 -f 2 -s 123 -d 0.2 -t 60 -q +``` + +Для каждого запуска сначала запишите прогноз, затем добавьте строку результатов в таблицу. Как изменились время и цена охвата? Почему больше контактов за раунд не обязательно означает пропорционально меньшую задержку? Что можно заключить, если охват не достигнут за 60 единиц времени? + +Один запуск показывает конкретное выполнение. При наличии времени повторите сравнение на seed `124` и `125`. Одинаковый seed воспроизводит запуск данной реализации; разные алгоритмы расходуют случайные числа по-разному, поэтому их контакты и потери не обязаны совпадать. Если меняете лимит времени, используйте один лимит для сопоставляемых запусков. + +## 3. Полный охват и прекращение обменов + +Изучите [push_pull_stop.py](push_pull_stop.py) и запустите его без `-q`: + +```sh +distsys-gossip -i push_pull_stop.py -n 1000 -f 2 -s 123 -d 0 -t 60 +``` + +При повторном получении информации через `got_info` узел с вероятностью 0,8 отменяет свой таймер и выдаёт `STOPPED`. Он продолжает отвечать на запросы. Неинформированные узлы продолжают искать данные; остановка таймера у держателя не удаляет доступную копию. + +Выпишите показатели первой строки с полным охватом и показатели завершения. Совпадают ли они? Какой трафик возникает между ними? Подтвердите по `Finished`, что запуск завершился из-за отсутствия событий, а не по лимиту. + +Для сравнения стоимости распространения используйте `M_all` всех четырёх вариантов. Число отправлений к концу запуска с остановкой — другая метрика. Итоговые `max/min/mean` отправлений по узлам также относятся к концу запуска, который может наступить позже полного охвата. + +## Обсуждение + +- Какие оптимизации могут уменьшить число повторных передач? +- Почему успешный запуск при потерях не доказывает Uniform Agreement при отказах процессов? +- Какая дополнительная информация и логика понадобятся для нескольких сообщений и причинного порядка доставки? + +Сформулируйте вывод о скорости распространения, сетевых затратах и прекращении активности отдельно. Свяжите каждый вывод с наблюдением из таблицы и предположениями модели. diff --git a/materials/04-group-comm/seminar/gossip/simulator/.dockerignore b/materials/04-group-comm/seminar/gossip/simulator/.dockerignore new file mode 100644 index 0000000..d291891 --- /dev/null +++ b/materials/04-group-comm/seminar/gossip/simulator/.dockerignore @@ -0,0 +1,5 @@ +target +**/target +**/__pycache__ +**/.pytest_cache +*.pyc diff --git a/materials/04-group-comm/seminar/gossip/simulator/Cargo.lock b/materials/04-group-comm/seminar/gossip/simulator/Cargo.lock new file mode 100644 index 0000000..622cd5a --- /dev/null +++ b/materials/04-group-comm/seminar/gossip/simulator/Cargo.lock @@ -0,0 +1,960 @@ +# 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-gossip" +version = "0.1.0" +dependencies = [ + "anysystem", + "clap", + "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 = "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 = "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", +] diff --git a/materials/04-group-comm/seminar/gossip/simulator/Cargo.toml b/materials/04-group-comm/seminar/gossip/simulator/Cargo.toml new file mode 100644 index 0000000..b1c2e44 --- /dev/null +++ b/materials/04-group-comm/seminar/gossip/simulator/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "distsys-gossip" +version = "0.1.0" +edition = "2021" +resolver = "3" + +[dependencies] +anysystem = "=0.3.0" +clap = { version = "3.1.17", features = ["cargo", "derive"] } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +sugars = "3" diff --git a/materials/04-group-comm/seminar/gossip/simulator/Dockerfile b/materials/04-group-comm/seminar/gossip/simulator/Dockerfile new file mode 100644 index 0000000..25685b0 --- /dev/null +++ b/materials/04-group-comm/seminar/gossip/simulator/Dockerfile @@ -0,0 +1,19 @@ +# syntax=docker/dockerfile:1 +FROM rust:1.97.1-bookworm@sha256:0e2bcaef56d041a486784e54104a81aebe0da44bd03019bd70bc0401e42e4a97 AS builder + +RUN apt-get update && apt-get install -y build-essential python3-dev + +WORKDIR /simulator +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=/simulator/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-gossip /usr/local/bin/distsys-gossip + +WORKDIR /impl +ENTRYPOINT ["distsys-gossip"] diff --git a/materials/04-group-comm/seminar/gossip/simulator/src/main.rs b/materials/04-group-comm/seminar/gossip/simulator/src/main.rs new file mode 100644 index 0000000..1a9d36a --- /dev/null +++ b/materials/04-group-comm/seminar/gossip/simulator/src/main.rs @@ -0,0 +1,204 @@ +use std::env; + +use clap::{CommandFactory, ErrorKind, Parser}; +use serde::Serialize; +use sugars::boxed; + +use anysystem::python::PyProcessFactory; +use anysystem::{Message, System}; + +// CLI ----------------------------------------------------------------------------------------------------------------- + +/// Gossip simulator. +#[derive(Parser, Debug)] +#[clap(about, long_about = None)] +struct Args { + /// Path to Python file with process implementations. + #[clap(long = "impl", short)] + impl_path: String, + + /// Number of nodes. + #[clap(long, short, default_value = "10")] + nodes: u32, + + /// Network drop rate. + #[clap(long, short, default_value = "0")] + drop_rate: f64, + + /// Fan-out (how many peers to contact on each round). + #[clap(long, short, default_value = "1")] + fanout: u32, + + /// Stop simulation when all nodes delivered info. + #[clap(long, short)] + quick_mode: bool, + + /// Time limit in simulation. + #[clap(long, short, default_value = "60")] + time_limit: u32, + + /// Random seed. + #[clap(long, short, default_value = "123")] + seed: u64, +} + +impl Args { + fn validate(&self) -> Result<(), &'static str> { + if self.nodes < 2 { + return Err("--nodes must be at least 2"); + } + if self.fanout == 0 || self.fanout >= self.nodes { + return Err("--fanout must be between 1 and nodes - 1"); + } + if !(0.0..=1.0).contains(&self.drop_rate) { + return Err("--drop-rate must be a finite number between 0 and 1"); + } + if self.time_limit == 0 { + return Err("--time-limit must be positive"); + } + Ok(()) + } +} + +// MAIN ---------------------------------------------------------------------------------------------------------------- + +fn main() { + let args = Args::parse(); + if let Err(message) = args.validate() { + Args::command() + .error(ErrorKind::ValueValidation, message) + .exit(); + } + env::set_var("PYTHONHASHSEED", args.seed.to_string()); + let proc_factory = PyProcessFactory::new(&args.impl_path, "Peer"); + println!("Nodes: {}", args.nodes); + println!("Fanout: {}", args.fanout); + println!("Network drop rate: {}", args.drop_rate); + println!("Implementation: {}", args.impl_path); + println!("Seed: {}", args.seed); + println!("Time limit: {}", args.time_limit); + println!("Quick mode: {}", args.quick_mode); + + let mut sys = build_system( + proc_factory, + args.nodes, + args.drop_rate, + args.fanout, + args.seed, + ); + sys.send_local_message( + "0", + Message::json( + "BROADCAST", + &BroadcastMessage { + info: "Some very important information to propagate to all nodes", + }, + ), + ); + println!( + "\n{:<10} {:<12} {:<12} {:<12}", + "time", "delivered", "stopped", "messages" + ); + let mut first_full_coverage = None; + let (reason, delivered, stopped) = loop { + let more_events = sys.step_for_duration(1.); + let (delivered, stopped) = get_stats(&sys); + println!( + "{:<10} {:<12} {:<12} {:<12}", + sys.time(), + delivered, + stopped, + sys.network().network_message_count() + ); + // Capture the first printed full-coverage sample, before any later traffic. + if delivered == args.nodes && first_full_coverage.is_none() { + first_full_coverage = Some((sys.time(), sys.network().network_message_count())); + } + if !more_events { + break ("no pending events", delivered, stopped); + } + if args.quick_mode && delivered == args.nodes { + break ("full coverage (--quick-mode)", delivered, stopped); + } + if sys.time() >= args.time_limit as f64 { + break ("time limit reached", delivered, stopped); + } + }; + println!("\nFinished: {}", reason); + println!( + "Final state: time={}, delivered={}/{}, stopped={}, messages={}", + sys.time(), + delivered, + args.nodes, + stopped, + sys.network().network_message_count() + ); + match first_full_coverage { + Some((time, messages)) => println!( + "First full-coverage sample: time={}, messages={} (sampled every 1 time unit)", + time, messages + ), + None => println!("Full coverage: not reached"), + } + let sent_counts: Vec = sys + .process_names() + .iter() + .map(|p| sys.sent_message_count(p)) + .collect(); + println!( + "\nMessages sent by each node: max={}, min={}, mean={:.2}", + sent_counts.iter().max().unwrap(), + sent_counts.iter().min().unwrap(), + sent_counts.iter().sum::() as f64 / sent_counts.len() as f64 + ) +} + +// UTILS --------------------------------------------------------------------------------------------------------------- + +#[derive(Serialize)] +struct StartMessage {} + +#[derive(Serialize)] +struct BroadcastMessage<'a> { + info: &'a str, +} + +fn build_system( + proc_factory: PyProcessFactory, + nodes: u32, + drop_rate: f64, + fanout: u32, + seed: u64, +) -> System { + let mut sys = System::new(seed); + sys.network().set_delay(0.1); + sys.network().set_drop_rate(drop_rate); + for proc_id in 0..nodes { + // process and node on which it runs have the same name + let name = proc_id.to_string(); + sys.add_node(&name); + let proc = proc_factory.build((proc_id, nodes, fanout), seed); + sys.add_process(&name, boxed!(proc), &name); + sys.send_local_message(&name, Message::json("START", &StartMessage {})) + } + sys +} + +fn get_stats(sys: &System) -> (u32, u32) { + let mut delivered_count = 0; + let mut stopped_count = 0; + for proc in sys.process_names() { + let outbox = sys.local_outbox(&proc); + let msg_count = outbox.len(); + if msg_count > 0 { + assert_eq!(outbox.first().unwrap().tip, "DELIVER"); + delivered_count += 1; + } + if msg_count == 2 { + assert_eq!(outbox.get(1).unwrap().tip, "STOPPED"); + stopped_count += 1; + } + assert!(msg_count <= 2); + } + (delivered_count, stopped_count) +} diff --git a/materials/04-group-comm/seminar/ip_multicast/docker-compose.yml b/materials/04-group-comm/seminar/ip_multicast/docker-compose.yml new file mode 100644 index 0000000..0d93f57 --- /dev/null +++ b/materials/04-group-comm/seminar/ip_multicast/docker-compose.yml @@ -0,0 +1,19 @@ +services: + sender: + build: + context: sender + depends_on: + - receiver1 + - receiver2 + - receiver3 + ports: + - 9999:9999/udp + receiver1: + build: + context: receiver + receiver2: + build: + context: receiver + receiver3: + build: + context: receiver diff --git a/materials/04-group-comm/seminar/ip_multicast/readme.md b/materials/04-group-comm/seminar/ip_multicast/readme.md new file mode 100644 index 0000000..e99fd50 --- /dev/null +++ b/materials/04-group-comm/seminar/ip_multicast/readme.md @@ -0,0 +1,70 @@ +# IP multicast + +Один контейнер принимает UDP-датаграммы на порту 9999 и пересылает их на multicast-адрес `224.0.2.0:10000`. Три контейнера-получателя присоединяются к этой группе и печатают полученные сообщения. В примере нет подтверждений, повторной передачи и восстановления пропусков. + +## Запуск + +Из корня репозитория перейдите в каталог примера и запустите контейнеры: + +```sh +cd materials/04-group-comm/seminar/ip_multicast +docker compose up --build -d +docker compose logs -f sender receiver1 receiver2 receiver3 +``` + +Дождитесь сообщения `ready` от отправителя и каждого получателя. Последняя команда показывает журналы и занимает терминал. Следующие команды выполняйте во втором терминале из того же каталога. + +Отправьте пробное сообщение через Python 3: + +```sh +python -c "import socket; s=socket.socket(socket.AF_INET,socket.SOCK_DGRAM); s.sendto(b'probe',('127.0.0.1',9999)); s.close()" +``` + +При необходимости замените `python` на имя Python 3 в своём окружении. Убедитесь, что `probe` появился в журналах всех трёх получателей. Отправитель также печатает каждую пересылку. Имена сервисов в журнале позволяют различить получателей. + +Если с хоста сообщения не доходят (нет `forwarded`), отправляйте из контейнера: `docker compose exec sender python -c "..."`. + +## Эксперимент: получатель пропустил сообщение + +До запуска предположите, какие получатели увидят `m1` и `m2` и получит ли вернувшийся участник пропущенные данные. + +1. Остановите третьего получателя: + + ```sh + docker compose stop receiver3 + ``` + +2. Отправьте `m1`: + + ```sh + python -c "import socket; s=socket.socket(socket.AF_INET,socket.SOCK_DGRAM); s.sendto(b'm1',('127.0.0.1',9999)); s.close()" + ``` + +3. Верните третьего получателя: + + ```sh + docker compose start receiver3 + ``` + +4. Дождитесь **нового** сообщения `ready` от `receiver3` и снова отправьте `probe`. Убедитесь, что третий получатель его видит. +5. Отправьте `m2`, заменив в команде `b'm1'` на `b'm2'`. + +При штатной работе стенда `m1` увидят первый и второй получатели, а `m2` — все три. Третий получатель не получит `m1` задним числом: программа не хранит историю и не запрашивает пропущенные сообщения. UDP допускает потери и без отключения получателя; успешная проба не является доказательством гарантированной доставки. + +Обсудите: + +- Какие действия выполняет `IP_ADD_MEMBERSHIP` и где задаётся адрес группы? +- Где в программе можно было бы добавить подтверждения или восстановление пропусков? +- Почему получение сетевого пакета и надёжная доставка сообщения приложению — разные события? + +Пустая UDP-датаграмма тоже является сообщением: `b''` не означает закрытие соединения. Можно отправить её и затем обычное сообщение, чтобы проверить, что оба процесса продолжают работу. + +Повторный запуск контейнера в этом эксперименте иллюстрирует пропуск данных. В домашнем задании используется модель crash-stop: отказавшие процессы не возвращаются. + +## Завершение + +Остановите просмотр журналов сочетанием Ctrl+C, затем удалите контейнеры стенда: + +```sh +docker compose down +``` diff --git a/materials/04-group-comm/seminar/ip_multicast/receiver/Dockerfile b/materials/04-group-comm/seminar/ip_multicast/receiver/Dockerfile new file mode 100644 index 0000000..18636cb --- /dev/null +++ b/materials/04-group-comm/seminar/ip_multicast/receiver/Dockerfile @@ -0,0 +1,5 @@ +FROM python:3.12-slim + +COPY . . + +CMD ["python", "-u", "receiver.py"] diff --git a/materials/04-group-comm/seminar/ip_multicast/receiver/__init__.py b/materials/04-group-comm/seminar/ip_multicast/receiver/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/materials/04-group-comm/seminar/ip_multicast/receiver/receiver.py b/materials/04-group-comm/seminar/ip_multicast/receiver/receiver.py new file mode 100644 index 0000000..b54eae9 --- /dev/null +++ b/materials/04-group-comm/seminar/ip_multicast/receiver/receiver.py @@ -0,0 +1,12 @@ +import socket +import struct + + +if __name__ == '__main__': + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.bind(('', 10000)) + sock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, struct.pack('4sL', socket.inet_aton('224.0.2.0'), socket.INADDR_ANY)) + print("ready: joined 224.0.2.0, listening on UDP :10000", flush=True) + while True: + data, addr = sock.recvfrom(1024) + print(f'got message {data!r} from {addr}', flush=True) diff --git a/materials/04-group-comm/seminar/ip_multicast/sender/Dockerfile b/materials/04-group-comm/seminar/ip_multicast/sender/Dockerfile new file mode 100644 index 0000000..cb15ba8 --- /dev/null +++ b/materials/04-group-comm/seminar/ip_multicast/sender/Dockerfile @@ -0,0 +1,5 @@ +FROM python:3.12-slim + +COPY . . + +CMD ["python", "-u" , "sender.py"] diff --git a/materials/04-group-comm/seminar/ip_multicast/sender/__init__.py b/materials/04-group-comm/seminar/ip_multicast/sender/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/materials/04-group-comm/seminar/ip_multicast/sender/sender.py b/materials/04-group-comm/seminar/ip_multicast/sender/sender.py new file mode 100644 index 0000000..d03186e --- /dev/null +++ b/materials/04-group-comm/seminar/ip_multicast/sender/sender.py @@ -0,0 +1,14 @@ +import socket +import struct + + +if __name__ == '__main__': + in_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + in_socket.bind(('', 9999)) + out_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + out_socket.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, struct.pack('b', 1)) + print("ready: listening on UDP :9999, forwarding to 224.0.2.0:10000", flush=True) + while True: + data, addr = in_socket.recvfrom(1024) + out_socket.sendto(data, ('224.0.2.0', 10000)) + print(f'forwarded {data!r} from {addr} to 224.0.2.0:10000', flush=True) diff --git a/materials/04-group-comm/seminar/readme.md b/materials/04-group-comm/seminar/readme.md new file mode 100644 index 0000000..bd1f771 --- /dev/null +++ b/materials/04-group-comm/seminar/readme.md @@ -0,0 +1,66 @@ +# Семинар 4. Групповые взаимодействия + +На семинаре закрепим материал лекции через демонстрацию IP multicast, разбор историй отказов, работу с причинным буфером и эксперименты с gossip. Будем различать получение сетевого сообщения и его доставку приложению, проверять гарантии рассылки и сравнивать стоимость распространения информации. + +## Подготовка и участие + +Для самостоятельных экспериментов заранее выполните [инструкции по подготовке gossip](gossip/readme.md#подготовка) и проверьте запуск `distsys-gossip -h`. Если окружение не готово, можно участвовать в разборе демонстрации и подготовленных журналов: предсказывать результат, находить нужные показатели и объяснять наблюдения. + +## 1. IP multicast: доставка сетевых сообщений + +Проследим путь сообщения от отправителя к нескольким получателям в [примере с контейнерами](ip_multicast/readme.md). Найдём в коде адрес группы, присоединение получателя и отправку датаграммы. + +Затем отключим одного получателя, отправим сообщение и вернём его в группу. Перед запуском предположим, кто увидит каждое сообщение, а после сравним прогноз с журналом. + +Обсудим, восстанавливаются ли пропущенные сообщения и какие механизмы понадобились бы для этого. Свяжем наблюдение с различием между сетевым получением и доставкой приложению. + +## 2. Надёжная рассылка: обязательства при отказах + +Разберём свойства Validity, No Duplication, No Creation, Agreement и Uniform Agreement на историях с остановкой процессов. В каждой истории сначала определим, какие процессы корректны, затем проверим обязательства по доставке. + +Основные вопросы: + +- Что меняется, если отправитель падает посреди рассылки? +- Обязаны ли остальные корректные процессы доставить сообщение, если получатель уже доставил его, а затем упал? +- Почему перед вызовом `deliver` может потребоваться дождаться, пока сообщение сохранится у других процессов? + +Сравним BEB, RB и URB и обсудим роль корректного большинства. + +## 3. Порядок доставки: зависимости и буфер + +На примерах последовательных сообщений и ответа в чате различим FIFO и Causal Order. Построим зависимости между сообщениями, полученными в перемешанном порядке, и пошагово разберём буфер доставки. + +Для каждого сообщения определим, выполнены ли его зависимости и как доставка изменяет состояние получателя. Проверим, почему после одной доставки нужно снова просмотреть буфер и когда допустимы разные последовательности доставок. + +В завершение обсудим пример независимых операций, для которых причинного порядка недостаточно, чтобы реплики получили одинаковый результат, и роль Total Order. + +## 4. Gossip: скорость, трафик и остановка + +В [лабораторной работе](gossip/readme.md) сравним push, pull и push-pull с одинаковыми параметрами. Перед каждым экспериментом сформулируем прогноз, затем сопоставим его с результатом. + +- Измерим время полного охвата и число сетевых сообщений. +- Изменим fanout и вероятность потерь, сохраняя остальные параметры. +- Сравним полный охват с прекращением периодических обменов. +- Разберём, почему `-q` останавливает симулятор, а `STOPPED` у процесса не означает его отказ. + +Объясним наблюдения через действия узлов и ограничения модели. Результат одного запуска используем как конкретный пример; для проверки общего вывода повторим эксперимент с другими seed. + +## Дополнительные материалы + +### IP multicast + +- [Адреса multicast](https://en.wikipedia.org/wiki/Multicast_address), [IGMP](https://linkmeup.gitbook.io/sdsm/9.-multicast/1.-igmp) и [подробный разбор multicast](https://linkmeup.gitbook.io/sdsm/9.-multicast). +- [Работа с multicast в Python](https://pymotw.com/3/socket/multicast.html). +- Примеры применения: [zeroconf](https://en.wikipedia.org/wiki/Zero-configuration_networking), [multicast DNS](https://en.wikipedia.org/wiki/Multicast_DNS), [multicast NTP](https://www.oreilly.com/library/view/cisco-ios-cookbook/0596527225/ch14s11.html), [обнаружение узлов в Apache Ignite](https://ignite.apache.org/docs/latest/clustering/tcp-ip-discovery). + +### Надёжность и порядок + +- [Отношение happens-before](https://en.wikipedia.org/wiki/Happened-before). +- [Конспект курса Distributed Systems](https://www.cl.cam.ac.uk/teaching/2021/ConcDisSys/dist-sys-notes.pdf), разделы 3 и 4. + +### Gossip и его применения + +- [Визуализация распространения информации](https://flopezluis.github.io/gossip-simulator/). +- Обнаружение узлов и сведения о составе группы: [Consul](https://developer.hashicorp.com/consul/docs/architecture/gossip), [обсуждение Serf, memberlist и SWIM](https://www.hashicorp.com/resources/everybody-talks-gossip-serf-memberlist-raft-swim-hashicorp-consul), [Cassandra](https://docs.datastax.com/en/cassandra-oss/3.x/cassandra/architecture/archGossipAbout.html). +- [Gossip в Hyperledger Fabric](https://hyperledger-fabric.readthedocs.io/en/latest/gossip.html). +- Распространение блоков и транзакций Bitcoin: [обзор P2P-сети](https://nakamoto.com/bitcoins-p2p-network/), [статья](https://arxiv.org/pdf/1703.08761.pdf), [видеоматериалы](https://www.dsn.kastel.kit.edu/bitcoin/videos.html).