dst

package
v0.0.0-...-7d04355 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 15, 2026 License: Apache-2.0 Imports: 6 Imported by: 0

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

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

func Since

func Since(c Clock, t time.Time) time.Duration

Since returns the time elapsed since t according to the clock — the seam-aware analog of time.Since. Kept as a free function so it works against any Clock.

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

func NewBuggifier(seed uint64, enabled bool) *Buggifier

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

func (b *Buggifier) Buggify(site string) bool

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

func (b *Buggifier) BuggifyWithProb(site string, prob float64) bool

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

func (b *Buggifier) Coin(site string) bool

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

func (b *Buggifier) Enabled() bool

Enabled reports whether this Buggifier can fire. A nil Buggifier is disabled.

func (*Buggifier) Fired

func (b *Buggifier) Fired() int

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

func (b *Buggifier) SetProbabilities(activation, fire float64)

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.

func (CryptoRandomness) Read

func (CryptoRandomness) Read(p []byte) (int, error)

Read fills p from 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

func NewSim(seed uint64) *Env

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

func (e *Env) Coin(site string) bool

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

func (e *Env) Fault(site string) bool

Fault reports whether the fault point site should fire, treating a nil Env or nil Buggify as production (never fires).

func (*Env) Now

func (e *Env) Now() time.Time

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

func (e *Env) Read(p []byte) (int, error)

Read fills p from the environment's randomness source, treating a nil Env or nil Random as production (crypto/rand).

func (*Env) Since

func (e *Env) Since(t time.Time) time.Duration

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.

func (RealClock) Now

func (RealClock) Now() time.Time

Now returns the wall-clock time.

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.

func (*SeededRandomness) Read

func (s *SeededRandomness) Read(p []byte) (int, error)

Read fills p with deterministic pseudo-random bytes. math/rand/v2's *Rand has no Read method (unlike v1), so bytes are drawn 8 at a time from Uint64 in little-endian order.

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

func NewSimClock(start time.Time) *SimClock

NewSimClock returns a SimClock pinned at start. A fixed, non-zero start (e.g. a known UTC instant) keeps persisted timestamps stable across runs.

func (*SimClock) Advance

func (c *SimClock) Advance(d time.Duration) time.Time

Advance moves the logical clock forward by d and returns the new time. d must be non-negative; a negative delta would let persisted timestamps go backwards and is treated as a no-op.

func (*SimClock) Now

func (c *SimClock) Now() time.Time

Now returns the current logical time.

func (*SimClock) Set

func (c *SimClock) Set(t time.Time)

Set pins the logical clock to t. Used by the driver to jump to a chosen instant.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL