Add week 4 materials

This commit is contained in:
2026-10-01 22:08:30 +03:00
parent 6210b46c1f
commit 6008cff9f6
21 changed files with 1707 additions and 0 deletions
@@ -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<u64> = 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::<u64>() 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)
}