View as Markdown, for AI agents Edit on GitHub

Config files, profiles and seeded disks

One seedfall.json can hold many variations of your test: a base setup, named profiles, ready-made fault presets and one-off command-line overrides. This page also covers starting nodes from an existing disk and restarting them into a new version of your program. Use it when you want to test more than one scenario without copying files around.

One seedfall.json can hold every variant you test: a base config, named profiles that change a few fields, fault presets instead of hand-tuned rates, and one-off overrides from the command line. Nodes can start from an existing disk instead of an empty one, get files from the host, and restart into a new version of their program.

Layers

A config is built from up to four layers, each merged over the one before:

  1. the file named in "extends", if any, itself built the same way
  2. the file you pass with -c
  3. the profile you pass with -profile name
  4. every -set path=value, in order

Merging works the same way at every layer:

  • objects merge key by key, so {"faults": {"crash_on_io": 0.1}} changes one rate and keeps the rest
  • lists of named entries, such as nodes and workloads, merge by name: an entry with a known name merges into it, a new name is appended
  • anything else, including args and other lists, replaces the value

Fields that no layer sets keep their defaults, as listed in the seedfall://schema/config MCP resource.

The merged config is checked once, before anything is built or run, and a config that fails the check does not load:

  • a key that is not a config field is an error, in the file, in a profile and in -set, so a typo such as crash_per_sc cannot silently leave a fault off
  • every probability (crash_on_io, fsync_eio, write_eio, write_enospc, read_eio, read_corrupt, lost_write, misdirected_write, mmap_early_writeback, udp_loss and udp_dup) is between 0 and 1, and every _per_sec rate is 0 or more
  • every range (latency, links[].latency, restart_after, partition_for, clog_for, clog_delay, slow_for, pause_for and disk_latency) is [min, max] with 0 <= min <= max
  • duration and watchdog are positive, and clock_jump_max is 0 or more
  • a node sets pkg or wasm, not both

extends

{
  "extends": "seedfall.json",
  "nodes": [{ "name": "server", "args": ["-sync", "always", "-repair"] }],
  "faults": { "bitrot_per_sec": 0.5 }
}

extends names another config file, relative to the file that extends it, and can chain. A file that extends itself, directly or through others, is an error. Paths inside the config, such as pkg, wasm, files and disk, are resolved from the directory of the file you pass with -c, so keep extended files in the same directory as the configs that use them. examples/kvlog keeps one file per variant this way, each a few lines over seedfall.json, because the getting started guide and CI name those files.

profiles

{
  "nodes": [{ "name": "counter", "pkg": "./counter" }],
  "workload": { "name": "client", "pkg": "./client" },
  "faults": { "buggify": true },
  "profiles": {
    "fixed": { "nodes": [{ "name": "counter", "args": ["-dedupe"] }] },
    "off": { "faults": "none" }
  }
}
seedfall run -c seedfall.json -profile fixed -seeds 1-20

A profile is a partial config merged over the file. Asking for a profile that does not exist fails and lists the ones that do. Every command that takes -c takes -profile, and so do the MCP tools ("profile": "fixed"). seedfall init writes three starter profiles: strict, resilient and everything.

A profile is part of the config's identity everywhere seedfall prints or saves it:

  • replay and shrink commands carry it: replay: seedfall replay -c seedfall.json -profile fixed -seed 2
  • failure artifacts go to .seedfall/failures/seedfall.fixed-seed-2/, the same folder a separate seedfall.fixed.json would use
  • seedfall pin records the profile and -set overrides in the pinned file ("profile": "fixed"), and a pinned schedule is replayed only by runs with the same config file, profile and overrides
  • -set overrides add a short hash of the overrides to artifact folder and pinned file names, seedfall.fixed.set-1a2b3c4d-seed-2, so a run with overrides never replaces the files of a run without them
  • reports, proofs and seedfall hook print the full config reference

seedfall hook reads the profile from $SEEDFALL_HOOK_PROFILE (agents.md).

-set

seedfall run -set faults.crash_per_sec=2 -set duration=10s -seeds 1-50
seedfall run -set 'nodes.server.args=["-sync","always"]' -seeds 1-50
seedfall replay -seed 7 -set faults=resilient

-set path=value overrides one value after every other layer, and can repeat. The path is a dot-separated list of keys. Inside a list, a part is either the name of an entry or its index, so nodes.server.env.MODE and nodes.0.env.MODE both work. Missing objects along the path are created. The value is read as JSON when it parses as JSON (2, true, ["-x"], {}) and as a plain string otherwise (10s, resilient). To set a string that looks like JSON, quote it: -set 'nodes.server.env.PORT="7000"'. When the JSON reading does not fit the field, such as the number in -set nodes.server.env.PORT=7000 for an env value, which must be a string, the value is used as a plain string instead.

Fault presets

{ "faults": "resilient" }
{ "faults": { "preset": "high_availability", "crash_per_sec": 2, "clog_per_sec": 0 } }

A preset fills in a documented fault level. Fields set next to it override it, and the string form is shorthand for {"preset": name}. Each level adds to the one before:

Preset Adds
none no faults
durability crash_per_sec 0.5, crash_on_io 0.002, torn_writes, reorder_writes, strict_dirs, mmap_early_writeback 0.1
resilient pause_per_sec 0.1, slow_per_sec 0.1, reset_per_sec 0.1, clock_jump_per_sec 0.05
high_availability partition_per_sec 0.2, clog_per_sec 0.2, swizzle_per_sec 0.05, udp_loss 0.01, udp_dup 0.01
everything fsync_eio 0.01, write_eio, write_enospc, read_eio, read_corrupt, lost_write and misdirected_write 0.005 each, corrupt_on_crash, bitrot_per_sec 0.05, buggify

Durations, topologies and clog modes keep their defaults, and every rate is per node per second, per disk operation for the I/O faults, or per datagram for udp_loss and udp_dup, as in faults. The levels are seedfall's own, named after the fault tolerance levels Antithesis offers, not a copy of them. high_availability and everything include partitions, clogs and swizzles, which need at least two nodes that take faults, workload included. everything includes faults such as corrupted reads that only a system with checksums can survive.

A layer that names a preset starts its faults from that preset, and fault fields from earlier layers are dropped, so a profile with "faults": "none" turns every fault off even when the base config sets rates. A layer without a preset merges its fault fields as usual. On the command line, -set faults=none replaces the faults the same way, while -set faults.preset=none changes only the preset and keeps the fault fields set so far, which still override it. The preset in effect shows up as faults.preset in the triage report JSON and in the HTML report's fault table.

Seeded disks and files

Every node starts with an empty disk unless its entry says otherwise:

{
  "name": "ledger",
  "pkg": "./v1",
  "disk": "image",
  "files": { "etc/ledger.conf": "fixtures/atomic.conf" }
}
  • disk names a host directory. Its files, directories and symlinks are copied onto the node's disk, the directory itself becoming /.
  • files maps a path on the node's disk to a host file. A file listed here replaces the same path from disk.

Both are read from the host once, when the config loads, and copied into memory, so a run never touches the host disk and stays deterministic. The copied files are durable and synced before the node first starts, as if an earlier life had written and fsynced them: a crash never loses them, and seedfall inspect shows them as durable from t=0. Directories leading to them are durable too, even with strict_dirs. Symlinks and other special files in disk are rejected. The trace opens with a line per seeded node, ledger's disk starts with 2 durable files, 86 bytes, and the config hash in failure artifacts covers the bytes of every seeded file, so changing a fixture changes config_sha256.

seedfall inspect -out dir exports each node's durable disk as dir/<node>/, in exactly the layout disk reads, so a failing state can become the starting point of new runs:

seedfall inspect -seed 21 -out state
seedfall run -set nodes.ledger.disk=state/ledger -seeds 1-20

Upgrades

{ "name": "ledger", "pkg": "./v1", "upgrade_to": "./v2" }

upgrade_to names a second Go package for the node. seedfall builds both, the node starts as pkg, and every start from its first restart on runs upgrade_to instead, over whatever the old version left on disk. Restarts come from crash faults, crash_on_io and supervise. The trace marks the switch with start ledger#2, upgraded to ./v2. upgrade_to takes a package, not a prebuilt wasm file, and seedfall mutate -app mutates only pkg.

Together with disk, this tests the upgrade path FoundationDB's restart tests cover: real old data, a crash, and the new version's recovery code. examples/upgrade seeds a v1 ledger, upgrades it to v2 on the first crash, and catches an in-place migration that a crash leaves half done.