Documentation
¶
Overview ¶
Package runbuild is what a long-lived `cloop run` knows about its own build, and what lets it move to a newer one without ending (Task 20389).
A deploy replaces the hub's binary, and a hub restart deliberately keeps runs alive (KillMode=process plus adoption). So a run in auto-evolve never ends and never upgrades: on 2026-10-06 this repository's own run was 132 commits behind the deployed hub, executing an orchestrator that predated "done means committed" and the disk floor, against a database six migrations ahead of what it knew. Nothing showed it, and only a manual restart fixed it.
Two halves live here:
Visibility. A run records its build — version, commit, sequence on main, and the schema it embeds — in its project's run-owner record (Owner) when it starts. Anything that reads the project can then compare it with its own build (Assess): the hub's Overview, `cloop status`, hub doctor.
Adoption. At a task boundary a host-process run may replace its own image with the binary at the path it was started from, keeping its pid, its process start time, its stdout pipe and everything the hub and the local driver identify it by. Candidate holds that binary open so the file that is validated is the file that is executed; Judge decides whether it may be adopted; Handoff carries what the new image needs to continue the same run rather than start a new one.
Everything here is the standard library and pkg/version, so pkg/state can carry the record without dragging the orchestrator in.
Index ¶
- Constants
- Variables
- func Behind(run, ref Build) (n int, ok bool)
- func BehindPhrase(run, ref Build, what string) string
- func Exec(c *Candidate, argv, env []string) error
- func HandoffPath(cloopDir string, pid int) string
- func Hostname() string
- func Judge(self, cand Build, dbSchema int) error
- func RestoreComm(path string)
- func Running(id Ident) (running, known bool)
- func StartPath() (string, error)
- func WriteHandoff(path string, h Handoff) error
- type Build
- type Candidate
- func (c *Candidate) CheckArgs(ctx context.Context, timeout time.Duration, args []string) error
- func (c *Candidate) Close() error
- func (c *Candidate) Fd() uintptr
- func (c *Candidate) ID() FileID
- func (c *Candidate) InstalledAt() time.Time
- func (c *Candidate) Probe(ctx context.Context, timeout time.Duration) (Build, error)
- type Child
- type FileID
- type Handoff
- type Ident
- type Owner
- type Request
- type Status
Constants ¶
const EnvHandoff = "CLOOP_RUN_HANDOFF"
EnvHandoff names the handoff file in the environment of an adopting run's new image. Its presence alone says "this process is continuing a run", even if the file turns out to be unreadable: the new image must then still skip everything a fresh run does at its start.
const HandoffFormat = 1
HandoffFormat is the version of the file layout.
const MaxHandoffAge = 10 * time.Minute
MaxHandoffAge is how old a handoff may be when the new image reads it. The exec takes milliseconds; a file minutes old was not written for this start.
const ProbeTimeout = 10 * time.Second
ProbeTimeout bounds `<binary> version --json`. A build that cannot say what it is within it is refused rather than waited for: the run is holding a task boundary open while it asks.
const Supported = true
Supported reports whether a run on this platform can adopt a build.
Variables ¶
var ErrSameBinary = errors.New("the path still holds the binary this run is executing")
ErrSameBinary means the path still holds the binary this process runs: there is nothing to adopt.
Functions ¶
func Behind ¶
Behind reports how many builds run trails ref along main, and whether that could be told at all.
The count is the difference of their sequences, so it is exact for any two stamped builds of main and zero (not negative) when run is level or ahead. Without both sequences only identity can be told: the same commit is level, anything else is not comparable — a "dev+g<sha>" pair has no order, and guessing one would put a wrong number on an operator's screen.
func BehindPhrase ¶
BehindPhrase renders Behind for a sentence: "132 builds behind this hub", "level with this hub", or "not comparable with this hub". what names the reference ("this hub", "this cloop").
func Exec ¶
Exec replaces this process's image with the candidate's file. It returns only if the exec failed; the process is then unchanged.
The file is executed through /proc/self/fd/<n>, the descriptor Open took and Probe asked — see Candidate. The descriptor is close-on-exec, which is fine for an ELF binary (Open refuses anything else): the kernel opens the file before it closes the descriptor. What the process keeps is everything the hub and the local driver know it by: its pid and start time, its parent, and its stdout and stderr — the hub's live log of the run.
func HandoffPath ¶
HandoffPath is where the run with process id pid writes its handoff in the project's .cloop directory.
func Judge ¶
Judge decides whether a run on self may adopt cand, given the schema the project's database is at. Nil means adopt; otherwise the error says why not, in words for the run's journal.
The sequence must be strictly greater: an equal one is the same build (or a rebuild of it), an earlier one a rollback, and a missing one cannot be ordered at all — and a run that moved to any of those would be the one thing worse than a run that is behind. The schema must be at least the database's: a build that embeds less would be running against tables from its future.
func RestoreComm ¶
func RestoreComm(path string)
RestoreComm puts the process's name back after an adoption.
The new image was executed through /proc/self/fd/<n> (see Exec), so the kernel named the process "<n>" — which is what ps, top and pgrep would show for the rest of the run. Writing /proc/self/comm renames the main thread, which is what all of them read. Best effort: a run named "7" works, it is only harder to find.
func Running ¶
Running reports whether the process id names is still executing. known is false when procfs could not answer — no /proc, or a pid namespace that hides it — and then running means nothing.
func StartPath ¶
StartPath is the path this process's binary was started from: the one a deploy replaces, and the one adoption validates. Not /proc/self/exe: that is the file the process *is*, which after a deploy is the deleted old binary — and for a run started through a symlink a deploy re-points, the old target.
func WriteHandoff ¶
WriteHandoff stores h at path, readable only by this user, replacing the file atomically so the new image can never read half of one.
Types ¶
type Build ¶
type Build struct {
// Version is version.String(): "v0.0.4", "dev+g4e35bf6".
Version string `json:"version"`
// Commit is the full commit the build was made from, when it knows it.
Commit string `json:"commit,omitempty"`
// Sequence is the commit's first-parent position on main (Task 20380),
// zero for a build that was not stamped with one. It is the only ordering
// two builds of main have: two "dev+g<sha>" versions do not compare.
Sequence int `json:"sequence,omitempty"`
// Schema is the newest database migration the build embeds, zero when the
// build did not say (`cloop version --json` reports it since Task 20389).
Schema int `json:"schema,omitempty"`
}
Build is one cloop build, in the terms adoption compares them by.
func ParseReport ¶
ParseReport reads a `cloop version --json` report.
func Self ¶
Self is this binary's build. schema is the newest migration it embeds, which pkg/statedb knows and this package cannot import.
type Candidate ¶
type Candidate struct {
Path string
// contains filtered or unexported fields
}
Candidate is the binary at a run's start path, held open.
Holding it is the point. A deploy writes the new binary in place (install unlinks the path and copies), so between a check of the path and an execve of the path the file there can be another one — or a half-written one. The probe and the exec both go through this descriptor, so the file that answered `version --json` is, byte for byte, the file that is executed, whatever happens to the path meanwhile.
func Open ¶
Open opens the binary at path for adoption. It refuses, before running anything, a path that does not hold a regular executable ELF file, one that someone other than root or this process's user could have written, and the binary this process already runs (ErrSameBinary).
func (*Candidate) CheckArgs ¶
CheckArgs asks the held file whether it accepts args — the run's own command line — by parsing them with --help appended: cobra parses every flag and prints usage before any pre-run hook, so a flag the new build dropped or renamed is an error here instead of a run that dies the moment it is re-executed.
func (*Candidate) InstalledAt ¶
InstalledAt is when the file held last changed as an inode — its ctime, which, unlike its mtime, no copy tool can set back: the moment a deploy put it in place.
func (*Candidate) Probe ¶
Probe runs `<the held file> version --json` and reads what it says it is.
The child runs the held descriptor, passed as its fd 3, through /proc/self/fd/3 — never the path. It runs in "/" in its own process group, with this process's environment less the handoff variable, and is killed with its group if it has not answered within timeout. `version` has no side effects by design (cmd/version.go overrides the root pre-run), which is what makes running an unvalidated binary to ask it acceptable at all.
type Child ¶
Child is a process whose parent is this one.
func LiveChildren ¶
LiveChildren lists the processes this one started that are still running.
It exists because an execve does not take children with it: they stay this pid's children, but the new image knows nothing of them — no goroutine waits for them, no pipe reads their output, and when they exit nobody reaps them. A run only replaces its image when this is empty. Exited-but-unreaped children are not counted: they run nothing.
type FileID ¶
FileID identifies the file a path held at one moment, so a refusal is remembered for that file and not repeated at every boundary — while a later deploy (a new inode, or new contents) is looked at afresh.
type Handoff ¶
type Handoff struct {
Format int `json:"format"`
// Run is the process that wrote the file — and, since an exec keeps the
// process, the one that must read it.
Run Ident `json:"run"`
WrittenAt time.Time `json:"written_at"`
// Reason says why the run adopted: who requested it, or that the project
// follows new builds. RequestID and RequestedBy name a request.
Reason string `json:"reason"`
RequestID string `json:"request_id,omitempty"`
RequestedBy string `json:"requested_by,omitempty"`
// From is the image that wrote this, To what its probe reported for the
// binary it executed, and Exe the path that binary was found at.
From Build `json:"from"`
To Build `json:"to"`
Exe string `json:"exe"`
// RunID is the execution id the run's tasks are stamped with; the new
// image keeps it so the run's audit rows still join to its leases.
RunID string `json:"run_id,omitempty"`
// ProcessStart is when the process first started. The placement record
// the hub wrote is judged stale against it, so a new image that took its
// own start time instead would stop attributing its tasks to the executor
// and identity the hub dispatched them under.
ProcessStart time.Time `json:"process_start"`
// SessionStart and SessionStartStep are where this session began, for
// --steps and the session summary; Deadline is --timeout's.
SessionStart time.Time `json:"session_start"`
SessionStartStep int `json:"session_start_step"`
Deadline time.Time `json:"deadline,omitzero"`
// The run's counters: the evolve iteration it had reached, the failures
// and aborts in a row its stop rule counts, and the empty evolve rounds in
// a row.
EvolveStep int `json:"evolve_step"`
ConsecutiveErrors int `json:"consecutive_errors"`
ConsecutiveEmptyEvolves int `json:"consecutive_empty_evolves"`
// Status and PauseReason are the run's status as it handed over.
Status string `json:"status"`
PauseReason json.RawMessage `json:"pause_reason,omitempty"`
// Reexecs is how many adoptions preceded this one.
Reexecs int `json:"reexecs"`
// Parallel says which loop handed over.
Parallel bool `json:"parallel,omitempty"`
}
Handoff is what an adopting run carries across its exec.
type Ident ¶
type Ident struct {
PID int `json:"pid"`
StartTicks uint64 `json:"start_ticks"`
BootID string `json:"boot_id"`
}
Ident names one process for as long as it lives.
type Owner ¶
type Owner struct {
Ident
// Host is the machine the process runs on. Its identity can only be
// checked from the same machine.
Host string `json:"host,omitempty"`
// Exe is the path the run was started from: what a deploy replaces and
// what adoption validates (see StartPath).
Exe string `json:"exe,omitempty"`
// Executor is the kind of executor the run was placed on, as the
// placement record says ("localprocess" for a run on this host).
Executor string `json:"executor,omitempty"`
// Adoptable says the run can adopt a newer build at a task boundary: a
// host-process run of a build that knows how. Device and container runs
// keep their own upgrade paths.
Adoptable bool `json:"adoptable,omitempty"`
// RunID is the execution id the run's tasks are stamped with.
RunID string `json:"run_id,omitempty"`
// StartedAt is when the process started. It survives adoptions, as the
// pid does.
StartedAt time.Time `json:"started_at"`
// Build is the build the process runs now, and BuildSince when that image
// took over: StartedAt for the first, the adoption for later ones.
Build Build `json:"build"`
BuildSince time.Time `json:"build_since"`
// Reexecs counts the adoptions, and Previous is the build the last one
// replaced.
Reexecs int `json:"reexecs,omitempty"`
Previous *Build `json:"previous,omitempty"`
}
Owner is the run-owner record: which process is running a project's plan, and on which build. The run writes it when it starts and again each time it adopts a newer build; it is stored with the project (the run_owner metadata key) and nothing but the run writes it, so a dashboard save or an older binary's save never touches it.
type Request ¶
type Request struct {
ID string `json:"id"`
RequestedAt time.Time `json:"requested_at"`
// RequestedBy is the identity that asked, as the hub's audit trail names it.
RequestedBy string `json:"requested_by,omitempty"`
// Run is the process the request is for.
Run Ident `json:"run"`
// Hub is the build of the hub that asked: what the requester expected
// the run to move to.
Hub Build `json:"hub"`
}
Request is a one-shot "adopt the hub's build at the next task boundary", stored with the project (the run_adopt_request metadata key) for the run to find at its next boundary.
It names the process it is for. A request is about one run: one left behind by a run that has since ended must not make the next run in the project replace its image the moment it reaches a boundary.
type Status ¶
type Status struct {
Build Build `json:"build"`
// Reference is the build the record is compared with: the reading
// process's own.
Reference Build `json:"reference"`
// Behind is how many builds the run trails Reference; Comparable says
// whether that could be told (see Behind).
Behind int `json:"behind"`
Comparable bool `json:"comparable"`
// Live says the recorded process is still running; LiveKnown is false
// when that could not be checked from here.
Live bool `json:"live"`
LiveKnown bool `json:"live_known"`
PID int `json:"pid,omitempty"`
// Since is when the running image took over (BuildSince).
Since time.Time `json:"since,omitzero"`
StartedAt time.Time `json:"started_at,omitzero"`
Reexecs int `json:"reexecs,omitempty"`
Previous *Build `json:"previous,omitempty"`
Executor string `json:"executor,omitempty"`
Adoptable bool `json:"adoptable"`
}
Status is a run-owner record as a reader sees it: compared with the reader's own build — the hub's on the dashboard, the CLI's in `cloop status` — and checked for liveness.