|
| 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