rt

package
v1.0.0 Latest Latest
Warning

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

Go to latest
Published: Aug 27, 2026 License: MPL-2.0 Imports: 13 Imported by: 0

Documentation

Overview

Package rt is a cooperative, single-stepping scheduler for real Go goroutines. Every spawned goroutine is a genuine go statement, but a turnstile only ever lets one run at a time, so an entire program's interleaving becomes seeded and replayable.

Example
package main

import (
	"fmt"

	"github.com/arshnah/detsim/rt"
)

func main() {
	sched := rt.NewSched(1)
	ch := rt.NewChan[int](sched, 0)

	sched.Go(func() {
		for i := 1; i <= 3; i++ {
			ch.Send(i * 10)
		}
		ch.Close()
	})

	sched.Go(func() {
		for {
			v, ok := ch.RecvOK()
			if !ok {
				return
			}
			fmt.Println("received:", v)
		}
	})

	sched.Run()
}

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	ErrTraceExhausted = errors.New("detsim/rt: replay trace ended before the scheduler quiesced, the code under test has changed")
	ErrTraceMismatch  = errors.New("detsim/rt: replay trace expected a goroutine that was never ready, the code under test has changed")
)

ErrTraceExhausted and ErrTraceMismatch mean the code under test has changed since the trace was recorded.

View Source
var ErrDecisionLimit = errors.New("detsim/rt: scheduler stopped after reaching its decision limit (peek mode)")

ErrDecisionLimit is returned by Run when SetDecisionLimit cuts a run short.

Functions

func DecisionLimitFromEnv

func DecisionLimitFromEnv(envVar string) int

DecisionLimitFromEnv reads a decision limit from envVar. 0 means no limit.

func DumpTraceOnFailure

func DumpTraceOnFailure(t TestingT, sched *Sched, path string)

DumpTraceOnFailure saves sched's trace to path only if t ends up failed.

func DumpTraceToEnvPath

func DumpTraceToEnvPath(t TestingT, sched *Sched, envVar string)

DumpTraceToEnvPath saves sched's trace to the path named by envVar, regardless of pass/fail.

func SaveTrace

func SaveTrace(path string, trace Trace) error

SaveTrace persists a Trace as indented JSON.

func SeedFromEnv

func SeedFromEnv(name string, fallback int64) int64

SeedFromEnv reads an int64 seed from the named environment variable, or returns fallback.

Types

type Addr

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

Addr is a network address, matching the shape of net.Addr.

func (*Addr) Network

func (a *Addr) Network() string

Network returns the fixed network type name "detsim".

func (*Addr) String

func (a *Addr) String() string

String returns the address string.

type Chan

type Chan[T any] struct {
	// contains filtered or unexported fields
}

Chan is a generic channel that blocks through the scheduler instead of the Go runtime.

func NewChan

func NewChan[T any](s *Sched, cap int) *Chan[T]

NewChan creates a channel with the given buffer capacity, 0 for unbuffered.

func (*Chan[T]) Cap

func (c *Chan[T]) Cap() int

Cap returns the configured buffer capacity.

func (*Chan[T]) Close

func (c *Chan[T]) Close()

Close closes the channel, panicking if already closed.

func (*Chan[T]) Closed

func (c *Chan[T]) Closed() bool

Closed reports whether the channel has been closed.

func (*Chan[T]) Len

func (c *Chan[T]) Len() int

Len returns the number of buffered elements.

func (*Chan[T]) Recv

func (c *Chan[T]) Recv() T

Recv blocks until a value is available, returning the zero value if closed and drained.

func (*Chan[T]) RecvOK

func (c *Chan[T]) RecvOK() (T, bool)

RecvOK blocks until a value is available or the channel closes.

func (*Chan[T]) Send

func (c *Chan[T]) Send(v T)

Send blocks until there's room, or a receiver is waiting on an unbuffered channel.

func (*Chan[T]) TryRecv

func (c *Chan[T]) TryRecv() (T, bool)

TryRecv attempts a non-blocking receive.

type Cond

type Cond struct {
	L *Mutex
	// contains filtered or unexported fields
}

Cond is a deterministic counterpart to sync.Cond.

func NewCond

func NewCond(s *Sched, l *Mutex) *Cond

NewCond builds a Cond bound to s, guarded by l.

func (*Cond) Broadcast

func (c *Cond) Broadcast()

Broadcast wakes all current waiters.

func (*Cond) Signal

func (c *Cond) Signal()

Signal wakes at most one waiter.

func (*Cond) Wait

func (c *Cond) Wait()

Wait unlocks L, blocks until signaled, then reacquires L. The caller must hold L.

type Conn

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

Conn is a connected pair of endpoints, one side's Write delivering to the other's Read.

func (*Conn) Close

func (c *Conn) Close() error

Close closes the connection. Idempotent.

func (*Conn) LocalAddr

func (c *Conn) LocalAddr() *Addr

LocalAddr returns the connection's local address.

func (*Conn) Read

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

Read returns io.EOF once the connection is closed and drained.

func (*Conn) RemoteAddr

func (c *Conn) RemoteAddr() *Addr

RemoteAddr returns the connection's remote address.

func (*Conn) Write

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

Write delivers p to the peer's Read after a randomized delay, or drops it per SetDropRate.

type DeadlockError

type DeadlockError struct {
	Goroutines []DeadlockGoroutine
}

DeadlockError is returned by Run when nothing is runnable and something is unfinished.

func (*DeadlockError) Error

func (e *DeadlockError) Error() string

Error renders one block per stuck goroutine, its ID, blocking reason, and stack trace.

type DeadlockGoroutine

type DeadlockGoroutine struct {
	ID     uint64
	Reason string
	Stack  string
}

DeadlockGoroutine describes one goroutine stuck when a DeadlockError was returned.

type File

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

File implements Read, Write, Sync, Close against a backing FaultyStorage.

func (*File) Close

func (f *File) Close() error

Close is a no-op, the backing storage isn't tied to a real OS handle.

func (*File) Name

func (f *File) Name() string

Name returns the file's name, matching os.File.Name.

func (*File) Read

func (f *File) Read(p []byte) (int, error)

Read returns io.EOF at end of file, matching os.File: a short read due to EOF reports nil and the following read returns (0, io.EOF).

func (*File) ReadAt

func (f *File) ReadAt(p []byte, off int64) (int, error)

ReadAt reads at an absolute offset without touching the file offset.

func (*File) Seek

func (f *File) Seek(offset int64, whence int) (int64, error)

Seek repositions the file offset, supporting io.SeekStart, io.SeekCurrent, and io.SeekEnd relative to the materialized size.

func (*File) Sync

func (f *File) Sync() error

Sync commits pending writes to the backing storage.

func (*File) Write

func (f *File) Write(p []byte) (int, error)

Write writes at the file's current offset.

func (*File) WriteAt

func (f *File) WriteAt(p []byte, off int64) (int, error)

WriteAt writes at an absolute offset without touching the file offset.

type FileInfo

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

FileInfo is a minimal io/fs.FileInfo-shaped value.

func (FileInfo) IsDir

func (fi FileInfo) IsDir() bool

IsDir always returns false.

func (FileInfo) ModTime

func (fi FileInfo) ModTime() time.Time

ModTime returns a fixed stand-in zero time.

func (FileInfo) Mode

func (fi FileInfo) Mode() iofs.FileMode

Mode returns a fixed stand-in mode, 0644.

func (FileInfo) Name

func (fi FileInfo) Name() string

Name returns the file's name.

func (FileInfo) Size

func (fi FileInfo) Size() int64

Size returns the file's size in bytes.

func (FileInfo) Sys

func (fi FileInfo) Sys() any

Sys always returns nil.

type FileSystem

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

FileSystem is a deterministic stand-in for os.Open/os.Create, backed by FaultyStorage.

func NewFileSystem

func NewFileSystem(s *Sched, profile detsim.FaultProfile) *FileSystem

NewFileSystem builds a FileSystem where every created file shares profile.

func (*FileSystem) Create

func (fs *FileSystem) Create(name string) (*File, error)

Create creates or truncates a file backed by a fresh FaultyStorage.

func (*FileSystem) Open

func (fs *FileSystem) Open(name string) (*File, error)

Open opens an existing file, failing with fs.ErrNotExist if it was never created.

func (*FileSystem) ReadFile

func (fs *FileSystem) ReadFile(name string) ([]byte, error)

ReadFile is a whole-file convenience helper, equivalent to os.ReadFile.

func (*FileSystem) Remove

func (fs *FileSystem) Remove(name string) error

Remove deletes a file, failing with fs.ErrNotExist if it doesn't exist.

func (*FileSystem) Rename

func (fs *FileSystem) Rename(oldName, newName string) error

Rename moves a file, failing with fs.ErrNotExist if the source doesn't exist.

func (*FileSystem) Stat

func (fs *FileSystem) Stat(name string) (FileInfo, error)

Stat returns size and name info for an existing file.

func (*FileSystem) WriteFile

func (fs *FileSystem) WriteFile(name string, data []byte) error

WriteFile is a whole-file convenience helper, equivalent to os.WriteFile, and syncs.

type Listener

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

Listener accepts incoming connections at a registered address.

func (*Listener) Accept

func (l *Listener) Accept() (*Conn, error)

Accept blocks until an incoming connection arrives or the listener is closed.

func (*Listener) Addr

func (l *Listener) Addr() *Addr

Addr returns the listener's own address.

func (*Listener) Close

func (l *Listener) Close() error

Close stops the listener. Idempotent.

type Mutex

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

Mutex is a deterministic counterpart to sync.Mutex.

func NewMutex

func NewMutex(s *Sched) *Mutex

NewMutex builds a Mutex bound to s.

func (*Mutex) Lock

func (m *Mutex) Lock()

Lock blocks while the mutex is held.

func (*Mutex) TryLock

func (m *Mutex) TryLock() bool

TryLock acquires the mutex without blocking, reporting whether it succeeded.

func (*Mutex) Unlock

func (m *Mutex) Unlock()

Unlock releases the mutex, panicking if it isn't locked.

type Network

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

Network is a deterministic, in-process stand-in for net.Dial/net.Listen.

func NewNetwork

func NewNetwork(s *Sched) *Network

NewNetwork builds a Network on s's clock with a default 1-5 delivery delay range.

func (*Network) Dial

func (n *Network) Dial(addr string) (*Conn, error)

Dial connects to addr with no particular local address.

func (*Network) DialFrom

func (n *Network) DialFrom(from, to string) (*Conn, error)

DialFrom connects to to from local address from, so partition checks are address-pair-aware.

func (*Network) HealAddr

func (n *Network) HealAddr(addr string)

HealAddr restores an address isolated by PartitionAddr.

func (*Network) HealAll

func (n *Network) HealAll()

HealAll clears every mesh partition set by Partition, not PartitionAddr isolation.

func (*Network) Listen

func (n *Network) Listen(addr string) (*Listener, error)

Listen registers a listener at addr, failing if it's already in use.

func (*Network) Partition

func (n *Network) Partition(groupA, groupB []string)

Partition cuts off every address in groupA from every address in groupB, both directions.

func (*Network) PartitionAddr

func (n *Network) PartitionAddr(addr string)

PartitionAddr fully isolates addr from every other address, in both directions.

func (*Network) SetDelayRange

func (n *Network) SetDelayRange(min, max VirtualTime)

SetDelayRange sets the random delay range applied to delivered writes.

func (*Network) SetDropRate

func (n *Network) SetDropRate(rate float64)

SetDropRate sets the probability a write is silently dropped.

type NoveltySearchConfig

type NoveltySearchConfig struct {
	StartSeed int64
	MaxTrials int
	DryLimit  int
	PrefixLen int
}

NoveltySearchConfig configures NoveltySearch. Zero values fall back to defaults.

type NoveltySearchResult

type NoveltySearchResult struct {
	TrialsRun      int
	DistinctTraces int
	StoppedDry     bool
	FailedSeed     int64
	FailedErr      error
}

NoveltySearchResult reports how a NoveltySearch run went.

func NoveltySearch

func NoveltySearch(cfg NoveltySearchConfig, trial func(seed int64) (Trace, error)) NoveltySearchResult

NoveltySearch tries increasing seeds until DryLimit consecutive trials produce no new schedule shape, calling trial once per seed.

type Once

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

Once is a deterministic counterpart to sync.Once.

func NewOnce

func NewOnce(s *Sched) *Once

NewOnce builds a Once bound to s.

func (*Once) Do

func (o *Once) Do(fn func())

Do runs fn exactly once across however many goroutines call it. If fn panics, Do considers the Once done anyway, matching sync.Once.

type PanicError

type PanicError struct {
	Value any
	Stack string
}

PanicError is returned by Run when a scheduled goroutine panics.

func (*PanicError) Error

func (e *PanicError) Error() string

Error reports the recovered panic value and its stack trace.

type RWMutex

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

RWMutex is a deterministic counterpart to sync.RWMutex.

func NewRWMutex

func NewRWMutex(s *Sched) *RWMutex

NewRWMutex builds an RWMutex bound to s.

func (*RWMutex) Lock

func (m *RWMutex) Lock()

Lock blocks for exclusive access, excluding all readers and other writers.

func (*RWMutex) RLock

func (m *RWMutex) RLock()

RLock blocks for shared read access, excluded only by a current writer.

func (*RWMutex) RUnlock

func (m *RWMutex) RUnlock()

RUnlock releases one reader, panicking if there is no outstanding reader.

func (*RWMutex) Unlock

func (m *RWMutex) Unlock()

Unlock releases exclusive access, panicking if not write-locked.

type Sched

type Sched struct {
	Seed int64
	Rand *rand.Rand
	// contains filtered or unexported fields
}

Sched is the deterministic goroutine scheduler.

Example (Deadlock)
package main

import (
	"fmt"

	"github.com/arshnah/detsim/rt"
)

func main() {
	sched := rt.NewSched(1)
	a := rt.NewChan[int](sched, 0)
	b := rt.NewChan[int](sched, 0)

	sched.Go(func() {
		a.Send(1)
		b.Recv()
	})
	sched.Go(func() {
		b.Send(1)
		a.Recv()
	})

	err := sched.Run()
	if _, ok := err.(*rt.DeadlockError); ok {
		fmt.Println("deadlock detected")
	}
}

func NewSched

func NewSched(seed int64) *Sched

NewSched builds a Sched that picks among ready goroutines uniformly at random, seeded.

func NewSchedFromTrace

func NewSchedFromTrace(trace Trace) *Sched

NewSchedFromTrace builds a Sched that replays trace exactly, erroring on any mismatch.

func NewSchedFromTraceLenient

func NewSchedFromTraceLenient(trace Trace) *Sched

NewSchedFromTraceLenient replays trace but falls back to a random pick on mismatch instead of erroring.

func SchedFromEnv

func SchedFromEnv(seedEnv, traceEnv string, fallbackSeed int64) (*Sched, error)

SchedFromEnv builds a Sched from traceEnv's trace file if set, otherwise from seedEnv's seed.

func (*Sched) After

func (s *Sched) After(d VirtualTime) *Chan[VirtualTime]

After is the rt equivalent of time.After: sends the wakeup time on a fresh channel.

func (*Sched) Go

func (s *Sched) Go(fn func())

Go spawns fn as a new scheduled goroutine, eligible to run once Run picks it.

func (*Sched) GoNamed

func (s *Sched) GoNamed(name string, fn func())

GoNamed is Go with a human-readable name for the goroutine. Names show up in traces (and therefore in detsim-trace output), which is the difference between reading "g7 ran" and "g7 sender ran" when staring at a minimized failure.

func (*Sched) Now

func (s *Sched) Now() VirtualTime

Now returns the scheduler's current virtual time.

func (*Sched) Run

func (s *Sched) Run() error

Run drives the scheduler until every goroutine finishes, a deadlock is detected, a goroutine panics, or the decision limit is hit.

func (*Sched) Select

func (s *Sched) Select(cases ...SelectCase)

Select evaluates every case, picking uniformly at random among the ready ones.

func (*Sched) SetDecisionLimit

func (s *Sched) SetDecisionLimit(n int)

SetDecisionLimit caps Run to n scheduling decisions. n <= 0 means unlimited.

func (*Sched) Shutdown

func (s *Sched) Shutdown()

Shutdown releases every goroutine the scheduler spawned that hasn't finished, so a run that ended in deadlock, a panic, or a decision limit doesn't leak them for the life of the process. Call it after Run returns, never while it's running; it's safe to call repeatedly and on an already-quiesced scheduler. Shutdown is terminal: none of the released goroutines resume their code under test.

func (*Sched) Sleep

func (s *Sched) Sleep(d VirtualTime)

Sleep blocks the calling goroutine until virtual time has advanced by at least d.

func (*Sched) Trace

func (s *Sched) Trace() Trace

Trace returns the sequence of decisions this Sched has made so far, plus the goroutine labels and per-pick steps recorded along the way.

type SelectCase

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

SelectCase is one case in a Select, built by RecvCase, SendCase, or DefaultCase.

func DefaultCase

func DefaultCase(commit func()) SelectCase

DefaultCase fires if no other case is ready when Select is first evaluated.

func RecvCase

func RecvCase[T any](ch *Chan[T], commit func()) SelectCase

RecvCase is ready when ch has a value or is closed. commit performs the actual receive.

func SendCase

func SendCase[T any](ch *Chan[T], commit func()) SelectCase

SendCase is ready when ch has room and isn't closed. commit performs the actual send.

type Step

type Step struct {
	At        VirtualTime `json:"at"`
	Goroutine uint64      `json:"goroutine"`
	After     string      `json:"after,omitempty"`
}

Step describes one scheduling decision: the scheduler picked a goroutine at virtual time At, gave it a turn, and when the turn ended that goroutine either finished or parked with the reason recorded in After ("chan send", "mutex lock", "sleep", ...).

type TestingT

type TestingT interface {
	Failed() bool
	Cleanup(func())
	Logf(format string, args ...any)
}

TestingT is the minimal subset of *testing.T DumpTraceOnFailure/DumpTraceToEnvPath need.

type Trace

type Trace struct {
	Seed      int64    `json:"seed"`
	Decisions []uint64 `json:"decisions"`
	Labels    []string `json:"labels,omitempty"`
	Steps     []Step   `json:"steps,omitempty"`
}

Trace is the exact sequence of scheduling decisions a Run made. Decisions alone are enough to replay a run byte-for-byte. Labels and Steps are optional enrichment for human readers: Labels maps goroutine id to the name it was spawned with (empty for unnamed goroutines, and absent entirely for traces recorded before labels existed), Steps records when each pick happened and what the goroutine blocked on afterward.

func LoadTrace

func LoadTrace(path string) (Trace, error)

LoadTrace reads a Trace previously written by SaveTrace.

func (Trace) Name

func (t Trace) Name(id uint64) string

Name returns the recorded name for a goroutine id, or "g<N>" when the trace has no label for it.

type VirtualTime

type VirtualTime = time.Duration

VirtualTime is simulated time, an alias for time.Duration.

type WaitGroup

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

WaitGroup is a deterministic counterpart to sync.WaitGroup.

func NewWaitGroup

func NewWaitGroup(s *Sched) *WaitGroup

NewWaitGroup builds a WaitGroup bound to s.

func (*WaitGroup) Add

func (w *WaitGroup) Add(delta int)

Add adjusts the counter, panicking if it goes negative.

func (*WaitGroup) Done

func (w *WaitGroup) Done()

Done is equivalent to Add(-1).

func (*WaitGroup) Wait

func (w *WaitGroup) Wait()

Wait blocks until the counter reaches zero.

Jump to

Keyboard shortcuts

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