No build reaches a machine without a gate; a release plan walks what waits (hq ADR 0236)
A send carries the machine's whole declaration, so at the switch to roll the next send of anything would have carried the old default's backlog, unjudged, to every machine. A gated send now carries and judges everything waiting on its machine; every other send is refused or leaves the machine; a release plan walks what waits one machine at a time, the control node last, and one that fails holds the next until a person releases it.
This commit is contained in:
@@ -46,8 +46,8 @@ type GateVerdict struct {
|
||||
Epoch uint64
|
||||
}
|
||||
|
||||
// RecordGate writes a build's verdict. **A failed build's row is written once**: a second failure for
|
||||
// the same build is refused with ErrGateKept, which is what keeps a rollback to one per build — the row
|
||||
// RecordGate writes a build's verdict. A build that passed on one machine may still fail on the next one
|
||||
// judged; **a failed build's row is written once**: any later verdict for it is refused with ErrGateKept, which is what keeps a rollback to one per build — the row
|
||||
// is written before the rollback's send, and a controller replaced in between finds it.
|
||||
func (i *Inventory) RecordGate(ctx context.Context, v GateVerdict) error {
|
||||
epoch, err := i.actingEpoch(ctx)
|
||||
@@ -64,7 +64,7 @@ func (i *Inventory) RecordGate(ctx context.Context, v GateVerdict) error {
|
||||
on conflict (build) do update set verdict = excluded.verdict, rollback = excluded.rollback,
|
||||
why = excluded.why, previous = excluded.previous, machines = excluded.machines,
|
||||
judged_at = now(), epoch = excluded.epoch
|
||||
where build_gate.verdict = 'passed' and excluded.verdict = 'passed'`,
|
||||
where build_gate.verdict = 'passed'`,
|
||||
v.Build, v.Module, v.Commit, v.Previous, v.Plan, v.Machines, v.Verdict, v.Rollback, v.Why, v.Component,
|
||||
v.JudgingFrom, epoch)
|
||||
if err != nil {
|
||||
@@ -245,3 +245,65 @@ func (i *Inventory) BuildFingerprints(ctx context.Context, module string) (map[s
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
// Fingerprints is BuildFingerprints for every module at once: module → commit → fingerprint.
|
||||
func (i *Inventory) Fingerprints(ctx context.Context) (map[string]map[string]string, error) {
|
||||
rows, err := i.store.Pool().Query(ctx,
|
||||
`select module, commit_hash, made, coalesce(manifest::text, '') from build
|
||||
where module is not null and failed = '' and commit_hash <> '' order by at desc`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
out := map[string]map[string]string{}
|
||||
for rows.Next() {
|
||||
var module, commit, manifest string
|
||||
var made []byte
|
||||
if err := rows.Scan(&module, &commit, &made, &manifest); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if out[module] == nil {
|
||||
out[module] = map[string]string{}
|
||||
}
|
||||
if _, seen := out[module][commit]; seen {
|
||||
continue
|
||||
}
|
||||
sum := sha256.Sum256(append(append(made, 0), []byte(manifest)...))
|
||||
out[module][commit] = hex.EncodeToString(sum[:])
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
// PassedCommits is, per module, the commits a build of which passed its gate on some machine.
|
||||
func (i *Inventory) PassedCommits(ctx context.Context) (map[string]map[string]bool, error) {
|
||||
rows, err := i.store.Pool().Query(ctx, `select module, commit_hash from build_gate where verdict = 'passed'`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
out := map[string]map[string]bool{}
|
||||
for rows.Next() {
|
||||
var module, commit string
|
||||
if err := rows.Scan(&module, &commit); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if out[module] == nil {
|
||||
out[module] = map[string]bool{}
|
||||
}
|
||||
out[module][commit] = true
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
// BuildOf is the id of the newest successful build of a module from a commit; empty when none is
|
||||
// recorded (a manifest handed over by hand).
|
||||
func (i *Inventory) BuildOf(ctx context.Context, module, commit string) (string, error) {
|
||||
var id string
|
||||
err := i.store.Pool().QueryRow(ctx,
|
||||
`select id from build where module = $1 and commit_hash = $2 and failed = '' order by at desc limit 1`,
|
||||
module, commit).Scan(&id)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return "", nil
|
||||
}
|
||||
return id, err
|
||||
}
|
||||
|
||||
@@ -65,3 +65,8 @@ create table bus_step (
|
||||
outcome text not null default '' check (outcome in ('', 'done', 'failed')),
|
||||
found text not null default ''
|
||||
);
|
||||
|
||||
-- 4. A release plan (ADR 0236): the builds that wait for a gate — the backlog the old default left, and
|
||||
-- whatever a plan built and did not send — walked through the machines one at a time, each judged
|
||||
-- before the next. Its walk is kept with the plan.
|
||||
alter table release_plan add column release jsonb;
|
||||
|
||||
@@ -38,6 +38,9 @@ type Plan struct {
|
||||
Revision int64 `json:"revision"`
|
||||
// Epoch is the controller lease epoch that wrote it last; zero for a write that claimed none.
|
||||
Epoch uint64 `json:"epoch,omitempty"`
|
||||
// Release is set on a release plan (novox/hq ADR 0236): not a merge's, but the builds waiting for a
|
||||
// gate, walked through the machines one at a time.
|
||||
Release *PlanRelease `json:"release,omitempty"`
|
||||
}
|
||||
|
||||
// ErrPlanMoved is a save against a plan written by somebody else since it was read.
|
||||
@@ -103,6 +106,35 @@ type PlanGate struct {
|
||||
Rollback string `json:"rollback,omitempty"`
|
||||
// Kept says a passing verdict was written to the gate's records.
|
||||
Kept bool `json:"kept,omitempty"`
|
||||
// Carried is every module whose build moved on the judged machines with the send — the plan's own
|
||||
// module and whatever else was waiting there for a gate (novox/hq ADR 0236): each is judged here, a
|
||||
// pass is its verdict too, and one that fails is put back.
|
||||
Carried []CarriedMove `json:"carried,omitempty"`
|
||||
// Failing names the modules the last judging found wanting.
|
||||
Failing []string `json:"failing,omitempty"`
|
||||
}
|
||||
|
||||
// CarriedMove is one module's build moving on a machine with a gated send.
|
||||
type CarriedMove struct {
|
||||
Module string `json:"module"`
|
||||
Node string `json:"node"`
|
||||
From string `json:"from,omitempty"`
|
||||
To string `json:"to"`
|
||||
Build string `json:"build,omitempty"`
|
||||
}
|
||||
|
||||
// PlanRelease is a release plan's walk through the machines (novox/hq ADR 0236): every module build
|
||||
// that waits for a gate, sent one machine at a time, each judged before the next.
|
||||
type PlanRelease struct {
|
||||
Order []string `json:"order"`
|
||||
Next int `json:"next"`
|
||||
// Gate is the machine being judged; nil between machines.
|
||||
Gate *PlanGate `json:"gate,omitempty"`
|
||||
Done []string `json:"done,omitempty"`
|
||||
// Skipped are the machines not heard from when their turn came, left as they were.
|
||||
Skipped []string `json:"skipped,omitempty"`
|
||||
// By is the person who released it, empty when the mesh did.
|
||||
By string `json:"by,omitempty"`
|
||||
}
|
||||
|
||||
// The states a plan passes through.
|
||||
@@ -138,6 +170,12 @@ func (i *Inventory) SavePlan(ctx context.Context, p *Plan) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var release []byte
|
||||
if p.Release != nil {
|
||||
if release, err = json.Marshal(p.Release); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// **And how long the tier it left took** (novox/hq to-be 45 Phase 0): measured here, where the
|
||||
// plan moves, in the same transaction as the move, so no save can move a tier unmeasured or
|
||||
// measure one twice.
|
||||
@@ -153,15 +191,16 @@ func (i *Inventory) SavePlan(ctx context.Context, p *Plan) error {
|
||||
var revision int64
|
||||
err = tx.QueryRow(ctx,
|
||||
`insert into release_plan (id, repository, commit_hash, created, updated, state, tier, tiers, modules, note,
|
||||
branch, tier_entered, revision, epoch)
|
||||
values ($1, $2, $3, $4, now(), $5, $6, $7, $8, $9, $10, $11, 1, $13)
|
||||
branch, tier_entered, revision, epoch, release)
|
||||
values ($1, $2, $3, $4, now(), $5, $6, $7, $8, $9, $10, $11, 1, $13, $14)
|
||||
on conflict (id) do update set updated = now(), state = excluded.state, tier = excluded.tier,
|
||||
tiers = excluded.tiers, modules = excluded.modules, note = excluded.note, branch = excluded.branch,
|
||||
tier_entered = excluded.tier_entered, revision = release_plan.revision + 1, epoch = excluded.epoch
|
||||
tier_entered = excluded.tier_entered, revision = release_plan.revision + 1, epoch = excluded.epoch,
|
||||
release = excluded.release
|
||||
where release_plan.revision = $12
|
||||
returning revision`,
|
||||
p.ID, p.Repository, p.Commit, p.Created, p.State, p.Tier, tiers, modules, p.Note, p.Branch, entered,
|
||||
p.Revision, epoch).Scan(&revision)
|
||||
p.Revision, epoch, release).Scan(&revision)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
// The row is there and at another revision — moved since this was read, or there already
|
||||
// when this one is new: either way not this writer's to overwrite. (A plan saved before plans
|
||||
@@ -216,7 +255,7 @@ func (i *Inventory) PlanByID(ctx context.Context, id string) (Plan, error) {
|
||||
func (i *Inventory) plans(ctx context.Context, tail string) ([]Plan, error) {
|
||||
rows, err := i.store.Pool().Query(ctx,
|
||||
`select id, repository, commit_hash, created, updated, state, tier, tiers, modules, note, branch,
|
||||
coalesce(tier_entered, created), revision, coalesce(epoch, 0)
|
||||
coalesce(tier_entered, created), revision, coalesce(epoch, 0), release
|
||||
from release_plan `+tail)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -225,12 +264,17 @@ func (i *Inventory) plans(ctx context.Context, tail string) ([]Plan, error) {
|
||||
var out []Plan
|
||||
for rows.Next() {
|
||||
var p Plan
|
||||
var tiers, modules []byte
|
||||
var tiers, modules, release []byte
|
||||
var epoch int64
|
||||
if err := rows.Scan(&p.ID, &p.Repository, &p.Commit, &p.Created, &p.Updated, &p.State,
|
||||
&p.Tier, &tiers, &modules, &p.Note, &p.Branch, &p.TierEntered, &p.Revision, &epoch); err != nil {
|
||||
&p.Tier, &tiers, &modules, &p.Note, &p.Branch, &p.TierEntered, &p.Revision, &epoch, &release); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(release) > 0 {
|
||||
if err := json.Unmarshal(release, &p.Release); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
p.Epoch = uint64(epoch)
|
||||
if err := json.Unmarshal(tiers, &p.Tiers); err != nil {
|
||||
return nil, err
|
||||
|
||||
Reference in New Issue
Block a user