Configuration and faults
This is the reference for seedfall.json: every field, and every fault seedfall can inject, such as crashes, partly written disk writes, lost network messages and clock jumps. Use it to look up what a setting does or to design the failures you want to test. If you are new, start with getting-started.md.
A world is described in a JSON file, seedfall.json by default. Paths inside it are relative to the file. One file can extend another, hold named profiles selected with -profile, take -set path=value overrides and start from a fault preset such as "faults": "resilient"; see config.md.
{
"nodes": [
{ "name": "n1", "pkg": "./node", "args": ["-id", "n1"] },
{ "name": "n2", "pkg": "./node", "args": ["-id", "n2"] }
],
"workload": { "name": "client", "pkg": "./client" },
"duration": "60s",
"latency": ["1ms", "5ms"],
"supervise": true,
"faults": {
"crash_per_sec": 0.5,
"restart_after": ["10ms", "500ms"],
"partition_per_sec": 0.2,
"partition_for": ["50ms", "1s"],
"torn_writes": true,
"crash_on_io": 0.005,
"fsync_eio": 0.01,
"strict_dirs": true,
"clock_skew": "200ms",
"pause_per_sec": 0.2,
"clock_jump_per_sec": 0.05,
"buggify": true,
"swarm": true
}
}
World
| Field | Default | Meaning |
|---|---|---|
nodes[] |
Crashable nodes. Each has a name, then either pkg (a Go package, built for wasip1 automatically) or wasm (a prebuilt module), plus optional args, env, faults, disk, files and upgrade_to. disk and files copy a host directory and host files onto the node's disk before it first starts, durable and synced, and upgrade_to names a package the node runs from its first restart on (seeded disks, upgrades). "faults": false keeps every fault away from the node: it is never crashed, paused, slowed or skewed, its disk never fails, and partitions and clogs never cut its links, like Antithesis's fault exclusions. sim.Buggify sites in its own code still fire |
|
workload |
The checker. Same fields as a node. It takes none of the per-node faults (crashes, pauses, clock skew and jumps, slowdowns, resets, disk faults) unless faults.include_workload or its own "faults": true says so; partitions and clogs reach it like any node. The run passes when it exits 0 |
|
workloads[] |
Several workloads instead of, or next to, workload. Each is a client like workload, and the run passes when every one of them has exited 0. See Faults on the workload |
|
finally |
A check that starts only after every workload has exited 0, with all faults stopped and the network healed. Same fields as a node; it never takes faults. The run passes when it exits 0, and its properties count like any other. See properties.md | |
liveness.within |
0s (off) |
Liveness mode. At duration the faults stop, partitions, clogs, slowdowns and pauses end, and crashed nodes restart at once; then the workload (and the finally check, if any) must finish within within, or the run fails with class liveness |
duration |
30s |
Virtual-time horizon, and with liveness the end of the fault period. A run that reaches it without failing counts as passed and is reported as "hit the time horizon"; with liveness, reaching it starts the recovery phase instead |
latency |
["1ms", "10ms"] |
Per-chunk network delay range |
links[] |
Per-link latency overrides, each {"from": "n1", "to": "n2", "latency": ["20ms", "80ms"]}. Links are one-way; leave out from or to to match any node, so {"to": "n3", ...} slows everything sent to n3. The first matching entry wins. See Network faults |
|
faults_after_setup |
false |
Suppress all faults until a node calls sim.SetupComplete() |
supervise |
false |
Restart a node that exits with an error, after restart_after. Go panics (exit code 2) still fail the run, and so does a workload that exits with an error, even under include_workload |
net_overlay |
false |
Route plain net.Listen / net.Dial through the simulated network. See networking.md |
compat_overlay |
false |
Build dependencies that need mmap or flock with wasip1 shims: a shim for bbolt, pebble or badger, or the generic shim for any dependency that calls Mmap or Flock. See compat.md |
watchdog |
30s |
Real-time limit for one node to run without yielding. Catches busy loops and livelocks. With preempt.loops, a busy loop parks like any other node and a run that never finishes fails on liveness instead, which does not depend on the host |
preempt.strategy |
none |
When the running goroutine is preempted inside a node: none only at the end of a 4,194,304-call time slice, random after a seeded 1 to 4,096 counted calls, pct at depth - 1 seeded change points per node, each recorded as a preempt fault, calls every preempt.calls counted calls. See how-it-works.md |
preempt.interleave |
false |
Shorthand for "strategy": "random". While it is true, strategy can only be random or none, and none still means random, so a profile that switches strategy sets interleave to false |
preempt.depth |
3 |
pct: the bug depth d to look for, giving d - 1 change points per node |
preempt.steps |
1048576 |
pct: the change points are drawn over each node's first steps counted calls. The trace's last pct: line shows how many each node ran |
preempt.calls |
4096 |
calls: counted calls between preemptions, at most 4,194,304 |
preempt.loops |
false |
Count loop iterations as well as function calls, so a loop without calls is preempted, uses virtual time and lets other nodes run. Costs guest CPU. See how-it-works.md |
Every node sees the environment variables SEEDFALL=1 and SEEDFALL_NODE=<name>, so sim.Enabled() and sim.Node() work.
Faults
All faults come from the seed. Faults that depend on what the program does, such as which disk operation a crash lands on, are recorded when they happen so that shrinking and replay are exact.
| Field | Default | Meaning |
|---|---|---|
preset |
Start from a built-in fault level, none, durability, resilient, high_availability or everything, each adding to the one before; fields set next to it override it, and "faults": "resilient" is shorthand for {"preset": "resilient"}. The values each level sets are in config.md |
|
crash_per_sec |
0 |
Rate of crashes per node, at random points in time |
restart_after |
["10ms", "1s"] |
Downtime after a crash, and the delay before a supervised restart |
partition_per_sec |
0 |
Rate of network partitions. A partition splits all nodes, workload included, into groups that cannot reach each other. A new partition replaces the previous one |
partition_for |
["50ms", "2s"] |
Partition duration. TCP traffic across the cut is held and delivered after the heal; UDP datagrams are dropped |
partition_topologies |
["half"] |
Shapes a partition can take; each partition picks one at random. half (two random sides), one (isolate one node), majority (a random majority against the rest), majorities_ring (every node sees a majority, and no two nodes see the same one) and random_groups (two or more random groups). See Network faults |
clog_per_sec |
0 |
Rate of clogs. A clog hits one direction of one link, or everything a node sends, receives, or both, and lasts clog_for |
clog_for |
["100ms", "2s"] |
Clog duration, also the length of a swizzle |
clog_modes |
["stop", "slow", "jam"] |
Modes a clog can take; each clog picks one at random. stop refuses new connections and holds traffic, jam holds connections and traffic without any error, slow adds clog_delay to every message. stop and jam drop UDP datagrams instead of holding them |
clog_delay |
["10ms", "200ms"] |
Extra latency per message of a slow clog, drawn once per clog |
swizzle_per_sec |
0 |
Rate of swizzles (FoundationDB's swizzle-clogging): a random subset of nodes is clogged one node at a time, then unclogged in a different random order |
reset_per_sec |
0 |
Rate of connection resets per node. Every open connection of the node is reset at once, as if a middlebox dropped them; the process keeps running and both ends see ECONNRESET |
udp_loss |
0 |
Probability that a UDP datagram is dropped. Each loss is recorded as a udp_drop fault with the sender and its datagram number. See UDP datagrams |
udp_dup |
0 |
Probability that a UDP datagram is delivered twice, the copies with independent latencies. Each one is recorded as a udp_dup fault |
slow_per_sec |
0 |
Rate of slowdowns per node: a gray failure where the node stays up but everything it does takes longer |
slow_for |
["500ms", "5s"] |
Slowdown duration |
slow_factor |
10 |
Largest slowdown. Each slowdown picks a factor between 1 and this value |
torn_writes |
false |
A crash keeps a random in-order prefix of unsynced writes, possibly ending in a partial write |
reorder_writes |
false |
A crash writes dirty pages back out of order, as the kernel does between fsyncs, within the limits of fs_model. This finds a commit record or header that reaches the disk before the data it points to. Combine with torn_writes to also tear the last write kept in each page. See File system models |
fs_model |
ext4_ordered |
Which crash states reorder_writes may produce: ext4_ordered, page_independent or dir_reorder, which needs strict_dirs. Setting it without reorder_writes is an error. See File system models |
crash_on_io |
0 |
Probability that a node crashes during each disk operation. This is how you hit the window between a write and its fsync |
fsync_eio |
0 |
Probability that a file fsync fails with EIO. The unsynced writes stay readable but are dropped from durable storage, and the next fsync succeeds (Linux "fsyncgate" semantics) |
disk_bytes |
0 |
Size of each node's disk in bytes; 0 means unlimited. A write that does not fit writes what fits and returns the short count, and the next write fails with ENOSPC. See Disk faults |
write_eio |
0 |
Probability that a file write fails with EIO and writes nothing |
write_enospc |
0 |
Probability that a file write fails with ENOSPC and writes nothing, however much space is free, as with a quota or a thin-provisioned volume |
read_eio |
0 |
Probability that a file read fails with EIO |
read_corrupt |
0 |
Probability that a file read returns one flipped bit. The disk holds the right bytes, so reading again returns them |
lost_write |
0 |
Probability that a file fsync succeeds but one 4 KiB page it should have written keeps its old content on disk. The program keeps reading the new content until a crash |
misdirected_write |
0 |
Probability that a file fsync succeeds but one page lands at another page offset of the same file, overwriting what was there, while the intended page keeps its old content |
corrupt_on_crash |
false |
Each crash flips one bit in every file whose pending writes cover bytes that are on disk after the crash, somewhere in those bytes, as a sector half-written when the power failed |
bitrot_per_sec |
0 |
Rate of bit rot per node: one bit of durable data, in a random file at a random offset, flips |
disk_latency |
["0s", "0s"] |
Extra time each fsync takes, drawn per fsync. Slow nodes stretch it |
mmap_early_writeback |
0 |
Probability that a dirty page of a writable shared mapping is already written back when a node crashes. Needs a shared mapping through compat_overlay. Recorded as a mmap_early_writeback fault. See compat.md |
strict_dirs |
false |
Directory entries survive a crash only after the parent directory is fsynced (strict POSIX) |
clock_skew |
0 |
Each node's wall clock is offset by a seeded amount in [-skew, +skew]. The monotonic clock is untouched, so timers and time.Since are unaffected |
pause_per_sec |
0 |
Rate of process pauses per node. A paused node stops running, like a long GC pause or a stalled VM, then resumes without crashing. Its connections stay open and messages queue up |
pause_for |
["100ms", "3s"] |
Pause duration |
clock_jump_per_sec |
0 |
Rate of wall-clock jumps per node. Each jump moves the node's wall clock forward or back by up to clock_jump_max; the monotonic clock is untouched |
clock_jump_max |
10s |
Largest clock jump in either direction |
buggify |
false |
Let sim.Buggify and sim.BuggifyWithProb sites in your code fire. Each site is enabled in a quarter of runs and then fires at its own rate; every firing is recorded as a buggify fault with its node, site and hit number. See buggify.md |
include_workload |
false |
Give the workloads the per-node faults too: crashes, pauses, clock skew and jumps, slowdowns, connection resets and disk faults. A crashed workload restarts as a fresh process after restart_after, with the same name and its disk as the crash left it. See Faults on the workload |
swarm |
false |
Swarm testing. Each seed turns every fault type on or off and rescales its rate between 0 and 2× the configured value, so faults that hide each other get explored separately |
Faults on the workload
By default the workload is the one process that never crashes, so a client's own retry, idempotency and session logic is never tested: what happens when a client restarts halfway through a write and sends it again? Set "include_workload": true to change that:
"workload": { "name": "client", "pkg": "./client" },
"faults": { "include_workload": true, "crash_per_sec": 0.3, "pause_per_sec": 0.3 }
- A crashed workload restarts as a fresh process after
restart_after, like a node. It keeps its name, sosim.Node()and any client id derived from it stay the same, and it finds its disk as the crash left it. A client that saves its progress there can resume; one that keeps it in memory starts over. - A workload crash does not end the run. The run passes only when the last incarnation of every workload exits 0.
- Operations the crashed workload had recorded with
sim.Put,sim.Get,sim.Invokeorsim.Txnand not yet completed stay open, and the trace says how many (client#1 died with 2 operations in flight, their outcome is unknown). The history check treats an open write as one that may take effect at any time after it started, and drops open reads, so the check stays sound under every model. - Per node,
"faults": trueorfalseoverrides the default:trueon one of several workloads faults only that one, andfalseon a node keeps it out of every fault, network faults included.
workloads[] runs several clients side by side, each its own process, so one can crash while the others keep going:
"workloads": [
{ "name": "writer", "pkg": "./client", "args": ["-mode", "write"], "faults": true },
{ "name": "reader", "pkg": "./client", "args": ["-mode", "read"] }
]
examples/liveness has a client that saves its progress and resends the add in flight after a crash. Without request deduplication on the server, one crash during an add applies it twice.
Network faults
Every network fault is directional underneath: it decides what happens to traffic from one node to another. A connection needs both directions to open, so a stopped direction either way refuses a dial, and a jammed one holds it.
| Mode | New connections | Data in flight | Errors the program sees |
|---|---|---|---|
partition, or clog stop |
refused | held, delivered after the heal | ECONNREFUSED on dial |
clog jam |
accepted only after the clog lifts | held, delivered after the clog lifts | none, only timeouts |
clog slow |
accepted clog_delay later when the slow direction points at the listener |
arrives clog_delay later, in order |
none |
A clog targets one of four shapes, chosen at random: one link in one direction (n1 -> n2), or a node's inbound, outbound, or inbound and outbound traffic. A one-way partition, where n1 can hear n2 but n2 cannot hear n1, is a stop clog on one link or an outbound clog on one node. Faults stack: a slow clog over a partition adds its delay to the held traffic.
Partition topologies follow Jepsen's nemeses. half is the original random split and stays the default, so existing seeds keep their schedules. majority needs at least 3 nodes and majorities_ring at least 4, workload included; the config is rejected otherwise. A ring partition is written as overlapping groups, and two nodes can talk only if some group holds both.
A slowdown by factor F stretches everything the node waits for: its timers and sleeps take F times as long, every message it sends or receives takes F times its latency, and a goroutine that spins is parked F times as long. The node still reads the true time, so it sees its own timeouts fire late, the way an overloaded machine does. CPU work between two waits stays free, as it is for every node in seedfall.
sim.QuietFor and the recovery phase of liveness heal every partition, clog and slowdown at once.
In a fault schedule, the new kinds look like this:
{ "at": "1.2s", "kind": "partition", "groups": [["n1", "n2"], ["n2", "n3"], ["n3", "client"], ["client", "n1"]], "until": "2s" }
{ "at": "1.5s", "kind": "clog", "from": "n1", "to": "n2", "mode": "stop", "until": "3s" }
{ "at": "1.6s", "kind": "clog", "node": "n3", "dir": "out", "mode": "slow", "delay": "80ms", "until": "2.4s" }
{ "at": "2.1s", "kind": "net_reset", "node": "n2", "until": "2.1s" }
{ "at": "2.5s", "kind": "slow", "node": "n1", "factor": 4.5, "until": "6s" }
dir is in, out or both. A swizzle shows up as a run of both clogs in stop mode. Like every other planned fault, each one replays and shrinks on its own, appears in the trace, seedfall cause and the report, and joins swarm testing.
UDP datagrams
TCP traffic is a byte stream, where the kernel retransmits and deduplicates, so a program never sees a packet dropped or repeated on a connection. A UDP program does. udp_loss and udp_dup are probabilities per datagram, drawn from their own stream of the seed, so they take no draws from any other fault:
{ "faults": { "udp_loss": 0.05, "udp_dup": 0.02 } }
A partition or a clog that stops or jams a link also drops a datagram sent across it, rather than holding it for the heal, and a slow clog delays it. Datagrams that a link or a missing socket drops are consequences of the rest of the schedule, so they are not faults of their own: only udp_loss and udp_dup choices are recorded. They count per sender node, from 1, across restarts:
{ "at": "2.1s", "kind": "udp_drop", "node": "n1", "to": "n2", "op": 17, "until": "0s" }
{ "at": "2.2s", "kind": "udp_dup", "node": "n2", "to": "n3", "op": 9, "until": "0s" }
op is the number of the datagram the sender sent, and the fault fires on that datagram whatever its destination; to is a record of where it went. Both replay exactly, shrink like any fault recorded at runtime, can be edited with seedfall faults, and join swarm testing. They do not apply when faults is off for either end of the datagram, or during a quiet period or the recovery phase. Semantics of sockets and addresses are in networking.md.
Disk faults
File system models
reorder_writes turns on out-of-order writeback at a crash. fs_model says which states a crash may then leave behind. A model that is too weak reports bugs your file system cannot produce; one that is too strong misses bugs it can. The models follow Table 1 of ALICE (All File Systems Are Not Created Equal, OSDI '14), which measured these properties on real file systems.
fs_model |
File contents | Directory operations (strict_dirs) |
Matches |
|---|---|---|---|
ext4_ordered (default) |
Appends and truncates of one file persist as an in-order prefix, split at 4 KiB page boundaries. Overwrites of existing bytes persist per page in any order, independently of the appends. Bytes past the surviving file size are not kept | An in-order prefix of the pending operations, under torn_writes |
ext4 data=ordered |
page_independent |
Any subset of the dirty pages of a file, in order within each page. A later appended page can survive without an earlier one, leaving zeros in the gap | As ext4_ordered |
Aggressive: useful to find code that depends on append order, but it reaches states ext4 data=ordered does not. ALICE records this for ext2, ext3 and ext4 in writeback mode, not for ordered |
dir_reorder |
As page_independent |
Each pending operation survives independently, so a rename can persist without the create before it | ext2 and btrfs, where ALICE found directory operations persist out of order |
Details that follow from these rules:
- Models apply per file. Different files are independent, so an append to one file can survive while an earlier append to another is lost. ALICE records
Append -> Any opfailing on ext4orderedfor this reason (delayed allocation). - Under
ext4_ordereda crash that keeps an overwrite past the surviving file size keeps nothing of it, because that page was never part of the file on disk. - A single directory operation is atomic in every model, as on btrfs. ext2 can also tear one rename so that both names or neither survive; that is not modelled.
- Plain
torn_writeswithoutreorder_writesdoes not usefs_model: it keeps an in-order prefix of all pending writes.
examples/symswap shows the difference. It publishes each generation by renaming a symlink over current, and fsyncs the new generation's file but not its directory entry first. Under ext4_ordered it passes, because a journal keeps directory operations in order. Under dir_reorder the rename can survive without the create, and current points at nothing.
Crashes, torn writes, reorder_writes and fsync_eio test what survives a crash. The faults below test what a storage engine does when the disk itself misbehaves: it fills up, it fails an operation, or it hands back or keeps the wrong bytes. These are the paths that WAL recovery and checksum code exist for, and the ones that are hardest to reach with a real disk.
"faults": {
"crash_per_sec": 0.5,
"torn_writes": true,
"disk_bytes": 1048576,
"write_eio": 0.01,
"read_eio": 0.01,
"read_corrupt": 0.05,
"lost_write": 0.01,
"misdirected_write": 0.01,
"corrupt_on_crash": true,
"bitrot_per_sec": 0.2,
"disk_latency": ["1ms", "20ms"]
}
| Fault | What the program sees | What is on disk afterwards |
|---|---|---|
disk_bytes |
A short write, then ENOSPC on the next write. Overwrites inside a file always fit, and space comes back when files shrink or are removed |
What fitted |
write_eio, write_enospc |
The write fails | Nothing from that write |
read_eio |
The read fails | Unchanged |
read_corrupt |
One bit flipped in the bytes returned by one read | Unchanged |
lost_write |
fsync succeeds, and reads return the new content | One page has its old content; zeros if the page is new. The next fsync that rewrites the page writes it in full |
misdirected_write |
fsync succeeds, and reads return the new content | The intended page has its old content, and another page of the file holds the new one |
corrupt_on_crash |
After the restart, one flipped bit per file among the bytes that were being written | Same |
bitrot_per_sec |
The flipped bit, from the moment it happens, unless that byte has a pending write | One flipped bit |
disk_latency |
fsync takes longer | Unchanged |
How each one is modelled, and where it stops:
- Disk usage is the sum of every file's size on the node. Holes left by
Truncateor by writing past the end count in full, so sparse files use more space than on ext4 or XFS. Directories and metadata use none. ATruncatethat grows a file is never refused. ENOSPCreaches your code asENOSPC. wazero's WASI layer has noENOSPCand reportsEIOfor it, so seedfall rewrites the result offd_writeandfd_pwritewhen the simulated disk was full, anderrors.Is(err, syscall.ENOSPC)works in the guest.- Read and write errors apply to file reads and writes only. Directory listings,
stat,openandTruncatenever fail. - Corrupted reads flip one bit in the buffer of one read, as a bad cable or controller would. A checksum plus a retry handles them; a checksum alone turns them into an error.
- Lost and misdirected writes pick one dirty page of the file being fsynced. The page cache keeps the right content, because the drive acknowledged the write, so the program only notices after a crash, or never if the page is rewritten first. A misdirected write stays inside the file; writes landing in another file are not modelled. A file needs at least two pages for a misdirected write.
- Crash corruption damages only bytes that were being written when the node crashed. With plain crashes, unsynced writes are dropped, so it matters only where they survive: with
torn_writes,reorder_writesorcrash_on_io. If the damaged range overlapped older durable data, that data is what flips. - Bit rot picks a byte uniformly over all durable data on the node, so large files are hit more often.
- Latency applies to file and directory fsyncs only, not to reads or writes, which go to the page cache. The whole node waits, because a wasip1 guest runs every goroutine on one thread.
Every one of them comes from the seed and replays exactly. The rates are drawn per operation, like crash_on_io, and each firing is recorded with its node and operation number: disk op #N counts the node's writes, truncates and fsyncs, and disk read #N counts its reads. Which page or bit is hit comes from a stream keyed by the seed, the fault kind, the node and that number, so it does not change when shrinking removes other faults. Bit rot is planned in time like a crash. In a fault schedule they look like this:
{ "at": "1.942589003s", "kind": "read_corrupt", "node": "server", "op": 3, "path": "data/kv.log", "offset": 70, "until": "0s" }
{ "at": "912.496ms", "kind": "write_eio", "node": "server", "op": 67, "path": "data/kv.log", "until": "0s" }
{ "at": "2.601158s", "kind": "lost_write", "node": "server", "op": 232, "path": "data/kv.log", "until": "0s" }
{ "at": "5.259168s", "kind": "misdirected_write", "node": "server", "op": 558, "path": "data/kv.log", "offset": 4096, "until": "0s" }
{ "at": "657.015787ms", "kind": "bitrot", "node": "server", "path": "data/kv.log", "offset": 22, "until": "657.015787ms" }
{ "at": "36ms", "kind": "mmap_early_writeback", "node": "server", "op": 3, "path": "data/pages", "offset": 65536, "until": "0s" }
The other kinds are write_enospc and read_eio. A recorded fault fires at the operation its kind, node and op name, and bit rot fires at at. path, offset and target are filled in by the run so that you can read the schedule, and are left out when they are 0. seedfall inspect lists every damaged range of a file under corrupted ranges, and with disk_bytes shows how full each disk is. Disk usage and latency are settings, not faults, so swarm testing leaves them alone and toggles the rest.
examples/kvlog shows each one: a corrupted read changes an acknowledged value, a checksum with a re-read fixes it, and a lost write, a misdirected write or bit rot breaks a store that keeps one copy, with or without checksums.
Editing a schedule
A fault schedule (.seedfall/seed-N.faults.json from shrink, an artifact's faults.json, or a pinned regression) is a list of faults that a replay applies instead of drawing new ones. Changing one fault and replaying is the quickest way to test an explanation of a failure. seedfall faults makes the change and checks it:
seedfall faults -c seedfall.always.json -seed 1 -faults torntail.faults.json -remove 1 -out hypothesis.json
seedfall replay -c seedfall.always.json -seed 1 -faults hypothesis.json
seed 1, 1 faults from torntail.faults.json, wrote hypothesis.json
1 802ms crash server, restart at 802ms
duration 991ms
replay: seedfall replay -c seedfall.always.json -seed 1 -faults hypothesis.json
On kvlog's seedfall.always.json, seed 1 fails with torntail.faults.json, still fails with the crash during disk op #56 moved one operation earlier (-shift 1=-1), and passes once that crash is removed. That pass is within the schedule's shrunk 991ms, and the replay prints a note saying so: it shows the crash is necessary for this failure, not that the store is fixed.
| Flag | Meaning |
|---|---|
-faults |
The schedule to start from. Without it, .seedfall/seed-N.faults.json next to the config, or, when that file does not exist, the faults seed N draws on its own, which takes one run |
-remove N |
Remove the fault at position N, counting from 1 as listed. Repeatable |
-shift N=AMOUNT |
Move fault N. A timed fault moves by a signed duration (2=-50ms), together with its until. A fault that fires at a disk operation, read, buggify hit, counted call or udp datagram (crash_io, fsync_eio, write_eio, write_enospc, read_eio, read_corrupt, lost_write, misdirected_write, buggify, preempt, udp_drop, udp_dup, mmap_early_writeback) moves by a signed count of those (1=+2), because its at only records when it happened. Repeatable |
-add JSON |
Add a fault in the format shown in this page, for example {"at":"1s","kind":"pause","node":"server","until":"1.5s"}. Repeatable |
-out |
Write the result here. Without it, the schedule is written back to the file it came from |
Positions in -remove and -shift always refer to the schedule as it was before the edit. An edited schedule drops the shrunk_from and shrink_runs that shrink recorded, since it is no longer what shrink found. The result is sorted by at and checked against the config before anything is written:
| Check | Applies to |
|---|---|
kind is a fault kind seedfall knows |
every fault |
node, from, to, side and groups name nodes of the config |
every fault |
at is not negative |
every fault |
at is not after the end of the run: the schedule's duration, else the config's |
every fault except those that fire at a disk operation, read, buggify hit, counted call or udp datagram, whose at only records when it happened |
until is not before at |
crash, crash_io, partition, pause, clog, slow |
side or groups, not both |
partition |
mode is stop, slow or jam; dir is in, out or both with a node, or empty with two different nodes in from and to |
clog |
node is set |
every kind except partition and clog |
factor is at least 1 |
slow |
op is at least 1, a buggify fault names its site, and a preempt fault needs preempt.strategy pct in the config |
faults that fire at a disk operation, read, buggify hit, counted call or udp datagram |
Without any edit, seedfall faults only lists and checks the schedule, and writes it only when it came from the seed's own faults, so that there is a file to edit. Over MCP, seedfall_faults_edit takes the same edits as JSON (agents.md).
A schedule file can also hold branches, which seedfall explore writes for a failure it found by branching:
"branches": [{ "at": "921.003003ms", "key": 257366709774643251 }, { "at": "1.2s", "key": 9, "stream": "sched" }]
Each branch re-keys the seed's random streams at the first scheduler step at or after at, in order. Without stream it re-keys every stream; with one of sched, net, disk, io, udp, disklatency, mutant or random it re-keys only that one. Re-keying io or udp changes nothing in a schedule, because those streams only decide faults and a schedule already fixes them; mutant matters only under seedfall mutate. replay, shrink, inspect, debug, logs and pin all honor them, and at values must not decrease.
Choosing rates
Start with crashes and torn writes, then add crash_on_io around 0.005, so a few hundred disk operations per run produce one or two mid-operation crashes. Add fsync_eio, strict_dirs and reorder_writes once the basic durability path passes. For consensus and leader-election code, add partition_topologies beyond half, then clog_per_sec and slow_per_sec: one-way links and slow leaders are the gray failures that break leases and stale-read checks. For a storage engine, add disk_bytes and write_eio to test the error paths of every write, then read_corrupt and corrupt_on_crash to test checksums. lost_write, misdirected_write and bitrot_per_sec lose data that a single copy cannot get back, so use them on replicated systems, or to check that a single node notices and stops instead of serving wrong data. Turn on buggify once you have marked sites in your code that should sometimes take the rare path. Turn on swarm for long CI sweeps. Use property coverage (properties.md) to confirm the faults are actually reaching the code you care about.
Durations
Every duration is a Go duration string, such as "250ms" or "2s". Ranges are two-element arrays.