Gate a release plan's first machine and roll a failed build back there (hq ADR 0236)

A build that reported applied was sent everywhere; one that then did nothing, served
no tools or broke its machine's word reached every machine. Now the first machine is
judged by the component's health (the core's definitions, as doctor probes H-*, or a
module's own) three times over two minutes within ten; a failing gate puts the previous
build back there once, marks the build, and says it as a condition and an event.
Upgrades roll out by default; the bus is a planned step; a module deleted at its
source is not built (the public-acme plan failure).
This commit is contained in:
jochen
2026-10-06 18:56:54 +02:00
parent 81f497e5dd
commit d9289ef6d4
41 changed files with 3297 additions and 61 deletions
+120 -19
View File
@@ -1194,15 +1194,62 @@ func (i *Inventory) Catalogued(ctx context.Context) ([]Entry, error) {
// providedBy is what the source column says for a module the control plane ships.
const providedBy = "the control plane"
// Upgrade is what the mesh decided to do when a module's current version moves.
// Upgrade is what the mesh does when a module's current version moves (ADR 0162 §3, ADR 0235).
type Upgrade struct {
// RollOut is true when the machines running it should be sent the new version. False means
// record it and stop — which needs no record of its own, because a machine not running what
// the mesh would send it is already something the mesh reports.
// RollOut is true when the machines running it are sent the new version: one machine first, judged
// at the gate, then the rest (ADR 0218, ADR 0235). False means record it and stop — the machines
// running it are behind until a person pushes, which the mesh already reports.
RollOut bool
// Together is true when every machine running it is sent the new version at once, rather than
// one after another. Only meaningful when RollOut is.
// Together is true when every machine running it is sent the new version at once. Only meaningful
// when RollOut is.
Together bool
// From is where the policy came from: a person, the module, the bus, its irreplaceable data, or the
// default (catalogue.From*); Why is the reason said with it.
From string
Why string
// By is the person who chose it, when one did.
By string
}
// Policy is the policy as a word: roll, together or record.
func (u Upgrade) Policy() string {
switch {
case !u.RollOut:
return catalogue.PolicyRecord
case u.Together:
return catalogue.PolicyTogether
}
return catalogue.PolicyRoll
}
// upgradeFrom is a module's policy from what the store holds of it: a person's choice, over the module's
// own word, over the default — except that the bus is never rolled out, whoever says so (ADR 0235).
func upgradeFrom(chosen *string, together bool, why, by string, manifest []byte) Upgrade {
var m catalogue.Manifest
// Leniently: a policy is read from what was registered, and a manifest registered before a field it
// carries was known is still a manifest whose bus and data can be read.
_ = json.Unmarshal(manifest, &m)
policy, from, said := catalogue.DerivedUpgrade(m)
if from != catalogue.FromBus && chosen != nil {
policy, from, said = catalogue.PolicyRecord, catalogue.FromPerson, why
if *chosen == "roll-out" {
policy = catalogue.PolicyRoll
if together {
policy = catalogue.PolicyTogether
}
}
}
u := Upgrade{From: from, Why: said}
if from == catalogue.FromPerson {
u.By = by
}
switch policy {
case catalogue.PolicyRoll:
u.RollOut = true
case catalogue.PolicyTogether:
u.RollOut, u.Together = true, true
}
return u
}
// UpgradeOf is what to do when this module moves.
@@ -1211,30 +1258,80 @@ type Upgrade struct {
// mesh has never registered, and being told one of them moved is information, not a fault. The
// answer is the safe one — record it — because there is nothing to roll out to.
func (i *Inventory) UpgradeOf(ctx context.Context, module string) (Upgrade, error) {
var u Upgrade
var policy string
var chosen *string
var together bool
var why, by string
var manifest []byte
err := i.store.Pool().QueryRow(ctx,
`select upgrade, upgrade_together from module where name = $1`, module).
Scan(&policy, &u.Together)
`select upgrade, upgrade_together, upgrade_why, upgrade_by, manifest from module where name = $1`, module).
Scan(&chosen, &together, &why, &by, &manifest)
if errors.Is(err, pgx.ErrNoRows) {
return Upgrade{}, nil
return Upgrade{From: catalogue.FromDefault, Why: "the mesh holds no such module"}, nil
}
if err != nil {
return Upgrade{}, err
}
u.RollOut = policy == "roll-out"
return u, nil
return upgradeFrom(chosen, together, why, by, manifest), nil
}
// SetUpgradeOf records what to do when this module moves.
// Upgrades is every module's policy, by name: what `upgrade` lists.
func (i *Inventory) Upgrades(ctx context.Context) (map[string]Upgrade, error) {
rows, err := i.store.Pool().Query(ctx,
`select name, upgrade, upgrade_together, upgrade_why, upgrade_by, manifest from module`)
if err != nil {
return nil, err
}
defer rows.Close()
out := map[string]Upgrade{}
for rows.Next() {
var name, why, by string
var chosen *string
var together bool
var manifest []byte
if err := rows.Scan(&name, &chosen, &together, &why, &by, &manifest); err != nil {
return nil, err
}
out[name] = upgradeFrom(chosen, together, why, by, manifest)
}
return out, rows.Err()
}
// ErrBusIsPlanned is a person asking the bus to be rolled out: its upgrade is a planned step (ADR 0235).
var ErrBusIsPlanned = errors.New("the bus is never rolled out: its upgrade is a planned step a person starts " +
"with `bus upgrade`, which snapshots its streams first and checks them after")
// SetUpgradeOf records a person's choice of what to do when this module moves, with why and who. A
// record says why; the bus is refused a roll-out.
func (i *Inventory) SetUpgradeOf(ctx context.Context, module string, u Upgrade) error {
policy := "record"
if u.RollOut {
policy = "roll-out"
var manifest []byte
err := i.store.Pool().QueryRow(ctx, `select manifest from module where name = $1`, module).Scan(&manifest)
if err == nil {
var m catalogue.Manifest
if json.Unmarshal(manifest, &m) == nil && catalogue.ProvidesBus(m) {
return fmt.Errorf("%s provides the mesh's bus: %w", module, ErrBusIsPlanned)
}
}
}
tag, err := i.store.Pool().Exec(ctx,
`update module set upgrade = $2, upgrade_together = $3 where name = $1`,
module, policy, u.Together)
`update module set upgrade = $2, upgrade_together = $3, upgrade_why = $4, upgrade_by = $5 where name = $1`,
module, policy, u.Together, u.Why, u.By)
if err != nil {
return err
}
if tag.RowsAffected() == 0 {
return fmt.Errorf("this mesh holds no module called %s", module)
}
return nil
}
// ClearUpgradeOf takes a person's choice back: the module's own word, or the default, decides again.
func (i *Inventory) ClearUpgradeOf(ctx context.Context, module string) error {
tag, err := i.store.Pool().Exec(ctx,
`update module set upgrade = null, upgrade_together = false, upgrade_why = '', upgrade_by = '' where name = $1`,
module)
if err != nil {
return err
}
@@ -1258,18 +1355,22 @@ type CurrentBuild struct {
// it carried, and what a push compares a machine's last send against.
func (i *Inventory) CurrentBuilds(ctx context.Context) (map[string]CurrentBuild, error) {
rows, err := i.store.Pool().Query(ctx,
`select name, coalesce(built_from, ''), upgrade = 'roll-out' from module`)
`select name, coalesce(built_from, ''), upgrade, upgrade_together, upgrade_why, upgrade_by, manifest from module`)
if err != nil {
return nil, err
}
defer rows.Close()
out := map[string]CurrentBuild{}
for rows.Next() {
var name string
var name, why, by string
var chosen *string
var together bool
var manifest []byte
var b CurrentBuild
if err := rows.Scan(&name, &b.Commit, &b.RollOut); err != nil {
if err := rows.Scan(&name, &b.Commit, &chosen, &together, &why, &by, &manifest); err != nil {
return nil, err
}
b.RollOut = upgradeFrom(chosen, together, why, by, manifest).RollOut
out[name] = b
}
return out, rows.Err()