Module std::sim
Deterministic simulation for tests: handlers for std::time::Clock, std::rand::Random,
std::rand::Draw, std::task::Tasks, std::net::Net, std::memfs::MemTree and the
prelude's Stdin whose answers depend only on their arguments, so code that reaches time,
randomness, tasks, the network, the disk and its input only through its row runs the same way
every time. Start with world (tasks), net_world (network) or disk_world (disk).
Source: std/sim.weir; committed interface: std/sim.iface. Index
- handler
SimClock: A clock stopped attthat runs no tasks:sleepmoves it forward and returns at once. Install it aswith SimClock(0) { … }. - handler
SimStdin: A scripted standard input:Stdin.readandStdin.read_lineanswer the bytes ofinputfrom offsetaton, as the root's answer the process's, and 0 once they are all read. Install it aswith SimStdin(b"one\ntwo\n", 0usize) { … }around code writtenwith Stdin. - handler
SimRandom: A linear congruential generator fromseed: each number is the next state, in0..2147483648. Install it aswith SimRandom(atomic(42)) { … }; one seed, one sequence (where draws do not race, as onworld's one thread). - handler
Sampled:std::rand::Drawsampled fromRandom(install inside aSimRandom):pick(n)is a draw modulon,delay(max)one modulomax + 1,fault(p)whether one modulo 100 is belowp,part(n)1 plus one modulon - 1. A draw is spent only where a choice exists, so a seed's history does not depend on choices it never had. - handler
Searched:std::rand::Drawsearched: each draw is aweir explorechoice point. - enum
Draws: Where a world's draws come from:Seed(s)samples them from a generator seeded withs(one seed, one history),Search(f)makes each aweir explorechoice point with at mostffaults a path (Searched; the body's ownRandomis then seeded with 0). Underweir run,Searchtakes every draw's first answer and no fault. - fn
world: Runsbodyin a simulated world: the deterministic executor a test uses. - struct
Network: Hownet_world's network misbehaves, each from the world's draws. Of a hundred completions,failfail withECONNRESET(104) when their op waits on a descriptor (accept,connect,recv,send), andshortof the others move fewer bytes than they could when they are arecvorsendthat could move more than one; and each completion takes up tolatencyticks of virtual time after the one before it. All three 0: every op completes whole in zero time. A failed op does nothing else: a failedsenddelivers none of its bytes, a failedrecvtakes none of those waiting, a failedconnectmakes no connection and a failedacceptleaves the connection queued for the next; the connection stays open on both ends. A shortsenddelivers the first bytes of its buffer, a shortrecvtakes the first of those waiting. - effect
Nodes:std::net::Netfor a simulated network of nodes (systems.md §21.12): the ops ofNet, and the processesnoderuns its code as.net_worldhandles it. A task belongs to the process of the task that spawned it, so a node's process is its code and every task that code started, and ending it ends them all, as a process's death ends its threads. - fn
net_world:worldover a simulated network: the deterministicNetexecutor a test uses. - struct
Disk: How a simulated disk misbehaves, each from the world's draws (systems.md §21.14). Of a hundredread_at,write_atandsync,failfail withEIO(5): a failed read or write leaves the disk as it was, and a failedsyncleaves what it was to make durable (a file's unsynced writes, a directory's unsynced names) readable but never durable: a latersyncsucceeds without making it so, and any crash takes it back, as Linux's has since 4.13 (its rare arm: taken back at once, as if evicted from the cache); of a hundredwrite_at,fullfail withENOSPC(28); of a hundredread_atandwrite_atof more than one byte,shortmove fewer, a strictly short part (Draw.part); and right after an op that changed the disk the machine crashescrashtimes in a hundred (disk_world). All four 0: the disk never misbehaves. - fn
lose_unsynced: What a crash leaves of theLoopbacktree (systems.md §21.14): its unsynced name changes (creations,mkdirs,removes andrenames no sync of their directory made durable), then each file holding unsynced writes, are settled asDraw.pick(n)answers among thenwaysdisk_unsyncedanddisk_settlecount: lost (the common arm), kept, or torn, a file's last write cut to its first half, missing its first half, or missing its second half with the file as long as if it were written (a missing byte reads as before the write, zero past the file's old end). What a failed sync left is lost either way. A crash calls it where it happens, before anything closes a file, since a close syncs. It settles all of the tree, not only the crashed program's part. - fn
disk_root: The root directory of theLoopbacktree, the simulated disk, opened asMemFs.root()opens it: a descriptor every handler of the tree's ops serves,net_world's among them. - fn
disk_faults: Runsbodywith its file ops misbehaving asdisksays (DiskFaults), each fault drawn from the world'sDraw: a node's disk innet_world, whoseNetserves the simulated tree's files, whichdisk_root()opens.disk.crashis not read: a node crashes where its program says, andlose_unsyncedsettles the disk there. - fn
disk_world: Runsboot, a program from its start, on a simulated disk (systems.md §21.14): a directory of the root'sLoopbacktree that no other world of the run uses, empty at the first boot and removed when the world ends, whichMemTree.root()opens, misbehaving asdisksays with each draw fromdraws. A write is durable once asyncor acloseof a descriptor opened withWRITEanswers; a name (a creation,mkdir,removeorrename) once asyncof its directory answers. A crash, at the first op after one that changed the disk, leaves the unsynced names and each file holding unsynced writes lost (as at the last sync), torn (every unsynced change but the last, and of a file's last write its first half) or kept, cancels the boot there, and boots again, until a boot runs to its end: its value is the world's. UnderSeed(s)one seed is one history of faults and crashes; underSearch(f)weir exploreenumerates every history with at mostfrare arms (a crash, a torn or kept outcome, a failed or short op). What a test checks across boots, what was acknowledged, lives outside the world, captured byboot. - effect
Crash: A simulated node's process dying (systems.md §21.12) [from FoundationDB's simulated reboots]: the op a crash point performs when it fires. The node's code stops there: every task it started is cancelled where it waits, and the code unwinds to thenodethat runs it, running its guards and drop glue on the way, so the descriptors it held close as a dead process's do and its peers see their connections reset, and what it kept in memory is gone.nodethen runs it again from the start, its recovery path, which knows only what it reads back: its durable state (a file, orportablestate the test lends it). - fn
crash_point: A point where the node may crash, a fault point (§21.5) whose fault is the crash: underDraws::Search(f)a rare arm that spends the budget, underSeedptimes in a hundred, and never understd::rand::Calm, so production code keeps its crash points. - fn
node: Runsbody, a node's code, as a process ofnet_world's, until it returns without crashing: each crash ends the process, every taskbodystarted with it as a process's threads die, unwindsbodyand runs it again from the start. Abodythat returns ends its process too, as amainthat returns does: a server whosebodyspawns a task per connection keeps its tasks whilebodyserves. Returns how many times it crashed. A node's state that should survive a crash lives outsidebody, as a disk outlives a process; a crash first leaves each write the simulated disk holds unsynced lost, torn or kept (lose_unsynced), so a node's files survive it as their last syncs left them.
handler SimClock
pub handler SimClock(mut t: Int) for Clock
A clock stopped at t that runs no tasks: sleep moves it forward and returns at once.
Install it as with SimClock(0) { … }.
Handles Clock, with clauses:
handler SimStdin
pub handler SimStdin(input: &[u8], mut at: usize) for Stdin
A scripted standard input: Stdin.read and Stdin.read_line answer the bytes of input
from offset at on, as the root's answer the process's, and 0 once they are all read. Install
it as with SimStdin(b"one\ntwo\n", 0usize) { … } around code written with Stdin.
Handles Stdin, with clauses:
handler SimRandom
pub handler SimRandom(seed: Atomic<Int>): shared for Random
A linear congruential generator from seed: each number is the next state, in
0..2147483648. Install it as with SimRandom(atomic(42)) { … }; one seed, one sequence (where
draws do not race, as on world's one thread).
Handles Random, with clauses:
handler Sampled
pub handler Sampled: shared for Draw with Random
std::rand::Draw sampled from Random (install inside a SimRandom): pick(n) is a
draw modulo n, delay(max) one modulo max + 1, fault(p) whether one modulo 100 is below
p, part(n) 1 plus one modulo n - 1. A draw is spent only where a choice exists, so a
seed's history does not depend on choices it never had.
Handles Draw, with clauses:
handler Searched
pub handler Searched(faults: Atomic<Int>): shared for Draw
std::rand::Draw searched: each draw is a weir explore choice point.
Each draw has a common arm and rare ones, and a path takes at most faults rare arms
(install as with Searched(atomic(2)) { … }).
pick(n)'s common arm is 0 (the oldest enabled op, the first task due), every other answer
rare; fault(p) for p > 0 is rare when true (p weighs nothing: a fault is reachable or
not), and delay(max) for max > 0 when it answers max. part(n), the size of a short
count, answers 1 as its common arm and n - 1 or n / 2 as rare ones, the sizes that take
different paths through a reader's loop. Past the budget every draw is its common arm and no
choice, so the search spends its paths on combinations of few deviations, and
weir explore --stop-on-failure reports a failure with as few rare arms as any has.
Handles Draw, with clauses:
enum Draws
pub enum Draws: copy {
Seed(Int),
Search(Int),
}
Where a world's draws come from: Seed(s) samples them from a generator seeded with s (one
seed, one history), Search(f) makes each a weir explore choice point with at most f faults
a path (Searched; the body's own Random is then seeded with 0). Under weir run,
Search takes every draw's first answer and no fault.
Variants:
fn world
pub fn world<T>(
start: Int,
draws: Draws,
body: fn() -> T with
Tasks any portable inside(Random, Draw, captures),
Random any portable,
Draw any portable env {S},
) -> T with Alloc {alloc: tail} portable, Panic
Runs body in a simulated world: the deterministic executor a test uses.
world(0, Draws::Seed(1), || { … }): its clock starts at start, its draws come from
draws, and the tasks it spawns run with it
on one thread, in virtual time (time passes only while every task sleeps), until every one has
finished; one seed is one schedule, and Search makes each pick among tasks due at once a
weir explore choice. No task moves to another thread, so body may hold what is not
portable; a task may capture the Tasks capability, the generator and what the body's literal
captures. Panics if body did not finish (stop() cancelled it), or on a deadlock: nothing
is due and a task waits on a key no task can wake (--explain-path names each).
Tasks interleave where one suspends (sleep, yield_now, park) and at a preemption point
(Tasks.preempt(), where every std::sync wait begins): under Search a preemption is a rare
arm that runs the other tasks ready at the time first. Under Search, weir explore may also
pause a task after each atomic operation, critical section left holding no lock, or std call
that synchronised, a rare arm too, so another task's work lands between two atomic operations
of one task (a lost update by load then store); natively a task runs on between its
suspensions (systems.md §21.17 says what the search covers and what it does not).
Committed interface (std/sim.iface):
bound Alloc: Alloc { alloc: tail } any portable
bound Panic: Panic { panic: terminal } any
may diverge
performs with no capability: Alloc
scheduling point: always
holds what may not move at a scheduling point: always
destroys values of T
callback body: envelope {S}; pinned: always
struct Network
pub struct Network: copy {
pub fail: Int,
pub short: Int,
pub latency: Int,
}
How net_world's network misbehaves, each from the world's draws. Of a hundred completions,
fail fail with ECONNRESET (104) when their op waits on a descriptor (accept, connect,
recv, send), and short of the others move fewer bytes than they could when they are a
recv or send that could move more than one; and each completion takes up to latency ticks
of virtual time after the one before it. All three 0: every op completes whole in zero time.
A failed op does nothing else: a failed send delivers none of its bytes, a failed recv
takes none of those waiting, a failed connect makes no connection and a failed accept
leaves the connection queued for the next; the connection stays open on both ends. A short
send delivers the first bytes of its buffer, a short recv takes the first of those waiting.
Fields:
effect Nodes
pub effect Nodes : Net default portable migrates cross {
fn enter_node() -> Int;
fn end_node(parent: Int);
}
std::net::Net for a simulated network of nodes (systems.md §21.12): the ops of Net, and
the processes node runs its code as. net_world handles it. A task belongs to the
process of the task that spawned it, so a node's process is its code and every task that
code started, and ending it ends them all, as a process's death ends its threads.
op enter_node
fn enter_node() -> Int;
Control kind tail: the clause's result is the op's, and the performer goes on, never aborted or suspended.
The running task starts a process of its own: it, and every task it spawns from now on, belong to the new process until it ends. Answers the process the task belonged to.
op end_node
fn end_node(parent: Int);
Control kind tail: the clause's result is the op's, and the performer goes on, never aborted or suspended.
Ends the running task's process: every other task of it is cancelled where it waits
(its op in flight asked to cancel, as stop() asks; one not yet started before it
starts), and one that runs on with a completion that came first is cancelled at its next
op. The running task goes back to process parent, which enter_node answered.
fn net_world
pub fn net_world<T>(
start: Int,
draws: Draws,
net: Network,
body: fn() -> T with
Nodes any portable inside(Draw, captures),
Random any,
Draw any portable env {S},
) -> T with Alloc {alloc: tail} portable, Loopback portable, Panic
world over a simulated network: the deterministic Net executor a test uses.
Runs body as world does, with Nodes, and so std::net::Net, served by the root's
Loopback, the in-memory network of one host (Net says which address reaches which
listener), on every backend:
net_world(0, Draws::Seed(1), Network { fail: 0, short: 0, latency: 0 }, || { … }). Which
enabled op completes next, after how many ticks, and whether it fails or comes up short are
drawn from draws as net says: under Seed one seed is one schedule and one network
history, under Search(f) weir explore enumerates every schedule and history with at most
f faults (a failed op, a short count, a full latency, a preemption). Panics on a deadlock,
when every task left waits on the network or on a key no task can wake (--explain-path names
each), or when the body did not finish. Call stop() when the body is done so that a
server still waiting is cancelled and its cleanup runs. A task that serves owns its listener
(move(l on_abort { close(l) })): the body runs first and its tasks start when it suspends or
returns, so a listener the body opened is moved into the acceptor task.
A task may borrow portable state declared outside the call (a server's counters the test
reads once net_world returns): Nodes is installed inside the call, so it ends before they
do. node runs a crashable node's code as a process of its own.
Committed interface (std/sim.iface):
bound Alloc: Alloc { alloc: tail } any portable
bound Loopback: Loopback any portable
bound Panic: Panic { panic: terminal } any
may diverge
performs with no capability: Alloc
scheduling point: always
holds what may not move at a scheduling point: always
destroys values of T
callback body: envelope {S}; pinned: always
struct Disk
pub struct Disk: copy {
pub fail: Int,
pub short: Int,
pub full: Int,
pub crash: Int,
}
How a simulated disk misbehaves, each from the world's draws (systems.md §21.14). Of a
hundred read_at, write_at and sync, fail fail with EIO (5): a failed read or write
leaves the disk as it was, and a failed sync leaves what it was to make durable (a file's
unsynced writes, a directory's unsynced names) readable but never durable: a later sync
succeeds without making it so, and any crash takes it back, as Linux's has since 4.13 (its
rare arm: taken back at once, as if evicted from the cache); of a hundred write_at, full
fail with ENOSPC (28); of a hundred read_at and write_at of more than one byte, short
move fewer, a strictly short part (Draw.part); and right after an op that changed the disk
the machine crashes crash times in a hundred (disk_world). All four 0: the disk never
misbehaves.
Fields:
fn lose_unsynced
pub fn lose_unsynced() with Draw any cross, Loopback any
What a crash leaves of the Loopback tree (systems.md §21.14): its unsynced name changes
(creations, mkdirs, removes and renames no sync of their directory made durable), then
each file holding unsynced writes, are settled as Draw.pick(n) answers among the n ways
disk_unsynced and disk_settle count: lost (the common arm), kept, or torn, a file's last
write cut to its first half, missing its first half, or missing its second half with the
file as long as if it were written (a missing byte reads as before the write, zero past the
file's old end). What a failed sync left is lost either way. A crash calls it where it
happens, before anything closes a file, since a close syncs. It settles all of the tree, not
only the crashed program's part.
Committed interface (std/sim.iface):
bound Draw: Draw any cross
bound Loopback: Loopback any
may diverge
scheduling point: Draw not quiet | Loopback not quiet
fn disk_root
pub fn disk_root() -> Result<Fd, Int> with Alloc {alloc: tail} portable, Loopback portable
The root directory of the Loopback tree, the simulated disk, opened as MemFs.root() opens
it: a descriptor every handler of the tree's ops serves, net_world's among them.
Committed interface (std/sim.iface):
bound Alloc: Alloc { alloc: tail } any portable
bound Loopback: Loopback any portable
may diverge
scheduling point: always
fn disk_faults
pub fn disk_faults<T>(disk: Disk, body: fn() -> T with Fs env {A, S}) -> T with
Fs any portable cross,
Draw any portable cross,
Alloc {alloc: tail} portable,
Loopback portable,
Runs body with its file ops misbehaving as disk says (DiskFaults), each fault drawn from
the world's Draw: a node's disk in net_world, whose Net serves the simulated tree's
files, which disk_root() opens. disk.crash is not read: a node crashes where its program
says, and lose_unsynced settles the disk there.
Committed interface (std/sim.iface):
bound Fs: Fs any portable cross
bound Draw: Draw any portable cross
bound Alloc: Alloc { alloc: tail } any portable
bound Loopback: Loopback any portable
aborts: Fs.list, Fs.mkdir, Fs.open, Fs.read_at, Fs.remove, Fs.rename, Fs.size, Fs.sync, Fs.write_at
scheduling point: always
callback body: envelope {A, S}
fn disk_world
pub fn disk_world<T>(
draws: Draws,
disk: Disk,
boot: fn() -> T with MemTree any portable, Draw any portable env {S},
) -> T with Alloc {alloc: tail} portable, Loopback portable, Panic
Runs boot, a program from its start, on a simulated disk (systems.md §21.14): a directory
of the root's Loopback tree that no other world of the run uses, empty at the first boot
and removed when the world ends, which MemTree.root() opens, misbehaving as disk says
with each draw from draws. A write is durable once a sync or a close of a descriptor
opened with WRITE answers; a name (a creation, mkdir, remove or rename) once a sync
of its directory answers. A crash, at the first op after one that changed the disk, leaves
the unsynced names and each file holding unsynced writes lost (as at the last sync), torn
(every unsynced change but the last, and of a file's last write its first half) or kept,
cancels the boot there, and boots again, until a boot runs to its end: its value is the
world's. Under Seed(s) one seed is one history of faults and crashes; under Search(f)
weir explore enumerates every history with at most f rare arms (a crash, a torn or kept
outcome, a failed or short op). What a test checks across boots, what was acknowledged,
lives outside the world, captured by boot.
Committed interface (std/sim.iface):
bound Alloc: Alloc { alloc: tail } any portable
bound Loopback: Loopback any portable
bound Panic: Panic { panic: terminal } any
may diverge
scheduling point: always
holds what may not move at a scheduling point: always
destroys values of T
callback boot: envelope {S}; pinned: always; holds boot
cleanup on an abort suspends on Loopback
effect Crash
pub effect Crash {
fallible fn crash();
}
A simulated node's process dying (systems.md §21.12) [from FoundationDB's simulated reboots]:
the op a crash point performs when it fires. The node's code stops there: every task it
started is cancelled where it waits, and the code unwinds to the node that runs it, running
its guards and drop glue on the way, so the descriptors it held close as a dead process's do
and its peers see their connections reset, and what it kept in memory is gone. node then
runs it again from the start, its recovery path, which knows only what it reads back: its
durable state (a file, or portable state the test lends it).
op crash
fallible fn crash();
Control kind fallible: the clause resumes at once or aborts the with (abort(v)), so the perform is an abort edge.
The node crashes here: its tasks end, and its code unwinds to node, which runs it again.
fn crash_point
pub fn crash_point(p: Int) with Draw cross, Crash migrates cross
A point where the node may crash, a fault point (§21.5) whose fault is the crash: under
Draws::Search(f) a rare arm that spends the budget, under Seed p times in a hundred, and
never under std::rand::Calm, so production code keeps its crash points.
Committed interface (std/sim.iface):
bound Draw: Draw any cross
bound Crash: std::sim::Crash cross
migrates: Crash.crash migrates inline
aborts: Crash.crash
scheduling point: Crash not quiet | Draw not quiet
fn node
pub fn node(body: fn() with Crash any portable) -> Int with
Nodes any portable cross,
Draw any portable cross,
Loopback any portable,
Runs body, a node's code, as a process of net_world's, until it returns without
crashing: each crash ends the process, every task body started with it as a process's
threads die, unwinds body and runs it again from the start. A body that returns ends its
process too, as a main that returns does: a server whose body spawns a task per connection
keeps its tasks while body serves. Returns how many times it crashed. A node's state that
should survive a crash lives outside body, as a disk outlives a process; a crash first leaves
each write the simulated disk holds unsynced lost, torn or kept (lose_unsynced), so a
node's files survive it as their last syncs left them.
Committed interface (std/sim.iface):
bound Nodes: std::sim::Nodes any portable cross
bound Draw: Draw any portable cross
bound Loopback: Loopback any portable
may diverge
scheduling point: Draw not quiet | Loopback not quiet | Nodes not quiet
callback body: envelope {A, S}; holds body