Read what a machine has from its haves: reported-and-current, the bus list, a given value's first good start; drop the digest of a send (migration 0110) (release 6, issue 1945 B8)

This commit is contained in:
2026-10-24 17:51:40 +02:00
parent 8f90d3ecd6
commit fe3faaef83
29 changed files with 498 additions and 776 deletions
-32
View File
@@ -9,8 +9,6 @@ import (
"strings"
"time"
"github.com/jackc/pgx/v5"
"github.com/novox/mesh-controller/internal/chain"
)
@@ -190,36 +188,6 @@ func (i *Inventory) chainUnassign(ctx context.Context, c *chain.Chain, node Node
return fmt.Errorf("the assignment of %s to %s kept moving under the write", module, node.Name)
}
// chainAwaitingSince is AwaitingSince over the bucket: the oldest assignment on the machine made after it was last sent.
func (i *Inventory) chainAwaitingSince(ctx context.Context, name string) (time.Time, error) {
var sentAt *time.Time
var created time.Time
err := i.store.Pool().QueryRow(ctx, `select sent_at, created from node where name = $1`, name).Scan(&sentAt, &created)
if errors.Is(err, pgx.ErrNoRows) {
return time.Time{}, fmt.Errorf("%w: %s", ErrNoSuchNode, name)
}
if err != nil {
return time.Time{}, err
}
rows, err := i.assignments(ctx)
if err != nil {
return time.Time{}, err
}
var oldest time.Time
for _, a := range rows {
if a.Node == name && (sentAt == nil || a.Assigned.After(*sentAt)) && (oldest.IsZero() || a.Assigned.Before(oldest)) {
oldest = a.Assigned
}
}
switch {
case !oldest.IsZero():
return oldest, nil
case sentAt != nil:
return *sentAt, nil
}
return created, nil
}
// KeepAssignmentPlans writes the provisioner's plan of each assignment into its value on the bus, and a value of its own for
// each module only pulled in by another on a machine (issue 765), and ends the value of one that is no longer pulled in. The
// plans are derived from what each machine was last sent, not part of the record of what is assigned, so they are read
-90
View File
@@ -158,96 +158,6 @@ func TestWhatANodeSaidGoesWhenTheNodeDoes(t *testing.T) {
}
}
// "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.RecordSentForTest(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.RecordSentForTest(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)
}
}
// **A failure that repeats is told apart from one that just happened.** A node re-applies on a
// steady interval and reports each time, so a resource that will never apply arrives as the same
// report over and over — one row, replaced, "failed" at a fresh time — and nothing distinguished
-7
View File
@@ -208,10 +208,3 @@ func (i *Inventory) PlanValuesForTest(t *testing.T) map[string][]byte {
}
return out
}
// RecordSentForTest gives a machine the digest and time of a last send, as a test needs one to stand in for a send: nothing
// sends a declaration any more (ADR 0539 §5), so nothing in the controller writes them.
func (i *Inventory) RecordSentForTest(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
}
+13 -13
View File
@@ -79,18 +79,18 @@ type GivenReplaced struct {
}
// ReplaceGivenAfterStart replaces every given value on a machine whose module has now started well
// under the mesh on it (novox/hq ADR 0228), and answers what it replaced; the caller sends the
// under the mesh on it (novox/hq ADR 0228), and answers what it replaced; the caller composes the
// machines each names.
//
// **The signal is the machine's clean report of the declaration it was last sent** — every resource
// applied, nothing failed or refused — **when that declaration was sent after the value was given**,
// so it is the start on the given value that counts, not an older one. On an adopted machine the
// module must also be taken: until then the mesh runs nothing of it, and nothing has started under
// the mesh. Each row is claimed by clearing its mark before it is remade, so two reports arriving
// together replace a value once; a remake that fails puts the mark back, and the next good report
// tries again.
func (i *Inventory) ReplaceGivenAfterStart(ctx context.Context, node, declared string) ([]GivenReplaced, error) {
if declared == "" {
// **The signal is the machine's have of the module being healthy** (ADR 0539 §9): started names, per
// module, when the node-engine wrote a have of it that is running and healthy at its composition.
// A value counts when that is **after it was given**, so it is the start on the given value that
// counts, not an older one. On an adopted machine the module must also be taken: until then the mesh
// runs nothing of it, and nothing has started under the mesh. Each row is claimed by clearing its mark
// before it is remade, so two haves arriving together replace a value once; a remake that fails puts
// the mark back, and the next healthy have tries again.
func (i *Inventory) ReplaceGivenAfterStart(ctx context.Context, node string, started map[string]time.Time) ([]GivenReplaced, error) {
if len(started) == 0 {
return nil, nil
}
// Only a module assigned there, from the assignments bucket.
@@ -106,10 +106,10 @@ func (i *Inventory) ReplaceGivenAfterStart(ctx context.Context, node, declared s
`select s.node, s.module, s.name, s.replace_after_start
from module_secret s
join node n on n.id = s.node
where n.name = $1 and n.sent = $2 and n.sent_at > s.replace_after_start
where n.name = $1
and s.origin = 'accepted' and s.replace_after_start is not null
and (not n.adopted or exists (select 1 from taken t where t.node = s.node and t.module = s.module))
order by s.module, s.name`, node, declared)
order by s.module, s.name`, node)
if err != nil {
return nil, err
}
@@ -125,7 +125,7 @@ func (i *Inventory) ReplaceGivenAfterStart(ctx context.Context, node, declared s
rows.Close()
return nil, err
}
if !assignedHere[d.module] {
if !assignedHere[d.module] || !started[d.module].After(d.given) {
continue
}
dues = append(dues, d)
+14 -23
View File
@@ -45,17 +45,9 @@ func assign(t *testing.T, inv *Inventory, ctx context.Context, node, module stri
}
}
// sent records a declaration sent to a machine now, as a push does, and answers its digest.
func sent(t *testing.T, inv *Inventory, ctx context.Context, node, digest string) string {
t.Helper()
n, err := inv.NodeByName(ctx, node)
if err != nil {
t.Fatal(err)
}
if err := inv.RecordSentForTest(ctx, n.ID, digest); err != nil {
t.Fatal(err)
}
return digest
// healthy is a module's have, running and healthy, written a while from now: the first good start the store asks about.
func healthy(module string, from time.Duration) map[string]time.Time {
return map[string]time.Time{module: time.Now().Add(from)}
}
func originOf(t *testing.T, inv *Inventory, ctx context.Context, node, module, name string) string {
@@ -68,22 +60,21 @@ func originOf(t *testing.T, inv *Inventory, ctx context.Context, node, module, n
}
// **A given value is replaced once, after the first good start on it, and never again** (ADR 0228):
// not on a report of a declaration sent before it was given, not on a report of a declaration the
// mesh has moved past, and not a second time.
// not on a start before it was given, and not a second time.
func TestAGivenValueIsReplacedAfterTheFirstGoodStartAndNeverAgain(t *testing.T) {
inv, ctx := aModuleWithGivenSecrets(t)
before := sent(t, inv, ctx, "consumer", "declaration-before")
before := healthy("letta", -time.Hour)
marked, err := inv.AcceptGivenSecret(ctx, "consumer", "letta", "server-password", "typed-by-a-person")
if err != nil || !marked {
t.Fatalf("a given value for a secret the mesh may make is marked to be replaced: %v %v", marked, err)
}
// The machine's report of what it was sent before the value was given is not a start on it.
// A start before the value was given is not a start on it.
if got, err := inv.ReplaceGivenAfterStart(ctx, "consumer", before); err != nil || len(got) != 0 {
t.Fatalf("a start before the value was given replaced it: %+v %v", got, err)
}
carrying := sent(t, inv, ctx, "consumer", "declaration-carrying-it")
// A report about a declaration other than the one last sent says nothing about this one.
carrying := healthy("letta", time.Minute)
// The same early start again says nothing about this value.
if got, err := inv.ReplaceGivenAfterStart(ctx, "consumer", before); err != nil || len(got) != 0 {
t.Fatalf("a report of an older declaration replaced it: %+v %v", got, err)
}
@@ -106,7 +97,7 @@ func TestAGivenValueIsReplacedAfterTheFirstGoodStartAndNeverAgain(t *testing.T)
if again, err := inv.ReplaceGivenAfterStart(ctx, "consumer", carrying); err != nil || len(again) != 0 {
t.Fatalf("the same start replaced it twice: %+v %v", again, err)
}
next := sent(t, inv, ctx, "consumer", "declaration-after")
next := healthy("letta", 2*time.Minute)
if again, err := inv.ReplaceGivenAfterStart(ctx, "consumer", next); err != nil || len(again) != 0 {
t.Fatalf("a later start replaced the mesh's own value: %+v %v", again, err)
}
@@ -126,7 +117,7 @@ func TestWhatIsNeverReplacedAfterAStart(t *testing.T) {
if err := inv.AcceptSecretForModule(ctx, "consumer", "letta", "broker", "issued-by-the-mesh"); err != nil {
t.Fatal(err)
}
declared := sent(t, inv, ctx, "consumer", "declaration")
declared := healthy("letta", time.Minute)
if got, err := inv.ReplaceGivenAfterStart(ctx, "consumer", declared); err != nil || len(got) != 0 {
t.Fatalf("a value that stays as given was replaced: %+v %v", got, err)
}
@@ -157,14 +148,14 @@ func TestAGivenValueWaitsForTheTakeOnAnAdoptedMachine(t *testing.T) {
if _, err := inv.AcceptGivenSecret(ctx, "consumer", "letta", "server-password", "the-found-one"); err != nil {
t.Fatal(err)
}
held := sent(t, inv, ctx, "consumer", "declaration-holding-it")
held := healthy("letta", time.Minute)
if got, err := inv.ReplaceGivenAfterStart(ctx, "consumer", held); err != nil || len(got) != 0 {
t.Fatalf("a module held as found, not yet taken, had its given value replaced: %+v %v", got, err)
}
if err := inv.Take(ctx, "consumer", "letta"); err != nil {
t.Fatal(err)
}
taken := sent(t, inv, ctx, "consumer", "declaration-taking-it")
taken := healthy("letta", time.Minute)
got, err := inv.ReplaceGivenAfterStart(ctx, "consumer", taken)
if err != nil || len(got) != 1 {
t.Fatalf("the first good start after the take did not replace the given value: %+v %v", got, err)
@@ -177,7 +168,7 @@ func TestAGivenValueWaitsForTheTakeOnAnAdoptedMachine(t *testing.T) {
if err := inv.Unassign(ctx, "consumer", "letta"); err != nil {
t.Fatal(err)
}
gone := sent(t, inv, ctx, "consumer", "declaration-without-it")
gone := healthy("letta", time.Minute)
if got, err := inv.ReplaceGivenAfterStart(ctx, "consumer", gone); err != nil || len(got) != 0 {
t.Fatalf("a module no longer assigned had its given value replaced: %+v %v", got, err)
}
@@ -196,7 +187,7 @@ func TestADefinitionThatNowSaysOutsideKeepsTheGivenValue(t *testing.T) {
if err := inv.RegisterModule(ctx, m, Source{}); err != nil {
t.Fatal(err)
}
declared := sent(t, inv, ctx, "consumer", "declaration")
declared := healthy("letta", time.Minute)
if got, err := inv.ReplaceGivenAfterStart(ctx, "consumer", declared); err != nil || len(got) != 0 {
t.Fatalf("a value the definition now says is an outside party's was replaced: %+v %v", got, err)
}
@@ -3,9 +3,8 @@
-- Nothing sends a declaration any more, so nothing writes what a send carried: the commit of each module (sent_builds, which
-- 0105 kept only as the source of the one-time copy onto the assignments, long done), the lease epoch (sent_epoch, 0068), the
-- summary `plan --diff` compared (sent_summary, 0078) and the assignment generation (sent_generation, 0094). **It is one-way**,
-- as 0108. What stays: sent and sent_at (the digest and time of the last send, which `status`' reported-and-current reads and the
-- given secrets' first good start is judged against), sent_bus_users, and sent_provisioning, which now holds the provisioner's
-- plans of each composition (inventory.RecordComposedPlans).
-- as 0108. What stays: sent and sent_at (the digest and time of the last send), sent_bus_users and sent_provisioning, which holds the
-- provisioner's plans of each composition (inventory.RecordComposedPlans); 0110 drops the first three.
alter table node drop column if exists sent_builds;
alter table node drop column if exists sent_epoch;
alter table node drop column if exists sent_summary;
@@ -0,0 +1,11 @@
-- The node row keeps no digest of a send, nor when it was made (novox/hq ADR 0539 §5, §9, §17, issue 1945).
--
-- A machine is not sent a declaration, so nothing records one: whether a machine has what is wanted of it is read from its
-- haves beside the assigner's wants, a given value is replaced when a have of its module is first running and healthy, and the
-- bus's user list is part of the bus module's composition, which the bus machine's have names or does not. This drops what
-- the last readers of a send's record read: node.sent and node.sent_at (0014), and node.sent_bus_users (0058). **It is
-- one-way**, as 0108 and 0109. node.sent_provisioning stays: it holds the provisioner's plans of each composition
-- (inventory.RecordComposedPlans).
alter table node drop column if exists sent;
alter table node drop column if exists sent_at;
alter table node drop column if exists sent_bus_users;
+8 -163
View File
@@ -906,16 +906,6 @@ type BusyWait struct {
// BusyWaiting is the state of a container left running because it said busy (link.BusyWaiting).
const BusyWaiting = "waiting"
// Waiting says whether any container waits because it said busy.
func (d Doing) Waiting() bool {
for _, b := range d.Busy {
if b.State == BusyWaiting {
return true
}
}
return false
}
// FailedResource is one thing a node could not do.
type FailedResource struct {
ID string `json:"id"`
@@ -1143,11 +1133,10 @@ type Reported struct {
Outcome string
// At is when it last reported; nil if it never has.
At *time.Time
// Sent is when the current declaration went to it; nil if nothing ever did.
// Sent is not kept: nothing is sent (ADR 0539 §5). Always nil.
Sent *time.Time
// Current is whether the last report names the declaration last sent — the machine has
// acted on the current words, not merely spoken after they were written. False also covers
// a machine that has not said which, which is every host from before reports carried it.
// Current is whether the machine has everything the assigner wants of it, read from its haves beside the wants
// (cmd/mesh-controller lastReports); false here, as this reads neither.
Current bool
// Declared is the digest of the declaration the last report was about, and ReportedSequence that
// declaration's sequence as the report claimed it (zero from an engine that claims none): what a
@@ -1158,67 +1147,12 @@ type Reported struct {
Busy []BusyWait
}
// SentDeclaration is what one send carried to a machine, as a gate keeps it: the declaration's digest and
// its sequence (novox/hq issue 352). A report about this declaration, or about one sequenced after it, is a
// report on what the gate sent — whatever the machine was sent since.
//
// At is when the controller sent it to that machine: the node's own send time, which is where that machine's
// `apply` duration starts. A walk measures its rest from it, not from its own record of the send, made only after
// every machine of the rest was sent (novox/hq issue 582). Zero for a send kept before it was recorded.
type SentDeclaration struct {
Digest string `json:"digest"`
Sequence int64 `json:"sequence,omitempty"`
At time.Time `json:"at,omitzero"`
// Provisioned is, for each module the send handed to the provisioner, the digest of the templates its plan
// named (novox/hq ADR 0349 §4, ADR 0362): the build the machine's provisioner client reports `prov.applied` on.
// The machine has applied the send of such a module only once both it and the client reported it.
Provisioned map[string]string `json:"provisioned,omitempty"`
}
// ReportsOn says a report is about this send: the declaration itself; one the same machine was sequenced
// after it; or the declaration the machine was sent last (Current), which is this send or a later one —
// sends to a machine are made one after another. A send kept without a sequence is matched by its digest
// and by the last send alone.
func (s SentDeclaration) ReportsOn(r Reported) bool {
if r.Current || (s.Digest != "" && r.Declared == s.Digest) {
return true
}
return s.Sequence > 0 && r.ReportedSequence >= s.Sequence
}
// SentTo is the declaration a machine was last sent, by name: its digest and sequence, and false when it
// was never sent one.
func (i *Inventory) SentTo(ctx context.Context, name string) (SentDeclaration, bool, error) {
var digest *string
var seq *int64
var at *time.Time
var plans []byte
err := i.store.Pool().QueryRow(ctx, `select sent, sequence, sent_at, sent_provisioning from node where name = $1`,
name).Scan(&digest, &seq, &at, &plans)
if errors.Is(err, pgx.ErrNoRows) {
return SentDeclaration{}, false, fmt.Errorf("%w: %s", ErrNoSuchNode, name)
}
if err != nil || digest == nil || *digest == "" {
return SentDeclaration{}, false, err
}
s := SentDeclaration{Digest: *digest}
if seq != nil {
s.Sequence = *seq
}
if at != nil {
s.At = at.UTC()
}
if s.Provisioned, err = provisionedBy(plans); err != nil {
return SentDeclaration{}, false, fmt.Errorf("what %s was sent of the provisioner's plans: %w", name, err)
}
return s, true, nil
}
// LastReports is every machine's last report beside when it was last sent a declaration.
// LastReports is every machine's last report: when, what it did, which declaration it was about and what it holds busy. Sent
// and Current are not kept here: nothing is sent (ADR 0539 §5), and whether a machine has what is wanted of it is read from
// its haves (cmd/mesh-controller lastReports).
func (i *Inventory) LastReports(ctx context.Context) ([]Reported, error) {
rows, err := i.store.Pool().Query(ctx,
`select n.name, coalesce(r.outcome, ''), r.at, n.sent_at,
coalesce(r.declared is not null and r.declared <> '' and r.declared = n.sent, false),
`select n.name, coalesce(r.outcome, ''), r.at,
coalesce(r.declared, ''), coalesce(r.reported_sequence, 0), coalesce(r.busy, '[]')
from node n left join node_report r on r.node = n.id
order by n.name`)
@@ -1230,8 +1164,7 @@ func (i *Inventory) LastReports(ctx context.Context) ([]Reported, error) {
for rows.Next() {
var r Reported
var busy []byte
if err := rows.Scan(&r.Node, &r.Outcome, &r.At, &r.Sent, &r.Current, &r.Declared, &r.ReportedSequence,
&busy); err != nil {
if err := rows.Scan(&r.Node, &r.Outcome, &r.At, &r.Declared, &r.ReportedSequence, &busy); err != nil {
return nil, err
}
if err := json.Unmarshal(busy, &r.Busy); err != nil {
@@ -1242,94 +1175,6 @@ func (i *Inventory) LastReports(ctx context.Context) ([]Reported, error) {
return out, rows.Err()
}
// AwaitingSince is since when a machine has had something waiting for its next send (novox/hq issue
// 275): the oldest assignment on it made after it was last sent, or — when nothing was assigned since —
// when it was last sent, or when it joined if it never was. The moment a bound on "not pushed yet" is
// read from: every change a push carries happened after the last push, and an assignment is the one
// that says when.
func (i *Inventory) AwaitingSince(ctx context.Context, name string) (time.Time, error) {
return i.chainAwaitingSince(ctx, name)
}
// RecordSentBusUsers keeps a digest of the bus's user list a machine was just sent, by its name
// (novox/hq issue 249): whether the machine holding the bus must go first is whether this differs
// from the list composed now.
func (i *Inventory) RecordSentBusUsers(ctx context.Context, name, digest string) error {
_, err := i.store.Pool().Exec(ctx,
`update node set sent_bus_users = $2 where name = $1`, name, digest)
return err
}
// SentBusUsers is the digest of the bus's user list a machine was last sent, empty for none.
func (i *Inventory) SentBusUsers(ctx context.Context, name string) (string, error) {
var sent string
err := i.store.Pool().QueryRow(ctx,
`select sent_bus_users from node where name = $1`, name).Scan(&sent)
if errors.Is(err, pgx.ErrNoRows) {
return "", nil
}
return sent, err
}
// Outstanding is the digest of the declaration a machine was last sent, by its name, and empty
// for one that has never been sent anything.
//
// **By name rather than by id**, because the caller is the serving loop and what a node puts in a
// report is its name. Asked of one machine rather than read from Waiting's sweep, because it is
// asked per message: a report names the declaration it is about, and a report about one the mesh
// has already moved past is not acted on (design 25 §3).
func (i *Inventory) Outstanding(ctx context.Context, name string) (string, error) {
var sent string
err := i.store.Pool().QueryRow(ctx,
`select coalesce(sent, '') from node where name = $1`, name).Scan(&sent)
if errors.Is(err, pgx.ErrNoRows) {
// Not an error worth carrying up: a report from a machine the mesh has no record of has
// nothing to be stale against, and whatever is wrong with it is the listener's to say.
return "", nil
}
return sent, 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
@@ -47,7 +47,7 @@ func TestMigration0108DropsTheTablesOfThePlansMachineryAndKeepsWhatTheController
}
// **Migration 0109 drops the columns of the node row that kept what a send carried** (ADR 0539 §17, issue 1945): the builds,
// the epoch, the summary and the generation. The digest and time of the last send stay, with the provisioner's plans.
// the epoch, the summary and the generation. The provisioner's plans stay (0110 drops the rest).
func TestMigration0109DropsWhatASendCarriedFromTheNodeRow(t *testing.T) {
inv, migrate := ForTestBefore(t, 109)
columns := func() map[string]bool {
@@ -72,9 +72,40 @@ func TestMigration0109DropsWhatASendCarriedFromTheNodeRow(t *testing.T) {
t.Errorf("node.%s is still in the store", name)
}
}
for _, stays := range []string{"sent", "sent_at", "sent_bus_users", "sent_provisioning"} {
for _, stays := range []string{"sent_provisioning"} {
if !after[stays] {
t.Errorf("node.%s is gone", stays)
}
}
}
// **Migration 0110 drops the digest and time of a send, and the bus users' digest, from the node row** (ADR 0539 §17, issue
// 1945): the haves answer what a machine has. The provisioner's plans stay.
func TestMigration0110DropsTheDigestOfASendFromTheNodeRow(t *testing.T) {
inv, migrate := ForTestBefore(t, 110)
columns := func() map[string]bool {
t.Helper()
out := map[string]bool{}
for _, row := range inv.QueryForTest(t, `select column_name::text from information_schema.columns where table_name = 'node'`) {
out[strings.Trim(row[0], `"`)] = true
}
return out
}
gone := []string{"sent", "sent_at", "sent_bus_users"}
before := columns()
for _, name := range gone {
if !before[name] {
t.Fatalf("node.%s is not in the store before the migration", name)
}
}
migrate()
after := columns()
for _, name := range gone {
if after[name] {
t.Errorf("node.%s is still in the store", name)
}
}
if !after["sent_provisioning"] {
t.Errorf("node.sent_provisioning is gone")
}
}
@@ -72,9 +72,6 @@ func TestAnOlderReportOfTheSameDeclarationIsRefused(t *testing.T) {
func TestAnOrderedAccountIsNotJudgedByTheDigestSent(t *testing.T) {
inv, heard, id := anOrderedMachine(t)
ctx := context.Background()
if err := inv.RecordSentForTest(ctx, id, "d13"); err != nil {
t.Fatal(err)
}
if _, err := heard.Heard(ctx, ordered("d12", 57, 12, 41, "a")); err != nil {
t.Fatal(err)
}
@@ -93,9 +90,6 @@ func TestAnUnorderedAccountClearsTheOrderAndTheEpoch(t *testing.T) {
if _, err := heard.Heard(ctx, ordered("d12", 57, 12, 41, "a")); err != nil {
t.Fatal(err)
}
if err := inv.RecordSentForTest(ctx, id, "d13"); err != nil {
t.Fatal(err)
}
if _, err := heard.Heard(ctx, link.Report{Node: "home-server", Declared: "d13", Applied: []string{"a", "b"}}); err != nil {
t.Fatal(err)
}
+2 -11
View File
@@ -102,9 +102,6 @@ func TestABareAliveDoesNotWipeTheDeclarationThatSaysANodeIsCurrent(t *testing.T)
}
// The mesh sent this node a declaration, and the node applied it and named which by digest.
const digest = "d640d1b6a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f6071829304152"
if err := inv.RecordSentForTest(ctx, node.ID, digest); err != nil {
t.Fatal(err)
}
if _, err := (link.Enrolment{Inventory: inv}).Heard(ctx, link.Report{
Node: "anchor", Applied: []string{"a", "b"}, Declared: digest, Carried: []int{5432},
}); err != nil {
@@ -270,13 +267,10 @@ func TestWhatFiltersAMachineIsKeptFromItsReport(t *testing.T) {
func TestAnAccountOfAnOlderDeclarationDoesNotReplaceTheNewer(t *testing.T) {
inv := inventory.ForTest(t)
ctx := context.Background()
node, err := inv.AddNode(ctx, "home-server")
_, err := inv.AddNode(ctx, "home-server")
if err != nil {
t.Fatal(err)
}
if err := inv.RecordSentForTest(ctx, node.ID, "d2"); err != nil {
t.Fatal(err)
}
heard := link.Enrolment{Inventory: inv}
if _, err := heard.Heard(ctx, link.Report{Node: "home-server", Declared: "d2",
Applied: []string{"a", "b"}, Outward: []string{"eth0"}}); err != nil {
@@ -304,7 +298,7 @@ func TestAnAccountOfAnOlderDeclarationDoesNotReplaceTheNewer(t *testing.T) {
func TestAnAccountOfTheSentDeclarationReplacesTheKeptOne(t *testing.T) {
inv := inventory.ForTest(t)
ctx := context.Background()
node, err := inv.AddNode(ctx, "home-server")
_, err := inv.AddNode(ctx, "home-server")
if err != nil {
t.Fatal(err)
}
@@ -312,9 +306,6 @@ func TestAnAccountOfTheSentDeclarationReplacesTheKeptOne(t *testing.T) {
if _, err := heard.Heard(ctx, link.Report{Node: "home-server", Declared: "d1", Applied: []string{"a"}}); err != nil {
t.Fatal(err)
}
if err := inv.RecordSentForTest(ctx, node.ID, "d2"); err != nil {
t.Fatal(err)
}
if _, err := heard.Heard(ctx, link.Report{Node: "home-server", Declared: "d2", Applied: []string{"a", "b"}}); err != nil {
t.Fatal(err)
}