"Behind" means not running what the mesh would send

It meant "failed or refused". So a machine that applied cleanly and whose
declaration has since changed was not behind — and novox/hq ADR 0010's
question, did my change go out?, was answerable exactly for the machines that
broke. For every machine that worked, the answer was silence whether the change
had gone out or not, which is the thing replacing a pipeline was supposed not
to cost.

The mesh now records a digest of what it last sent each machine. A digest
rather than the declaration: it can compute what a machine should be at any
moment, and keeping a copy would be a second account of it able to disagree
with the first. What cannot be recomputed is what was actually sent.

Recorded after the send, not before — a digest kept for something that failed
to send would make the machine look current for a declaration it never
received.

Never told stays separate from out of date. The remedy is the same push and the
situations are not alike: nobody has ever asked that machine to be anything.
And a machine the mesh could not work out is not reported as waiting, because
saying so would invent a comparison — that is `plan`'s answer to give.

`status` says it and `push --behind` sends it, or the flag would know something
the person reading the status does not.
This commit is contained in:
2026-08-31 05:17:21 +02:00
parent dfca21fa55
commit 5a28434ba8
4 changed files with 290 additions and 5 deletions
+63
View File
@@ -513,3 +513,66 @@ func (i *Inventory) DoingOf(ctx context.Context, name string) (Doing, bool, erro
}
return d, true, nil
}
// RecordSent keeps a digest of the declaration a machine was last sent.
//
// **A digest rather than the declaration.** The mesh can compute what a machine should be at any
// moment; keeping a copy would be a second account of it, able to disagree with the first. What
// cannot be recomputed is what was *actually sent*, and that is the whole difference between a
// machine that is out of date and one that has never been told.
func (i *Inventory) RecordSent(ctx context.Context, node, digest string) error {
_, err := i.store.Pool().Exec(ctx,
`update node set sent = $2, sent_at = now() where id = $1`, node, digest)
return err
}
// Waiting is every machine whose declaration has changed since it was last sent one.
//
// The caller works out what each machine should be now, because only it can — resolution is the
// control plane's and this context holds records. What is answered here is the comparison.
//
// **A machine that has never been sent anything is waiting**, and says so differently: it is not
// out of date, it has never been told, and the remedy is the same push while the situation is not
// the same at all.
func (i *Inventory) Waiting(ctx context.Context, would map[string]string) ([]Machine, error) {
rows, err := i.store.Pool().Query(ctx,
`select name, coalesce(sent, ''), sent_at from node order by name`)
if err != nil {
return nil, err
}
defer rows.Close()
var out []Machine
for rows.Next() {
var m Machine
var at *time.Time
if err := rows.Scan(&m.Node, &m.Sent, &at); err != nil {
return nil, err
}
m.SentAt = at
wanted, known := would[m.Node]
if !known {
// Nothing was computed for it — it resolves to nothing, or the caller did not ask.
// Silence rather than a guess: saying "waiting" about a machine nobody worked out
// would be inventing a comparison.
continue
}
if m.Sent == wanted {
continue
}
m.Never = m.Sent == ""
out = append(out, m)
}
return out, rows.Err()
}
// Machine is one machine that has not been sent what it should be.
type Machine struct {
Node string
// Sent is the digest it last received, empty if it has never received one.
Sent string
SentAt *time.Time
// Never is true when it has never been sent anything, which is a different situation from
// being out of date and reads differently to whoever is looking.
Never bool
}