"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:
@@ -155,3 +155,93 @@ func TestWhatANodeSaidGoesWhenTheNodeDoes(t *testing.T) {
|
||||
t.Fatalf("%d report(s) outlived the machine", left)
|
||||
}
|
||||
}
|
||||
|
||||
// "Behind" must mean not running what the mesh would send, not only "failed".
|
||||
//
|
||||
// novox/hq ADR 0010 names losing "did my change go out?" as the real risk of replacing a pipeline
|
||||
// with a comparison. With behind meaning only failed-or-refused, that question was answerable
|
||||
// exactly for the machines that broke — and for every machine that worked, the answer was silence
|
||||
// whether the change had gone out or not.
|
||||
func TestAMachineIsWaitingWhenWhatItWasSentIsNotWhatItShouldBe(t *testing.T) {
|
||||
inv := ForTest(t)
|
||||
ctx := t.Context()
|
||||
anchor, err := inv.AddNode(ctx, "anchor")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := inv.AddNode(ctx, "laptop"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Never sent anything: waiting, and said differently. Nobody has ever asked it to be
|
||||
// anything, which is not the same as it being out of date.
|
||||
waiting, err := inv.Waiting(ctx, map[string]string{"anchor": "aaa", "laptop": "bbb"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(waiting) != 2 {
|
||||
t.Fatalf("machines that were never sent anything are not waiting: %+v", waiting)
|
||||
}
|
||||
for _, m := range waiting {
|
||||
if !m.Never {
|
||||
t.Fatalf("%s was never sent anything and does not say so: %+v", m.Node, m)
|
||||
}
|
||||
}
|
||||
|
||||
// Sent what it should be: not waiting.
|
||||
if err := inv.RecordSent(ctx, anchor.ID, "aaa"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
waiting, err = inv.Waiting(ctx, map[string]string{"anchor": "aaa", "laptop": "bbb"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(waiting) != 1 || waiting[0].Node != "laptop" {
|
||||
t.Fatalf("a machine sent exactly what it should be is still waiting: %+v", waiting)
|
||||
}
|
||||
|
||||
// The declaration changes: waiting again, and no longer "never".
|
||||
waiting, err = inv.Waiting(ctx, map[string]string{"anchor": "ccc", "laptop": "bbb"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var found bool
|
||||
for _, m := range waiting {
|
||||
if m.Node != "anchor" {
|
||||
continue
|
||||
}
|
||||
found = true
|
||||
if m.Never {
|
||||
t.Fatal("a machine that has been sent something is reported as never told")
|
||||
}
|
||||
if m.Sent != "aaa" {
|
||||
t.Fatalf("what it was last sent was lost: %+v", m)
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Fatal("a machine whose declaration changed since it was sent is not waiting")
|
||||
}
|
||||
}
|
||||
|
||||
// A machine nobody worked out is not reported as waiting: saying so would invent a comparison.
|
||||
func TestAMachineWithNothingComputedForItIsNotWaiting(t *testing.T) {
|
||||
inv := ForTest(t)
|
||||
ctx := t.Context()
|
||||
node, err := inv.AddNode(ctx, "unresolvable")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// It has been sent something before, which is what makes this the case the guard is for: a
|
||||
// machine with a digest and nothing computed for it would compare against the empty string
|
||||
// and look out of date, when the truth is that nobody worked out what it should be.
|
||||
if err := inv.RecordSent(ctx, node.ID, "what-it-got-last-time"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
waiting, err := inv.Waiting(ctx, map[string]string{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(waiting) != 0 {
|
||||
t.Fatalf("a machine the caller could not work out was reported as waiting: %+v", waiting)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
-- What the mesh last sent a machine, as a digest.
|
||||
--
|
||||
-- Not the declaration itself: the mesh can compute that again at any moment, and keeping a copy
|
||||
-- would be a second account of what a machine should be, able to disagree with the first. What
|
||||
-- cannot be recomputed is *what was actually sent*, and that is the whole of the difference
|
||||
-- between "this machine is out of date" and "this machine has never been told".
|
||||
--
|
||||
-- Without it, "behind" could only mean "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 only for machines that broke.
|
||||
|
||||
alter table node add column sent text;
|
||||
-- When, so a machine sent something long ago and silent since is distinguishable from one sent
|
||||
-- something a moment ago that has not had time to answer.
|
||||
alter table node add column sent_at timestamptz;
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user