sim

package
v0.5.0 Latest Latest
Warning

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

Go to latest
Published: Jun 17, 2026 License: MIT Imports: 35 Imported by: 0

Documentation

Overview

Package sim implements a deterministic simulation testing (DST) harness for the GoGraph engine, modelled on TigerBeetle's VOPR. The simulator is seed-reproducible, single-goroutine, and tick-driven: it drives the real cypher.Engine against an in-memory store, maintains a shadow oracle model of what the graph must contain, and verifies ACID and graph invariants after every operation.

The whole point of the harness is determinism: a given seed always produces the exact same sequence of actors, operations, parameters, and injected faults, so any violation can be reproduced bit-for-bit from its seed alone. To preserve that property, every probabilistic decision anywhere in the package must draw from a single Seed; no other source of randomness (no global math/rand, no time.Now, no map-iteration ordering decisions) may influence control flow.

Concurrency contract

No type in this package is safe for concurrent use. The simulator runs on a single goroutine and spawns none; the determinism guarantee depends on a single, totally-ordered stream of draws from Seed. Sharing any value in this package across goroutines is a programmer error.

Index

Constants

View Source
const (
	ScenarioCrashStorm   = "crash-storm"
	ScenarioWriteHeavy   = "write-heavy"
	ScenarioReadHeavy    = "read-heavy"
	ScenarioSchemaChaos  = "schema-chaos"
	ScenarioBadActors    = "bad-actors"
	ScenarioOverload     = "overload"
	ScenarioBulkVsOnline = "bulk-vs-online"
	ScenarioLongRunning  = "long-running"
)

Standard scenario names. They are the kebab-case keys the CLI and the integration tests use to select a scenario from DefaultRegistry.

Variables

View Source
var ErrSimConnClosed = errors.New("sim: SimConn is closed")

ErrSimConnClosed is returned by SimConn I/O after the connection is closed.

View Source
var ErrSimFault = errors.New("sim: injected disk fault")

ErrSimFault is the sentinel returned by a simulated file operation that the seed-driven fault injector chose to fail. Callers match it with errors.Is. It models a durability fault: data the caller believed it was flushing did not reach stable storage.

View Source
var ErrSimListenerClosed = errors.New("sim: SimListener is closed")

ErrSimListenerClosed is returned by SimListener.Accept and SimListener.Dial after the listener is closed, mirroring net.ErrClosed semantics for the bolt/server accept loop (which treats a closed listener as a clean shutdown).

Functions

func CheckCorruptImageRejected

func CheckCorruptImageRejected(ctx context.Context, seed uint64) error

CheckCorruptImageRejected verifies the FAIL-STOP guarantee: a durable image whose committed WAL prefix has been corrupted must be REJECTED by the reopen path, never silently opened onto. It writes a small workload, closes, corrupts the durable WAL bytes inside an already-committed frame, and asserts the reopen returns an error (the production recovery fail-stop contract, mirrored by OpenSimStore).

It returns nil when the corruption was correctly rejected, and a descriptive error when a corrupt image was silently accepted (a durability bug).

func DefaultVariantPair

func DefaultVariantPair() (EngineVariant, EngineVariant)

DefaultVariantPair returns the PRIMARY differential pair: the engine's default configuration versus the same engine with the disconnected-equi-join hash-join optimisation turned OFF. The engine documents DisableHashJoin as existing "for the differential test that proves both plans return an identical result multiset" — so the two MUST agree on every observable output. This is a real, in-process, equivalent-result toggle, not a contrived comparison.

func RangeSeekVariantPair

func RangeSeekVariantPair() (EngineVariant, EngineVariant)

RangeSeekVariantPair returns a second PRIMARY pair: the default configuration versus the same engine with the range-predicate B+tree index seek turned OFF. Like DisableHashJoin, DisableRangeIndexSeek exists for the differential proof that both plans return an identical result multiset.

func RecordTrace

func RecordTrace(ctx context.Context, cfg Config) (Trace, *SimReport, error)

RecordTrace runs a deterministic engine-API simulation under cfg and captures the full ordered op stream (plus any crash ticks) into a Trace, alongside the run's report (nil when the run passed). Recording adds no nondeterminism: it only observes the op stream the simulator already produces from the seed, so the recorded run behaves identically to an unrecorded one and the returned Trace replays (via ReplayTrace) to the same end-state.

cfg.OnOp and cfg.OnCrash are overridden by the recorder; any caller-supplied hooks are chained AFTER the recording hook so verbose tracing still works.

func ReplayInstructions

func ReplayInstructions(trace Trace) string

ReplayInstructions renders a human-readable, copy-pasteable description of a (possibly shrunk) trace: the seed it came from and the ordered op list, so a failure can be reproduced and inspected. It is included in the SimReport for a shrunk reproducer.

func RunSwarmWithMetricsOracle

func RunSwarmWithMetricsOracle(ctx context.Context, sw *Swarm, goroutineSlack int) (SwarmResult, MetricsOracleResult, error)

RunSwarmWithMetricsOracle runs a swarm bracketed by a metrics oracle and asserts the reliability bound: after the (concurrent) swarm spawns and joins every worker, the live goroutine count must return to its baseline (within slack). It is the swarm-level wiring of the metrics oracle. Because the metrics sink is global it must run serially with respect to other metrics-emitting work; it restores the no-op backend before returning.

The returned SwarmResult is the swarm's own aggregate; the MetricsOracleResult carries the goroutine-baseline verdict.

func SimEngineForServer

func SimEngineForServer() *cypher.Engine

SimEngineForServer builds a fresh in-memory directed-multigraph engine with a finite result-row cap, suitable for backing a SimServer. The multigraph model matches openCypher's additive-CREATE relationship semantics that the Bolt e2e path expects.

Types

type AbuseFamily

type AbuseFamily int

AbuseFamily identifies one class of Bolt wire-protocol violation the BoltAbuser emits. The set is fixed so a unit test can assert every family is reachable and exercised.

const (
	// AbuseBadHandshake sends an invalid version-handshake preamble (wrong magic).
	AbuseBadHandshake AbuseFamily = iota
	// AbuseNoCommonVersion offers only versions the server does not support, so
	// negotiation must fail (server responds 0.0 and closes).
	AbuseNoCommonVersion
	// AbuseTruncatedChunk sends a chunk header advertising more bytes than follow,
	// then closes — a partial/truncated message.
	AbuseTruncatedChunk
	// AbuseOversizedChunk sends a single well-framed message whose total size
	// exceeds the server's MaxMessageBytes cap (but is bounded by the harness).
	AbuseOversizedChunk
	// AbusePullBeforeRun sends PULL immediately after auth, with no preceding RUN
	// (wrong session state).
	AbusePullBeforeRun
	// AbuseRunBeforeLogon sends RUN before authenticating (wrong session state on
	// a deferred-auth version, or before HELLO on inline-auth).
	AbuseRunBeforeLogon
	// AbuseGarbageOpcode sends a correctly-framed message carrying an unknown
	// struct tag (a garbage opcode).
	AbuseGarbageOpcode
	// AbuseDuplicateHello sends two HELLOs back to back (a duplicate/interleaved
	// marker for an already-progressed session).
	AbuseDuplicateHello
)

Bolt abuse families.

func (AbuseFamily) String

func (f AbuseFamily) String() string

String renders an AbuseFamily for reports.

type AbuseOutcome

type AbuseOutcome struct {
	Family     AbuseFamily
	GotFailure bool   // server replied with a typed FAILURE
	GotClose   bool   // server closed the connection cleanly (or it became unreadable)
	FailureMsg string // populated when GotFailure
}

AbuseOutcome records how the server responded to one abuse attempt. Exactly one of GotFailure or GotClose is the expected acceptable result; a third outcome (a normal SUCCESS where a violation was sent, or a hang) is a defect the checker flags. The Family and the seed that chose it are retained so a finding is reproducible.

func (AbuseOutcome) Acceptable

func (o AbuseOutcome) Acceptable() bool

Acceptable reports whether the outcome is one the robustness contract allows: a typed FAILURE or a clean connection close. Anything else (no terminal response, or an unexpected SUCCESS) is a violation.

type Actor

type Actor interface {
	// Name returns a stable identifier for the actor (used in reports).
	Name() string
	// NextOp returns the next operation to execute, drawing all randomness from
	// seed and reading current contents from oracle.
	NextOp(seed *Seed, oracle *GraphOracle) Op
}

Actor produces operations for the workload. An actor is stateless beyond the arguments to Actor.NextOp; all randomness comes from the supplied Seed and all knowledge of current graph contents from the supplied GraphOracle, so the operation an actor emits is a pure function of (seed state, oracle state).

Concurrency contract

Actors are NOT safe for concurrent use; they are invoked from the single simulation goroutine.

type BoltAbuser

type BoltAbuser struct{}

BoltAbuser emits protocol-level wire abuse over a SimConn and classifies the server's response. Each abuse runs on its own fresh connection in LOCK-STEP (send the violation, then block reading the terminal response or observing the close), so a given seed reproduces the exact violation and the exact server reaction. The server must respond with a typed FAILURE or close the connection cleanly — never panic, never leak a goroutine, never corrupt state.

Concurrency contract

BoltAbuser is stateless and its BoltAbuser.Abuse method may be called from any goroutine, but each call drives one connection it owns end-to-end.

func (BoltAbuser) Abuse

func (a BoltAbuser) Abuse(srv *SimServer, family AbuseFamily) (AbuseOutcome, error)

Abuse opens a fresh connection to srv, emits the chosen abuse family over the wire, and returns the classified outcome. The connection is always closed before return, so no goroutine or handle leaks regardless of how the server reacted. An error is returned only for a harness-level failure (e.g. the listener is closed), never for an expected server FAILURE/close.

func (BoltAbuser) Name

func (BoltAbuser) Name() string

Name returns the abuser's identifier.

func (BoltAbuser) PickFamily

func (BoltAbuser) PickFamily(seed *Seed) AbuseFamily

PickFamily chooses an abuse family from the seed. It draws exactly one int so the workload draw stream is stable.

type BoundedChurnWriter

type BoundedChurnWriter struct{}

BoundedChurnWriter is an honest writer whose create/delete bias is steered by the current modelled node count so the graph stays BOUNDED near [churnHighWater] over a very long run: below the high-water mark it favours creates and links; at or above it, it favours deletes. It reuses HonestWriter's well-formed statement builders, so every op it emits is a statement the engine accepts. It is the long-running scenario's writer.

Concurrency contract

BoundedChurnWriter is NOT safe for concurrent use; it is invoked from the single simulation goroutine.

func (BoundedChurnWriter) Name

func (BoundedChurnWriter) Name() string

Name returns the actor's identifier.

func (BoundedChurnWriter) NextOp

func (BoundedChurnWriter) NextOp(seed *Seed, oracle *GraphOracle) Op

NextOp steers create-vs-delete by the current node count to keep the working set bounded. Below the high-water mark it creates (and occasionally links); at or above it, it deletes (and occasionally updates), so the modelled graph oscillates around [churnHighWater] indefinitely.

type CheckSelection

type CheckSelection struct {
	// IndexConsistency runs the full index-vs-base-data consistency check
	// ([CheckIndexConsistency]) at the end of the run (and, for the schema-chaos
	// scenario, after DDL churn). It is meaningful only for modes that exercise
	// indexes. The set of indexes to cross-check is [CheckSelection.IndexSpecs].
	IndexConsistency bool
	// IndexSpecs are the (Label, Property) indexes the consistency check walks.
	// They are declared by the scenario because the engine's index manager
	// exposes only opaque names, not (label, property) pairs.
	IndexSpecs []IndexSpec
}

CheckSelection chooses which extra invariant checks a scenario runs beyond the always-on per-tick parity check (InvariantChecker.Check). The zero value runs none of the extras.

type ConcurrentConfig

type ConcurrentConfig struct {
	// Seed controls WHAT each connection sends and WHEN its faults fire. Goroutine
	// interleaving is NOT seed-controlled (see the package note on the hybrid
	// determinism model): this mode is robustness/liveness/leak-checked, not
	// bit-reproducible.
	Seed uint64
	// Connections is the number of concurrent client connections (one goroutine
	// each). Values <= 0 are normalised to 1.
	Connections int
	// OpsPerConn is the number of operations each connection performs before it
	// closes. Values <= 0 are normalised to 1.
	OpsPerConn int
	// Mix selects the per-connection actor behaviour. When nil, an honest
	// read/write mix is used.
	Mix *ConcurrentMix
}

ConcurrentConfig parameterises a concurrent multi-connection run. Every field is bounded: the harness spawns exactly Connections goroutines, each performing at most OpsPerConn operations, so total work is Connections×OpsPerConn and the connection count and per-connection work are both explicit upper bounds (the reliability mandate's bounded-resources rule).

type ConcurrentMix

type ConcurrentMix struct {
	// WriterWeight, ReaderWeight, OverloadWeight are the relative weights for the
	// three honest-ish roles. They need not sum to 1.
	WriterWeight   float64
	ReaderWeight   float64
	OverloadWeight float64
}

ConcurrentMix is the per-connection actor selection for a concurrent run. Each connection draws one role from its own seed-derived sub-stream and plays it for the whole connection, so the population is a deterministic function of the master seed even though interleaving is not.

type ConcurrentResult

type ConcurrentResult struct {
	Seed             uint64
	Connections      int
	AckedCreates     int64 // nodes connections committed (eventual oracle)
	EngineNodeCount  int64 // engine's live node count at quiescence
	Panics           int64 // recovered panics across all connection goroutines (must be 0)
	TransportErrors  int64 // unexpected transport errors (must be 0 on a healthy run)
	BoundedRejects   int64 // typed bound errors (overload caps) — acceptable, not a fault
	BaselineRoutines int   // goroutine count captured before the run
	FinalRoutines    int   // goroutine count after teardown
}

ConcurrentResult summarises a concurrent run for assertions and reports. It is the eventual-consistency oracle at quiescence: AckedCreates is the number of node-creating operations connections acknowledged as committed, which must equal the engine's live node count once every goroutine has drained (no committed write lost, no phantom write gained).

func RunConcurrent

func RunConcurrent(ctx context.Context, srv *SimServer, cfg ConcurrentConfig) (ConcurrentResult, error)

RunConcurrent drives cfg.Connections concurrent client connections through the real Bolt server srv, one goroutine per connection, each performing cfg.OpsPerConn seed-derived operations, then waits for every goroutine to finish (quiescence) and reconciles the eventual-consistency oracle against the engine. It honours ctx cancellation: a cancelled context stops connections at their next op boundary and the harness still drains every goroutine before returning.

Determinism

Per the hybrid model, this mode is NOT bit-reproducible: the SEED fixes each connection's role and op sequence, but goroutine interleaving is real and non-deterministic. Correctness is guarded by the returned ConcurrentResult (no panic, no unexpected transport error, eventual oracle==engine) plus the caller's goleak check — not by replay.

Concurrency contract

RunConcurrent spawns exactly cfg.Connections goroutines, each with a defined lifecycle bounded by cfg.OpsPerConn and ctx; all are joined before return, so no goroutine outlives the call. Every goroutine recovers a panic (recording it in the result and terminating cleanly) so one connection's bug cannot crash the harness or mask a leak.

func (ConcurrentResult) Consistent

func (r ConcurrentResult) Consistent() bool

Consistent reports whether the eventual-consistency oracle holds: the engine's node count equals the acknowledged creates, with no panics and no unexpected transport errors. Bounded rejects (overload caps) are expected and do not break consistency because a rejected write is never acknowledged and so is never counted in AckedCreates.

type Config

type Config struct {
	// Seed is the master seed; the entire run is a pure function of it.
	Seed uint64
	// MaxTicks is the number of ticks (operations) the safety phase runs.
	MaxTicks int
	// Workload is the actor mix. When nil, [DefaultWorkload] is used.
	Workload *Workload
	// CheckEvery is the invariant-check cadence in ticks. Values <= 0 are
	// normalised to 1 (check every tick).
	CheckEvery int
	// OnOp, when non-nil, is called synchronously with each tick and the
	// operation about to run, before it is executed. It is an observation hook
	// (e.g. for verbose tracing); it must not mutate state or draw from any
	// randomness, or it would break reproducibility. It runs on the simulation
	// goroutine.
	OnOp func(tick int64, op Op)
	// Crash configures deterministic crash/recovery injection. The zero value
	// disables it (Enabled == false), which is the safe default: a run that does
	// not opt in drives a plain in-memory engine exactly as before, byte for
	// byte. When enabled, the simulator instead drives a real SimDisk-backed
	// persistence stack (WAL append+sync + recovery replay) so a scheduled crash
	// drops the live engine and the store is reopened from the durable image.
	Crash CrashConfig
	// OnCrash, when non-nil, is called synchronously after each crash+recovery
	// cycle with the crash tick and how many WAL ops recovery replayed. Like
	// OnOp it is an observation hook and must not mutate state or draw
	// randomness.
	OnCrash func(tick int64, replayedWALOps int)
}

Config parameterises a simulation run.

type CoverageBucket

type CoverageBucket struct {
	Dimension string
	Key       string
	Count     int
}

CoverageBucket is one reported bucket: its dimension, key, and hit count.

func (CoverageBucket) Exercised

func (b CoverageBucket) Exercised() bool

Exercised reports whether the bucket has been hit at least once.

type CoverageSummary

type CoverageSummary struct {
	// Buckets lists every tracked bucket (dimension-then-key sorted).
	Buckets []CoverageBucket
	// Exercised is the number of buckets with a non-zero count.
	Exercised int
	// Unexplored is the number of tracked buckets still at zero.
	Unexplored int
}

CoverageSummary is the tracker's report: every bucket across every dimension, in a deterministic order, plus the count of exercised vs unexplored buckets.

func (CoverageSummary) String

func (s CoverageSummary) String() string

String renders the coverage summary as a human-readable block (one line per dimension with its bucket counts), suitable for the CLI -coverage-report output. It ends without a trailing newline.

type CoverageTracker

type CoverageTracker struct {
	// contains filtered or unexported fields
}

CoverageTracker accumulates which coverage buckets a swarm has exercised and biases new-run scenario selection toward under-covered scenarios. It is fed one SwarmRun at a time (typically via the swarm's Observe hook) and is the completeness-critic that stops the swarm from re-testing the same happy path.

The tracker derives every signal from already-observable sim-side data — the scenario name, the run outcome, and (for a failing run) the report's failed op kind and violation classes. It adds NO production hook. Signals that would require instrumenting production code (which Cypher exec operators a query used, which crashpoint sites a run hit) are NOT observable from the test side and are reported by CoverageTracker.UnobservableSignals rather than faked.

Concurrency contract

CoverageTracker is safe for concurrent use: every method takes the internal mutex. Swarm workers feed and query it from many goroutines.

func NewCoverageTracker

func NewCoverageTracker(scenarios []string) *CoverageTracker

NewCoverageTracker builds a tracker that biases selection over the given scenario names (typically a registry's Registry.Names). The scenario set is the bounded universe Select chooses from; it is copied so the caller may mutate its slice afterwards. Every dimension starts at zero coverage.

func (*CoverageTracker) Record

func (ct *CoverageTracker) Record(run SwarmRun)

Record folds one completed run into the coverage tally. It tallies the scenario, the coarse outcome, and — for a failing run — the failed op kind and every violation class in the report. It is safe to call from many goroutines (the swarm's Observe hook runs under the aggregator lock, but Record takes its own lock so it is also safe to call directly).

func (*CoverageTracker) ScenarioCoverage

func (ct *CoverageTracker) ScenarioCoverage() map[string]int

ScenarioCoverage returns the per-scenario hit counts (a copy), for tests that assert the bias steered runs toward under-covered scenarios.

func (*CoverageTracker) Select

func (ct *CoverageTracker) Select(_ int, defaultScenario string) string

Select implements ScenarioSelector: it returns the scenario name the next run should execute, biased toward the least-covered scenario in the tracked universe. Ties are broken round-robin (by the Select call counter) so no tied scenario is starved, keeping the bias fair and deterministic for a given call order. When the tracker has no scenario universe it falls back to the default.

func (*CoverageTracker) Summary

func (ct *CoverageTracker) Summary() CoverageSummary

Summary returns a snapshot of the current coverage across every dimension, in a deterministic (dimension, key) order. Zero-count scenario buckets are included so the report distinguishes "tracked but never hit" from "unknown".

func (*CoverageTracker) UnobservableSignals

func (ct *CoverageTracker) UnobservableSignals() []string

UnobservableSignals reports the coverage signals the task brief names that CANNOT be observed from the test side without adding a production hook, so they are deliberately NOT tracked (the constitution forbids adding a hook to production code from the DST harness). It documents the boundary rather than faking the signal.

Returned, in order:

  • "cypher-exec-operators": which physical operators (Expand, NodeByLabelScan, hash join, index seek, …) a query's plan used. The engine does not export a per-run operator-usage counter through any public API; observing it would require instrumenting cypher/exec. The differential test (#1567) instead exercises operator EQUIVALENCE via the DisableHashJoin / DisableRangeIndexSeek toggles, which is the observable proxy.
  • "crashpoint-sites": which internal/crashpoint sites a run armed/hit. Crash points are a test-only injection seam with no public hit-counter; the crash-storm scenario exercises them but does not expose which fired. The metrics oracle (#1568) reads only already-exported metrics for the same reason.

type CrashConfig

type CrashConfig struct {
	// Enabled turns crash injection on. When false the schedule never fires and
	// never draws from the seed, so the workload stream is unperturbed.
	Enabled bool
	// CrashProb is the per-eligible-tick crash probability, clamped to [0,1].
	// A non-positive value falls back to [defaultCrashProb].
	CrashProb float64
	// StabilityWindow is the minimum tick gap enforced after a restart before
	// another crash may fire. A non-positive value falls back to
	// [defaultStabilityWindow].
	StabilityWindow int64
}

CrashConfig parameterises a CrashSchedule. The zero value disables crashes entirely (Enabled == false), which is the safe default: an existing run that does not opt in behaves exactly as before. When Enabled is true, non-positive fields fall back to their defaults.

type CrashSchedule

type CrashSchedule struct {
	// contains filtered or unexported fields
}

CrashSchedule decides, deterministically from the seed, at which ticks a crash (a SIGKILL-equivalent: drop the in-memory engine, keep only the durable SimDisk bytes) occurs. After a crash the simulator reopens the store from the durable image via real recovery; CrashSchedule then enforces a stability window during which no further crash is scheduled, so recovery is given time to settle and be re-validated before the next fault (mirroring TigerBeetle VOPR's crash probability plus replica_stability).

The decision is a pure function of (seed, tick, last-crash tick): given the same seed it produces the identical crash tick sequence on every run, which is what lets a failure be replayed bit-for-bit. CrashSchedule draws from its own sub-seed (derived from the master seed via [crashSeedMix]) so toggling crashes on or off never shifts the workload's op stream.

Concurrency contract

CrashSchedule is NOT safe for concurrent use; it is consulted from the single simulation goroutine and its draw order is load-bearing for reproducibility.

func NewCrashSchedule

func NewCrashSchedule(seed *Seed, cfg CrashConfig) *CrashSchedule

NewCrashSchedule builds a crash schedule driven by seed and parameterised by cfg. When cfg.Enabled is false the returned schedule is inert: [ShouldCrash] always returns false and consumes no draws, so a run that does not opt into crashes is byte-identical to one built before crash support existed.

The seed passed here must be the crash sub-seed (derived via [crashSeedMix]), never the master workload seed, so that enabling crashes does not shift the workload draw stream.

func (*CrashSchedule) Enabled

func (c *CrashSchedule) Enabled() bool

Enabled reports whether crash injection is active for this schedule.

func (*CrashSchedule) LastCrashTick

func (c *CrashSchedule) LastCrashTick() int64

LastCrashTick returns the tick of the most recent crash this schedule fired, or a negative sentinel before the first crash. It is exposed for reports and tests asserting on crash timing.

func (*CrashSchedule) ShouldCrash

func (c *CrashSchedule) ShouldCrash(tick int64) bool

ShouldCrash reports whether a crash should occur at the given tick. It returns false without drawing when crashes are disabled or when the tick is still inside the post-restart stability window (so the draw stream position depends only on the eligible ticks, keeping the crash sequence a pure function of the seed). On an eligible tick it draws exactly one Bool(crashProb); when that draw fires it records the tick as the most recent crash, opening a fresh stability window before the next eligible tick.

tick must be non-decreasing across calls (the simulator advances it monotonically); calling out of order would corrupt the stability-window bookkeeping.

type CrossReleaseDiffResult

type CrossReleaseDiffResult struct {
	// Tag is the prior release compared against.
	Tag string
	// Agreed reports whether no UNEXPECTED (non-benign) divergence occurred.
	Agreed bool
	// Divergences lists every op-level difference, each classified. A run can
	// agree overall while still carrying benign divergences here.
	Divergences []CrossReleaseDivergence
	// FinalCountsMatch reports whether the prior and current end-state counts
	// matched.
	FinalCountsMatch bool
	// PriorNodes/PriorEdges/CurrentNodes/CurrentEdges are the end-state counts.
	PriorNodes   int64
	PriorEdges   int64
	CurrentNodes int64
	CurrentEdges int64
}

CrossReleaseDiffResult is the outcome of a cross-release DIFFERENTIAL run: whether the prior and current releases agreed on every op (modulo benign, classified divergences) and the full list of classified divergences.

func RunCrossReleaseDifferential

func RunCrossReleaseDifferential(ctx context.Context, repoRoot, tag string, seed uint64, ops int) (CrossReleaseDiffResult, error)

RunCrossReleaseDifferential replays the SAME deterministic op stream against a prior release (over its store, via the helper) and against the CURRENT in-process engine, then diffs the observable per-op results and the end-state. Each per-op difference is CLASSIFIED: a legitimately plan-dependent result (e.g. an unordered LIMIT) is benign; any other difference is an unexpected divergence that fails the comparison.

repoRoot is the GoGraph working-tree root. A build/worktree failure is returned as an error (clean environment-precondition skip); a behavioural divergence is carried in the result.

func (CrossReleaseDiffResult) String

func (r CrossReleaseDiffResult) String() string

String renders the differential result.

type CrossReleaseDivergence

type CrossReleaseDivergence struct {
	// Index is the op index that diverged.
	Index int
	// Op is the diverging op.
	Op Op
	// PriorRows / CurrentRows are the two canonical row signatures.
	PriorRows   string
	CurrentRows string
	// Benign reports whether the divergence is an expected/benign class.
	Benign bool
	// Reason explains the classification.
	Reason string
}

CrossReleaseDivergence classifies one op's prior-vs-current observable difference. Benign divergences (a query whose result is legitimately plan-dependent, or a deliberately-fixed-bug behaviour) are recorded as classified, NOT flagged as failures; an unexpected difference is a regression.

type CrossReleaseUpgradeResult

type CrossReleaseUpgradeResult struct {
	// Tag is the prior release that wrote the image.
	Tag string
	// PriorLiveNodes / PriorLiveEdges are the prior release's LIVE engine counts
	// after the write phase (before any reopen).
	PriorLiveNodes int64
	PriorLiveEdges int64
	// PriorSelfNodes / PriorSelfEdges are the counts the prior release's OWN
	// recovery rebuilds from the image — the durable truth the current code must
	// reproduce.
	PriorSelfNodes int64
	PriorSelfEdges int64
	// RecoveredNodes / RecoveredEdges are the CURRENT code's counts after reopen.
	RecoveredNodes int64
	RecoveredEdges int64
	// ReplayedWALOps is how many WAL ops the current recovery replayed.
	ReplayedWALOps int
	// DataCompatError is set when the current code FAILED-STOP opening the prior
	// image (refused to recover it). This is the explicit, non-silent
	// data-compatibility signal: a clear error rather than a silent mis-recovery.
	DataCompatError error
	// CountMismatch is set when the current code OPENED the image but recovered
	// different node/edge counts than the prior release's own recovery — a genuine
	// current-code data-compatibility regression.
	CountMismatch string
	// PriorWALFidelityGap is true when the prior release's OWN recovery already
	// diverges from its live counts (its WAL does not round-trip in its own
	// release). This is a PRIOR-release defect, recorded but NOT charged to the
	// current code; the current/prior-self contract can still hold.
	PriorWALFidelityGap bool
}

CrossReleaseUpgradeResult summarises a cross-release UPGRADE run: a PRIOR release wrote a durable store image, then BOTH the prior release (via its own recovery) and the CURRENT code reopened it. The cross-version data-compatibility contract is that the current code recovers the prior image IDENTICALLY to the prior release's own recovery of it — so a prior-release WAL that does not round-trip in its own release (a pre-existing prior defect) is surfaced as [PriorWALFidelityGap] rather than blamed on the current code.

func RunCrossReleaseUpgrade

func RunCrossReleaseUpgrade(ctx context.Context, repoRoot, tag string, seed uint64, ops int) (CrossReleaseUpgradeResult, error)

RunCrossReleaseUpgrade performs a true CROSS-VERSION upgrade test: it builds a prior-release helper from tag, has the prior release WRITE a durable store image (running a deterministic op stream derived from seed), then reopens that SAME image with BOTH the prior release's own recovery and the CURRENT recovery code, and asserts the current code recovers the image IDENTICALLY to the prior release.

This is the genuine guard for the data-compatibility regression class the project hit (v0.2.0 -> v0.3.x adjlist recovery panic): if the current code cannot faithfully rebuild a prior release's image it must fail-stop with a clear error (carried in CrossReleaseUpgradeResult.DataCompatError) or recover a different graph than the prior release did (CountMismatch) — never silently lose or fabricate data. A prior-release WAL that does not even round-trip in its own release is flagged (PriorWALFidelityGap) but not charged to the current code, because the current code's job is to read the prior image faithfully, not to retroactively fix a prior release's persistence bug.

repoRoot is the GoGraph working-tree root. A build/worktree failure is returned as an error for the caller to treat as a clean environment-precondition skip; an honest data-compatibility fault is carried in the result, not the error.

func (*CrossReleaseUpgradeResult) Parity

func (r *CrossReleaseUpgradeResult) Parity() bool

Parity reports whether the current code reopened the prior image faithfully: no fail-stop and the current recovery matched the prior release's own recovery. A prior-release WAL fidelity gap does not, by itself, fail parity.

func (CrossReleaseUpgradeResult) String

func (r CrossReleaseUpgradeResult) String() string

String renders the result for a test failure message.

type DiffResult

type DiffResult struct {
	// Agreed reports whether the two variants produced identical observable
	// output for every op and an identical end-state.
	Agreed bool
	// DivergedAt is the 0-based op index of the first divergence (-1 when the
	// variants agreed).
	DivergedAt int
	// DivergedOp is the op at the first divergence (zero value when agreed).
	DivergedOp Op
	// SignatureA / SignatureB are the diverging observable signatures of variant
	// A and B at DivergedAt (empty when agreed).
	SignatureA string
	SignatureB string
	// VariantA / VariantB are the variant names, for the report.
	VariantA string
	VariantB string
	// Reason is a human-readable description of the divergence (empty when
	// agreed): an end-state mismatch or a per-op result mismatch.
	Reason string
}

DiffResult is the outcome of a differential run: whether the two variants agreed, and on a divergence the first op index, the op, and the two (canonicalised) observable signatures that differed.

func DifferentialTrace

func DifferentialTrace(ctx context.Context, trace Trace, a, b *EngineVariant) (DiffResult, error)

DifferentialTrace replays the SAME recorded Trace against two engine variants and compares their observable outputs op-by-op, reporting the FIRST divergence. The observable output of an op is its canonicalised result-row multiset (for reads) plus the running engine node/edge counts (for writes), and the comparison also asserts the two variants reach an identical end-state.

Because the engine guarantees the default and toggled plans are result-equivalent (DisableHashJoin / DisableRangeIndexSeek exist precisely for this proof), a clean trace must replay to identical output on both.

It spawns no goroutines and is a pure function of trace + the two variants.

func DifferentialTraceInjectB

func DifferentialTraceInjectB(ctx context.Context, trace Trace, a, b *EngineVariant, injectAt int) (DiffResult, error)

DifferentialTraceInjectB replays the trace against both variants but injects a deterministic lost-write fault into variant B at op index injectAt (a write op). It exists for the test that proves the differential CATCHES a behavioural divergence: variant B drops one write, so its end-state diverges from variant A and the first comparison after the drop fails. injectAt < 0 injects nothing (equivalent to DifferentialTrace).

func (DiffResult) String

func (r DiffResult) String() string

String renders a differential result. On a divergence it names the variants, the first diverging op, and the two signatures so the regression is actionable.

type EdgeState

type EdgeState struct {
	SrcID, DstID uint64
	Label        string
	Properties   map[string]any
}

EdgeState is the oracle's record of a single directed edge between two oracle node ids, carrying its relationship label and properties.

type Engine

type Engine interface {
	// Run executes a Cypher query with string-keyed parameters and returns a
	// Result the caller must Close.
	Run(ctx context.Context, query string, params map[string]any) (Result, error)
	// NodeCount returns the number of live nodes in the engine.
	NodeCount() (int64, error)
	// EdgeCount returns the number of live edges in the engine.
	EdgeCount() (int64, error)
}

Engine is the minimal surface the checker drives. The simulator supplies a thin adapter over the real cypher.Engine (see EngineAdapter).

Concurrency contract

Implementations need only be safe for single-goroutine use; the simulator never calls them concurrently.

type EngineAdapter

type EngineAdapter struct {
	// contains filtered or unexported fields
}

EngineAdapter wraps the real github.com/FlavioCFOliveira/GoGraph/cypher.Engine so it satisfies the simulator's minimal Engine interface. It converts the simulator's string-keyed parameter maps into the engine's map[string]expr.Value and projects the engine's rich *cypher.Result onto the checker's narrow Result view.

Concurrency contract

EngineAdapter is NOT safe for concurrent use; the simulator drives it from a single goroutine.

func NewEngineAdapter

func NewEngineAdapter(eng *cypher.Engine) *EngineAdapter

NewEngineAdapter wraps eng. eng must be non-nil.

func (*EngineAdapter) EdgeCount

func (a *EngineAdapter) EdgeCount() (int64, error)

EdgeCount returns the live edge count by running a whole-graph relationship count query through the real engine.

func (*EngineAdapter) NodeCount

func (a *EngineAdapter) NodeCount() (int64, error)

NodeCount returns the live node count by running a whole-graph count query through the real engine, so it exercises the same execution path the workload uses.

func (*EngineAdapter) Run

func (a *EngineAdapter) Run(ctx context.Context, query string, params map[string]any) (Result, error)

Run converts params and executes a read-only query, returning a Result over the engine's result. The returned Result must be closed by the caller. It routes through the engine's read path (cypher.Engine.Run); use EngineAdapter.RunWrite for statements that mutate the graph.

func (*EngineAdapter) RunWrite

func (a *EngineAdapter) RunWrite(ctx context.Context, query string, params map[string]any) (Result, error)

RunWrite converts params and executes a mutating query through the engine's autocommit write path (cypher.Engine.RunInTx), which the engine requires for CREATE / MERGE / SET / DELETE statements. The returned Result must be closed by the caller.

type EngineVariant

type EngineVariant struct {
	// Name is a short label for the variant, used in divergence reports.
	Name string
	// Options configures the engine. The zero value selects the engine's
	// defaults (NewEngineWithOptions fills them in).
	Options cypher.EngineOptions
}

EngineVariant names and builds one side of a differential comparison: a fresh engine over a fresh directed simple graph, configured by Options. Two variants that the engine guarantees are result-equivalent (e.g. the default planner vs the same planner with a physical optimisation disabled) MUST produce identical observable output on the same trace; any divergence is a regression.

The build is a factory so each differential run gets an isolated engine — the two variants never share graph state.

type ExecMode

type ExecMode int

ExecMode selects which harness a Scenario drives. The deterministic modes are bit-reproducible from a seed and are the only modes trace recording, replay, and shrinking apply to; the concurrent and liveness modes use real goroutines whose interleaving is not seed-controlled and are convergence/leak-guarded rather than bit-replayable (see the package note on the hybrid determinism model).

const (
	// ModeDeterministic is the single-goroutine, tick-driven engine-API safety
	// loop ([Simulator.Run]). It is fully bit-reproducible from a seed and is the
	// mode trace recording, scripted replay, and shrinking operate on.
	ModeDeterministic ExecMode = iota
	// ModeConcurrent drives N real client goroutines over the Bolt wire
	// ([RunConcurrent]). Interleaving is non-deterministic; correctness is the
	// eventual-consistency oracle plus goleak/no-panic.
	ModeConcurrent
	// ModeLiveness drives the two-phase safety->liveness flow ([RunLiveness]),
	// asserting convergence within a bounded budget plus a deadlock watchdog.
	ModeLiveness
	// ModeBulkVsOnline drives a concurrent bulk store-load alongside
	// transactional online writes (see [runBulkVsOnline]).
	ModeBulkVsOnline
)

Execution modes.

func (ExecMode) Reproducible

func (m ExecMode) Reproducible() bool

Reproducible reports whether a scenario in this mode is bit-reproducible from its seed and therefore eligible for trace recording, scripted replay, and shrinking. Only ModeDeterministic qualifies.

func (ExecMode) String

func (m ExecMode) String() string

String renders an ExecMode for reports and the catalogue listing.

type GraphOracle

type GraphOracle struct {
	// contains filtered or unexported fields
}

GraphOracle is a correct-by-construction shadow model of what the graph must contain after a sequence of Phase-1 workload operations. It is deliberately minimal: it models only the five templates the workload emits and treats the Person name property as a logical key (the workload binds names uniquely and MERGE de-duplicates on it), which is what makes its predictions obviously correct without re-implementing the engine.

Concurrency contract

GraphOracle is NOT safe for concurrent use; it is mutated and read from the single simulation goroutine.

func NewGraphOracle

func NewGraphOracle() *GraphOracle

NewGraphOracle returns an empty oracle. Node ids start at 1 so zero can mean "no node".

func (*GraphOracle) ApplyCreate

func (o *GraphOracle) ApplyCreate(cypher string, params map[string]any) OracleResult

ApplyCreate models the CREATE templates: a bare Person create ([tmplCreatePerson]) or a KNOWS edge between two existing Person nodes ([tmplCreateKnows]). It mutates the oracle to reflect the predicted committed state and returns the prediction.

func (*GraphOracle) ApplyDelete

func (o *GraphOracle) ApplyDelete(cypher string, params map[string]any) OracleResult

ApplyDelete models [tmplDetachDelete]: DETACH DELETE removes the Person matched by name together with every incident edge. A miss is a committed zero-effect result.

func (*GraphOracle) ApplyMalformed

func (o *GraphOracle) ApplyMalformed(cypher string, params map[string]any) OracleResult

ApplyMalformed models an intentionally ill-formed operation (OpMalformed from MalformedSender): the engine is expected to reject it with a typed error and apply no mutation, so the oracle records it as an expected-error no-op and changes no modelled state. Recording it keeps the operation history complete for replay/shrinking.

func (*GraphOracle) ApplyMatch

func (o *GraphOracle) ApplyMatch(cypher string, params map[string]any) OracleResult

ApplyMatch models read-only and SET templates. Pure reads ([RETURN]/aggregate queries) never change state and commit trivially; the SET template ([tmplSetAge]) updates the matched node's age in place.

func (*GraphOracle) ApplyMerge

func (o *GraphOracle) ApplyMerge(cypher string, params map[string]any) OracleResult

ApplyMerge models [tmplMergePerson]: MERGE by name creates the Person only when absent (setting created=true on the new one) and is a no-op otherwise.

func (*GraphOracle) EdgeCount

func (o *GraphOracle) EdgeCount() int

EdgeCount returns the number of edges the oracle currently models.

func (*GraphOracle) HasEdge

func (o *GraphOracle) HasEdge(src, dst uint64, label string) bool

HasEdge reports whether the oracle models a directed edge of the given label between src and dst.

func (*GraphOracle) HasNode

func (o *GraphOracle) HasNode(id uint64) bool

HasNode reports whether the oracle models a node with the given id.

func (*GraphOracle) NodeCount

func (o *GraphOracle) NodeCount() int

NodeCount returns the number of nodes the oracle currently models.

func (*GraphOracle) NodeNames

func (o *GraphOracle) NodeNames() []string

NodeNames returns the Person names currently modelled, in ascending sorted order. The deterministic order is load-bearing: actors index into this slice with seed-derived integers, so a non-deterministic (map-range) order would make the op stream depend on Go's randomised map iteration and break reproducibility. The returned slice is freshly allocated and owned by the caller.

func (*GraphOracle) Ops

func (o *GraphOracle) Ops() []OracleOp

Ops returns the recorded operation history (for replay and Phase-4 shrinking). The returned slice aliases the oracle's backing store and must not be mutated.

func (*GraphOracle) String

func (o *GraphOracle) String() string

String renders a compact summary of the oracle state for inclusion in a failure report.

type HelperOpResult

type HelperOpResult struct {
	Committed bool
	Rows      string
}

HelperOpResult is the prior release's observable outcome for one op: whether it committed and a canonical, order-independent signature of its result rows.

type HelperRunResult

type HelperRunResult struct {
	Ops   []HelperOpResult
	Nodes int64
	Edges int64
}

HelperRunResult is the full outcome of driving an op stream through the prior release: the per-op results in order, plus the prior engine's final counts.

type HonestReader

type HonestReader struct{}

HonestReader emits valid read-only operations: projections, a relationship join, a filtered aggregate, and a bounded variable-length path query. It never mutates the graph, so its operations always commit with no effect on the oracle state.

func (HonestReader) Name

func (HonestReader) Name() string

Name returns the reader's identifier.

func (HonestReader) NextOp

func (HonestReader) NextOp(seed *Seed, _ *GraphOracle) Op

NextOp picks one read template at random and binds its parameters from the seed.

type HonestWriter

type HonestWriter struct{}

HonestWriter emits valid mutating operations: it creates Person nodes, links existing ones with KNOWS edges, updates ages, merges by name, and detaches and deletes. Every edge, SET, and DELETE references a node the oracle already knows about, so the writer never emits a statement that the engine would reject on well-formedness grounds.

func (HonestWriter) Name

func (HonestWriter) Name() string

Name returns the writer's identifier.

func (HonestWriter) NextOp

func (w HonestWriter) NextOp(seed *Seed, oracle *GraphOracle) Op

NextOp chooses a mutating operation. When the oracle is empty it can only create (there is nothing to reference yet); otherwise it picks among create, link, update, merge, and delete with fixed seed-driven weights.

type IndexSpec

type IndexSpec struct {
	// Label is the node label the index is declared on (e.g. "Person").
	Label string
	// Property is the property key the index covers (e.g. "name").
	Property string
}

IndexSpec declares one secondary index the simulator created during a run, by the (Label, Property) it covers. The index-consistency checker cross-checks each declared spec against the engine's base data. Specs are declared by the scenario (the simulator does not introspect (label, property) from the engine index manager, which exposes only opaque names), so the registry of specs is the authoritative set the checker walks.

type InvariantChecker

type InvariantChecker struct {
	// contains filtered or unexported fields
}

InvariantChecker compares the engine against the oracle after operations and accumulates any Violation it finds. It samples a bounded, seed-driven subset of oracle state per call so its cost stays bounded on large graphs.

Concurrency contract

InvariantChecker is NOT safe for concurrent use; it is driven from the single simulation goroutine.

func NewInvariantChecker

func NewInvariantChecker(seed *Seed) *InvariantChecker

NewInvariantChecker returns a checker whose sampling draws from seed.

func (*InvariantChecker) Check

func (c *InvariantChecker) Check(tick int64, oracle *GraphOracle, engine Engine) []Violation

Check verifies the engine against the oracle at the given tick and returns any newly-found violations (also accumulated internally). It performs:

  • node- and edge-count parity (oracle vs engine);
  • sampled oracle-node existence in the engine (no missing nodes);
  • sampled oracle-edge existence in the engine (no ghost or missing edges).

Each check that fails appends a typed Violation; a clean pass returns nil.

func (*InvariantChecker) CheckDurability

func (c *InvariantChecker) CheckDurability(tick int64, oracle *GraphOracle, engine Engine) []Violation

CheckDurability verifies ACID Durability at a crash-recovery boundary: every operation the engine ACKed as committed before the crash (which the oracle models exactly, because [Simulator.applyToOracle] advances the oracle only on a committed write) must be present in the recovered engine, and nothing that was never committed may have leaked in as partial state. Unlike InvariantChecker.Check it scans the FULL oracle node and edge set, not a bounded sample, because a single dropped committed op is a durability violation that sampling could miss.

It performs:

  • exact node- and edge-count parity (a recovered count below the oracle's means a committed op was lost — a Durability breach; a count above means uncommitted state leaked in — an Atomicity breach at the crash boundary);
  • full-scan oracle-node presence (every committed node survived recovery);
  • full-scan oracle-edge presence (every committed edge survived recovery).

Count mismatches are tagged ViolationACIDDurability; a missing node or edge is tagged ViolationACIDDurability (the committed datum did not survive). Each failing check appends a typed Violation; a clean pass returns nil.

func (*InvariantChecker) ChecksRun

func (c *InvariantChecker) ChecksRun() int

ChecksRun reports how many times InvariantChecker.Check has executed since construction. It exposes the realised invariant-check cadence so callers can confirm, for a given CheckEvery, that the expected number of checks ran (including the simulator's terminal check).

func (*InvariantChecker) HasViolations

func (c *InvariantChecker) HasViolations() bool

HasViolations reports whether any violation has been recorded.

func (*InvariantChecker) Violations

func (c *InvariantChecker) Violations() []Violation

Violations returns all recorded violations. The returned slice aliases the checker's backing store and must not be mutated.

type LivenessConfig

type LivenessConfig struct {
	// Seed controls the honest convergence workload.
	Seed uint64
	// Connections / OpsPerConn bound the honest convergence workload (same
	// bounded-resource discipline as the concurrent harness).
	Connections int
	OpsPerConn  int
	// ConvergeBudget is the maximum simulated time the harness waits for the
	// pending() predicate to reach null. Exceeding it is a liveness failure
	// (the system did not converge). Measured on the injected clock.
	ConvergeBudget time.Duration
	// PollStep is the simulated interval between pending() samples. Values <= 0
	// default to ConvergeBudget/100 (at least 1ms).
	PollStep time.Duration
	// NoProgressGrace is the number of consecutive non-improving samples the
	// watchdog tolerates before declaring a resonance (no progress despite
	// pending work). Values <= 0 default to 8.
	NoProgressGrace int
}

LivenessConfig parameterises the liveness phase. It runs AFTER the safety phase, with all fault injection healed/disabled: only honest actors run, and the harness asserts the system CONVERGES (drains to quiescence) within a bounded tick budget. A watchdog catches the resonance class — pending work that never makes progress (deadlock/livelock).

type LivenessOutcome

type LivenessOutcome struct {
	Seed         uint64
	Converged    bool
	Resonance    bool
	Ticks        int
	FinalPending PendingState
}

LivenessOutcome is the result of the liveness phase. Converged is true when the system reached quiescence within the budget. When false, FinalPending and Resonance describe why: Resonance == true means the watchdog detected no-progress-despite-pending (deadlock/livelock); Resonance == false means the budget simply elapsed while still making progress (under-provisioned budget).

func RunLiveness

func RunLiveness(ctx context.Context, srv *SimServer, clk clock.Clock, cfg LivenessConfig) (LivenessOutcome, error)

RunLiveness runs the LIVENESS phase against srv: it executes a bounded honest convergence workload (no faults) and then polls the pending() predicate on the injected clock until the system is quiescent or the budget elapses, with a watchdog for the resonance (no-progress) class.

The honest workload is run to completion first (every connection goroutine is joined), so at the polling stage the only residual pending work is structural (an oracle divergence the engine never reconciled, or a leaked goroutine). A healthy system is quiescent on the first poll; a system with a real liveness bug (a stuck writer, a leaked stream goroutine, a permanent oracle divergence) never reaches quiescence and the budget/watchdog fires.

Determinism

The convergence workload follows the concurrent (non-bit-reproducible) model, but the polling and watchdog are driven by the injected clock.Clock, so the liveness decision (converged vs budget-exceeded vs resonance) is deterministic under a clock.Fake for a given pending() trajectory.

func (LivenessOutcome) Report

func (o LivenessOutcome) Report() string

Report renders a VOPR-style liveness failure report with the seed and the pending-work dump, mirroring the safety phase's SimReport.String. It returns the empty string for a converged outcome.

type MalformedSender

type MalformedSender struct{}

MalformedSender is a bad actor: it emits intentionally ill-formed operations — invalid Cypher syntax, missing parameters, wrong parameter types, type-mismatched predicates, and oversized-but-bounded inputs — to assert that the engine rejects each with a typed error WITHOUT panicking, corrupting state, or applying any partial mutation. Every operation it emits is modelled by the oracle as a no-op (OpMalformed), so a clean run sees the engine error and the modelled state stay in lock-step (unchanged) after each one.

Concurrency contract

MalformedSender is NOT safe for concurrent use; it is invoked from the single simulation goroutine.

func (MalformedSender) Name

func (MalformedSender) Name() string

Name returns the actor's identifier.

func (MalformedSender) NextOp

func (m MalformedSender) NextOp(seed *Seed, _ *GraphOracle) Op

NextOp returns one malformed operation, chosen by a single seed draw across the malformed families. Each family is constructed to be rejected by the engine for a distinct reason, so the workload exercises several rejection paths (parser, parameter binding, type checking, input caps) rather than one.

type MetricsOracle

type MetricsOracle struct {
	// contains filtered or unexported fields
}

MetricsOracle reads the engine's exported metrics and the goroutine count around a run and certifies that the observed deltas match an oracle's accounting (committed writes, observed errors) and the reliability bounds (goroutine baseline restored). It installs a test-side recording metrics.Backend; because that backend is the global metrics sink, an oracle must be used SERIALLY (the caller must not run two concurrent oracles or any other metrics-emitting work in parallel) — NewMetricsOracle documents this.

Concurrency contract

A MetricsOracle is NOT safe for concurrent use and must be the only metrics consumer active for the duration of its MetricsOracle.Snapshot bracket.

func NewMetricsOracle

func NewMetricsOracle() *MetricsOracle

NewMetricsOracle installs a recording backend as the global metrics sink and returns an oracle over it. The caller MUST call MetricsOracle.Restore when done (typically deferred) to put the previous backend back. Because the metrics sink is global, the oracle and the work it brackets must run serially — install it, run the workload, snapshot, restore, all on one goroutine with no concurrent metrics-emitting work.

func (*MetricsOracle) Check

func (o *MetricsOracle) Check(before, after MetricsSnapshot, expectedWrites, expectedWriteErrors uint64, goroutineSlack int) MetricsOracleResult

Check certifies a before/after pair against the oracle's accounting and the reliability bounds, returning the verdict. expectedWrites is how many write-path statements the workload executed; expectedWriteErrors is how many of those the engine should have rejected (the oracle's count of expected failures). goroutineSlack is the tolerance on the goroutine delta — a healthy run returns to its baseline, but a small positive slack absorbs the runtime's own bookkeeping goroutines that a single -count run may leave parked.

func (*MetricsOracle) CheckGoroutineBaseline

func (o *MetricsOracle) CheckGoroutineBaseline(before, after MetricsSnapshot, goroutineSlack int) MetricsOracleResult

CheckGoroutineBaseline certifies ONLY the reliability bound — that the live goroutine count returned to its baseline (within slack) — between the before and after snapshots. It is the metrics-oracle check that applies to the CONCURRENT swarm: per-run write/error counts cannot be attributed to one run when many workers share the global metrics sink, but a goroutine leak across the whole swarm IS observable and is the bound that matters there. A clean swarm spawns its workers and joins them all, so the count must return to baseline.

func (*MetricsOracle) Restore

func (o *MetricsOracle) Restore()

Restore reinstalls the no-op default metrics backend. It is safe to call more than once.

func (*MetricsOracle) Snapshot

func (o *MetricsOracle) Snapshot() MetricsSnapshot

Snapshot reads the current exported-metric values and the live goroutine count into a MetricsSnapshot.

type MetricsOracleResult

type MetricsOracleResult struct {
	// Before / After are the snapshots bracketing the run.
	Before MetricsSnapshot
	After  MetricsSnapshot
	// ExpectedWrites / ExpectedWriteErrors are the oracle's accounting of how
	// many write statements ran and how many the engine should have rejected.
	ExpectedWrites      uint64
	ExpectedWriteErrors uint64
	// Discrepancies lists every mismatch found (empty when the metrics are
	// consistent with the oracle and the reliability bounds hold).
	Discrepancies []string
}

MetricsOracleResult is the verdict of a metrics-oracle check: the before/after snapshots, the accounting the oracle expected, and any discrepancy.

func RunWithMetricsOracle

func RunWithMetricsOracle(ctx context.Context, seed uint64, ops int, wlFactory func(*Seed) *Workload) (MetricsOracleResult, error)

RunWithMetricsOracle drives a deterministic write workload for the seed against a fresh in-memory engine while a MetricsOracle brackets it, and returns the verdict. It is the wired metrics-oracle check used by the swarm and the integration tests: after the run, the engine's exported RunInTx observation count and error counter must match the oracle's own count of write statements and rejections, and the goroutine count must return to its baseline.

It must run SERIALLY (it installs the global metrics backend); the caller must not run concurrent metrics-emitting work. It restores the no-op backend before returning.

func (*MetricsOracleResult) Consistent

func (r *MetricsOracleResult) Consistent() bool

Consistent reports whether the metrics matched the oracle and the reliability bounds (no discrepancy).

func (*MetricsOracleResult) String

func (r *MetricsOracleResult) String() string

String renders the result: the deltas and every discrepancy.

type MetricsRunStats

type MetricsRunStats struct {
	// Writes is the number of write-path statements issued.
	Writes uint64
	// WriteErrors is the number of write statements the engine rejected (the
	// per-op execute reported not-committed because RunWrite returned an error).
	WriteErrors uint64
}

MetricsRunStats is the oracle-side accounting of a metrics-bracketed run: how many write statements were issued and how many the engine rejected, derived from the run's own outcomes (the per-op committed flag). It is the ground truth the metric deltas are checked against.

type MetricsSnapshot

type MetricsSnapshot struct {
	// RunInTxCount / RunCount are the per-invocation latency-observation counts
	// for the write and read paths.
	RunInTxCount uint64
	RunCount     uint64
	// RunInTxErrors / RunErrors are the engine error counters.
	RunInTxErrors uint64
	RunErrors     uint64
	// Goroutines is the live goroutine count at the snapshot instant.
	Goroutines int
}

MetricsSnapshot is an immutable read of the engine's exported metrics plus the goroutine count at one instant. The oracle takes one before and one after a run and asserts the deltas match the oracle's accounting and the reliability bounds.

type NodeState

type NodeState struct {
	ID         uint64
	Labels     []string
	Properties map[string]any
}

NodeState is the oracle's record of a single node: its synthetic oracle id, labels, and properties. It mirrors what the engine must hold, not how the engine stores it.

type Op

type Op struct {
	Cypher string
	Params map[string]any
	Kind   OpKind
}

Op is a single Cypher operation an actor emits: the query text, its bound parameters (string-keyed, value kinds limited to those toExprParams supports), and its kind.

func GenerateCrossReleaseOps

func GenerateCrossReleaseOps(seed uint64, n int) ([]Op, error)

GenerateCrossReleaseOps produces a deterministic write-biased op stream from seed for the cross-release harness. It is the SAME workload the in-process upgrade harness drives (so the two are directly comparable), captured as a flat slice the harness can serialise to the prior-release helper AND replay in-process. Params are normalised through a JSON round-trip so the prior helper (which receives them as JSON) and the current side bind byte-identical parameter values.

type OpKind

type OpKind string

OpKind classifies an operation so the simulator can route it to the engine's read or write path and the oracle to the matching Apply method.

const (
	OpCreate OpKind = "OpCreate"
	OpMatch  OpKind = "OpMatch"
	OpMerge  OpKind = "OpMerge"
	OpDelete OpKind = "OpDelete"
	OpUpdate OpKind = "OpUpdate"
	// OpMalformed is an intentionally ill-formed operation emitted by
	// [MalformedSender]. The engine must reject it with a typed error without
	// panicking, corrupting state, or applying any partial mutation; the oracle
	// models it as a no-op (it never changes modelled state).
	OpMalformed OpKind = "OpMalformed"
)

Operation kinds.

func (OpKind) IsWrite

func (k OpKind) IsWrite() bool

IsWrite reports whether an operation of this kind mutates the graph and must therefore run through the engine's write (RunInTx) path. Malformed operations are routed through the write path too: it is the stricter, atomicity-bearing path, so proving a malformed statement is rejected there (with a full rollback and no partial application) is the stronger guarantee. A malformed read-shaped statement run through the write path still simply errors.

type OracleOp

type OracleOp struct {
	Tick     int64
	Cypher   string
	Params   map[string]any
	Expected OracleResult
}

OracleOp is one entry in the oracle's operation history: the tick at which it ran, the Cypher and parameters issued, and the predicted result. The history is retained so a future phase can shrink a failing trace to a minimal reproducer.

type OracleResult

type OracleResult struct {
	Committed    bool
	NodesCreated int
	EdgesCreated int
	ErrorMsg     string
}

OracleResult is the oracle's prediction for one operation: whether it commits, how many nodes and edges it creates, and (when it predicts a failure) the reason. The simulator records it for comparison with the engine outcome and for replay/shrinking.

type OracleSnapshot

type OracleSnapshot struct {
	NodeCount int
	EdgeCount int
	OpCount   int
}

OracleSnapshot is an immutable summary of the oracle state at the moment a simulation failed, captured for the report. It deliberately holds only aggregate counts and the operation history length, not the full node/edge maps, so a report stays compact; the seed plus the failing tick are enough to replay the full state.

type OverloadActor

type OverloadActor struct{}

OverloadActor issues legitimately heavy work over the real Bolt wire and classifies the engine's response. It asserts the engine enforces its declared bounds (a typed FAILURE or a bounded, fully-streamed success) and degrades gracefully — never OOM, panic, deadlock, or drop an acknowledged write.

Concurrency contract

OverloadActor is stateless; each OverloadActor.Run call drives one connection it owns. It is safe to call from many goroutines (the concurrent harness does), each with its own connection.

func (OverloadActor) Name

func (OverloadActor) Name() string

Name returns the actor's identifier.

func (OverloadActor) PickFamily

func (OverloadActor) PickFamily(seed *Seed) OverloadFamily

PickFamily chooses an overload family from the seed (one int draw).

func (OverloadActor) Run

Run executes one heavy operation of the given family over c and returns the classified outcome. The connection must already be Connected. It never returns an error for an expected engine bound (that is a BoundedError outcome); it returns an error only for a harness/transport failure.

type OverloadFamily

type OverloadFamily int

OverloadFamily identifies one class of legitimately-heavy operation the OverloadActor issues. Each is well-formed Cypher that pushes a resource dimension (transaction size, list size, traversal breadth/depth, result-set size) toward or past the engine's declared bound.

const (
	// OverloadHugeUnwind unwinds a very large literal range, producing many rows.
	OverloadHugeUnwind OverloadFamily = iota
	// OverloadLargeCreateTx creates many nodes in a single autocommit transaction,
	// pushing the per-transaction op count toward DefaultMaxTxnOps.
	OverloadLargeCreateTx
	// OverloadLargeResultSet matches a Cartesian product to materialise a large
	// result set, exercising the engine's MaxResultRows cap.
	OverloadLargeResultSet
	// OverloadDeepVLE runs a deep/wide variable-length expansion over the seeded
	// graph, exercising traversal bounds.
	OverloadDeepVLE
)

Overload families.

func (OverloadFamily) String

func (f OverloadFamily) String() string

String renders an OverloadFamily for reports.

type OverloadOutcome

type OverloadOutcome struct {
	Family       OverloadFamily
	Succeeded    bool   // the op completed and drained cleanly
	BoundedError bool   // the engine refused it with a typed FAILURE (a declared bound)
	Rows         int    // rows actually streamed back (always bounded)
	FailureMsg   string // populated when BoundedError
}

OverloadOutcome records the result of one heavy operation. Exactly one of Succeeded or BoundedError is the acceptable result: the engine either served the work within its limits or refused it with a typed limit/bound error. A hang, panic, OOM, or dropped acknowledged write is a violation (a hang is caught by the caller's deadline; the others by goleak/no-panic and the durability re-read).

func (OverloadOutcome) Acceptable

func (o OverloadOutcome) Acceptable() bool

Acceptable reports whether the outcome honours the graceful-degradation contract: success within limits OR a typed bound error.

type PendingState

type PendingState struct {
	InFlightOps     int   // operations not yet acknowledged (0 at quiescence)
	UngatedStreams  int   // result streams opened but not drained (0 at quiescence)
	OracleDivergent bool  // expected engine node count != observed engine node count
	ExpectedNodes   int64 // for the divergence report (baseline + acked creates)
	EngineNodeCount int64
}

PendingState is the liveness predicate's snapshot of outstanding work. The system is QUIESCENT when every field is at its converged value: no in-flight operations, no ungated (undrained) streams, and the oracle equal to the engine. Goroutine-leak detection is delegated to goleak in test teardown (process-global goroutine counts are too noisy to gate convergence on), so it is deliberately NOT a pending() term.

func (PendingState) Magnitude

func (p PendingState) Magnitude() int

Magnitude is a scalar measure of outstanding work the watchdog tracks for progress: a strictly decreasing magnitude is progress, a flat non-zero magnitude across the grace window is resonance.

func (PendingState) Pending

func (p PendingState) Pending() bool

Pending reports whether any work is still outstanding. False means quiescent.

func (PendingState) String

func (p PendingState) String() string

String renders a PendingState for the liveness report's pending-work dump.

type PriorReleaseHelper

type PriorReleaseHelper struct {
	// Tag is the git tag the helper was built from (e.g. "v0.3.0").
	Tag string
	// BinPath is the absolute path of the built helper binary.
	BinPath string
	// contains filtered or unexported fields
}

PriorReleaseHelper is a built prior-release helper binary plus the worktree it was compiled in. Close removes both, deterministically. It is the cross-release equivalent of [subproc]: instead of re-execing the current test binary, it spawns a binary built from a PRIOR git tag's source so the harness can observe genuine cross-version behaviour.

Concurrency contract

A PriorReleaseHelper is not safe for concurrent use across its own methods, but PriorReleaseHelper.WriteImage is a pure spawn-and-wait and may be called from one goroutine at a time. Close is idempotent.

func BuildPriorReleaseHelper

func BuildPriorReleaseHelper(ctx context.Context, repoRoot, tag string) (*PriorReleaseHelper, error)

BuildPriorReleaseHelper checks out tag into a temporary git worktree, copies the current helper source ([xreleaseHelperMainRel]) into it, and builds the helper binary against that tag's packages. The returned helper's Close removes the worktree and the temporary build root.

repoRoot must be the absolute path of the GoGraph repository working tree (the directory holding .git). The build runs `go build` inside the worktree so the binary links the tag's store/txn/wal/cypher code.

An error from this function is an ENVIRONMENT-PRECONDITION failure (the tag is not present, git worktree is unavailable, or the tag's tree does not build with the current toolchain): callers gate on it as a clean skip, exactly like an optional external tool being absent, NOT as a test failure.

func (*PriorReleaseHelper) Close

func (h *PriorReleaseHelper) Close() error

Close removes the worktree and temporary build artefacts. It is idempotent.

func (*PriorReleaseHelper) SelfRecoverCounts

func (h *PriorReleaseHelper) SelfRecoverCounts(ctx context.Context, dir string) (nodes, edges int64, err error)

SelfRecoverCounts reopens a dir this helper previously wrote using the PRIOR release's OWN recovery and returns the node/edge counts it recovers. It is the durable truth of what the prior release wrote, as the prior release itself reads it back — the reference the current code's recovery must reproduce. It discriminates a prior-release WAL that does not round-trip in its own release (a prior defect) from one the current code mis-reads (a current regression): the cross-version contract is current-recovery == prior-self-recovery.

func (*PriorReleaseHelper) WriteImage

func (h *PriorReleaseHelper) WriteImage(ctx context.Context, dir string, ops []Op) (HelperRunResult, error)

WriteImage drives ops through the prior-release helper, which opens a WAL-backed store under dir, runs each op, and closes (flush+fsync) so dir holds a durable store image written ENTIRELY by the prior release. It returns the prior release's per-op results and final counts.

dir must be an existing, empty directory the current process owns; after this returns, the current code can reopen dir via recovery.Open to perform the cross-version upgrade check.

type Registry

type Registry struct {
	// contains filtered or unexported fields
}

Registry maps scenario names to scenarios and lists them in a stable order. It holds no global mutable state: a Registry is built explicitly with NewRegistry (or DefaultRegistry for the standard catalogue) and is read-only after construction.

Concurrency contract

A Registry is immutable after NewRegistry returns and safe for concurrent reads.

func DefaultRegistry

func DefaultRegistry() (*Registry, error)

DefaultRegistry builds the standard Phase-4 scenario catalogue. It holds no global mutable state — every call returns a freshly-built Registry — so two callers never share scenario state. The returned registry lists every scenario named by the Scenario* constants.

The tick/connection budgets here are the SHORT-layer defaults: small enough that the catalogue runs well under the per-package 60s short-test ceiling. The long-running scenario carries a larger budget and is exercised only under the soak layer (see the integration tests).

func NewRegistry

func NewRegistry(scenarios ...Scenario) (*Registry, error)

NewRegistry builds a registry from the given scenarios. It returns an error if two scenarios share a name (a programmer error) so the catalogue cannot silently shadow one scenario with another.

func (*Registry) Len

func (r *Registry) Len() int

Len returns the number of registered scenarios.

func (*Registry) Lookup

func (r *Registry) Lookup(name string) (Scenario, bool)

Lookup returns the scenario registered under name and whether it was found.

func (*Registry) Names

func (r *Registry) Names() []string

Names returns the registered scenario names in sorted order. The returned slice is freshly allocated and owned by the caller.

func (*Registry) Scenarios

func (r *Registry) Scenarios() []Scenario

Scenarios returns every registered scenario in sorted-name order. The returned slice is freshly allocated and owned by the caller.

type Result

type Result interface {
	// Next advances to the next row and reports whether one is available.
	Next() bool
	// ScalarInt returns the integer value of the first column of the current
	// row. It is only valid after a successful Next.
	ScalarInt() (int64, bool)
	// IntAt returns the integer value of column i of the current row. It is only
	// valid after a successful Next.
	IntAt(i int) (int64, bool)
	// StringAt returns the string value of column i of the current row. It is
	// only valid after a successful Next.
	StringAt(i int) (string, bool)
	// RowCount reports how many rows the result has produced so far via Next.
	RowCount() int
	// Err returns any error accumulated during iteration.
	Err() error
	// Close releases the result.
	Close() error
}

Result is the minimal row-iterator the checker needs from a query. It is a thin projection of the engine's real result type, exposing only forward iteration and a single scalar read, which is all the count and existence probes require.

Concurrency contract

A Result is single-use and not safe for concurrent use; drive it from one goroutine and Close it when done.

type Scenario

type Scenario struct {
	// Name is the stable catalogue key (kebab-case).
	Name string
	// Description is a one-line human summary of what the scenario stresses,
	// printed by the catalogue listing.
	Description string
	// Mode selects the harness (see [ExecMode]).
	Mode ExecMode
	// DefaultSeed is the seed used when a caller does not supply one.
	DefaultSeed uint64

	// MaxTicks bounds the deterministic safety loop (ModeDeterministic).
	MaxTicks int
	// CheckEvery is the invariant-check cadence in ticks (ModeDeterministic). A
	// value <= 0 checks every tick. A long run sets it higher so the per-tick
	// full-graph parity probes do not dominate a millions-of-ops workload (the
	// scenario's value there is heap/goroutine stability, not per-tick parity).
	CheckEvery int
	// Workload is the deterministic-mode actor mix factory. When nil,
	// [DefaultWorkload] is used. It is a factory (not a built Workload) so each
	// run gets a fresh, seed-parameterised mix.
	Workload func(*Seed) *Workload
	// Crash configures deterministic crash/recovery injection (ModeDeterministic).
	Crash CrashConfig
	// Checks selects the extra invariant checks.
	Checks CheckSelection

	// Connections / OpsPerConn bound the concurrent and liveness modes.
	Connections int
	OpsPerConn  int
	// Mix is the per-connection role mix for the concurrent/liveness modes. When
	// nil, the harness default is used.
	Mix *ConcurrentMix
	// ConvergeBudget bounds the liveness convergence phase (ModeLiveness).
	ConvergeBudget time.Duration
	// contains filtered or unexported fields
}

Scenario is a named, self-contained simulation configuration: a default seed, a workload mix, a fault/crash schedule, a tick/time budget, the execution mode, and which extra checks to run. A scenario is a pure config — it carries no mutable run state — so the same (scenario, seed) always describes the same run. The registry maps names to scenarios; Scenario.Run executes one.

Concurrency contract

A Scenario value is immutable after construction and safe to read from many goroutines. Scenario.Run itself drives a single run; the concurrent modes it dispatches to spawn and join their own goroutines internally.

func (*Scenario) DeterministicConfig

func (sc *Scenario) DeterministicConfig(seed uint64) Config

DeterministicConfig builds the Config for a deterministic-mode run from the scenario plus a resolved seed. It is exported-internal so trace recording and shrinking can build the identical config.

func (*Scenario) Run

func (sc *Scenario) Run(ctx context.Context, seed uint64) (*SimReport, error)

Run executes the scenario once with the given seed and returns a report (nil means the scenario passed) or an error for a harness/transport failure that is not itself an invariant violation. It dispatches on Scenario.Mode; a scenario with a custom run override delegates to it.

For the deterministic mode the run is fully reproducible from seed and the returned report (on failure) carries enough to replay and shrink. For the concurrent and liveness modes the run is convergence/leak-guarded and the report, when non-nil, describes the inconsistency found at quiescence.

type ScenarioSelector

type ScenarioSelector interface {
	// Select returns the scenario name for the run identified by runIndex. The
	// default scenario is supplied so a selector can fall back to it.
	Select(runIndex int, defaultScenario string) string
}

ScenarioSelector chooses the scenario name a given swarm run should execute. The coverage tracker implements it to steer runs toward under-covered paths; a nil selector means "always run the configured scenario". Implementations must be safe for concurrent use.

type SchemaChangeFamily

type SchemaChangeFamily int

SchemaChangeFamily identifies one DDL operation the SchemaChanger issues.

const (
	// SchemaCreateIndex creates a RANGE index on (:Person).name (idempotent via
	// IF NOT EXISTS so it never errors on a re-create race).
	SchemaCreateIndex SchemaChangeFamily = iota
	// SchemaDropIndex drops the (:Person).name index (idempotent via IF EXISTS).
	SchemaDropIndex
	// SchemaCreateConstraint creates a UNIQUE constraint on (:Account).email
	// (idempotent via IF NOT EXISTS).
	SchemaCreateConstraint
	// SchemaDropConstraint drops the (:Account).email UNIQUE constraint
	// (idempotent via IF EXISTS).
	SchemaDropConstraint
)

Schema-change families.

func (SchemaChangeFamily) String

func (f SchemaChangeFamily) String() string

String renders a SchemaChangeFamily for reports.

type SchemaChangeOutcome

type SchemaChangeOutcome struct {
	Family     SchemaChangeFamily
	Succeeded  bool
	Failed     bool
	FailureMsg string
}

SchemaChangeOutcome records one DDL attempt. A DDL either succeeds or returns a typed FAILURE (e.g. a transient conflict under contention); both are acceptable. A panic, leak, or torn index/lost constraint is a violation, checked structurally after the run rather than per-attempt.

func RunSchemaChurn

func RunSchemaChurn(ctx context.Context, srv *SimServer, seed *Seed, rounds int) ([]SchemaChangeOutcome, error)

RunSchemaChurn drives a SchemaChanger through rounds DDL statements over a single connection, returning the per-round outcomes. It stops early on ctx cancellation. It is the unit the concurrent integration test runs alongside honest writers.

func (SchemaChangeOutcome) Acceptable

func (o SchemaChangeOutcome) Acceptable() bool

Acceptable reports whether the DDL completed cleanly (success or typed FAILURE) without wedging the connection.

type SchemaChanger

type SchemaChanger struct{}

SchemaChanger issues DDL (CREATE/DROP INDEX, CREATE/DROP CONSTRAINT) over the real Bolt wire, concurrently with honest writers and readers, to exercise index and constraint maintenance under races. Every statement is idempotent (IF [NOT] EXISTS) so a create/drop race never produces a spurious error; the invariants the harness asserts after the churn are that the index stays consistent with its base data and that a UNIQUE constraint, when present, stays enforced.

SchemaChanger runs in the CONCURRENT mode (one goroutine), so its DDL interleaves non-deterministically with concurrent writes; correctness is the structural invariants at quiescence, not bit-replay.

Concurrency contract

SchemaChanger is stateless; each SchemaChanger.Run call drives one connection it owns and may run on its own goroutine.

func (SchemaChanger) Name

func (SchemaChanger) Name() string

Name returns the actor's identifier.

func (SchemaChanger) PickFamily

func (SchemaChanger) PickFamily(seed *Seed) SchemaChangeFamily

PickFamily chooses a DDL family from the seed (one int draw).

func (SchemaChanger) Run

Run issues one DDL statement of the given family over c and returns the classified outcome. The connection must already be Connected.

type ScriptedResult

type ScriptedResult struct {
	Report    *SimReport
	NodeCount int64
	EdgeCount int64
	OracleN   int
	OracleE   int
}

ScriptedResult is the outcome of a scripted replay: the report (nil when the replay found no violation) and the engine/oracle end-state counts, so a caller can assert two runs reach the identical end-state.

func ReplayTrace

func ReplayTrace(ctx context.Context, trace Trace) (ScriptedResult, error)

ReplayTrace executes a recorded Trace against a FRESH plain in-memory engine, oracle, and checker — WITHOUT drawing from any seed — applying each op in order and checking invariants after every op exactly as the deterministic safety loop does. It is the foundation for both exact failure replay and shrinking: because the deterministic engine-API mode is a pure function of its op stream, replaying the stream reproduces the same end-state and re-triggers the same violation.

An op carrying an injected TraceFault is applied with that fault (e.g. FaultDropEngineWrite applies the write to the oracle but skips the engine, producing a deterministic divergence). A trace with no faults replays cleanly iff the original run did.

ReplayTrace spawns no goroutines and is a pure function of trace.

func (ScriptedResult) Violated

func (r ScriptedResult) Violated() bool

Violated reports whether the scripted replay detected a violation.

type Seed

type Seed struct {
	// contains filtered or unexported fields
}

Seed is the single source of randomness for an entire simulation. Every probabilistic decision — which actor runs, which operation it emits, the parameter values it binds, and whether the disk injects a fault — draws from one Seed, so the complete simulation is a pure function of the seed value.

Seed wraps a deterministic PCG generator (math/rand/v2.PCG) seeded from the seed value alone; it never consults the operating system, a global generator, or the wall clock. The original value is retained so it can be reported and replayed.

Concurrency contract

Seed is NOT safe for concurrent use. It backs the single-goroutine simulation loop and its draw order is load-bearing for reproducibility; concurrent draws would interleave non-deterministically and break replay.

func NewSeed

func NewSeed(val uint64) *Seed

NewSeed returns a Seed whose generator is initialised deterministically from val. The two PCG stream words are val and val^seedMix, so distinct seed values yield distinct generator states.

func (*Seed) Bool

func (s *Seed) Bool(p float64) bool

Bool returns true with probability p and false otherwise. p is clamped to [0.0, 1.0]: p <= 0 always returns false, p >= 1 always returns true. Each call consumes exactly one float64 draw, keeping the draw stream stable regardless of p.

func (*Seed) Float64

func (s *Seed) Float64() float64

Float64 returns a uniform float64 in [0.0, 1.0).

func (*Seed) IntN

func (s *Seed) IntN(n int) int

IntN returns a uniform integer in [0, n). It panics if n <= 0, mirroring the contract of math/rand/v2.Rand.IntN.

func (*Seed) Pick

func (s *Seed) Pick(items []string) string

Pick returns a uniformly-chosen element of items. It panics if items is empty, which signals a programmer error (a workload that offers no choices).

func (*Seed) Shuffle

func (s *Seed) Shuffle(items []string) []string

Shuffle returns a new slice holding the elements of items in a deterministically-shuffled order, using an in-place Fisher–Yates pass over the copy. The input slice is never mutated. The result is a function of the generator state alone, so the same seed produces the same permutation.

func (*Seed) Uint64N

func (s *Seed) Uint64N(n uint64) uint64

Uint64N returns a uniform unsigned integer in [0, n). It panics if n == 0, mirroring the contract of math/rand/v2.Rand.Uint64N.

func (*Seed) Value

func (s *Seed) Value() uint64

Value returns the seed value this Seed was constructed with. Printing it lets a failing run be replayed exactly.

type ShrinkConfig

type ShrinkConfig struct {
	// MaxIterations caps the scripted-replay attempts. A non-positive value uses
	// [defaultMaxShrinkIterations].
	MaxIterations int
}

ShrinkConfig parameterises trace shrinking. The zero value is valid and uses the defaults.

type ShrinkResult

type ShrinkResult struct {
	// Minimal is the reduced trace that still reproduces the target violation.
	Minimal Trace
	// OriginalLen / MinimalLen are the op counts before and after shrinking.
	OriginalLen int
	MinimalLen  int
	// Iterations is the number of scripted-replay attempts performed.
	Iterations int
	// Violation is a representative violation the minimal trace reproduces (the
	// first one found on the final replay), for the report.
	Violation Violation
}

ShrinkResult is the outcome of shrinking: the minimal trace found, the violation it still reproduces, and the work the shrinker did.

func ShrinkTrace

func ShrinkTrace(ctx context.Context, trace Trace, cfg ShrinkConfig) (ShrinkResult, error)

ShrinkTrace reduces a failing trace to a (near-)minimal subsequence that still reproduces the SAME violation, via delta-debugging (ddmin). It first confirms the full trace fails under scripted replay and captures the target violation signature, then repeatedly partitions the op sequence into n chunks and tries (a) removing each chunk and (b) keeping each chunk's complement, accepting the smallest candidate whose replay still reproduces the target signature and increasing the granularity otherwise — the classic Zeller–Hildebrandt ddmin.

Determinism: every step is a scripted ReplayTrace (no seed draws, no goroutines, no wall clock), so shrinking the same failing trace always yields the same minimal trace. Boundedness: the search is capped at cfg.MaxIterations replay attempts; the best reduction found by then is returned.

Cross-op dependencies are preserved implicitly: a candidate that drops an op the violation depends on (e.g. the lost-write CREATE itself, or a node a later edge needs) replays to a DIFFERENT signature (or to no violation), so ddmin rejects it and keeps the op. No explicit reference repair is required because the "still reproduces the same violation" oracle is exact.

It returns an error only when the input trace does NOT reproduce a violation under scripted replay (there is nothing to shrink), or when ctx is cancelled.

func (*ShrinkResult) Ratio

func (r *ShrinkResult) Ratio() float64

Ratio returns the reduction factor (original / minimal); 1 means no reduction. It is reported so a caller can assert an orders-of-magnitude shrink.

type SimConn

type SimConn struct {
	// contains filtered or unexported fields
}

SimConn is one end of an in-memory, bounded-buffer net.Conn pair. It carries the real Bolt wire bytes between the in-sim client harness and the genuine bolt/server, with no OS socket, so the server's actual handshake, framing, and message loop run unchanged.

A SimConn pair supports two usage modes that share one implementation:

  • LOCK-STEP single-connection mode: the client writes a complete request and then blocks reading the server's full terminal response. Because the bounded buffer is far larger than any single request/response and exactly one logical exchange is in flight, the byte stream is fully deterministic and a given seed replays identically. Used for protocol round-trips and the BoltAbuser so violations reproduce exactly.
  • CONCURRENT mode: one real goroutine drives each end. Interleaving across connections is non-deterministic, but each end's reads and writes are individually safe and the bounded buffer applies backpressure (a stalled reader parks the peer's writer), which is what the SlowConsumer and overload actors exercise.

Deadlines route through the injected clock.Clock; with clock.Real a SimConn behaves like an ordinary socket, and with a clock.Fake a deadline fires only when virtual time is advanced.

Concurrency contract

A single SimConn end is safe for use by one reader goroutine and one writer goroutine concurrently (the full-duplex net.Conn contract). It is NOT safe to share one end across multiple readers or multiple writers.

func NewSimConnPair

func NewSimConnPair(clk clock.Clock) (clientEnd, serverEnd *SimConn)

NewSimConnPair returns the two ends of a connected in-memory pipe. Bytes written to one end are read from the other. Both ends share the injected clock for deadline handling; pass clock.Real for ordinary timing or a clock.Fake for deterministic virtual deadlines. clk must be non-nil.

The conventional use is to hand the server end to the bolt/server (via SimListener) and drive the client end with the wire client harness.

func (*SimConn) Close

func (c *SimConn) Close() error

Close implements net.Conn.Close. It closes both directions of this end so a blocked peer read returns io.EOF and a blocked peer write returns ErrSimConnClosed. It is idempotent.

func (*SimConn) CloseWithError

func (c *SimConn) CloseWithError(err error) error

CloseWithError closes this end abruptly, delivering err to the peer's blocked reads and writes instead of a clean io.EOF. It models a connection reset (an abrupt client disconnect mid-stream) so the harness can assert the server neither panics nor leaks a goroutine on a hard close. It is idempotent.

func (*SimConn) LocalAddr

func (c *SimConn) LocalAddr() net.Addr

LocalAddr implements net.Conn.LocalAddr.

func (*SimConn) Read

func (c *SimConn) Read(p []byte) (int, error)

Read implements net.Conn.Read.

func (*SimConn) ReadBuffered

func (c *SimConn) ReadBuffered() int

ReadBuffered reports how many bytes the peer has written that this end has not yet read. It never exceeds [simConnBufferSize]; the bound holding under a stalled reader is the backpressure property the SlowConsumer asserts.

func (*SimConn) RemoteAddr

func (c *SimConn) RemoteAddr() net.Addr

RemoteAddr implements net.Conn.RemoteAddr.

func (*SimConn) SetDeadline

func (c *SimConn) SetDeadline(t time.Time) error

SetDeadline implements net.Conn.SetDeadline, setting both the read and write deadlines to t. A zero t clears the deadlines.

func (*SimConn) SetReadDeadline

func (c *SimConn) SetReadDeadline(t time.Time) error

SetReadDeadline implements net.Conn.SetReadDeadline.

func (*SimConn) SetWriteDeadline

func (c *SimConn) SetWriteDeadline(t time.Time) error

SetWriteDeadline implements net.Conn.SetWriteDeadline.

func (*SimConn) Write

func (c *SimConn) Write(p []byte) (int, error)

Write implements net.Conn.Write.

type SimDisk

type SimDisk struct {
	// contains filtered or unexported fields
}

SimDisk is an in-memory filesystem with seed-driven fault injection. It backs the durability layer of the simulation: files live entirely in memory, and a per-sector fault bitmap plus a per-Sync fault probability let the simulator reproduce torn writes and failed flushes deterministically.

SimDisk is built in Phase 1 but is not yet wired into the engine (that is Phase 2 work); it must compile, implement the WAL file interface, and be unit-tested standalone.

Concurrency contract

SimDisk's directory operations are guarded by an internal mutex so the file table cannot be corrupted, but the simulation drives it from a single goroutine and the fault decisions draw from the shared single-goroutine Seed; it must not be used concurrently.

The "Sim" prefix is part of the DST harness's deliberate naming scheme (SimDisk / SimFileHandle / SimReport), which reads clearly at call sites and matches the design specification; the apparent stutter is intentional.

func NewSimDisk

func NewSimDisk(seed *Seed, faultRate float64) *SimDisk

NewSimDisk returns an empty in-memory filesystem. faultRate is the probability (clamped to [0,1]) that any individual Sync fails with ErrSimFault and that a freshly written sector is marked faulted. seed drives every fault decision so the fault sequence is reproducible.

func (*SimDisk) Exists

func (d *SimDisk) Exists(path string) bool

Exists reports whether a file is present at path.

func (*SimDisk) MkdirAll

func (d *SimDisk) MkdirAll(_ string, _ fs.FileMode) error

MkdirAll is a no-op for the in-memory filesystem: there is no directory tree, paths are opaque keys. It exists to satisfy the filesystem surface the WAL and snapshot writers expect. perm is ignored.

func (*SimDisk) OpenFile

func (d *SimDisk) OpenFile(path string, flag int) (*SimFileHandle, error)

OpenFile opens (creating when os.O_CREATE is set) the file at path and returns a handle positioned per the flags: at end when os.O_APPEND is set, at zero otherwise. When os.O_TRUNC is set the file's contents are discarded. It returns an error wrapping fs.ErrNotExist when the file is absent and os.O_CREATE is not set.

func (*SimDisk) Remove

func (d *SimDisk) Remove(path string) error

Remove deletes the file at path. Removing an absent path is a no-op, matching the tolerant cleanup the snapshot writer relies on.

func (*SimDisk) Rename

func (d *SimDisk) Rename(oldPath, newPath string) error

Rename atomically moves the file at oldPath to newPath, replacing any existing destination. It returns an error wrapping fs.ErrNotExist when the source is absent.

func (*SimDisk) Snapshot

func (d *SimDisk) Snapshot() map[string][]byte

Snapshot returns an independent deep copy of every file's contents keyed by path. Mutating the returned maps or slices never affects the live filesystem, so a caller can capture disk state for comparison after a crash.

type SimFileHandle

type SimFileHandle struct {
	// contains filtered or unexported fields
}

SimFileHandle is an open handle onto a SimDisk file. It implements the WAL file interface (io.Reader, io.Writer, io.Seeker, Sync, Truncate, Close) so it can substitute for *os.File.

Concurrency contract

SimFileHandle is NOT safe for concurrent use; it is driven from the single simulation goroutine.

func (*SimFileHandle) Close

func (h *SimFileHandle) Close() error

Close releases the handle. It is idempotent: a second Close is a no-op.

func (*SimFileHandle) Read

func (h *SimFileHandle) Read(p []byte) (int, error)

Read copies up to len(p) bytes from the current position into p, advancing the position. It returns io.EOF when the position is at or past end of file.

func (*SimFileHandle) Seek

func (h *SimFileHandle) Seek(offset int64, whence int) (int64, error)

Seek repositions the handle per the standard io.Seeker whence values and returns the resulting absolute offset. A negative resulting offset is an error.

func (*SimFileHandle) Sync

func (h *SimFileHandle) Sync() error

Sync models flushing OS buffers to stable storage. With probability faultRate (drawn from the disk's seed) it fails with ErrSimFault, modelling a durability fault. Each call consumes exactly one draw from the seed so the fault sequence is reproducible.

func (*SimFileHandle) Truncate

func (h *SimFileHandle) Truncate(size int64) error

Truncate resizes the file to size bytes, zero-filling when growing and dropping fault marks for sectors that no longer exist.

func (*SimFileHandle) Write

func (h *SimFileHandle) Write(p []byte) (int, error)

Write copies p to the file at the current position, growing the file as needed, and advances the position. Any byte written into a sector that the fault injector has marked faulted is corrupted deterministically (a single byte in that sector is flipped), modelling a torn or mis-directed write.

type SimListener

type SimListener struct {
	// contains filtered or unexported fields
}

SimListener is an in-memory net.Listener that feeds SimConn server-ends to a real bolt/server running under github.com/FlavioCFOliveira/GoGraph/bolt/server.Server.Serve. Each SimListener.Dial creates a connected SimConn pair, queues the server-end for the server's Accept loop, and returns the client-end to the caller (the wire client harness). No OS socket is involved, so the server runs its genuine handshake and message loop over purely in-memory bytes.

Concurrency contract

SimListener is safe for concurrent use: Dial may be called from any number of goroutines (one per simulated connection) while the server calls Accept from its single accept goroutine. This is what lets the concurrent harness open N connections against one server.

func NewSimListener

func NewSimListener(clk clock.Clock) *SimListener

NewSimListener returns an in-memory listener whose connections route deadlines through clk. Hand it to Server.Serve; drive new connections with Dial. clk must be non-nil (clock.Real for ordinary timing, a clock.Fake for deterministic virtual deadlines).

func (*SimListener) Accept

func (l *SimListener) Accept() (net.Conn, error)

Accept implements net.Listener.Accept. It blocks until a connection is dialed or the listener is closed, returning the server-end of the next SimConn pair. After Close it returns ErrSimListenerClosed.

func (*SimListener) Addr

func (l *SimListener) Addr() net.Addr

Addr implements net.Listener.Addr.

func (*SimListener) Close

func (l *SimListener) Close() error

Close implements net.Listener.Close. It stops further Accept and Dial calls. In-flight connections already accepted by the server are unaffected (they live until the server or harness closes them). It is idempotent.

func (*SimListener) Dial

func (l *SimListener) Dial() (*SimConn, error)

Dial creates a new connected SimConn pair, queues the server-end for Accept, and returns the client-end. It blocks if the accept backlog is full (bounded by [defaultAcceptBacklog]) until the server accepts a queued connection or the listener is closed. After Close it returns ErrSimListenerClosed.

type SimReport

type SimReport struct {
	Seed        uint64
	FailedTick  int64
	FailedOp    Op
	Violations  []Violation
	OracleState OracleSnapshot
	// Shrunk, when non-nil, carries the minimal failing reproducer the shrinker
	// produced for this failure ([ShrinkTrace]). It is attached by the CLI replay
	// path after a deterministic failure is shrunk; a report from a live run
	// leaves it nil.
	Shrunk *ShrinkResult
}

SimReport is the result of a failed simulation: the seed that produced it, the tick and operation at which the first violation was detected, every violation found at that tick, and a snapshot of the oracle state. A nil *SimReport returned from Simulator.Run means the simulation passed.

The DST harness types share a SimXxx naming scheme by design (see SimDisk in disk.go).

func (*SimReport) String

func (r *SimReport) String() string

String renders a human-readable failure report. It always includes a "Reproduce with:" line carrying the seed so a failure can be replayed verbatim.

type SimServer

type SimServer struct {
	// contains filtered or unexported fields
}

SimServer runs a real github.com/FlavioCFOliveira/GoGraph/bolt/server.Server over an in-memory SimListener. It exists so the Phase-3 actors drive the GENUINE Bolt wire path — handshake, framing, message loop, streaming — with no OS socket and no reimplementation of the server. New client connections are obtained with SimServer.Dial.

The server is started with server.NoAuthHandler (development/testing mode): the DST harness asserts robustness and ACID under abuse, not credential handling, which has its own dedicated test battery in bolt/server. A finite result-row cap is configured on the engine so a single overload query cannot materialise an unbounded result set.

Concurrency contract

SimServer is safe for concurrent use: SimServer.Dial may be called from many goroutines (the concurrent harness opens one connection per goroutine) while the embedded server's accept loop runs. SimServer.Close is idempotent and drains the server before returning.

func NewSimServer

func NewSimServer(eng *cypher.Engine, clk clock.Clock) (*SimServer, error)

NewSimServer builds a SimServer over the given engine and starts it serving on an in-memory listener whose connection deadlines route through clk. The engine must be non-nil; callers typically pass an engine with a finite result-row cap (see SimEngineForServer). The returned server is already accepting; obtain connections with SimServer.Dial and tear it down with SimServer.Close.

func (*SimServer) Close

func (s *SimServer) Close() error

Close stops accepting new connections, cancels the serve context, and waits for the server to drain. It is idempotent and returns the server's exit error (nil on a clean shutdown).

func (*SimServer) Dial

func (s *SimServer) Dial() (*WireClient, error)

Dial opens a new client connection to the server over the in-memory listener, returning a WireClient ready to negotiate. The caller must Close the client when done. It returns an error only if the listener is closed.

func (*SimServer) DialConn

func (s *SimServer) DialConn() (*SimConn, error)

DialConn opens a new client connection and returns the raw SimConn, for callers (notably the BoltAbuser) that need to write malformed bytes the WireClient would never produce.

type SimStore

type SimStore struct {
	// contains filtered or unexported fields
}

SimStore is a real GoGraph persistence stack — a WAL-backed txn.Store and a cypher.Engine — whose durability layer is an in-memory SimDisk rather than the OS filesystem. It lets the deterministic simulation harness exercise the genuine WAL append+sync and recovery-replay code paths without touching real disk, so a crash (drop the in-memory engine, keep the SimDisk byte image) and a restart (reopen via real recovery) are fully reproducible from a seed.

The crash/restart boundary is the SimDisk: SimStore.Crash discards the live engine and store but the WAL bytes (and their injected fault state) persist in the SimDisk, and OpenSimStore reopens them through recovery.ReplayWAL — the same replay core that recovery.Open drives over an OS file.

Concurrency contract

SimStore is NOT safe for concurrent use; the simulator drives it from a single goroutine.

func OpenSimStore

func OpenSimStore(disk *SimDisk, cfg simStoreConfig) (*SimStore, error)

OpenSimStore opens (or reopens) a store whose WAL lives in disk under [simWALPath]. When the WAL is absent the store starts empty; when it holds bytes from a prior session, recovery.ReplayWAL rebuilds the graph from the committed WAL prefix before the writer is reopened for further appends.

Reopen-for-append truncates the WAL to the last durable frame boundary (recovery.ReplayResult.WALTailOffset) BEFORE the writer seeks to end (auditor finding F1): a crash between two fsyncs leaves a benign torn tail past the committed prefix, and appending after it would strand every new frame behind junk that every subsequent reader stops at. Truncating to the recovered offset makes the reopened WAL a clean append target.

A reopen that detects genuine corruption (recovery.ReplayResult.IsClean == false) is a hard fault: the function returns an error rather than appending onto the corruption (which would permanently embed it and drop every op past the bad frame), mirroring the production recovery.Open fail-stop contract.

func (*SimStore) Clean

func (s *SimStore) Clean() bool

Clean reports whether the most recent recovery completed without genuine on-disk corruption (a benign torn tail counts as clean).

func (*SimStore) Close

func (s *SimStore) Close() error

Close shuts the store down gracefully, flushing and fsyncing the WAL so every acknowledged commit is durable, then releasing the WAL writer. Use it for a clean teardown (end of a run); use SimStore.Crash to model a crash.

func (*SimStore) Crash

func (s *SimStore) Crash()

Crash models a SIGKILL: it discards the in-memory engine, store, and WAL writer WITHOUT a graceful close, so any buffered-but-unsynced frame is lost exactly as a real crash would lose it. The durable WAL byte image inside the SimDisk (and its fault state) survives untouched, ready for OpenSimStore to reopen and replay. The SimStore must not be used after Crash.

Crash deliberately does NOT call s.wlog.Close(): a clean Close would flush and fsync the buffer, which is the opposite of a crash. Dropping the references lets the GC reclaim them; the only durable state is the SimDisk image.

func (*SimStore) Engine

func (s *SimStore) Engine() *cypher.Engine

Engine returns the live cypher engine bound to the recovered graph and the WAL-backed store, for the simulator to drive queries through.

func (*SimStore) Graph

func (s *SimStore) Graph() *lpg.Graph[string, float64]

Graph returns the live recovered graph.

func (*SimStore) WALOps

func (s *SimStore) WALOps() int

WALOps reports how many WAL ops the most recent recovery replayed back into the graph on open (0 for a freshly-created store).

type Simulator

type Simulator struct {
	// contains filtered or unexported fields
}

Simulator drives the real cypher.Engine against a shadow GraphOracle under a deterministic, single-goroutine, tick-driven loop, verifying ACID and graph invariants after operations.

Concurrency contract

Simulator is NOT safe for concurrent use and spawns no goroutines. Its determinism guarantee depends on a single, totally-ordered stream of draws from one Seed; Simulator.Run must be called from one goroutine.

func New

func New(cfg Config) (*Simulator, error)

New builds a Simulator with a fresh in-memory engine, oracle, checker, clock, and (Phase-2-bound, currently unwired) SimDisk, all driven by cfg.Seed. It returns an error only for an invalid configuration.

func (*Simulator) Close

func (s *Simulator) Close() error

Close releases the simulator's durable resources. In crash mode it gracefully closes the live SimDisk-backed store (flushing and releasing the WAL writer) so no handle or goroutine leaks past the run; in the default in-memory mode it is a no-op. It is safe to call more than once.

func (*Simulator) CrashCount

func (s *Simulator) CrashCount() int

CrashCount returns how many crash+recovery cycles the run performed (always 0 when crashes are disabled).

func (*Simulator) Oracle

func (s *Simulator) Oracle() *GraphOracle

Oracle returns the simulator's shadow model, for tests that assert on the modelled state after a run.

func (*Simulator) ReplayedOps

func (s *Simulator) ReplayedOps() int

ReplayedOps returns the cumulative number of WAL ops recovery replayed across every crash cycle in the run.

func (*Simulator) Run

func (s *Simulator) Run(ctx context.Context) (*SimReport, error)

Run executes the safety-phase tick loop. Each tick advances the clock, selects an actor, asks it for an operation, runs that operation against the engine, applies it to the oracle, and (every CheckEvery ticks) verifies the invariants. On the first violation it returns a populated SimReport; on clean completion it returns (nil, nil). It honours ctx cancellation and deadlines, returning the ctx error if the run is interrupted.

The loop runs entirely on the calling goroutine and spawns none; engine operations are synchronous.

type SlowConsumer

type SlowConsumer struct {
	// contains filtered or unexported fields
}

SlowConsumer opens a large result stream and then pulls records very slowly (or stalls entirely), exercising the server's streaming backpressure. Because the SimConn write buffer is bounded ([simConnBufferSize]), a stalled consumer forces the server's record-write to BLOCK once the buffer fills rather than letting it buffer the whole result in memory — the bounded-resource property under a slow reader. When the connection is finally closed (or its read deadline, driven by the injected Clock, elapses) the server must tear the session down without leaking a goroutine.

SlowConsumer runs in the CONCURRENT mode: the slow pulls happen on a real goroutine while the server's writer goroutine is parked on backpressure. The SEED controls the stall timing; interleaving is real (per the hybrid model), so correctness here is backpressure + no-leak, not bit-replay.

Concurrency contract

A SlowConsumer drives one connection it owns; each SlowConsumer.Stall call is independent and may run on its own goroutine.

func NewSlowConsumer

func NewSlowConsumer(clk clock.Clock) *SlowConsumer

NewSlowConsumer returns a SlowConsumer whose stall timing is driven by clk.

func (*SlowConsumer) Name

func (*SlowConsumer) Name() string

Name returns the actor's identifier.

func (*SlowConsumer) Stall

func (s *SlowConsumer) Stall(ctx context.Context, srv *SimServer, stallFor time.Duration, onStalled func(*WireClient)) (SlowConsumerResult, error)

Stall opens a large result stream over the server and then deliberately stalls without pulling, holding the stream open for stallFor (measured on the injected clock). While stalled, the server's writer is parked on the bounded SimConn buffer (backpressure). It then closes the connection and returns the result. The connection is always closed before return, so no goroutine leaks.

onStalled, when non-nil, is invoked once while the consumer is stalled, with the client so a caller can inspect the bounded buffer (WireClient.ConnSimConn.ReadBuffered) and confirm the server did not buffer the whole result.

stallFor stalls are driven by the injected Clock so a Fake makes the stall deterministic; with clock.Real it is a real (short) sleep.

type SlowConsumerResult

type SlowConsumerResult struct {
	// RecordsPulled is how many RECORDs the slow consumer drained before it
	// stopped. It is always far less than the total result size when the consumer
	// stalls, which is the proof the server did not push the whole result eagerly.
	RecordsPulled int
	// ServerParked reports whether the server's write blocked on the bounded
	// SimConn buffer while the consumer stalled (backpressure observed) rather
	// than the whole result being buffered ahead of the consumer.
	ServerParked bool
	// ClosedCleanly reports whether the connection tore down without a transport
	// fault other than the expected close/EOF.
	ClosedCleanly bool
}

SlowConsumerResult summarises one slow-consumer run. It records whether the server kept its memory bounded while the consumer stalled (proved by the SimConn write-buffer never being allowed to grow past its bound — the server parks on its blocked record write rather than buffering the whole result) and whether the connection torn down without leaking a goroutine.

type Swarm

type Swarm struct {
	// contains filtered or unexported fields
}

Swarm runs many independent seeds of a scenario across a bounded worker pool, time-boxed by run count and/or wall-clock duration, and aggregates the outcomes. Each worker runs ONE seed at a time against its own freshly-built scenario harness; the only shared mutable state is the aggregator, guarded by a mutex. Per-run determinism is whatever the scenario's mode guarantees (a deterministic scenario reproduces bit-for-bit from its derived seed); only the across-run scheduling is concurrent.

Concurrency contract

A Swarm is built with NewSwarm and run once with Swarm.Run, which spawns exactly Workers goroutines and joins them all before returning — it leaks no goroutine. Swarm is not intended for reuse across Swarm.Run calls.

func NewSwarm

func NewSwarm(reg *Registry, cfg *SwarmConfig) (*Swarm, error)

NewSwarm builds a swarm over reg with cfg. It validates that the budget is well-formed (at least one of Runs or Duration is positive) and that the configured scenario resolves, returning an error otherwise so a misconfiguration fails fast rather than running an empty or unbounded swarm.

func (*Swarm) Run

func (s *Swarm) Run(ctx context.Context) (SwarmResult, error)

Run executes the swarm and returns its aggregated result. It spawns the resolved number of worker goroutines, each pulling run indices from a bounded dispatch channel, running the scenario for the derived seed, and pushing the outcome through the mutex-guarded aggregator. Scheduling stops when the run budget is exhausted, the duration budget elapses, or ctx is cancelled; in-flight runs always finish so no goroutine is abandoned. The returned error is non-nil only for a ctx cancellation that interrupted scheduling.

type SwarmConfig

type SwarmConfig struct {
	// MasterSeed seeds the deterministic derivation of every per-run seed, so
	// the whole swarm reproduces from this one value. Two swarm runs with the
	// same MasterSeed, Scenario, and Runs execute the identical set of seeds.
	MasterSeed uint64
	// Scenario is the name of the catalogue scenario each run executes. It must
	// resolve in the registry the swarm was built with.
	Scenario string
	// Workers is the worker-pool cap. Values <= 0 are normalised to
	// min(GOMAXPROCS, max(1, Runs)). The pool never spawns more than this many
	// run goroutines at once.
	Workers int
	// Runs is the seed-count budget: the swarm executes exactly this many runs
	// (each a distinct derived seed) unless Duration elapses first. When Runs <=
	// 0 the swarm is duration-bounded only and MUST carry a positive Duration.
	Runs int
	// Duration is the wall-clock budget. When > 0 the swarm stops scheduling new
	// runs once it elapses (in-flight runs finish). Zero means no time bound, in
	// which case Runs MUST be positive.
	Duration time.Duration
	// Clock is the time source the duration budget reads. When nil, the real
	// clock is used. The per-run simulations remain seed-driven and never read
	// this clock — it bounds only the across-run scheduling, which is concurrent
	// and not bit-reproducible by construction.
	Clock clock.Clock
	// Selector, when non-nil, is consulted before each run to choose the
	// scenario for that run (coverage-biased selection, Phase 5 #1565). When
	// nil, every run uses Scenario. The selector must be safe for concurrent use
	// because workers call it from many goroutines.
	Selector ScenarioSelector
	// Observe, when non-nil, is called once per completed run with its outcome.
	// It runs under the aggregator's lock (so it is serialised across workers)
	// and must not block; it is an observation hook for coverage feeding and
	// live reporting.
	Observe func(SwarmRun)
}

SwarmConfig parameterises a Swarm run: the master seed, the scenario to run, the worker cap, and the budget (by run count, wall-clock duration, or both — whichever bound is hit first ends the swarm).

type SwarmResult

type SwarmResult struct {
	// MasterSeed is the seed the schedule was derived from (for reproducing the
	// whole swarm).
	MasterSeed uint64
	// Runs is how many runs actually executed (<= the Runs budget; fewer when
	// the duration budget cut it short).
	Runs int
	// Passes is the number of runs that found no violation and no harness error.
	Passes int
	// Failures lists every failing run in ascending run-index order, so a report
	// is deterministic regardless of worker completion order.
	Failures []SwarmRun
	// Elapsed is the wall-clock time the swarm took.
	Elapsed time.Duration
	// Workers is the worker cap the swarm actually ran with.
	Workers int
}

SwarmResult aggregates a completed swarm: the totals, the wall-clock elapsed, and every failing run with its reproduce line. It is the value the CLI and the integration tests assert on.

func (SwarmResult) FailureCount

func (r SwarmResult) FailureCount() int

FailureCount returns the number of failing runs.

func (SwarmResult) Summary

func (r SwarmResult) Summary() string

Summary renders a one-block human-readable summary: totals, throughput, and every failing seed's reproduce line. It always ends without a trailing newline so the caller controls spacing.

func (SwarmResult) Throughput

func (r SwarmResult) Throughput() float64

Throughput returns runs per second over the elapsed wall-clock, or 0 when no time elapsed (avoids a divide-by-zero in a degenerate empty run).

type SwarmRun

type SwarmRun struct {
	// Index is the run's position in the deterministic schedule (0-based).
	Index int
	// Seed is the derived per-run seed.
	Seed uint64
	// Scenario is the scenario name this run executed.
	Scenario string
	// Report is the failure report when the run found an invariant violation,
	// else nil.
	Report *SimReport
	// Err is a harness error (setup/transport failure), else nil. A run with a
	// non-nil Err counts as a failure distinct from an invariant violation.
	Err error
}

SwarmRun is the outcome of one seed in a swarm: the seed it ran, the scenario name, whether it failed, the failure report (nil on pass), and any harness error (a transport/setup failure that is not itself an invariant violation).

func (SwarmRun) Failed

func (r SwarmRun) Failed() bool

Failed reports whether the run failed for any reason (an invariant violation or a harness error).

func (SwarmRun) ReproduceLine

func (r SwarmRun) ReproduceLine() string

ReproduceLine returns a copy-pasteable command that re-runs exactly this (scenario, seed) under the CLI, so a swarm failure is reproducible verbatim.

type Trace

type Trace struct {
	// Seed is the seed the recording was driven by (informational; a scripted
	// replay does NOT draw from it — it executes Ops directly).
	Seed uint64
	// Ops is the ordered operation stream.
	Ops []TracedOp
	// CrashTicks lists the ticks at which a crash+recovery cycle fired during
	// recording, in order. Scripted replay runs against a plain in-memory engine
	// and does not re-inject crashes (the violations the shrinker targets are
	// oracle/engine divergences reproducible without a crash); the list is
	// retained for the report and for completeness.
	CrashTicks []int64
}

Trace is the full recorded operation stream of a deterministic run, plus a note of which crash ticks fired. Only the deterministic engine-API mode produces a Trace: it is bit-reproducible, so replaying the same Trace against a fresh engine/oracle/checker reaches the identical end-state. Concurrent and liveness modes are not bit-replayable and are not recorded.

Concurrency contract

A Trace is a plain value; callers own their copies and do not share them across goroutines mid-mutation.

func (Trace) Len

func (t Trace) Len() int

Len returns the number of operations in the trace.

type TraceFault

type TraceFault string

TraceFault names a deterministic fault a scripted replay injects at a specific op, so a trace can carry a reproducible failure for replay verification and shrinking. Faults are a test-only mechanism of the scripted executor; a recording from a real run carries FaultNone on every op unless a fault is explicitly injected.

const (
	// FaultNone is the absence of an injected fault (a normal op).
	FaultNone TraceFault = ""
	// FaultDropEngineWrite makes the scripted executor APPLY a write to the oracle
	// but SKIP it on the engine, creating a deterministic oracle-vs-engine
	// divergence the per-op check detects. It models a lost-write bug and is the
	// canonical injected violation the shrinking demo reduces.
	FaultDropEngineWrite TraceFault = "drop-engine-write"
)

Trace faults.

type TracedOp

type TracedOp struct {
	// Tick is the simulated tick the op ran at during recording. On replay the
	// scripted executor re-derives ticks positionally, so this is informational
	// (it lets a report point at the original tick).
	Tick int64
	// Op is the Cypher operation that was issued.
	Op Op
	// Fault, when non-empty, is a marker injected for replay/shrinking: the
	// scripted executor applies the named fault deterministically when it reaches
	// this op. The empty string means no fault (a normal op). See [TraceFault].
	Fault TraceFault
}

TracedOp is one entry in a recorded Trace: the tick it ran at, the operation issued, and an optional injected fault marker. A trace is the ordered list of these, captured during a deterministic run, and is the unit a scripted replay executes and the shrinker reduces.

type UpgradeConfig

type UpgradeConfig struct {
	// Seed drives the deterministic write workload and the SimDisk sub-seed.
	Seed uint64
	// Ops is the number of write operations applied before the upgrade boundary.
	// Values <= 0 default to 400 — enough to populate nodes, edges, and a few
	// deletes so the parity check is meaningful.
	Ops int
	// Workload is the actor mix used for the write phase. When nil,
	// [WriteHeavyWorkload] is used so the durable image carries real structure.
	Workload func(*Seed) *Workload
	// IndexSpecs, when non-empty, are created before the write phase and
	// cross-checked for consistency after reopen, so the upgrade also guards
	// index durability across the boundary.
	IndexSpecs []IndexSpec
}

UpgradeConfig parameterises an upgrade simulation: the seed driving the write workload, how many write ops to apply before the simulated upgrade boundary, and the workload factory. The write phase is fully deterministic from Seed so a failure reproduces exactly.

type UpgradeResult

type UpgradeResult struct {
	// WrittenNodes / WrittenEdges are the oracle-modelled counts at close.
	WrittenNodes int
	WrittenEdges int
	// RecoveredNodes / RecoveredEdges are the engine counts after the reopen.
	RecoveredNodes int64
	RecoveredEdges int64
	// ReplayedWALOps is how many committed WAL ops recovery replayed on reopen.
	ReplayedWALOps int
	// Report is non-nil when the post-reopen parity/durability/index check found
	// a violation (data loss, ghost state, or index drift).
	Report *SimReport
}

UpgradeResult summarises an upgrade simulation: the durable counts written, the counts recovered after the reopen, and how many WAL ops the reopen's recovery replayed. A nil UpgradeResult.Report means full parity held.

func RunUpgrade

func RunUpgrade(ctx context.Context, cfg UpgradeConfig) (UpgradeResult, error)

RunUpgrade performs the PRIMARY upgrade simulation: it writes a deterministic workload through a real WAL-backed SimStore on a SimDisk image, closes the store gracefully (every ACKed commit durable), then reopens the SAME durable image through the real recovery path (recovery.ReplayWAL, via OpenSimStore) — the cross-version boundary — and runs the full oracle parity, durability, and (optional) index-consistency check against the recovered engine.

This guards the class of data-compatibility regressions the project has hit before (e.g. the v0.2.0->v0.3.x adjlist recovery panic): if the current recovery code cannot faithfully rebuild the graph from a durable image, the parity check reports the divergence rather than letting it pass silently.

The returned error is a harness failure (store open/close or a write-phase engine error that should not happen on an honest workload); an invariant divergence is carried in the result's Report, not the error.

func (UpgradeResult) Parity

func (r UpgradeResult) Parity() bool

Parity reports whether the upgrade reopened to full parity (no violation).

type Violation

type Violation struct {
	Kind    ViolationKind
	Message string
	Tick    int64
	Op      string
}

Violation is a single detected invariant breach, tagged with its kind, a human-readable message, the tick at which it was found, and the operation that immediately preceded it.

func CheckIndexConsistency

func CheckIndexConsistency(tick int64, _ *GraphOracle, engine indexConsistencyEngine, specs ...IndexSpec) []Violation

CheckIndexConsistency performs a THOROUGH (not sampled) index-vs-base-data consistency check for every declared index in specs. For each index on (Label, Property) it:

  • full-scans the base data via the engine (MATCH (n:Label) RETURN id(n), n.Property) and builds the authoritative value -> {node id} map directly from the nodes that carry the property;
  • for every distinct indexed value, runs the index-seek probe (MATCH (n:Label {Property:$v}) RETURN id(n)) — which the engine resolves through the index when one is present — and asserts the seek returns EXACTLY the node ids the full scan attributed to that value.

A value the seek over-reports (a node id the full scan does not carry) is a torn/orphaned index entry; a value the seek under-reports (a node id the full scan carries but the seek misses) is a stale/lost index entry. Either is a ViolationACIDConsistency (the index disagrees with the base data it indexes). The check is bounded but exhaustive over the CURRENT graph: it walks every node of each indexed label exactly once for the scan and issues one seek per distinct value.

The check probes the engine through the same execution path the workload uses, so it observes whatever the engine would serve a real query — which is precisely the property an index must preserve. It cross-checks the engine against itself (seek path vs scan path) rather than against the oracle, because an index covers DDL-created labels the minimal Phase-1 oracle does not model.

func (Violation) String

func (v Violation) String() string

String renders a Violation for a report.

type ViolationKind

type ViolationKind string

ViolationKind classifies an invariant breach. The ACID_* kinds map to the module's four transactional guarantees; GRAPH_INTEGRITY covers structural invariants (e.g. an edge whose endpoints are absent); ORACLE_DEVIATION covers any disagreement between the shadow model and the engine that is not more specifically classified.

const (
	ViolationACIDAtomicity   ViolationKind = "ACID_ATOMICITY"
	ViolationACIDConsistency ViolationKind = "ACID_CONSISTENCY"
	ViolationACIDIsolation   ViolationKind = "ACID_ISOLATION"
	ViolationACIDDurability  ViolationKind = "ACID_DURABILITY"
	ViolationGraphIntegrity  ViolationKind = "GRAPH_INTEGRITY"
	ViolationOracleDeviation ViolationKind = "ORACLE_DEVIATION"
)

Violation kinds.

type VirtualClock

type VirtualClock struct {
	// contains filtered or unexported fields
}

VirtualClock is the simulation's logical clock. It models the passage of time as a monotonically-increasing tick counter rather than reading the wall clock, so the simulation never observes real time and stays fully deterministic. One tick represents one unit of simulated time whose duration is fixed at construction (1 tick == 1ms by convention).

VirtualClock deliberately exposes no way to read time.Now: the entire sim package is free of wall-clock reads, which is what lets a given seed replay identically.

Concurrency contract

VirtualClock is NOT safe for concurrent use. It is advanced and read from the single simulation goroutine only.

func NewVirtualClock

func NewVirtualClock(tickSize time.Duration) *VirtualClock

NewVirtualClock returns a clock at tick zero whose every tick advances simulated time by tickSize. A non-positive tickSize is normalised to 1ms so VirtualClock.SimulatedTime always advances.

func (*VirtualClock) Now

func (c *VirtualClock) Now() int64

Now returns the current tick count (the number of ticks elapsed since construction).

func (*VirtualClock) SimulatedTime

func (c *VirtualClock) SimulatedTime() time.Duration

SimulatedTime returns the simulated elapsed time, computed as the tick count multiplied by the per-tick duration.

func (*VirtualClock) Tick

func (c *VirtualClock) Tick() int64

Tick advances the clock by one tick and returns the new tick count.

type WireClient

type WireClient struct {
	// contains filtered or unexported fields
}

WireClient speaks the REAL Bolt v5 wire protocol over a SimConn: the 20-byte version handshake, then chunked PackStream request/response messages encoded and decoded with the genuine github.com/FlavioCFOliveira/GoGraph/bolt/proto and github.com/FlavioCFOliveira/GoGraph/bolt/packstream codecs (it does NOT reimplement the wire format). It drives well-formed requests for the honest, overload, and slow-consumer actors and decodes RECORD/SUCCESS/FAILURE/IGNORED responses.

Lock-step determinism

In single-connection use the client writes one request and blocks reading the server's complete terminal response (SUCCESS or FAILURE, after any RECORDs). Because exactly one logical exchange is in flight and the SimConn buffer holds it whole, the byte stream — and therefore the decoded response — is a pure function of the request, so a given seed replays the op stream and the responses identically.

Concurrency contract

A WireClient is NOT safe for concurrent use; the Bolt protocol is itself single-flight per connection (one request, then its response). The concurrent harness gives each goroutine its own WireClient on its own SimConn.

func NewWireClient

func NewWireClient(conn *SimConn, clk clock.Clock) *WireClient

NewWireClient wraps conn with chunked reader/writer framing. clk is retained for deadline-bearing operations; conn and clk must be non-nil.

func (*WireClient) Begin

func (c *WireClient) Begin() (any, error)

Begin sends BEGIN and returns the response.

func (*WireClient) Close

func (c *WireClient) Close() error

Close closes the underlying connection.

func (*WireClient) Commit

func (c *WireClient) Commit() (any, error)

Commit sends COMMIT and returns the response.

func (*WireClient) Conn

func (c *WireClient) Conn() *SimConn

Conn returns the underlying SimConn, for callers that need a hard reset (CloseWithError) to model an abrupt disconnect.

func (*WireClient) Connect

func (c *WireClient) Connect(ctx context.Context) error

Connect drives the full ready-to-query handshake: the wire handshake, a HELLO, and — when the negotiated version is Bolt 5.1+ (which defers authentication to a dedicated LOGON message) — a LOGON. It returns an error if any step does not produce a SUCCESS, leaving the session ready for RUN. It is the convenience path the honest, overload, and slow-consumer actors use; the BoltAbuser drives the lower-level primitives directly.

func (*WireClient) Goodbye

func (c *WireClient) Goodbye() error

Goodbye sends GOODBYE. No response is expected; the server tears the session down.

func (*WireClient) Handshake

func (c *WireClient) Handshake(_ context.Context) (proto.Version, error)

Handshake performs the 20-byte Bolt client handshake, offering versions 5.6 down to 5.0 across the four slots, and records the negotiated version. It returns an error if the server rejects negotiation (responds with 0.0) or an I/O error occurs.

func (*WireClient) Hello

func (c *WireClient) Hello(extra map[string]packstream.Value) (any, error)

Hello sends a HELLO with scheme="none" (the NoAuth server admits it) and returns the response message (typically *proto.Success). For Bolt 5.1+ the server defers auth to a LOGON message; this client targets the inline (<=5.0-style) HELLO auth the NoAuth handler accepts, which the server honours across the supported versions in the DST harness.

func (*WireClient) Logon

func (c *WireClient) Logon() (any, error)

Logon sends a LOGON with scheme="none" for the Bolt 5.1+ deferred-auth path and returns the response.

func (*WireClient) Pull

func (c *WireClient) Pull(n int64) (records []*proto.Record, terminal any, err error)

Pull sends PULL {n:n} and reads up to n RECORDs plus the terminal message. It is used by the SlowConsumer, which pulls in small batches with deliberate stalls between calls.

func (*WireClient) PullAll

func (c *WireClient) PullAll() (records []*proto.Record, terminal any, err error)

PullAll sends PULL {n:-1} and reads every RECORD up to the terminal SUCCESS or FAILURE, returning the records and the terminal message. A FAILURE terminates the pull with the records gathered so far.

func (*WireClient) Recv

func (c *WireClient) Recv() (any, error)

Recv reads and decodes the next response message; exported for actors that read a server-initiated response outside the Request/Pull helpers.

func (*WireClient) RecvRaw

func (c *WireClient) RecvRaw() ([]byte, error)

RecvRaw reads one chunked message and returns its raw bytes without decoding, for the abuser to inspect a FAILURE the standard decoder would also handle.

func (*WireClient) Request

func (c *WireClient) Request(msg any) (any, error)

Request is the LOCK-STEP primitive: it sends one request and reads exactly one response message back. For messages whose reply is a single SUCCESS/FAILURE (HELLO, LOGON, RUN, BEGIN, COMMIT, ROLLBACK, RESET, ROUTE) this is the full terminal exchange. It returns the decoded *proto.Success, *proto.Failure, or *proto.Ignored.

func (*WireClient) Reset

func (c *WireClient) Reset() (any, error)

Reset sends RESET and returns the response.

func (*WireClient) Rollback

func (c *WireClient) Rollback() (any, error)

Rollback sends ROLLBACK and returns the response.

func (*WireClient) Run

func (c *WireClient) Run(query string, params map[string]any) (any, error)

Run sends a RUN for query with params and returns the response (a *proto.Success carrying the field metadata, or a *proto.Failure). It does NOT pull records; follow with WireClient.PullAll or WireClient.Pull.

func (*WireClient) Version

func (c *WireClient) Version() proto.Version

Version reports the negotiated protocol version (zero before Handshake).

func (*WireClient) WriteChunkedRaw

func (c *WireClient) WriteChunkedRaw(payload []byte) error

WriteChunkedRaw writes payload as one well-framed chunked message regardless of whether payload decodes to a valid Bolt message. The BoltAbuser uses it to deliver garbage opcodes and wrong-state messages that are correctly framed but semantically invalid, exercising the server's message-level (not framing-level) rejection.

func (*WireClient) WriteRaw

func (c *WireClient) WriteRaw(p []byte) (int, error)

WriteRaw writes raw bytes directly to the connection, bypassing chunked framing. It is the seam the BoltAbuser uses to emit deliberately malformed wire bytes (bad handshakes, truncated chunks, garbage opcodes) the framed send path would never produce.

type WireExchange

type WireExchange struct {
	// Op identifies the operation sent (kind + cypher).
	Op string
	// Response is the terminal response class (SUCCESS, FAILURE:<code>, …).
	Response string
}

WireExchange is one decoded request/response pair from a lock-step wire session, rendered to stable strings so two runs can be compared byte-for-byte.

type WireTranscript

type WireTranscript struct {
	Seed      uint64
	Exchanges []WireExchange
}

WireTranscript is the ordered list of exchanges from one lock-step session. Two transcripts produced from the same seed must be equal — the determinism guarantee of the single-connection lock-step Bolt-wire path.

func RunLockStepWire

func RunLockStepWire(seed uint64, nOps int) (WireTranscript, error)

RunLockStepWire drives a deterministic single-connection LOCK-STEP session against a fresh real bolt/server: for the given seed it draws a fixed sequence of nOps honest write operations, sends each over the wire, blocks for the terminal response, and records the exchange. Because exactly one exchange is in flight at a time, the transcript is a pure function of the seed, so two calls with the same seed return equal transcripts (assert with WireTranscript.Equal).

It is the engine behind the cmd/sim `--mode wire` reproducibility demo and the determinism proof for the lock-step path.

func (WireTranscript) Equal

func (t WireTranscript) Equal(other WireTranscript) bool

Equal reports whether two transcripts are byte-identical (same length, same ordered exchanges). It is the reproducibility predicate.

type Workload

type Workload struct {
	Actors  []Actor
	Weights []float64
}

Workload is a weighted mix of actors. On each tick the simulator asks the workload to select an actor (by weight) and that actor produces the next operation. The weights need not sum to 1; Workload.SelectActor normalises against their running total.

Concurrency contract

Workload is NOT safe for concurrent use; it is consulted from the single simulation goroutine. The Seed passed to SelectActor is the same shared single-goroutine seed, preserving determinism.

func BadActorWorkload

func BadActorWorkload(_ *Seed) *Workload

BadActorWorkload returns a mix that injects a MalformedSender alongside the honest actors (50% writer, 30% reader, 20% malformed), so the safety loop continuously exercises the engine's rejection paths while honest traffic keeps the graph populated. The malformed traffic must never panic, corrupt state, or trip an invariant: each ill-formed op is modelled by the oracle as a no-op, so a clean run sees engine and oracle stay in lock-step across every rejection.

func DefaultWorkload

func DefaultWorkload(_ *Seed) *Workload

DefaultWorkload returns a balanced mix: 40% writer, 60% reader. The seed is accepted for symmetry with the other constructors (and to allow future seed-dependent compositions) but the default mix is fixed.

func ReadHeavyWorkload

func ReadHeavyWorkload(_ *Seed) *Workload

ReadHeavyWorkload returns a 20% writer / 80% reader mix, stressing the read path and isolation.

func SteadyStateWorkload

func SteadyStateWorkload(_ *Seed) *Workload

SteadyStateWorkload returns a mix tuned to keep the modelled graph BOUNDED over a very long run: a writer that creates and deletes in equal measure (via BoundedChurnWriter) plus a reader. It is the long-running scenario's workload — the point there is heap/goroutine stability across millions of small ops, which requires the working set not to grow without bound.

func WriteHeavyWorkload

func WriteHeavyWorkload(_ *Seed) *Workload

WriteHeavyWorkload returns an 80% writer / 20% reader mix, stressing the write and recovery paths.

func (*Workload) SelectActor

func (w *Workload) SelectActor(seed *Seed) Actor

SelectActor returns one actor chosen with probability proportional to its weight, drawing a single float64 from seed. It panics if the workload has no actors (a programmer error). A non-positive total weight falls back to a uniform first-actor choice rather than dividing by zero.

Jump to

Keyboard shortcuts

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