The mesh acts on what the catalogue decided a build meant

The builder says what it built and the catalogue decides whether that was an
upgrade. Only the control plane knows which machines run the thing, so it is
the one that acts — and what it does is a choice somebody recorded, not a
behaviour compiled in: record that they are behind, or send it, one machine at
a time or together.

Recording is the absence of an action rather than a second path: a machine not
running what the mesh would send it is already something the mesh reports.

Defaulted to recording. A mesh that rolls out everything it builds the moment
it builds it is reasonable to want and a bad thing to arrive by default — the
first module to inherit it would be the control plane, upgrading itself out
from under the push applying it.
This commit is contained in:
2026-09-13 01:55:07 +02:00
parent ca689073c4
commit 588aa424e2
7 changed files with 391 additions and 0 deletions
+75
View File
@@ -796,3 +796,78 @@ 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.
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 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 bool
}
// UpgradeOf is what to do when this module moves.
//
// A module the mesh does not hold is not an error here: the catalogue may know of modules this
// 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
err := i.store.Pool().QueryRow(ctx,
`select upgrade, upgrade_together from module where name = $1`, module).
Scan(&policy, &u.Together)
if errors.Is(err, pgx.ErrNoRows) {
return Upgrade{}, nil
}
if err != nil {
return Upgrade{}, err
}
u.RollOut = policy == "roll-out"
return u, nil
}
// SetUpgradeOf records what to do when this module moves.
func (i *Inventory) SetUpgradeOf(ctx context.Context, module string, u Upgrade) error {
policy := "record"
if u.RollOut {
policy = "roll-out"
}
tag, err := i.store.Pool().Exec(ctx,
`update module set upgrade = $2, upgrade_together = $3 where name = $1`,
module, policy, u.Together)
if err != nil {
return err
}
if tag.RowsAffected() == 0 {
return fmt.Errorf("this mesh holds no module called %s", module)
}
return nil
}
// Running is every machine assigned a module, in a stable order.
//
// **Assigned, not reported.** A machine that is assigned the module and has not applied it yet is
// exactly the machine an upgrade most needs to reach; waiting for it to report the old version
// first would mean the machines furthest behind are the last to be caught up.
func (i *Inventory) Running(ctx context.Context, module string) ([]string, error) {
rows, err := i.store.Pool().Query(ctx,
`select n.name from assignment a join node n on n.id = a.node
where a.module = $1 order by n.name`, module)
if err != nil {
return nil, err
}
defer rows.Close()
var out []string
for rows.Next() {
var name string
if err := rows.Scan(&name); err != nil {
return nil, err
}
out = append(out, name)
}
return out, rows.Err()
}
@@ -0,0 +1,26 @@
-- What the mesh should do when the catalogue says a module has been upgraded.
--
-- novox/hq ADR 0072. The builder announces a build, the catalogue decides whether that is an
-- upgrade, and the control plane is the only one of the three that knows which machines are
-- running the thing. So it is the one that acts -- and what it does has to be a decision somebody
-- made, not a behaviour compiled in.
--
-- Two separate questions, deliberately not one:
--
-- whether -- roll the new version out, or record that the machine is behind and stop there.
-- how -- one machine at a time, or all of them together.
--
-- They are separate because the safe answer to the first is not the safe answer to the second: a
-- mesh may well want every upgrade applied automatically and still never want its only two
-- machines restarted in the same breath.
--
-- Defaulted to recording rather than rolling out, and per module rather than mesh-wide. A mesh
-- that upgrades everything it builds the moment it builds it is a reasonable thing to want and a
-- terrible thing to arrive by default -- the first module to inherit it would be the control plane
-- itself, upgrading itself out from under the push that was applying it.
alter table module add column upgrade text not null default 'record'
check (upgrade in ('record', 'roll-out'));
-- Only meaningful when upgrade is 'roll-out'. Kept anyway when it is not, so turning roll-out on
-- does not silently also decide this.
alter table module add column upgrade_together boolean not null default false;