Documentation
¶
Overview ¶
Package bench runs one command on a Linux bench against a copy of a local tree: `nova-ci bench run`.
THE HURT. Every card, every read and the coordinator ran Go on a bench with a recipe typed into each brief: ssh <bench> 'mkdir -p <d>' && rsync -a --delete <repo>/ <bench>:<d>/repo/ && ssh <bench> 'cd <d>/repo && export GOCACHE=... GOFLAGS=-mod=readonly NOVA_TEST_NO_HOST=1 && nice -n 19 go ...', a second bench by hand when the first did not answer, then ssh <bench> 'rm -rf <d>'. Each copy of the recipe was a place to get a path, a variable or the cleanup wrong (the owner, 2026-10-05: "We need to get away from these one shot shell scripts."). This package is the recipe, once, behind a Transport the tests fake.
THE RUN, per host, in order:
- make: one ssh that makes the root and a fresh run directory under it with mktemp -d, so two runs never share a directory and the run knows exactly the one it made. A host that does not answer (ssh's own exit 255, or ssh not starting) is passed over for the fallback; a host that answers and refuses is the end of the run, never a reason to try another.
- copy: the tree into <run>/repo, .git left out unless WithGit; or, with a Stage, the tree staged from the bench's own mirror at one commit (stage_mirror.go), so only the sha crosses the wire. Either way the run's Stage says what it did, and a refusal is a *StageError naming the step, its exit, its stderr's tail and the wall time.
- exec: cd <run>/repo, then the command under nice -n 19 with GOCACHE, GOFLAGS=-mod=readonly and NOVA_TEST_NO_HOST=1, its output streamed to the caller as it arrives. Its exit status is the run's.
- remove: rm -rf of the run directory step 1 printed and nothing else, whatever happened in 2 and 3, under a context the caller's cancellation does not end, so an interrupted run still cleans up after itself.
THE MODEL. tla/BenchRun.tla is the run as a state machine: TLC holds OnlyTheMadeDirIsRemoved, AtMostOneHostAnswers, FallbackOnlyOnNoAnswer, ExitIsTheCommands and NothingLeftBehind on two hosts (MCBenchRun.cfg), and its reversed witness MCBenchRunBrokenNoRemove.cfg, a failed copy that skips the deferred remove, must break NothingLeftBehind.
Index ¶
- Constants
- Variables
- func CheckHost(h string) error
- func CheckPath(flag, p string) error
- func CopyLine(dst string) string
- func ExecLine(dir, cache string, argv []string) string
- func GateRef(key, sha string) string
- func MakeLine(root string) string
- func MirrorDir(name string) string
- func Quote(s string) string
- func RemoveLine(dir string) string
- func StageLine(s MirrorStage, dst string) string
- func WithGateRef(ctx context.Context, g RefGit, sha, ref string, gate func() error) error
- func WriteTree(w io.Writer, src string, withGit bool) error
- type Exec
- func (e Exec) Copy(ctx context.Context, host, src, dst string, withGit bool, stderr io.Writer) error
- func (Exec) CopySized(ctx context.Context, host, src, dst string, withGit bool, stderr io.Writer) (int64, error)
- func (Exec) Shell(ctx context.Context, host, line string, stdout, stderr io.Writer) (int, error)
- type MirrorStage
- type Options
- type RefGit
- type Result
- type Stage
- type StageError
- type StageSkips
- type Transport
Constants ¶
const ( // DefaultRoot is where the run directories are made. DefaultRoot = "nova-bench/runs" // DefaultCache is the bench's Go build cache, shared by every run on it. DefaultCache = "nova-bench/cache/go-build" // Nice is the niceness of the command: the bench is shared with the work // of every other card on it. Nice = 19 // NoAnswer is ssh's own exit status: it could not reach or log in to the // host, so the remote command never ran. NoAnswer = 255 // StepBudget bounds the make and remove steps, which are one short remote // command each; the copy and the command itself are bounded only by the // caller's context. StepBudget = 2 * time.Minute )
Defaults of the paths on the bench, relative to the login's home.
const GateRefPrefix = "refs/nova-gate/"
GateRefPrefix is where the lander's temporary refs live on the remote.
const NoMirror = 3
NoMirror is the stage line's own exit when the bench keeps no mirror at the path.
const SkipAfter = 2
SkipAfter is how many stage failures in one land pass pass a bench over for the rest of it.
Variables ¶
var Env = []string{"GOFLAGS=-mod=readonly", "NOVA_TEST_NO_HOST=1"}
Env is the environment the command runs under, after GOCACHE: the same on every bench, so a run is a run wherever it lands.
var ErrNoBench = errors.New("no bench answered")
ErrNoBench is a run in which no host answered.
var SSHOptions = []string{"-o", "BatchMode=yes", "-o", "ConnectTimeout=10", "-o", "ForwardAgent=no"}
SSHOptions are on every ssh this package starts: BatchMode so a missing key is a refusal rather than a prompt nobody answers, ConnectTimeout so a sleeping bench costs seconds and the fallback is tried, and no agent forwarded to a bench.
Functions ¶
func CheckHost ¶
CheckHost refuses a bench name ssh could read as an option or as more than one host.
func CheckPath ¶
CheckPath refuses a bench path that is not plain: one the remote shell would read differently from how it is written (~, a blank, a quote), one that climbs (..), or the home or the filesystem root itself.
func GateRef ¶
GateRef is the temporary ref of one gate: key (the batch's stream) made a ref name component, and the sha's first twelve, so two gates never share one.
func MirrorDir ¶
MirrorDir is the bench's mirror of the repository named name, relative to the login's home: the directory swarm.MirrorPath names under the bench's home.
func RemoveLine ¶
RemoveLine is the remote line of the remove step: the one directory made.
func StageLine ¶
func StageLine(s MirrorStage, dst string) string
StageLine is the remote line of the mirror stage into dst: the mirror must exist; the commit is fetched from the mirror's origin (else Remote) by the temporary ref unless the mirror holds it already, no ref or FETCH_HEAD written; then dst is a clone of the mirror borrowing its objects, checked out detached at the sha.
func WithGateRef ¶
WithGateRef pushes sha to ref, runs gate, and deletes ref whatever gate did, under a context the caller's cancellation does not end (bounded by StepBudget), so an interrupted gate still removes its ref. A push that fails is gate not run, the error saying so; a delete that fails is joined to gate's error.
func WriteTree ¶
WriteTree writes the tree under src to w as a tar stream, paths relative to src: directories, regular files and symlinks, each with its mode. The .git at the top of the tree is left out unless withGit; anything else that is not one of those three (a socket, a device) is refused, never skipped.
Types ¶
type Exec ¶
type Exec struct{}
Exec is the production transport: the system ssh, and nothing else. The copy is a tar stream this process writes onto ssh's stdin, so no rsync or local tar is needed here and the bench needs only tar. The lander's tree gate does not copy: it stages from the bench's mirror (stage_mirror.go).
func (Exec) Copy ¶
func (e Exec) Copy(ctx context.Context, host, src, dst string, withGit bool, stderr io.Writer) error
Copy copies src's contents into host:dst: dst is made, and the tree goes over as a tar stream on ssh's stdin that the bench's tar unpacks there.
func (Exec) CopySized ¶
func (Exec) CopySized(ctx context.Context, host, src, dst string, withGit bool, stderr io.Writer) (int64, error)
CopySized is Copy, and how many bytes of tar stream it wrote. A refusal names its cause: WriteTree's own (a socket, a device), else the bench's tar exit, whose stderr the caller's stderr receives.
type MirrorStage ¶
type MirrorStage struct {
// Mirror is the bare mirror on the bench, relative to the login's home or absolute
// (MirrorDir).
Mirror string
// Remote is the URL the ref is fetched from when the mirror names no origin of its
// own; the mirror's origin is preferred, since that is the URL the bench can read.
Remote string
// Ref is the temporary ref the lander pushed (GateRef); Sha the commit it names, the
// tree the run's command runs in.
Ref, Sha string
}
MirrorStage is a run's tree staged from the bench's mirror in place of a copy.
func (*MirrorStage) Validate ¶
func (s *MirrorStage) Validate() error
Validate refuses a stage the remote line could read as more than it says.
type Options ¶
type Options struct {
// Hosts are the benches in the order they are tried: the host, then the
// fallback. Only a host that does not answer moves the run to the next.
Hosts []string
// Dir is the local tree to copy; unread when Stage is set.
Dir string
// Stage, when set, stages the tree from the bench's mirror in place of the copy.
Stage *MirrorStage
// Root is the directory on the bench the run directory is made in, and
// Cache is GOCACHE there; each is relative to the login's home or
// absolute. Empty is the default.
Root, Cache string
// WithGit copies the tree's .git as well.
WithGit bool
// Argv is the command, run in the copy.
Argv []string
// Stdout and Stderr receive the command's output as it arrives; Notes
// receives the run's own lines (a host passed over).
Stdout, Stderr, Notes io.Writer
// Now is the run's clock, for the stage's wall time; nil is the wall's.
Now func() time.Time
// Staged, when set, is told the stage as it ends, refused or not, before the
// command runs: the lander's beat says it while the command runs.
Staged func(Stage)
}
Options is one run.
type RefGit ¶
type RefGit interface {
Push(ctx context.Context, sha, ref string) error
Delete(ctx context.Context, ref string) error
}
RefGit is the lander's side of a mirror stage: the gated commit pushed to a temporary ref on the remote the bench fetches from, and that ref deleted after.
type Result ¶
type Result struct {
// Host is the bench that ran the command; RunDir the directory made there.
Host, RunDir string
// Code is the command's exit status.
Code int
// Removed is whether the run directory was removed; RemoveErr says why not.
Removed bool
RemoveErr error
// Stage is what the copy or the mirror stage did (Stage.Line).
Stage Stage
}
Result is what a run did.
func Run ¶
Run is one run: the first host that answers makes a run directory, takes the copy, runs the command and has the directory removed. The error is a run that could not reach the command (no host answered, the copy failed, a step was refused); a command that ran and failed is Result.Code, never an error.
type Stage ¶
type Stage struct {
// Host is the bench; Via how the tree got there: "mirror" or "tar".
Host, Via string
// Bytes is what crossed the wire from this machine (the tar stream; nothing but the
// line for a mirror stage); Wall the stage's time on the run's clock.
Bytes int64
Wall time.Duration
// Err is the stage's refusal, nil when the tree is staged.
Err *StageError
}
Stage is what a run's stage did, for the lander's lines.
type StageError ¶
StageError is a stage that did not put the tree on the bench: the step that refused, its exit (0 when it has none), the tail of its stderr and the wall time it took.
func (*StageError) Error ¶
func (e *StageError) Error() string
Error is the refusal's one line: "copy refused: <host> after <t>s: <step>[ exit <n>][: <err>][: <stderr tail>]".
func (*StageError) Unwrap ¶
func (e *StageError) Unwrap() error
type StageSkips ¶
type StageSkips struct {
// contains filtered or unexported fields
}
StageSkips is one land pass's record of the benches whose stage failed. Safe for the pass's streams at once.
func (*StageSkips) Fail ¶
func (s *StageSkips) Fail(host, why string) (skipped bool)
Fail records one failed stage on host, why its refusal; skipped says host is now passed over for the rest of the pass.
func (*StageSkips) Ring ¶
func (s *StageSkips) Ring(ring []string) (keep, notes []string)
Ring is ring without the hosts passed over this pass, in its order, and one line for each host left out.
type Transport ¶
type Transport interface {
// Shell runs one line under the login shell on host, streaming its output.
// The int is the remote status (NoAnswer when ssh itself failed); the error
// is only for a transport that could not start at all.
Shell(ctx context.Context, host, line string, stdout, stderr io.Writer) (int, error)
// Copy copies the contents of the local directory src into dst on host
// (dst is created; its parent exists), leaving .git out unless withGit.
Copy(ctx context.Context, host, src, dst string, withGit bool, stderr io.Writer) error
}
Transport is what a run asks of the wire. The production transport is ssh alone (Exec), the copy a tar stream on its stdin; the tests pass a fake that records each call.