runbuild

package
v0.0.6 Latest Latest
Warning

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

Go to latest
Published: Oct 16, 2026 License: MIT Imports: 15 Imported by: 0

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

View Source
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.

View Source
const HandoffFormat = 1

HandoffFormat is the version of the file layout.

View Source
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.

View Source
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.

View Source
const Supported = true

Supported reports whether a run on this platform can adopt a build.

Variables

View Source
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

func Behind(run, ref Build) (n int, ok bool)

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

func BehindPhrase(run, ref Build, what string) string

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

func Exec(c *Candidate, argv, env []string) error

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

func HandoffPath(cloopDir string, pid int) string

HandoffPath is where the run with process id pid writes its handoff in the project's .cloop directory.

func Hostname

func Hostname() string

Hostname is this machine's name as records store it.

func Judge

func Judge(self, cand Build, dbSchema int) error

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

func Running(id Ident) (running, known bool)

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

func StartPath() (string, error)

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

func WriteHandoff(path string, h Handoff) error

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

func ParseReport(raw []byte) (Build, error)

ParseReport reads a `cloop version --json` report.

func Self

func Self(schema int) Build

Self is this binary's build. schema is the newest migration it embeds, which pkg/statedb knows and this package cannot import.

func (Build) IsZero

func (b Build) IsZero() bool

IsZero reports whether b says nothing at all.

func (Build) Label

func (b Build) Label() string

Label is Short with the build's place on main, when it has one: "dev+g4e35bf6 (sequence 831 on main)".

func (Build) Short

func (b Build) Short() string

Short names the build the way an operator reads it: the version, which for a build of main already carries the commit.

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

func Open(path string) (*Candidate, error)

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

func (c *Candidate) CheckArgs(ctx context.Context, timeout time.Duration, args []string) error

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) Close

func (c *Candidate) Close() error

Close releases the file.

func (*Candidate) Fd

func (c *Candidate) Fd() uintptr

Fd is the descriptor Exec runs.

func (*Candidate) ID

func (c *Candidate) ID() FileID

ID is the identity of the file held.

func (*Candidate) InstalledAt

func (c *Candidate) InstalledAt() time.Time

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

func (c *Candidate) Probe(ctx context.Context, timeout time.Duration) (Build, error)

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

type Child struct {
	PID  int
	Comm string
}

Child is a process whose parent is this one.

func LiveChildren

func LiveChildren() ([]Child, error)

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.

func (Child) String

func (c Child) String() string

type FileID

type FileID struct {
	Dev, Ino uint64
	Size     int64
	MTime    int64
}

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.

func FileIDOf

func FileIDOf(info os.FileInfo) FileID

FileIDOf reads info's identity.

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.

func TakeHandoff

func TakeHandoff(path string, self Ident, now time.Time) (*Handoff, error)

TakeHandoff reads the handoff at path for the process self and removes the file, whatever it held: it is single-use, and one that cannot be used now never will be.

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.

func IdentOf

func IdentOf(pid int) (id Ident, alive bool, err error)

IdentOf reads pid's identity. alive is false for a process that has exited but not been reaped (state Z): it holds its pid and its start time, and runs nothing.

func SelfIdent

func SelfIdent() (Ident, error)

SelfIdent is this process's identity.

func (Ident) Same

func (a Ident) Same(b Ident) bool

Same reports whether a and b name the same process. An identity missing its start time or boot names nothing, and matches nothing.

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.

func (*Owner) Clone

func (o *Owner) Clone() *Owner

Clone returns a copy that shares nothing with o.

func (*Owner) Live

func (o *Owner) Live() (live, known bool)

Live reports whether the recorded process is still running, and whether that could be told from here: a record from another machine, or a host without procfs, cannot be checked.

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.

func (*Request) Clone

func (r *Request) Clone() *Request

Clone returns a copy of r.

func (*Request) For

func (r *Request) For(id Ident) bool

For reports whether r is addressed to the process id names.

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.

func Assess

func Assess(o *Owner, ref Build) *Status

Assess compares o with ref. Nil when there is no record.

Jump to

Keyboard shortcuts

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