Documentation
¶
Overview ¶
Package sched turns goroutine interleaving into a tape decision.
fuzztape.Machine Bubble already makes time virtual and every case a goroutine-leak check, but it leaves the order in which goroutines run to the runtime. That order is the one dimension of a concurrent system a fuzzer otherwise cannot steer, and it is where the bugs that take longest to find live: a value read before a yield and written after it, a resource released on one path and not on another, a reader parked at the moment a late message arrives.
A Scheduler makes that order part of the input. Goroutines started with Scheduler.Go begin parked; a goroutine calls Scheduler.Yield at each point where another may legally interleave; and Scheduler.Step — driven by an ordinary op — releases one parked goroutine, chosen by the tape. The schedule is therefore a front-to-back sequence of tape decisions like any other, so a race reproduces exactly from its corpus file and shrinks to the shortest interleaving that still triggers it.
The cost is honest and worth stating: the system under test has to call Yield at the points where interleaving matters. That is the same bargain deterministic simulation testing always makes. A scheduler cannot preempt a goroutine that never offers a scheduling point, and nothing here weakens the case for also running under -race.
A Scheduler requires Machine.Bubble, because it uses testing/synctest.Wait to know when every other goroutine has stopped.
Index ¶
Constants ¶
const LabelKey = "fuzztape.sched"
LabelKey is the runtime/pprof goroutine label under which Scheduler.Go records a goroutine's name and spawn order.
Variables ¶
This section is empty.
Functions ¶
Types ¶
type Scheduler ¶
type Scheduler struct {
// contains filtered or unexported fields
}
A Scheduler runs registered goroutines one at a time, in an order drawn from the tape. Use New to make one; the zero value is not usable.
func New ¶
New returns a Scheduler drawing its decisions from t, and registers a cleanup that drains the remaining goroutines when the op sequence ends. Call it from fuzztape.Machine Init.
Draining matters for the diagnosis: a goroutine still parked when the sequence ends is one the schedule never released, and the bubble's exit check would report it as a leak, which it is not. A goroutine blocked on something the scheduler does not control is a different matter, and the exit check should — and still does — report it.
func (*Scheduler) Go ¶
Go starts f as a scheduled goroutine. It begins parked, so the tape chooses when it first runs, not only how it interleaves afterward.
The goroutine carries a runtime/pprof label, LabelKey, holding name and the order in which it was started — "withdraw#3" for the third. Two goroutines started under the same name are otherwise indistinguishable in a crash, and which one panicked is usually the first thing worth knowing.
The runtime prints those labels in panic tracebacks. It does so by default when the main module declares go1.27 or later, and on request otherwise: GODEBUG=tracebacklabels=1 works on every toolchain this module supports, though go1.26 and go1.27 spell the line differently.
func (*Scheduler) Order ¶
Order returns the names of the goroutines released so far, in order. It is the schedule that produced the current state, and belongs in the failure message of any invariant a schedule can break.
func (*Scheduler) Parked ¶
Parked returns the number of goroutines waiting to be scheduled, counting those started by Scheduler.Go that have not yet reached their first park. Counting them is what makes it usable from an fuzztape.Op When gate: a goroutine started by the immediately preceding op has not necessarily run yet, and an uncounted one would disable the very op meant to release it.
Scheduler.Step does not rely on this count. It waits for the bubble to quiesce first, by which point every started goroutine has parked.
func (*Scheduler) Settle ¶
func (s *Scheduler) Settle()
Settle steps until no goroutine is parked, running the schedule to completion. It is registered as a cleanup by New, and is also worth calling from an op that must observe a quiesced system.
A goroutine that parks again every time it is released never lets Settle finish; rather than hang, Settle gives up after a large number of steps and fails the test.
func (*Scheduler) Step ¶
Step releases one parked goroutine, chosen by the tape, and reports whether there was one to release. It must be called from the op goroutine — that is, from an fuzztape.Op Apply — and never from a scheduled goroutine.
Step waits on both sides of the release, and both waits are load bearing.
Before choosing, it waits for every other goroutine to be durably blocked, so the choice is made against the complete set of goroutines that could run rather than whichever happened to have parked already — a goroutine started by the immediately preceding op may not have reached its first park yet.
After releasing, it waits again, so Step returns only once the released goroutine has run to its next scheduling point or finished. That is what makes exactly one goroutine runnable at a time. Without it the op goroutine would continue while the released one was still executing, and everything after Apply — the invariant in fuzztape.Machine Check above all — would be reading the system under test concurrently with it. That is a data race in the harness, not in the system under test, and it would be reported against whichever line of the system under test happened to be running.
func (*Scheduler) Yield ¶
func (s *Scheduler) Yield()
Yield offers a scheduling point: the calling goroutine parks, and runs again only when the tape selects it. Call it wherever another goroutine may legally observe or modify shared state — around the gap between a read and the write that depends on it, before and after a lock is released, at every send and receive worth interleaving.