sim

package module
v0.1.0 Latest Latest
Warning

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

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

README

d-raft

CI Go Reference License

Deterministic Raft experiments with durable, replayable decisions.

d-raft is a research platform for turning distributed-systems failures into small, independently checkable execution artifacts. It combines a pure Raft reference state machine with virtual time, a faultable network, explicit persistence acknowledgements, crash/restart simulation, semantic decision recording, and package-separated safety checks with structured witnesses.

The project is created and maintained by Mohammadamin Khanbabaei (aminkbi).

Research status: the deterministic kernel, durable reference Raft model, cluster harness, safety checker, observational trace decoder, and exact semantic decision replay are implemented. Self-describing run artifacts and the research CLI are also usable. Bounded prefix exploration and fingerprint-preserving semantic minimization are implemented. Durable snapshots, safe log compaction, and snapshot-bearing run artifacts are also implemented. Joint-consensus membership changes and learners are implemented, including durable recovery and snapshot-aware configuration state. An experimental adapter for the production-used go.etcd.io/raft/v3 core is implemented for a declared fixed-membership capability subset. A versioned, portable binary KV application oracle now produces independently checkable state/history commitments in both adapters, including reference snapshot recovery. A versioned six-fault corpus and isolated, repository-pinned runner are implemented with a clean Go 1.26.6 result: three checker-backed safety kills and three separately reported conformance kills. Strict adapter-neutral semantic plans, bilateral capability preflight, projection accounting, normalized outcomes/comparisons, source-provenance verification, and a two-adapter research CLI are implemented. Immutable, CI-verified cross-adapter control and faulted-workload cases are published. A clean-provenance 21-trial bounded harness/accounting study, raw trial observations, paired cache contrast, and pinned related-work matrix are also published; real-bug effectiveness and diagnosis-time claims remain outside the measured evidence.

Why d-raft?

A seed says how to repeat one pseudorandom run in one implementation. A useful counterexample should say which semantic choices mattered, retain evidence of the violated invariant, survive irrelevant changes in random-number consumption, and be portable to another implementation.

d-raft's working research direction is therefore:

Portable, minimized, independently checkable semantic Raft counterexamples that replay across implementations and versions.

Deterministic simulation, pure Step APIs, seeded replay, and trace reduction all have substantial prior art. d-raft does not present those techniques alone as novel; RESEARCH.md defines the narrower thesis and evaluation plan.

What works today

Package Role
sim Protocol-neutral virtual-time scheduler, stable RNG streams, typed network, partitions, and observational JSONL traces
raft Pure deterministic Raft reference state machine with elections, replication, current-term commit, snapshots, compaction, joint consensus, learners, and leader no-op entries
raftsim Durable storage, timers, network delivery, partitions, crash/restart, snapshot installation, membership actions, process incarnations, and persistence barriers
check Package-separated election, voting, term, log, commit, apply, snapshot, and membership-transition witnesses with stable fingerprints
decision Versioned semantic choices, seeded selection, recording, exact tape replay, and domain-drift detection
trace Bounded, line-aware, payload-lossless decoder for known d-raft.trace/v1 fields
artifact Strict, self-describing d-raft.run/v3 artifacts with scenarios, voter/learner roles, configuration actions, environment, tape, outcome, digest, and witnesses; legacy v1/v2 decoding remains available
apporacle Strict binary KV commands, canonical checkpoints, known-answer vectors, and adapter-neutral state/history commitments
semanticplan Strict portable plan/capability/execution schemas, plan-aware projection proof checks, negotiated invariant universes, normalized outcomes, and outcome-bound comparisons
experiment Clean-run execution of versioned and named canonical scenarios with proposal, snapshot, membership, process, and network actions
evaluation Strict balanced-trial evaluator with raw accounting, paired cache contrasts, machine/build provenance, and publication validation
cmd/draft run, canonical, explore, replay, minimize, and inspect research workflow
cmd/draft-eval Linux research-evaluation runner with clean-build provenance gating and durable, private, no-clobber publication
explore Clean-rerun bounded DFS with deterministic suffix completion and collision-safe canonical-state pruning
minimize Scenario ddmin, sparse semantic guidance reduction, and domain-aware selection shrinking
mutant Strict pinned mutant manifests, isolated worktree execution, bounded evidence, and closed outcome classification
cmd/draft-mutants Seeded-mutant corpus runner with atomic, no-overwrite JSON result publication
adapters/etcdraft Experimental go.etcd.io/raft/v3 v3.7.0 production-core adapter with conservative checking and adapter-local exact replay
adapters/etcdraft/cmd/draft-cross Replay-verified plan derivation plus private, no-clobber, manifest-committed comparative bundles and end-to-end verification

The root module is dependency-free and uses no wall-clock sleeps or background goroutines. The isolated nested etcd/raft adapter carries its production-core dependency. Both target Go 1.26 and declare the current Go 1.26.6 toolchain.

See MUTANTS.md for the seeded-fault evaluation contract and runner trust boundary. See SEMANTIC_PLANS.md for the cross-adapter portability and comparison contract. See EVALUATION.md for the bounded study, RELATED_WORK.md for pinned positioning, and REPRODUCIBILITY.md for verification and archival steps. Release-level changes are summarized in CHANGELOG.md.

Architecture

flowchart LR
    Scenario[Scenario / faults / proposals]
    Decisions[Semantic decider]
    Harness[Durable Raft harness]
    Model[Pure Raft model]
    Runtime[Virtual time + network]
    Checker[Package-separated checker]
    Artifact[Decision tape + observations]

    Scenario --> Harness
    Decisions --> Harness
    Harness <--> Model
    Harness <--> Runtime
    Harness --> Checker
    Decisions --> Artifact
    Runtime --> Artifact
    Checker --> Artifact

The persistence boundary is explicit:

Step(input)
  -> Persist(token, state)
  -> simulated durable completion
  -> Step(Persisted(token))
  -> dependent messages, timers, apply, and snapshot-install effects

A crash destroys volatile raft.Node state. Restart creates a fresh node only from the durable store. This lets tests distinguish a crash immediately before persistence from one immediately after it.

Quick start

config := raftsim.DefaultConfig("a", "b", "c")
config.Seed = 42

cluster, err := raftsim.New(config)
if err != nil {
    log.Fatal(err)
}
if _, err := cluster.RunUntil(2 * time.Second); err != nil {
    log.Fatal(err)
}

leader, ok := cluster.Leader()
if !ok {
    log.Fatal("no unique leader")
}
if err := cluster.ProposeTo(leader, []byte("set x=1")); err != nil {
    log.Fatal(err)
}

Partitions, scheduled crashes, restarts, and crash-after-persist boundaries are available directly on raftsim.Cluster. Every semantic step is checked; when StopOnViolation is enabled, a run stops at the first safety violation.

Replay semantic decisions

Recording is separate from the observational trace. The choice tape describes election timeouts, network loss and latency, and storage completion latency by stable semantic identity.

recorder := decision.NewRecorder(decision.NewSeedDecider(42))
config.Decider = recorder
original, _ := raftsim.New(config)
_, _ = original.RunUntil(2 * time.Second)

tape := recorder.Tape()
replay, _ := decision.NewTapeDecider(tape)
config.Seed = 999 // infrastructure RNG no longer controls these choices
config.Decider = replay
replayed, _ := raftsim.New(config)
_, _ = replayed.RunUntil(2 * time.Second)
if err := replay.Finish(); err != nil {
    log.Fatal(err)
}

TapeDecider stops at the first choice ID, kind, domain, or selection mismatch. This makes replay drift explicit instead of silently producing a different run. Exact execution also requires the same scenario version, external actions, and run horizon; d-raft.run/v3 bundles those inputs with the tape.

Command-line workflow

Build the research CLI and create a self-contained run artifact:

go build -o draft ./cmd/draft
./draft run --seed 42 --duration 2s --out run.json
./draft canonical --seed 1 --out portable-faults.json portable-faults-v1
./draft inspect run.json
./draft replay run.json
./draft explore --depth 6 --max-runs 1000
./draft explore --cache=false --depth 6 --max-runs 1000 # matched baseline
./draft minimize --out minimized.json failing.json

go build -buildvcs=true -o draft-eval ./cmd/draft-eval
./draft-eval --trials 21 --out d-raft-evaluation.json
./draft-eval --verify d-raft-evaluation.json

draft replay starts from a clean cluster, consumes the stored choice tape, rejects any semantic drift, and verifies the recorded outcome status, step count, virtual end time, violation fingerprints, and versioned canonical observation digest. Artifact writes use a temporary file and atomic no-clobber publication, so an encoding or filesystem failure does not leave a plausible partial result. Artifacts remain private (0600) by default.

draft run and draft explore generate a steady all-voter scenario. The named portable-faults-v1 canonical scenario generates four portable KV proposals around a minority partition/heal and a crash/restart, followed by a 2.6-second quiet convergence tail. Its version fixes the three-node timing, 2% network loss, command bytes, and seed 1; a different semantic stream requires a new canonical version rather than silently changing the published experiment. The v3 schema and Go APIs can also execute scheduled proposals, snapshots, joint membership transitions, partitions, healing, crashes, and restarts, including the crash-after-persistence-before-acknowledgement boundary. Role changes are limited to a pre-provisioned Members universe; they do not perform dynamic discovery or process creation. Additional named fault suites and broader production adapters are later milestones. The experimental etcd/raft production-core adapter and separate draft-etcd CLI are documented in ADAPTERS.md. Canonical reference-state caching is enabled by default for draft explore and bounded with --cache-entries and --cache-bytes. See ARTIFACTS.md, MEMBERSHIP.md, SNAPSHOTS.md, EXPLORATION.md, CANONICAL_STATE.md, and MINIMIZATION.md. The opt-in cross-adapter application profile is specified in APPLICATION_ORACLE.md.

draft-eval runs the fixed balanced study only from a clean, VCS-stamped Linux build. It records all raw trials, machine and schema identity, status-separated terminal outcomes, processed event attempts, and the paired cache-on minus cache-off contrast. Its common runner-invocation ceiling does not make random full runs and DFS frontier probes equal computational work. If the workload has no cache hits or state pruning, the result characterizes cache overhead with no repeated exact cache identity; it is not evidence of pruning efficacy or real-bug detection probability. The output path is preflighted before the run and published as a durable, 0600, no-clobber artifact.

Observational traces

sim.JSONLRecorder emits globally ordered d-raft.trace/v1 JSON Lines. trace.Decoder enforces schema and sequence rules, supports compatible and strict validation, bounds record size, and preserves protocol payloads as json.RawMessage so 64-bit terms and indexes are never routed through float64.

The observational trace and semantic decision tape are deliberately distinct: the former explains what happened; the latter drives an execution.

Determinism boundary

For a fixed d-raft version, initial state, API-call sequence, decision tape or seed, and deterministic callbacks, d-raft reproduces virtual time, event and packet order, protocol state, durable state, and trace output. Unordered map iteration, wall-clock reads, external I/O, concurrent callbacks, and mutation after an inadequately cloned send are outside that guarantee. See COMPATIBILITY.md.

Development

go test ./...
go vet ./...
go test -race ./...

cd adapters/etcdraft
go test ./...
go vet ./...
go test -race ./...

Contributions should include a deterministic regression test and, for protocol changes, a crash-boundary test where persistence matters. See CONTRIBUTING.md.

Prior art and positioning

d-raft builds on ideas demonstrated by etcd/raft's deterministic core and TLA+ trace validation, FoundationDB simulation, DEMi, SAMC, MadSim/MadRaft, Oddity, Coyote, Turmoil, VOPR, and commercial deterministic-testing systems such as Antithesis. The research target is a portable semantic counterexample format and reduction/evaluation workflow, not another claim that seeded simulation itself is new.

Citation and license

If d-raft supports published work, cite the repository using CITATION.cff and record the release or commit, Go toolchain, scenario version, adapter version, and semantic tape schema.

Licensed under Apache 2.0.

Documentation

Overview

Package sim provides d-raft's deterministic, single-threaded discrete-event simulation runtime for testing distributed systems.

Time advances only when Simulator executes an event. The package never starts a goroutine and never waits on wall-clock time. Events scheduled for the same instant execute in scheduling order. Rand provides package-owned reproducible random streams, and Router provides a typed network with latency, loss, endpoint lifecycle, and directed partitions.

Components can write their transitions to a shared JSONLRecorder. The resulting versioned JSON Lines stream gives scheduler, random, network, and protocol activity one global observational order. Exact execution replay is driven separately by the semantic choices in package decision.

Index

Examples

Constants

View Source
const CanonicalStateSchema = "d-raft.sim-state/v1"
View Source
const RouterStateSchema = "d-raft.router-state/v1"
View Source
const TraceSchemaVersion = "d-raft.trace/v1"

TraceSchemaVersion identifies the JSON Lines trace format emitted by JSONLRecorder. It is versioned independently from the Go module.

Variables

View Source
var (
	ErrNilSimulator           = errors.New("sim: router requires a simulator")
	ErrNilRand                = errors.New("sim: router requires a random source")
	ErrEmptyNode              = errors.New("sim: node identifier must not be empty")
	ErrNilHandler             = errors.New("sim: node handler must not be nil")
	ErrDuplicateNode          = errors.New("sim: node is already registered")
	ErrUnknownSource          = errors.New("sim: packet source is not registered")
	ErrUnknownTarget          = errors.New("sim: packet target is not registered")
	ErrInvalidLink            = errors.New("sim: invalid network link configuration")
	ErrInvalidMatrix          = errors.New("sim: invalid partition matrix")
	ErrPacketExhausted        = errors.New("sim: packet identifier space exhausted")
	ErrInvalidNetworkDecision = errors.New("sim: invalid network decision")
	ErrUncacheableRouter      = errors.New("sim: router state is not canonically inspectable")
)
View Source
var (
	// ErrNegativeTime is returned when an operation is given negative virtual
	// time or a negative delay.
	ErrNegativeTime = errors.New("sim: virtual time and delays must not be negative")
	// ErrPast is returned when an operation would move virtual time backwards.
	ErrPast = errors.New("sim: cannot schedule or advance into the past")
	// ErrTimeOverflow is returned when adding a delay would overflow a
	// time.Duration.
	ErrTimeOverflow = errors.New("sim: virtual time overflow")
	// ErrEventIDExhausted is returned in the practically unreachable case that
	// all uint64 event identifiers have been allocated.
	ErrEventIDExhausted = errors.New("sim: event identifier space exhausted")
	// ErrUncacheableState reports hidden callback or trace-sink behavior.
	ErrUncacheableState = errors.New("sim: state is not canonically inspectable")
	// ErrInvalidEventTag reports a malformed canonical callback descriptor.
	ErrInvalidEventTag = errors.New("sim: invalid event tag")
)

Functions

func CloneBytes

func CloneBytes(message []byte) []byte

CloneBytes returns an independent copy of a byte slice and is suitable as a Router clone function.

Types

type Action

type Action func(*Simulator)

Action is invoked synchronously when an event reaches the front of the queue. It may schedule or cancel other events.

type CanonicalLinkState

type CanonicalLinkState struct {
	From                NodeID `json:"from,omitempty"`
	To                  NodeID `json:"to,omitempty"`
	MinLatencyNS        int64  `json:"min_latency_ns"`
	MaxLatencyNS        int64  `json:"max_latency_ns"`
	LossProbabilityBits uint64 `json:"loss_probability_bits"`
}

type CanonicalRouteState

type CanonicalRouteState struct {
	From NodeID `json:"from"`
	To   NodeID `json:"to"`
}

type Clock

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

Clock exposes the current virtual time. It has no wall-clock behavior.

func (*Clock) Now

func (c *Clock) Now() time.Duration

Now returns the current virtual time since the start of the simulation.

type CloneFunc

type CloneFunc[M any] func(M) M

CloneFunc snapshots a message when Send is called. A nil CloneFunc performs assignment, which is sufficient for immutable messages and value types.

type DropReason

type DropReason uint8

DropReason explains why a packet was discarded.

const (
	NotDropped DropReason = iota
	DropLoss
	DropPartition
	DropTargetUnavailable
)

type EventID

type EventID uint64

EventID identifies a pending event. The zero value is never issued.

type EventKind

type EventKind string

EventKind identifies the closed set of callbacks used by the reference experiment executor. EventTag data is a canonical JSON value whose schema is owned by the producer of that kind.

const (
	EventNetworkDelivery   EventKind = "network_delivery"
	EventStorageCompletion EventKind = "storage_completion"
	EventPersistenceAck    EventKind = "persistence_ack"
	EventElectionTimer     EventKind = "election_timer"
	EventHeartbeatTimer    EventKind = "heartbeat_timer"
	EventExternalAction    EventKind = "external_action"
	EventScheduledCrash    EventKind = "scheduled_crash"
	EventScheduledRestart  EventKind = "scheduled_restart"
)

type EventTag

type EventTag struct {
	Kind EventKind       `json:"kind"`
	Data json.RawMessage `json:"data"`
}

EventTag is the inspectable semantic replacement for an opaque callback.

func JSONPacketEventTagger

func JSONPacketEventTagger[M any](packet Packet[M]) (EventTag, error)

JSONPacketEventTagger returns the reference JSON delivery descriptor.

type Handler

type Handler[M any] func(Packet[M])

Handler receives packets synchronously on the simulator's event loop.

type JSONLRecorder

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

JSONLRecorder writes one TraceRecord per line. It is intentionally synchronous and is not safe for concurrent use. After the first encoding error it records no further events; Err reports that error.

func NewJSONLRecorder

func NewJSONLRecorder(writer io.Writer) *JSONLRecorder

NewJSONLRecorder returns a recorder that writes to writer.

func (*JSONLRecorder) Err

func (r *JSONLRecorder) Err() error

Err returns the first JSON encoding or writing error observed by r.

func (*JSONLRecorder) RecordTrace

func (r *JSONLRecorder) RecordTrace(event TraceEvent)

RecordTrace implements TraceSink.

type LinkConfig

type LinkConfig struct {
	MinLatency      time.Duration
	MaxLatency      time.Duration
	LossProbability float64
}

LinkConfig describes a directed network link. Latency is sampled uniformly in nanoseconds from the inclusive range [MinLatency, MaxLatency].

func (LinkConfig) Validate

func (c LinkConfig) Validate() error

Validate verifies that a link configuration is usable.

type NetworkDecisionSource

type NetworkDecisionSource[M any] interface {
	Drop(Packet[M], LinkConfig) (bool, error)
	Latency(Packet[M], LinkConfig) (time.Duration, error)
}

NetworkDecisionSource replaces Router's random loss and latency draws with semantic decisions suitable for recording, replay, and exploration.

type NetworkEvent

type NetworkEvent[M any] struct {
	Kind        NetworkEventKind
	Packet      Packet[M]
	At          time.Duration
	DeliveryAt  time.Duration
	Reason      DropReason
	WasInFlight bool
}

NetworkEvent is emitted synchronously by a Router. DeliveryAt is set for a scheduled delivery and for a packet dropped when delivery became due. Reason is set for a drop, and WasInFlight distinguishes a delivery-time drop from one decided by Send (including when virtual time is zero).

type NetworkEventKind

type NetworkEventKind uint8

NetworkEventKind describes an observable router transition.

const (
	PacketScheduled NetworkEventKind = iota + 1
	PacketDelivered
	PacketDropped
)

type NodeID

type NodeID string

NodeID identifies a simulated network endpoint.

type Observer

type Observer[M any] func(NetworkEvent[M])

Observer sees packet lifecycle transitions. It runs inline and may affect scheduling order, so observers used for tracing should avoid side effects.

type Packet

type Packet[M any] struct {
	ID      uint64
	From    NodeID
	To      NodeID
	Message M
	SentAt  time.Duration
}

Packet is a message in transit. Packet values delivered to handlers and observers must be treated as read-only.

type PacketEventTagger

type PacketEventTagger[M any] func(Packet[M]) (EventTag, error)

PacketEventTagger encodes the semantic payload captured by an in-flight delivery callback. Reference explorers install one; generic routers may continue using opaque callbacks.

type PartitionMatrix

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

PartitionMatrix is an immutable directed connectivity matrix. A true cell permits traffic; false blocks it. Nodes absent from a matrix are isolated while that matrix is active.

func NewPartitionMatrix

func NewPartitionMatrix(nodes []NodeID, allowed [][]bool) (*PartitionMatrix, error)

NewPartitionMatrix builds a matrix whose row and column order is nodes. allowed must be a square len(nodes)-by-len(nodes) matrix.

func NewPartitions

func NewPartitions(groups ...[]NodeID) (*PartitionMatrix, error)

NewPartitions constructs a symmetric matrix from disjoint connectivity groups. Nodes in the same group can communicate in both directions; nodes in different groups cannot. Every node must occur exactly once.

func (*PartitionMatrix) Allows

func (m *PartitionMatrix) Allows(source, target NodeID) bool

Allows reports whether traffic from source to target is permitted.

func (*PartitionMatrix) Nodes

func (m *PartitionMatrix) Nodes() []NodeID

Nodes returns a copy of the matrix's node order.

type PendingEventState

type PendingEventState struct {
	ID    EventID  `json:"id"`
	AtNS  int64    `json:"at_ns"`
	Order uint64   `json:"order"`
	Tag   EventTag `json:"tag"`
}

PendingEventState is one future callback in execution order.

type Rand

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

Rand is a small, deterministic pseudo-random number generator.

Rand uses SplitMix64. Its output is deliberately implemented in this package, rather than delegated to a standard-library generator whose stream may change between Go releases. A seed therefore reproduces the same stream on every supported platform and package version.

Rand is intended for simulation, randomized testing, and deriving independent simulation streams. It is not cryptographically secure.

func NewRand

func NewRand(seed uint64) *Rand

NewRand returns a generator initialized with seed. Every uint64 value, including zero, is a valid seed.

func (*Rand) Chance

func (r *Rand) Chance(p float64) bool

Chance reports whether an event with probability p occurs. It panics when p is outside [0, 1] or is NaN.

func (*Rand) Duration

func (r *Rand) Duration(min, max time.Duration) time.Duration

Duration returns a uniformly distributed duration in the inclusive range [min, max]. It panics if min is negative or max is less than min.

func (*Rand) Float64

func (r *Rand) Float64() float64

Float64 returns a uniformly distributed value in [0.0, 1.0).

func (*Rand) IntN

func (r *Rand) IntN(n int) int

IntN returns a uniformly distributed value in [0, n). It panics if n is not positive.

func (*Rand) SetTraceSink

func (r *Rand) SetTraceSink(sink TraceSink, stream string)

SetTraceSink sets the synchronous trace destination and stream label. Empty labels are recorded as "default". Child generators created by Split inherit the sink and receive stable labels below this stream.

func (*Rand) Split

func (r *Rand) Split() *Rand

Split derives a deterministic child stream. Calling Split consumes one value from r, so stream allocation order is significant.

func (*Rand) State

func (r *Rand) State() RandState

State returns an immutable checkpoint. Trace metadata is deliberately excluded because canonical exploration rejects side-effecting trace sinks.

func (*Rand) Uint64

func (r *Rand) Uint64() uint64

Uint64 returns the next value in the stream.

func (*Rand) Uint64N

func (r *Rand) Uint64N(n uint64) uint64

Uint64N returns a uniformly distributed value in [0, n). It panics if n is zero. Rejection sampling avoids modulo bias.

type RandState

type RandState struct {
	State      uint64 `json:"state"`
	SplitCount uint64 `json:"split_count"`
}

RandState is the future-relevant state of one deterministic stream.

type Router

type Router[M any] struct {
	// contains filtered or unexported fields
}

Router is a deterministic, directed, in-memory network. It performs all work through its Simulator and never starts goroutines.

Example
package main

import (
	"fmt"
	"time"

	sim "github.com/aminkbi/d-raft"
)

func main() {
	simulation := sim.New()
	random := sim.NewRand(20260814)
	router, err := sim.NewRouter(
		simulation,
		random,
		sim.LinkConfig{MinLatency: 10 * time.Millisecond, MaxLatency: 10 * time.Millisecond},
		func(message voteRequest) voteRequest { return message },
	)
	if err != nil {
		panic(err)
	}

	must(router.Register("candidate", func(sim.Packet[voteRequest]) {}))
	must(router.Register("voter", func(packet sim.Packet[voteRequest]) {
		fmt.Printf("term=%d candidate=%s at=%s\n", packet.Message.Term, packet.From, simulation.Now())
	}))

	_, err = router.Send("candidate", "voter", voteRequest{Term: 7})
	must(err)
	simulation.Run()

}

type voteRequest struct {
	Term uint64
}

func must(err error) {
	if err != nil {
		panic(err)
	}
}
Output:
term=7 candidate=candidate at=10ms

func NewRouter

func NewRouter[M any](simulator *Simulator, random *Rand, defaultLink LinkConfig, clone CloneFunc[M]) (*Router[M], error)

NewRouter constructs a router. Passing nil for clone uses ordinary Go assignment; provide a clone function for slices, maps, pointers, or other mutable messages that senders might modify after Send returns.

func (*Router[M]) CanonicalState

func (r *Router[M]) CanonicalState() (RouterState, error)

CanonicalState returns an independently owned, map-order-independent router checkpoint. Trace sinks and observers are rejected because arbitrary callbacks can introduce hidden side effects.

func (*Router[M]) Register

func (r *Router[M]) Register(node NodeID, handler Handler[M]) error

Register adds a node. Duplicate registration returns ErrDuplicateNode.

func (r *Router[M]) ResetLink(from, to NodeID)

ResetLink removes a directed-link override.

func (*Router[M]) Send

func (r *Router[M]) Send(from, to NodeID, message M) (SendResult, error)

Send submits a packet to the simulated network.

func (*Router[M]) SetDecisionSource

func (r *Router[M]) SetDecisionSource(source NetworkDecisionSource[M])

SetDecisionSource replaces random network choices. Passing nil restores the Router's Rand-based behavior.

func (r *Router[M]) SetLink(from, to NodeID, config LinkConfig) error

SetLink overrides the default configuration for a directed link.

func (*Router[M]) SetObserver

func (r *Router[M]) SetObserver(observer Observer[M])

SetObserver sets the lifecycle observer. Passing nil disables observation.

func (*Router[M]) SetPacketEventTagger

func (r *Router[M]) SetPacketEventTagger(tagger PacketEventTagger[M])

SetPacketEventTagger makes future delivery callbacks canonically visible. Passing nil restores ordinary opaque simulator callbacks.

func (*Router[M]) SetPartition

func (r *Router[M]) SetPartition(matrix *PartitionMatrix)

SetPartition activates a connectivity matrix. Passing nil restores full connectivity. The matrix is checked both when a packet is sent and when it is due for delivery, so a new partition can cut packets already in flight.

func (*Router[M]) SetTraceSink

func (r *Router[M]) SetTraceSink(sink TraceSink)

SetTraceSink sets the synchronous machine-readable trace destination. Passing nil disables tracing. Use the same sink for the simulator, random source, and router to obtain one globally ordered execution trace.

func (*Router[M]) Unregister

func (r *Router[M]) Unregister(node NodeID) bool

Unregister removes a node and reports whether it existed. Packets already in flight to it will be dropped at delivery time.

type RouterState

type RouterState struct {
	Schema          string                `json:"schema"`
	DefaultLink     CanonicalLinkState    `json:"default_link"`
	Links           []CanonicalLinkState  `json:"links"`
	Endpoints       []NodeID              `json:"endpoints"`
	PartitionActive bool                  `json:"partition_active"`
	PartitionNodes  []NodeID              `json:"partition_nodes"`
	Allowed         []CanonicalRouteState `json:"allowed"`
	NextPacket      uint64                `json:"next_packet"`
	Exhausted       bool                  `json:"exhausted"`
	Random          RandState             `json:"random"`
}

RouterState contains topology, endpoint, allocation, and random-stream state. In-flight packets are represented by Simulator delivery event tags.

type SendResult

type SendResult struct {
	PacketID   uint64
	Scheduled  bool
	DeliveryAt time.Duration
	DropReason DropReason
}

SendResult reports the immediate outcome of Send.

type Simulator

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

Simulator is a single-threaded deterministic discrete-event scheduler. Its zero value is ready to use.

Simulator is intentionally not safe for concurrent use. Running it from one OS goroutine makes event execution, random draws, and network delivery fully reproducible.

func New

func New() *Simulator

New returns an empty simulator at virtual time zero.

func (*Simulator) Cancel

func (s *Simulator) Cancel(id EventID) bool

Cancel removes a pending event. It reports false if id is zero, unknown, or already executed or canceled.

func (*Simulator) CanonicalState

func (s *Simulator) CanonicalState() (SimulatorState, error)

CanonicalState returns an independent scheduler checkpoint. It refuses to guess the meaning of opaque Action closures.

func (*Simulator) Clock

func (s *Simulator) Clock() *Clock

Clock returns the simulator's read-only virtual clock.

func (*Simulator) Len

func (s *Simulator) Len() int

Len returns the number of pending events.

func (*Simulator) NextEventTime

func (s *Simulator) NextEventTime() (time.Duration, bool)

NextEventTime returns the time of the next event and whether one exists.

func (*Simulator) Now

func (s *Simulator) Now() time.Duration

Now returns the current virtual time.

func (*Simulator) Run

func (s *Simulator) Run() int

Run executes events until the queue is empty and returns the number run.

func (*Simulator) RunSteps

func (s *Simulator) RunSteps(limit int) int

RunSteps executes at most limit events and returns the number run. A negative limit executes no events.

func (*Simulator) RunUntil

func (s *Simulator) RunUntil(end time.Duration) (int, error)

RunUntil executes every event scheduled at or before end, then advances the clock to end. Events scheduled for end by another event at end are also run.

func (*Simulator) Schedule

func (s *Simulator) Schedule(delay time.Duration, action Action) (EventID, error)

Schedule adds an action delay after the current virtual time.

func (*Simulator) ScheduleAt

func (s *Simulator) ScheduleAt(when time.Duration, action Action) (EventID, error)

ScheduleAt adds an action at the specified absolute virtual time. Events at the same time execute in the order in which they were scheduled.

func (*Simulator) ScheduleAtTagged

func (s *Simulator) ScheduleAtTagged(when time.Duration, tag EventTag, action Action) (EventID, error)

ScheduleAtTagged adds an inspectable callback at an absolute virtual time.

func (*Simulator) ScheduleTagged

func (s *Simulator) ScheduleTagged(delay time.Duration, tag EventTag, action Action) (EventID, error)

ScheduleTagged adds an inspectable callback delay after the current time.

func (*Simulator) SetTraceSink

func (s *Simulator) SetTraceSink(sink TraceSink)

SetTraceSink sets the synchronous machine-readable trace destination. Passing nil disables tracing.

func (*Simulator) Step

func (s *Simulator) Step() bool

Step executes the next event and reports whether an event was available.

type SimulatorState

type SimulatorState struct {
	Schema    string              `json:"schema"`
	NowNS     int64               `json:"now_ns"`
	NextID    EventID             `json:"next_id"`
	Exhausted bool                `json:"exhausted"`
	Events    []PendingEventState `json:"events"`
}

SimulatorState contains every future-relevant scheduler field. Events are sorted by execution order rather than heap layout.

type TraceEvent

type TraceEvent struct {
	Kind TraceEventKind `json:"kind"`

	AtNS           *int64  `json:"at_ns,omitempty"`
	EventID        EventID `json:"event_id,omitempty"`
	ScheduledForNS *int64  `json:"scheduled_for_ns,omitempty"`

	RandomStream    string            `json:"random_stream,omitempty"`
	RandomOperation string            `json:"random_operation,omitempty"`
	RandomArguments map[string]string `json:"random_arguments,omitempty"`
	RandomResult    string            `json:"random_result,omitempty"`

	PacketID     uint64 `json:"packet_id,omitempty"`
	From         NodeID `json:"from,omitempty"`
	To           NodeID `json:"to,omitempty"`
	Message      any    `json:"message,omitempty"`
	DeliveryAtNS *int64 `json:"delivery_at_ns,omitempty"`
	DropReason   string `json:"drop_reason,omitempty"`

	Component string `json:"component,omitempty"`
	Action    string `json:"action,omitempty"`
	Details   any    `json:"details,omitempty"`

	Node             NodeID           `json:"node,omitempty"`
	Link             *TraceLinkConfig `json:"link,omitempty"`
	PartitionActive  *bool            `json:"partition_active,omitempty"`
	PartitionNodes   []NodeID         `json:"partition_nodes,omitempty"`
	PartitionAllowed []TraceRoute     `json:"partition_allowed,omitempty"`
}

TraceEvent is an event before it is assigned a schema version and sequence number by a recorder. Fields irrelevant to Kind are omitted from JSON. Message must be JSON-encodable when the event is sent to JSONLRecorder.

type TraceEventKind

type TraceEventKind string

TraceEventKind identifies a deterministic simulator transition.

const (
	TraceEventScheduled   TraceEventKind = "event_scheduled"
	TraceEventCanceled    TraceEventKind = "event_canceled"
	TraceEventExecuted    TraceEventKind = "event_executed"
	TraceClockAdvanced    TraceEventKind = "clock_advanced"
	TraceRandomDraw       TraceEventKind = "random_draw"
	TraceNodeRegistered   TraceEventKind = "node_registered"
	TraceNodeUnregistered TraceEventKind = "node_unregistered"
	TraceLinkSet          TraceEventKind = "link_set"
	TraceLinkReset        TraceEventKind = "link_reset"
	TracePartitionChanged TraceEventKind = "partition_changed"
	TracePacketScheduled  TraceEventKind = "packet_scheduled"
	TracePacketDelivered  TraceEventKind = "packet_delivered"
	TracePacketDropped    TraceEventKind = "packet_dropped"
	TraceProtocolInput    TraceEventKind = "protocol_input"
	TraceProtocolState    TraceEventKind = "protocol_state"
	TracePersistence      TraceEventKind = "persistence"
	TraceProcessLifecycle TraceEventKind = "process_lifecycle"
	TraceProtocolDrop     TraceEventKind = "protocol_drop"
)

type TraceFunc

type TraceFunc func(TraceEvent)

TraceFunc adapts a function to TraceSink.

func (TraceFunc) RecordTrace

func (f TraceFunc) RecordTrace(event TraceEvent)

RecordTrace calls f(event).

type TraceLinkConfig

type TraceLinkConfig struct {
	MinLatencyNS    int64   `json:"min_latency_ns"`
	MaxLatencyNS    int64   `json:"max_latency_ns"`
	LossProbability float64 `json:"loss_probability"`
}

TraceLinkConfig is the stable, nanosecond-based trace representation of a LinkConfig.

type TraceRecord

type TraceRecord struct {
	Schema   string `json:"schema"`
	Sequence uint64 `json:"sequence"`
	TraceEvent
}

TraceRecord is one versioned, globally ordered machine-readable record.

type TraceRoute

type TraceRoute struct {
	From NodeID `json:"from"`
	To   NodeID `json:"to"`
}

TraceRoute is one permitted directed route in a partition trace event.

type TraceSink

type TraceSink interface {
	RecordTrace(TraceEvent)
}

TraceSink consumes trace events synchronously. Implementations must not call back into the component producing an event because doing so can change deterministic scheduling order.

Directories

Path Synopsis
Package apporacle implements a versioned, Raft-independent application commitment for portable cross-adapter experiments.
Package apporacle implements a versioned, Raft-independent application commitment for portable cross-adapter experiments.
Package artifact defines d-raft's self-describing, versioned run artifact.
Package artifact defines d-raft's self-describing, versioned run artifact.
Package check implements independent, history-aware Raft safety checkers.
Package check implements independent, history-aware Raft safety checkers.
cmd
draft command
draft-eval command
Command draft-eval runs the bounded d-raft comparative evaluation.
Command draft-eval runs the bounded d-raft comparative evaluation.
draft-mutants command
Command draft-mutants executes a pinned mutant corpus and writes strict JSON.
Command draft-mutants executes a pinned mutant corpus and writes strict JSON.
Package decision defines semantic choices, seeded selection, recording, and exact tape replay for deterministic distributed-system executions.
Package decision defines semantic choices, seeded selection, recording, and exact tape replay for deterministic distributed-system executions.
Package evaluation runs the bounded, repeated empirical comparison shipped with d-raft.
Package evaluation runs the bounded, repeated empirical comparison shipped with d-raft.
Package experiment executes versioned scenarios against d-raft adapters.
Package experiment executes versioned scenarios against d-raft adapters.
Package explore performs bounded depth-first exploration by clean prefix reruns over semantic choices.
Package explore performs bounded depth-first exploration by clean prefix reruns over semantic choices.
internal
strictjson
Package strictjson provides lexical checks that encoding/json's typed decoder does not perform itself.
Package strictjson provides lexical checks that encoding/json's typed decoder does not perform itself.
Package minimize reduces scenarios and semantic guidance while preserving a specific independently checked violation fingerprint.
Package minimize reduces scenarios and semantic guidance while preserving a specific independently checked violation fingerprint.
Package mutant executes a versioned, repository-pinned corpus of source mutants.
Package mutant executes a versioned, repository-pinned corpus of source mutants.
Package raft implements d-raft's deterministic reference Raft state machine.
Package raft implements d-raft's deterministic reference Raft state machine.
Package raftsim integrates the pure Raft reference model with d-raft's deterministic scheduler, network, storage, timers, and process lifecycle.
Package raftsim integrates the pure Raft reference model with d-raft's deterministic scheduler, network, storage, timers, and process lifecycle.
Package semanticplan projects adapter-neutral semantic directives onto adapter-local decision streams.
Package semanticplan projects adapter-neutral semantic directives onto adapter-local decision streams.
Package trace decodes and validates d-raft's observational JSON Lines trace without converting full-width protocol integers through float64.
Package trace decodes and validates d-raft's observational JSON Lines trace without converting full-width protocol integers through float64.

Jump to

Keyboard shortcuts

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