Documentation
¶
Overview ¶
Package dst provides the deterministic-simulation-testing foundation (RFC-199 Tier 0): a Clock seam, a seeded randomness source, and FoundationDB-style Buggify fault points.
The production defaults are drop-in and cost nothing measurable: RealClock reads the wall clock, CryptoRandomness delegates to crypto/rand, and a disabled Buggifier never fires. A simulation swaps in a SimClock advanced by the driver, a seeded PCG randomness source, and an enabled Buggifier over the same seed — so a single seed reproduces the run's time, randomness, and injected faults exactly.
This is the shared foundation for both Track A (record + relational, via SimFDB) and Track B (client transport). It depends only on the standard library so every layer can import it without a cycle.
Index ¶
Constants ¶
This section is empty.
Variables ¶
var Epoch = time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)
Epoch is a stable, arbitrary non-zero start instant for simulation clocks: 2020-01-01 UTC. Chosen to be recent enough that any code asserting "timestamp is after some sensible past date" holds, and fixed so persisted bytes are identical across runs.
Functions ¶
Types ¶
type Buggifier ¶
type Buggifier struct {
// contains filtered or unexported fields
}
Buggifier ports FoundationDB's getSBVar plus the BUGGIFY firing gate (flow/flow.cpp:356). Every call site has two independent seeded gates:
- activation: decided ONCE per site the first time it is hit, then cached — the site is "active" with probability ActivationProb (FDB default 0.25). An inactive site never fires for the rest of the run.
- firing: re-rolled on every hit of an active site — fires with probability FireProb (FDB default 0.25) or a per-call probability.
Each site draws from its OWN generator, derived from (seed, site label) — the rand.go pattern of giving each consumer its own PCG stream off one seed, applied per site. A single shared generator would make the fault schedule an artifact of the ORDER and COUNT of site hits across the whole run: adding a fault point anywhere, or hitting an existing one one extra time, re-phases every later draw and silently changes what a seed injects. Then a "reproducer for seed N" stops reproducing the moment an unrelated site is added, which is the one property the whole seam exists to provide.
Production uses DisabledBuggifier so a BUGGIFY point costs only a nil/bool check and never fires.
FDB identifies a site by (__FILE__, __LINE__). Go has no such macros, so a site is identified by a caller-supplied stable string label (e.g. "simfdb.commit.conflict"); the label plays the exact role of the file:line pair. Use a distinct constant per site.
func DisabledBuggifier ¶
func DisabledBuggifier() *Buggifier
DisabledBuggifier returns a Buggifier that never fires faults — the production default. A nil *Buggifier behaves identically, so callers may hold a nil field.
func NewBuggifier ¶
NewBuggifier returns a Buggifier seeded by seed. When enabled is false it behaves like DisabledBuggifier for fault points (never fires) but still remembers the seed, so a run can be started disabled and enabled later without reseeding — and so Coin, which is not a fault gate, stays deterministic either way.
func (*Buggifier) Buggify ¶
Buggify reports whether the fault point identified by site should fire on this hit, using the default fire probability. Direct port of the BUGGIFY macro.
func (*Buggifier) BuggifyWithProb ¶
BuggifyWithProb is BUGGIFY_WITH_PROB: the cached activation gate AND a per-call fire roll at probability prob. Returns false immediately when disabled (no RNG draw), so disabled Buggify points are free and never perturb the seeded sequence.
func (*Buggifier) Coin ¶
Coin returns a deterministic per-site coin flip (fair, 50/50).
It is NOT a fault gate and deliberately ignores the enabled flag: a coin resolves a MODELLING choice the simulator has to make either way — which of two equally-real behaviours the simulated system exhibits this time — rather than injecting a failure that would otherwise not happen. The archetype is commit_unknown_result: the mutations either landed or did not, both outcomes are real FDB, and the sim must pick one and be consistent about it for the rest of the run.
Like a fault site, a coin site draws from its own (seed, label)-derived generator, so coins at one site never re-phase another site's schedule. A nil Buggifier has no seed and so cannot flip: it returns false. Callers that need both branches without a seeded Env must choose the branch explicitly rather than rely on that value.
func (*Buggifier) Enabled ¶
Enabled reports whether this Buggifier can fire. A nil Buggifier is disabled.
func (*Buggifier) Fired ¶
Fired returns how many fault points have fired this run. A brute-force hunter reads it to confirm a seed actually injected faults (a run with zero firings exercises only the happy path). Deterministic for a given seed. A nil or disabled Buggifier reports 0.
func (*Buggifier) SetProbabilities ¶
SetProbabilities overrides the activation and firing probabilities (both in [0,1]). Used by a driver that wants faults denser or sparser than FDB's 0.25/0.25 default. A value outside [0,1] is clamped.
Sites already hit keep the activation decision they were given; the new activation probability governs sites first hit afterwards. Call it before the run starts.
type Clock ¶
type Clock interface {
// Now returns the current time. Callers that persist the result (store headers,
// heartbeats, lease TTLs, SQL CURRENT_*) must route through here rather than
// time.Now so a simulation run is byte-reproducible.
Now() time.Time
}
Clock abstracts the passage of time so simulation code advances a logical clock deterministically instead of reading the wall clock. It mirrors FoundationDB's INetwork::now() seam (fdbrpc/sim2.actor.cpp), where in simulation time only advances when the ready queue drains.
Only Now() is required for the record/relational (Track A) persisted-byte sites; the full timer surface (After/NewTimer/Sleep) belongs to Track B and is intentionally left off this interface until that work lands.
type CryptoRandomness ¶
type CryptoRandomness struct{}
CryptoRandomness is the production source: crypto/rand.
type Env ¶
type Env struct {
Clock Clock
Random Randomness
Buggify *Buggifier
}
Env bundles the three Tier-0 seams — Clock, Randomness, and Buggifier — for injection through the record and relational layers. Threading one Env keeps the seam surface small: a store, indexer, or session holds an *Env and reaches Clock/Random/Buggify off it.
PRODUCTION IS A NIL *Env. There is deliberately no constructor for it. Every accessor (Now/Read/Fault/Coin) treats a nil Env — and a nil field within an Env — as wall clock, crypto/rand, and never-fire, so an unset env is byte-identical to the code before the seam. NewSim(seed) builds a fully deterministic environment for a simulation run.
A "production Env" constructor existed and was dead, and its existence was actively harmful: several seam sites are deliberately ASYMMETRIC — they divert only when an env is present, because routing them through the nil-default would have CHANGED production bytes (the SPFresh builder token used math/rand, not crypto/rand; the SPFresh process nonce was minted once per process, not per call). Those sites test `env != nil`, so installing a hand-built "production" Env would have flipped them onto the simulation path while claiming to be production. With nil as the only spelling of production, `env != nil` means "a simulation environment is installed", which is exactly what those guards intend.
func NewSim ¶
NewSim returns a fully deterministic environment seeded by seed, with its logical clock pinned at Epoch. All three seams share the one seed (via distinct PCG streams), so the run is reproducible from seed alone.
func (*Env) Coin ¶
Coin flips the deterministic per-site coin (Buggifier.Coin) — the seam for a modelling choice between two equally-real behaviours, as opposed to Fault, which injects a failure. A nil Env has no seed and returns false; callers that must reach both branches without a seeded Env choose the branch explicitly instead.
func (*Env) Fault ¶
Fault reports whether the fault point site should fire, treating a nil Env or nil Buggify as production (never fires).
func (*Env) Now ¶
Now returns the environment's current time, treating a nil Env or nil Clock as production (wall clock). Convenience for call sites that hold a possibly-nil *Env.
func (*Env) Read ¶
Read fills p from the environment's randomness source, treating a nil Env or nil Random as production (crypto/rand).
func (*Env) Since ¶
Since returns the time elapsed since t on the environment's clock — the seam-aware analog of time.Since, and the spelling every ELAPSED-TIME decision must use.
It exists because a duration is where a wall clock hides best. Now() at a site that persists a timestamp is obvious; time.Since at a site that only compares against a budget looks like instrumentation, and is not, whenever the comparison decides how much work gets done — and therefore how many bytes are durably recorded. Mixing the two spellings is worse than either: an expiry minted on the sim clock and measured against the wall clock is not merely nondeterministic, it is decided by the gap between two unrelated epochs.
type Randomness ¶
type Randomness interface {
// Read fills p with random bytes and returns len(p). Matches the crypto/rand.Read and
// io.Reader contract; the sim implementation never returns an error.
Read(p []byte) (int, error)
}
Randomness abstracts a source of random bytes. Production uses CryptoRandomness (crypto/rand); simulation uses SeededRandomness so nonces, UUIDs, and other bytes that end up persisted (R-tree node IDs, HNSW sample keys, indexer UUIDs, spfresh tokens) come from a seeded pool instead of the OS CSPRNG — the single worst byte-determinism offender the RFC calls out. Mirrors FDB's deterministicRandom() seam.
Determinism caveat (RFC-199 §1a coverage ledger): the record layer generates some of these nonces inside concurrent goroutine fan-out (indexer/spfresh). A seeded source makes the pool of drawn bytes deterministic, but which goroutine draws which value is still scheduler-dependent, so per-node assignment is not bit-exact under fan-out. Full bit-exactness comes from driving those paths single-goroutine (Tier 2). The seam still removes the crypto/rand nondeterminism that defeats replay outright.
type RealClock ¶
type RealClock struct{}
RealClock is the production Clock: it reads the wall clock via time.Now.
type SeededRandomness ¶
type SeededRandomness struct {
// contains filtered or unexported fields
}
SeededRandomness is a deterministic byte source backed by a seeded math/rand/v2 PCG generator. NOT cryptographically secure — simulation only. Guarded by a mutex so the record layer's concurrent fan-out cannot data-race the generator (the draw order under fan-out is still scheduler-dependent — see the Randomness doc).
func NewSeededRandomness ¶
func NewSeededRandomness(seed uint64) *SeededRandomness
NewSeededRandomness returns a deterministic Randomness seeded by seed.
type SimClock ¶
type SimClock struct {
// contains filtered or unexported fields
}
SimClock is a deterministic Clock whose time only moves when the driver advances it — the analog of FDB Sim2's logical clock. It is safe for concurrent reads (the record layer's goroutine fan-out may read the clock from several goroutines); Advance/Set are driver-controlled and serialized by the same lock.
func NewSimClock ¶
NewSimClock returns a SimClock pinned at start. A fixed, non-zero start (e.g. a known UTC instant) keeps persisted timestamps stable across runs.