Skip to content

Commit 331d472

Browse files
frankmcsherryclaude
andcommitted
timely: deterministic simulation of multi-worker computations
Adds `timely::simulate`: several workers hosted on the calling thread, using ordinary `Bytes` process allocators with holding gates. An execution is a pure function of the applied sequence of `Decision`s: step a worker, or deliver up to N messages from one worker to another. Any decision sequence is valid, since deliveries clamp to what is pending, so schedule traces are recordable, replayable, and shrinkable by construction. Tests run multi-worker barriers under seeded random schedules, asserting progress safety. A property-testing grind checks frontier safety, conservation, quiescence, and same-seed determinism. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LEpSYjkqw2zfhGSMAXNXyD
1 parent 86e9007 commit 331d472

4 files changed

Lines changed: 567 additions & 0 deletions

File tree

‎timely/src/lib.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ pub mod dataflow;
9292
pub mod synchronization;
9393
pub mod execute;
9494
pub mod order;
95+
pub mod simulate;
9596

9697
pub mod logging;
9798
// pub mod log_events;

‎timely/src/simulate.rs‎

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
//! Deterministic simulation of multi-worker timely computations.
2+
//!
3+
//! A [`Simulation`] hosts several workers on the calling thread, using the ordinary
4+
//! serializing intra-process allocator with each worker's receive [`Gate`] holding: every
5+
//! message is physically received, but is logically delivered only when the caller says
6+
//! so. Each worker is deterministic given the messages delivered to it, and delivery is
7+
//! constrained only by per-source FIFO order, so an execution is a pure function of the
8+
//! sequence of [`Decision`]s applied: the decision trace *is* the execution. Traces are
9+
//! trivially recordable, replayable, and shrinkable, which makes this the substrate for
10+
//! randomized schedule exploration ("deterministic simulation testing") of
11+
//! progress-tracking behavior.
12+
//!
13+
//! # Examples
14+
//! ```rust
15+
//! use timely::simulate::{Decision, Simulation};
16+
//! use timely::dataflow::operators::{ToStream, Inspect};
17+
//!
18+
//! let mut sim = Simulation::new(2);
19+
//! for index in 0 .. 2 {
20+
//! sim.worker_mut(index).dataflow::<(),_,_>(|scope| {
21+
//! (0 .. 10).to_stream(scope)
22+
//! .container::<Vec<_>>()
23+
//! .inspect(|x| println!("seen: {:?}", x));
24+
//! });
25+
//! }
26+
//! // Interleave worker steps and message deliveries however the test likes ...
27+
//! sim.apply(Decision::Step(0));
28+
//! sim.apply(Decision::Deliver { source: 0, target: 1, count: 1 });
29+
//! // ... then run to completion.
30+
//! assert!(sim.drain(1_000));
31+
//! ```
32+
33+
use std::rc::Rc;
34+
use std::cell::RefCell;
35+
use std::sync::Arc;
36+
37+
use crate::WorkerConfig;
38+
use crate::communication::Allocator;
39+
use crate::communication::allocator::{Process, ProcessBuilder};
40+
use crate::communication::allocator::zero_copy::gate::Gate;
41+
use crate::communication::allocator::zero_copy::bytes_slab::BytesRefill;
42+
use crate::worker::Worker;
43+
44+
/// One decision in a simulation schedule.
45+
///
46+
/// Any sequence of decisions is valid: stepping a worker with nothing to do and
47+
/// delivering on an empty stream are both no-ops. This keeps randomly generated
48+
/// and mechanically shrunk schedules well-formed by construction.
49+
#[derive(Clone, Debug)]
50+
pub enum Decision {
51+
/// Run one step of the indicated worker.
52+
Step(usize),
53+
/// Deliver up to `count` undelivered messages from `source` to `target`, in FIFO order.
54+
Deliver {
55+
/// The sending worker.
56+
source: usize,
57+
/// The receiving worker.
58+
target: usize,
59+
/// The maximum number of messages to deliver.
60+
count: usize,
61+
},
62+
}
63+
64+
/// Several workers on one thread, with caller-controlled message delivery.
65+
pub struct Simulation {
66+
/// Each worker's receive gate, holding for the duration of the simulation.
67+
gates: Vec<Rc<RefCell<Gate>>>,
68+
workers: Vec<Worker>,
69+
}
70+
71+
impl Simulation {
72+
/// Creates a simulation of `peers` workers with default worker configuration.
73+
///
74+
/// The workers are constructed without a timer, so time-based reschedulings
75+
/// (`activate_after`) degrade to immediate activation and logging is disabled;
76+
/// nothing in a simulated execution reads the wall clock.
77+
pub fn new(peers: usize) -> Self {
78+
let refill = BytesRefill {
79+
logic: Arc::new(|size| Box::new(vec![0_u8; size]) as Box<dyn std::ops::DerefMut<Target=[u8]>+Send>),
80+
limit: None,
81+
};
82+
let mut gates = Vec::with_capacity(peers);
83+
let mut workers = Vec::with_capacity(peers);
84+
let builders = ProcessBuilder::new_bytes_vector(peers, refill, None)
85+
.into_iter()
86+
.map(|builder| builder.holding(true))
87+
.collect();
88+
for process in ProcessBuilder::build_all(builders) {
89+
let gate = match &process {
90+
Process::Bytes(allocator) => allocator.gate(),
91+
_ => unreachable!("new_bytes_vector produces Bytes allocators"),
92+
};
93+
gates.push(gate);
94+
workers.push(Worker::new(WorkerConfig::default(), Allocator::Process(process), None));
95+
}
96+
Simulation { gates, workers }
97+
}
98+
99+
/// The number of simulated workers.
100+
pub fn peers(&self) -> usize { self.workers.len() }
101+
102+
/// Mutable access to a worker, e.g. to install dataflows or inspect probes.
103+
pub fn worker_mut(&mut self, index: usize) -> &mut Worker {
104+
&mut self.workers[index]
105+
}
106+
107+
/// The gate of `target`, with everything sent to it so far fetched and held.
108+
fn gate(&self, target: usize) -> std::cell::RefMut<'_, Gate> {
109+
let mut gate = self.gates[target].borrow_mut();
110+
gate.fetch();
111+
gate
112+
}
113+
114+
/// The number of undelivered messages from `source` to `target`.
115+
pub fn pending(&self, source: usize, target: usize) -> usize {
116+
self.gate(target).held(source)
117+
}
118+
119+
/// Applies one schedule decision.
120+
pub fn apply(&mut self, decision: Decision) {
121+
match decision {
122+
Decision::Step(index) => { self.workers[index].step(); }
123+
Decision::Deliver { source, target, count } => { self.deliver(source, target, count); }
124+
}
125+
}
126+
127+
/// Runs one step of the indicated worker, returning whether dataflows remain.
128+
pub fn step_worker(&mut self, index: usize) -> bool {
129+
self.workers[index].step()
130+
}
131+
132+
/// Delivers up to `count` messages from `source` to `target`; returns the number delivered.
133+
pub fn deliver(&mut self, source: usize, target: usize, count: usize) -> usize {
134+
self.gate(target).release(source, count)
135+
}
136+
137+
/// Runs the simulation to completion under a fair schedule: repeatedly deliver
138+
/// everything and step every worker, until no dataflows and no messages remain.
139+
///
140+
/// Returns `true` if the simulation quiesced within `bound` rounds. A `false`
141+
/// return after a generous bound indicates a genuine liveness problem, as the
142+
/// schedule from here on is maximally fair.
143+
pub fn drain(&mut self, bound: usize) -> bool {
144+
for _ in 0 .. bound {
145+
let mut active = false;
146+
for target in 0 .. self.peers() {
147+
active |= self.gate(target).release_all() > 0;
148+
}
149+
for worker in self.workers.iter_mut() {
150+
active |= worker.step();
151+
}
152+
if !active { return true; }
153+
}
154+
false
155+
}
156+
}

0 commit comments

Comments
 (0)