The send record of issue 234 is now readable only through the controller's verbs (issue 506), and only plan said it. status says per machine when it was last sent a declaration, by whom, from which generation and what it answered; node show says it in full with the refusal's words. A machine's answer is read from its report only when that report is about the send and came after it.
283 lines
11 KiB
Go
283 lines
11 KiB
Go
package inventory
|
|
|
|
import (
|
|
"errors"
|
|
"slices"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// The assignment generation and the record of every send (novox/hq issue 234). On 2026-10-04 a declaration
|
|
// newer in sequence than every other named four fewer modules than the assignments held, a machine applied
|
|
// it, and nothing the mesh kept could say who sent it or from what view.
|
|
|
|
func generationNow(t *testing.T, inv *Inventory) int64 {
|
|
t.Helper()
|
|
g, err := inv.AssignmentGeneration(t.Context())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return g
|
|
}
|
|
|
|
// **Every change to what is assigned where raises the generation, in its own transaction** — an
|
|
// assignment, an unassignment, and one taken by the machine's removal, which no writer in Go makes — and
|
|
// a repeated assignment, which changes nothing, does not.
|
|
func TestEveryAssignmentChangeRaisesTheGeneration(t *testing.T) {
|
|
inv, node := aNodeWithModules(t, "postgres", "web")
|
|
ctx := t.Context()
|
|
start := generationNow(t, inv)
|
|
if start < 1 {
|
|
t.Fatalf("the generation starts at %d; zero is \"none claimed\" on the wire", start)
|
|
}
|
|
if _, err := inv.Assign(ctx, node, "postgres"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
assigned := generationNow(t, inv)
|
|
if assigned <= start {
|
|
t.Fatalf("an assignment left the generation at %d", assigned)
|
|
}
|
|
if _, err := inv.Assign(ctx, node, "postgres"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if again := generationNow(t, inv); again != assigned {
|
|
t.Errorf("an assignment repeated, which changed nothing, moved the generation from %d to %d", assigned, again)
|
|
}
|
|
if err := inv.Unassign(ctx, node, "postgres"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
unassigned := generationNow(t, inv)
|
|
if unassigned <= assigned {
|
|
t.Fatalf("an unassignment left the generation at %d", unassigned)
|
|
}
|
|
if _, err := inv.Assign(ctx, node, "web"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
before := generationNow(t, inv)
|
|
if _, err := inv.RemoveNodeForTest(ctx, node); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if removed := generationNow(t, inv); removed <= before {
|
|
t.Errorf("a machine's removal took its assignments and left the generation at %d", removed)
|
|
}
|
|
}
|
|
|
|
// **The generation never goes down, and is raised past what a machine applied when the mesh's own counter
|
|
// is behind it** — a store put back from a backup — so the next send is not refused for ever.
|
|
func TestTheGenerationIsRaisedPastWhatAMachineApplied(t *testing.T) {
|
|
inv, _ := aNodeWithModules(t)
|
|
now := generationNow(t, inv)
|
|
if raised, err := inv.RaiseAssignmentGeneration(t.Context(), now+40); err != nil || raised != now+41 {
|
|
t.Fatalf("raised past %d to %d (%v)", now+40, raised, err)
|
|
}
|
|
if raised, err := inv.RaiseAssignmentGeneration(t.Context(), 2); err != nil || raised != now+41 {
|
|
t.Fatalf("a raise below the counter moved it to %d (%v)", raised, err)
|
|
}
|
|
}
|
|
|
|
// **Every send is recorded: its sequence, its sender, the generation it came from and the modules it
|
|
// named** — and the machine's last send is what `plan` reads, the would-send is stamped with the generation
|
|
// written with the digest, and a refusal is kept on the send it refused, naming its sender.
|
|
func TestASendIsRecordedWithItsSenderGenerationAndModules(t *testing.T) {
|
|
inv, node := aNodeWithModules(t)
|
|
ctx := t.Context()
|
|
record, err := inv.NodeByName(ctx, node)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
first := Send{Sequence: 11, Epoch: 57, Sender: "the controller's daemon (pid 7 on anchor)",
|
|
Generation: 40, Digest: "d11", Modules: []string{"docker", "pacman", "sudo"}}
|
|
if err := inv.RecordSentWith(ctx, record.ID, "d11", nil, 57, 40, first); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
stale := Send{Sequence: 12, Epoch: 57, Sender: "a one-shot push by jochen at a shell on anchor",
|
|
Generation: 38, Digest: "d12", Modules: []string{"docker"}}
|
|
if err := inv.RecordSentWith(ctx, record.ID, "d12", nil, 57, 38, stale); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
last, found, err := inv.LastSend(ctx, node)
|
|
if err != nil || !found {
|
|
t.Fatalf("the last send is not kept: %v", err)
|
|
}
|
|
if last.Sequence != 12 || last.Sender != stale.Sender || last.Generation != 38 || !last.Recorded ||
|
|
!slices.Equal(last.Modules, []string{"docker"}) || last.SentAt.IsZero() {
|
|
t.Fatalf("the last send reads %+v", last)
|
|
}
|
|
if generation, err := inv.SentGeneration(ctx, record.ID); err != nil || generation != 38 {
|
|
t.Fatalf("what the machine was last sent of it reads %d (%v)", generation, err)
|
|
}
|
|
|
|
refused, found, err := inv.RefusedSend(ctx, record.ID, 12, 40, 0)
|
|
if err != nil || !found || refused.Sender != stale.Sender || refused.RefusedApplied != 40 ||
|
|
refused.CounterRaisedTo != 0 {
|
|
t.Fatalf("the refusal is not kept on the send it refused: %+v, %v, %v", refused, found, err)
|
|
}
|
|
since, err := inv.RefusedSendsSince(ctx, time.Now().Add(-time.Hour))
|
|
if err != nil || len(since) != 1 || since[0].NodeName != node || since[0].Sequence != 12 {
|
|
t.Fatalf("the refused sends within the hour read %+v (%v)", since, err)
|
|
}
|
|
if _, found, err := inv.RefusedSend(ctx, record.ID, 99, 40, 0); err != nil || found {
|
|
t.Errorf("a refusal of a send never recorded was found: %v, %v", found, err)
|
|
}
|
|
}
|
|
|
|
// **A refusal of a sequence the mesh has no send for is kept too, its sender unknown**, so S20 raises it;
|
|
// it is not the machine's last send, and it says what the counter was raised to.
|
|
func TestARefusalOfASendNeverRecordedIsKeptWithAnUnknownSender(t *testing.T) {
|
|
inv, node := aNodeWithModules(t)
|
|
ctx := t.Context()
|
|
record, err := inv.NodeByName(ctx, node)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
kept, err := inv.RecordUnrecordedRefusal(ctx, Send{Node: record.ID, Sequence: 99, Epoch: 57, Generation: 41,
|
|
Digest: "d99", RefusedApplied: 44, CounterRaisedTo: 45})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if kept.Recorded || kept.Sender != unknownSender || kept.RefusedAt == nil || kept.CounterRaisedTo != 45 ||
|
|
kept.NodeName != node {
|
|
t.Fatalf("the refusal reads %+v", kept)
|
|
}
|
|
since, err := inv.RefusedSendsSince(ctx, time.Now().Add(-time.Hour))
|
|
if err != nil || len(since) != 1 || since[0].Sequence != 99 {
|
|
t.Fatalf("the refused sends within the hour read %+v (%v)", since, err)
|
|
}
|
|
if _, found, err := inv.LastSend(ctx, node); err != nil || found {
|
|
t.Errorf("a refusal of a send never recorded reads as the machine's last send: %v, %v", found, err)
|
|
}
|
|
}
|
|
|
|
// **The machine's record and the send's are written together, and a send record that cannot be written
|
|
// leaves the machine's record standing and says so**: the digest and generation are the machine's, and a
|
|
// send that is away must not read as failed, nor the machine as behind.
|
|
func TestASendRecordThatCannotBeWrittenLeavesTheMachinesRecord(t *testing.T) {
|
|
inv, node := aNodeWithModules(t)
|
|
ctx := t.Context()
|
|
record, err := inv.NodeByName(ctx, node)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// A NUL byte is text PostgreSQL refuses: the send's own row cannot be written.
|
|
broken := Send{Sequence: 13, Sender: "a test", Generation: 42, Digest: "d\x0013"}
|
|
err = inv.RecordSentWith(ctx, record.ID, "d13", nil, 0, 42, broken)
|
|
var unrecorded *SendNotRecordedError
|
|
if !errors.As(err, &unrecorded) {
|
|
t.Fatalf("a send record that could not be written read as %v", err)
|
|
}
|
|
if generation, err := inv.SentGeneration(ctx, record.ID); err != nil || generation != 42 {
|
|
t.Fatalf("the machine's generation was not written with its digest: %d (%v)", generation, err)
|
|
}
|
|
if sent, err := inv.Outstanding(ctx, node); err != nil || sent != "d13" {
|
|
t.Fatalf("the machine's digest was not written: %q (%v)", sent, err)
|
|
}
|
|
}
|
|
|
|
// **An update that leaves an assignment as it was raises nothing** (the trigger's WHEN).
|
|
func TestAnUpdateThatChangesNoAssignmentRaisesNothing(t *testing.T) {
|
|
inv, node := aNodeWithModules(t, "web")
|
|
ctx := t.Context()
|
|
if _, err := inv.Assign(ctx, node, "web"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
before := generationNow(t, inv)
|
|
if _, err := inv.store.Pool().Exec(ctx, `update assignment set module = module`); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if after := generationNow(t, inv); after != before {
|
|
t.Errorf("an update that changed nothing moved the generation from %d to %d", before, after)
|
|
}
|
|
}
|
|
|
|
// **A machine that reads a generation is recorded from its reports**, and one rolled back says so no longer.
|
|
func TestWhetherAMachineReadsAGenerationIsItsLatestWord(t *testing.T) {
|
|
inv, node := aNodeWithModules(t)
|
|
record, err := inv.NodeByName(t.Context(), node)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, reads := range []bool{false, true, false} {
|
|
if err := inv.RecordReadsGeneration(t.Context(), record.ID, reads); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got, err := inv.ReadsGeneration(t.Context(), record.ID); err != nil || got != reads {
|
|
t.Fatalf("recorded %v, read %v (%v)", reads, got, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// **The last send says what the machine answered of it** (novox/hq issue 485): applied, refused with its
|
|
// words, or not answered yet — read from the machine's report only when that report is about the send's
|
|
// declaration and came after it, so a report about an earlier send is never this one's answer. And every
|
|
// machine's last send is read at once, for status, by name; a machine never sent anything has none.
|
|
func TestTheLastSendSaysWhatTheMachineAnswered(t *testing.T) {
|
|
inv, node := aNodeWithModules(t)
|
|
ctx := t.Context()
|
|
record, err := inv.NodeByName(ctx, node)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := inv.AddNode(ctx, "laptop"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
send := func(sequence int64, digest string) {
|
|
t.Helper()
|
|
if err := inv.RecordSentWith(ctx, record.ID, digest, nil, 57, 40, Send{Sequence: sequence, Epoch: 57,
|
|
Sender: "the controller's daemon (pid 7 on anchor)", Generation: 40, Digest: digest,
|
|
Modules: []string{"docker", "sudo"}}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
report := func(d Doing) {
|
|
t.Helper()
|
|
if _, err := inv.RecordDoing(ctx, record.ID, d); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
answer := func() SendAnswer {
|
|
t.Helper()
|
|
last, found, err := inv.LastSend(ctx, node)
|
|
if err != nil || !found {
|
|
t.Fatalf("the last send is not read back: %v, %v", found, err)
|
|
}
|
|
return last.Answer
|
|
}
|
|
|
|
send(11, "d11")
|
|
if a := answer(); a.Outcome != "" || a.At != nil {
|
|
t.Fatalf("a send not reported on reads as answered: %+v", a)
|
|
}
|
|
report(Doing{Outcome: OutcomeApplied, Declared: "d11", Applied: 3})
|
|
if a := answer(); a.Outcome != OutcomeApplied || a.At == nil {
|
|
t.Fatalf("a send the machine reported applying reads %+v", a)
|
|
}
|
|
|
|
send(12, "d12")
|
|
if a := answer(); a.Outcome != "" {
|
|
t.Fatalf("the report about the send before is taken for this one's answer: %+v", a)
|
|
}
|
|
report(Doing{Outcome: OutcomeRefused, Declared: "d12", Refused: "unknown field \"generation\"\nand more"})
|
|
if a := answer(); a.Outcome != OutcomeRefused || a.Refused != "unknown field \"generation\"\nand more" {
|
|
t.Fatalf("a send the machine refused reads %+v", a)
|
|
}
|
|
|
|
// Sent again a declaration it reported applying before the send: not answered until it reports again.
|
|
report(Doing{Outcome: OutcomeApplied, Declared: "d12", Applied: 3})
|
|
send(13, "d12")
|
|
if a := answer(); a.Outcome != "" {
|
|
t.Fatalf("a report from before the send is taken for its answer: %+v", a)
|
|
}
|
|
|
|
all, err := inv.LastSends(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(all) != 1 || all[node].Sequence != 13 || all[node].Epoch != 57 || all[node].Generation != 40 ||
|
|
!slices.Equal(all[node].Modules, []string{"docker", "sudo"}) {
|
|
t.Fatalf("every machine's last send reads %+v", all)
|
|
}
|
|
if _, found := all["laptop"]; found {
|
|
t.Error("a machine never sent anything has a last send")
|
|
}
|
|
}
|