Files
mesh-host/internal/link/overtaken_test.go
T
jochen a79972577c Never say a reconcile's report after a newer apply's (hq issue 267)
A reconcile that held the machine to the kept declaration just before a
delivery arrived queued its report while the delivery's apply waited for
it; the link published the apply's report and then the reconcile's, so the
mesh's last word from the machine named the older declaration and the
release plan waited on a report it had already been given. The link now
sets aside an unasked report about a declaration other than the one it
has applied since.
2026-10-06 01:45:34 +02:00

95 lines
3.4 KiB
Go

package link
import (
"context"
"testing"
"time"
)
// A reconcile's report about the declaration kept before a delivery is never said after that
// delivery's report (novox/hq issue 267).
// Measured on the home server: the reconcile timer fired three seconds before a declaration
// arrived. The reconcile held the machine to the declaration kept then; the delivery's apply waited
// for it, applied the new one and was reported — and then the reconcile's report, queued meanwhile,
// went out and was stored as the machine's latest account, naming the older declaration. The
// release plan waited on a report it had already been given.
func TestAReconcileReportOlderThanTheApplyIsNotSaidAfterIt(t *testing.T) {
m, key := aMember(t)
l := newQuietLink(&said{body: signedBy(t, key, []byte(`{"declaration":2}`))})
outbox := make(chan Unasked, 1)
ctx, stop := context.WithCancel(context.Background())
defer stop()
settled := make(chan bool, 1)
apply := func(context.Context, []byte, []byte) Report {
// The reconcile ran first, on what was kept then, and queued its report while this waited.
outbox <- Unasked{Report: Report{Declared: "d1", Applied: []string{"a"}, Outward: []string{"eth0"}},
Done: func(published bool) { settled <- published; stop() }}
return Report{Declared: "d2", Applied: []string{"a", "b"}, Outward: []string{"eth0"}}
}
done := make(chan error, 1)
go func() { done <- serve(ctx, l, m, apply, nil, time.Second, outbox, &keptInMemory{}) }()
select {
case published := <-settled:
if published {
t.Fatal("the reconcile's report was counted as said")
}
case <-time.After(5 * time.Second):
t.Fatal("the reconcile's report was never settled")
}
if err := <-done; err != nil {
t.Fatal(err)
}
reports := l.said()
if len(reports) != 1 || reports[0].Declared != "d2" {
t.Fatalf("the mesh heard %+v; it should have heard only the apply of d2", reports)
}
}
// A reconcile's report about the declaration this link applied is news, and is said.
func TestAReconcileReportAboutTheAppliedDeclarationIsSaid(t *testing.T) {
m, key := aMember(t)
l := newQuietLink(&said{body: signedBy(t, key, []byte(`{"declaration":2}`))})
outbox := make(chan Unasked, 1)
ctx, stop := context.WithCancel(context.Background())
defer stop()
settled := make(chan bool, 1)
apply := func(context.Context, []byte, []byte) Report {
outbox <- Unasked{Report: Report{Declared: "d2", Applied: []string{"a", "b"}, Firewall: "nftables"},
Done: func(published bool) { settled <- published; stop() }}
return Report{Declared: "d2", Applied: []string{"a", "b"}}
}
go func() { _ = serve(ctx, l, m, apply, nil, time.Second, outbox, nil) }()
select {
case published := <-settled:
if !published {
t.Fatal("a reconcile's report about the applied declaration was not said")
}
case <-time.After(5 * time.Second):
t.Fatal("the reconcile's report was never settled")
}
if reports := l.said(); len(reports) != 2 || reports[1].Declared != "d2" {
t.Fatalf("the mesh heard %+v", reports)
}
}
func TestOvertaken(t *testing.T) {
for _, c := range []struct {
declared, applied string
want bool
}{
{"d1", "d2", true},
{"d2", "d2", false},
{"", "d2", false}, // names no declaration
{"d1", "", false}, // this link has applied nothing yet
} {
if got := overtaken(Report{Declared: c.declared}, c.applied); got != c.want {
t.Errorf("overtaken(%q, %q) = %v, want %v", c.declared, c.applied, got, c.want)
}
}
}