The outbound half went behind `Bus` and the transport stopped reaching its callers; this is the other half, and the larger one. Every handler took `amqp.Delivery`, so the serving loop could not move to another bus without moving enrolment, reports, builds, upgrades and catch-up with it in one breath. `Control` states one message in the mesh's words — took it, dropped it, or held it for the store — and `Inbound` is where messages come from. The AMQP implementation is today's loop moved rather than changed: same queues, same prefetch, same holding, because the mesh is running on it and a bus nothing speaks yet is no reason to alter the one every node is on. The window (window.go) is now what decides, instead of the conditions that were inlined in the loop. Two things that surfaced in the wiring: **Supersession is asked before the store, not after.** A report about a declaration the mesh has moved past would otherwise wait out a restarting store to be written and then overwrite what the node is doing now. **Half of a report is not about a declaration, and that half is never stale.** What the machine *is* — the tunnel it took over, the ports its own bundle holds, what an adopted node found, a node moving its overlay key — reaches the mesh on a report and nowhere else. A rekey set aside as stale is a node whose overlay key never moves, and no retry is coming, because the node said it once. So staleness is asked only of a report that is purely an apply's account. The one thing holding-in-memory can do that holding-in-the-server cannot is named rather than hidden: `About` sets aside a held message when a newer one about the same thing arrives, and the bus being built ignores it because the digest answers the same question.
136 lines
5.4 KiB
Go
136 lines
5.4 KiB
Go
package link
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"testing"
|
|
)
|
|
|
|
// Supersession as a check rather than a memory (design 25 §3).
|
|
//
|
|
// Holding a message in memory let the controller drop an older report when a newer one for the
|
|
// same node arrived. On the bus being built the message belongs to the server and comes back
|
|
// whatever happened meanwhile — so the older report is redelivered *after* the newer was applied,
|
|
// and acting on it would undo the newer.
|
|
//
|
|
// The answer was already in the message: a report carries the digest of the declaration it is
|
|
// about, so "is this the past?" is a question the message answers.
|
|
|
|
// sentAndHeard records reports and knows what was last sent, which is the pair the check needs.
|
|
type sentAndHeard struct {
|
|
sent string
|
|
heard []Report
|
|
err error
|
|
}
|
|
|
|
func (s *sentAndHeard) Heard(_ context.Context, r Report) error {
|
|
if s.err != nil {
|
|
return s.err
|
|
}
|
|
s.heard = append(s.heard, r)
|
|
return nil
|
|
}
|
|
|
|
func (s *sentAndHeard) Outstanding(context.Context, string) (string, error) { return s.sent, nil }
|
|
|
|
// A report about a declaration the mesh has moved past is settled and not acted on. Settled rather
|
|
// than dropped, because there is nothing wrong with the message — it is simply the past, and
|
|
// redelivering it for ever is worse than letting it go.
|
|
func TestAReportAboutASupersededDeclarationIsNotActedOn(t *testing.T) {
|
|
store := &sentAndHeard{sent: "d2"}
|
|
s, in := serving()
|
|
s.listener = store
|
|
to := &settled{}
|
|
s.act(context.Background(), in.sends(t, to, KindReport, aReport("anchor", "d1")))
|
|
|
|
if len(store.heard) != 0 {
|
|
t.Fatalf("a report about a superseded declaration was acted on: %+v", store.heard)
|
|
}
|
|
if !to.acked {
|
|
t.Fatalf("a superseded report was not settled, so it comes back for ever: %+v", *to)
|
|
}
|
|
}
|
|
|
|
// The report about the declaration that *is* outstanding is acted on, and so is one from a node
|
|
// the mesh has no digest for — an older host that says nothing about which declaration it applied
|
|
// has nothing to be judged against, and refusing it would silence every node built before reports
|
|
// carried the digest.
|
|
func TestAReportAboutTheOutstandingDeclarationIsActedOn(t *testing.T) {
|
|
for _, c := range []struct{ what, sent, declared string }{
|
|
{"the one outstanding", "d2", "d2"},
|
|
{"a report that says nothing about which", "d2", ""},
|
|
{"a node nothing was ever sent", "", "d1"},
|
|
} {
|
|
store := &sentAndHeard{sent: c.sent}
|
|
s, in := serving()
|
|
s.listener = store
|
|
to := &settled{}
|
|
s.act(context.Background(), in.sends(t, to, KindReport, aReport("anchor", c.declared)))
|
|
if len(store.heard) != 1 || !to.acked {
|
|
t.Errorf("%s: was not acted on and acknowledged: heard %+v, settled %+v",
|
|
c.what, store.heard, *to)
|
|
}
|
|
}
|
|
}
|
|
|
|
// **Staleness is decided before the store is waited on**, not after: a redelivery that lost its
|
|
// race is not worth holding a slot in the window that a current message needs.
|
|
func TestASupersededReportIsNotHeldForTheStore(t *testing.T) {
|
|
store := &sentAndHeard{sent: "d2", err: errors.Join(ErrTryAgain, errors.New("starting up"))}
|
|
s, in := serving()
|
|
s.listener = store
|
|
to := &settled{}
|
|
s.act(context.Background(), in.sends(t, to, KindReport, aReport("anchor", "d1")))
|
|
if !to.acked || len(in.held) != 0 {
|
|
t.Fatalf("a superseded report waited for the store: %+v, %d held", *to, len(in.held))
|
|
}
|
|
}
|
|
|
|
// **Half of a report is not about a declaration, and that half is never stale.**
|
|
//
|
|
// What the machine *is* — the tunnel it took over, the ports its own bundle holds, what an adopted
|
|
// node found and is keeping, a node moving its overlay key — reaches the mesh on a report and
|
|
// nowhere else. A rekey set aside as stale is a node whose overlay key never moves, and no retry is
|
|
// coming, because the node said it once. So a report carrying any of these is acted on whenever it
|
|
// arrives, however far the mesh has moved on.
|
|
func TestAReportCarryingWhatOnlyTheNodeKnowsIsActedOnHoweverOldItIs(t *testing.T) {
|
|
for _, c := range []struct {
|
|
what string
|
|
report Report
|
|
}{
|
|
{"a rekey", Report{Node: "anchor", Declared: "d1",
|
|
Rekey: &Rekey{Previous: "k1", OverlayKey: "k2"}}},
|
|
{"the tunnel it carried", Report{Node: "anchor", Declared: "d1",
|
|
Tunnel: &CarriedTunnel{Interface: "wg0", State: "taken"}}},
|
|
{"what an adopted node holds", Report{Node: "anchor", Declared: "d1",
|
|
Held: []Held{{ID: "conf", Module: "web", Kind: "file"}}}},
|
|
{"the firewall it found", Report{Node: "anchor", Declared: "d1", Firewall: "ufw"}},
|
|
{"what is reachable on it", Report{Node: "anchor", Declared: "d1",
|
|
Reachable: []Reach{{Protocol: "tcp", Port: 443}}}},
|
|
{"the ports its own bundle holds", Report{Node: "anchor", Declared: "d1",
|
|
Carried: []int{5432}}},
|
|
} {
|
|
store := &sentAndHeard{sent: "d9"}
|
|
s, in := serving()
|
|
s.listener = store
|
|
to := &settled{}
|
|
s.act(context.Background(), in.sends(t, to, KindReport, c.report))
|
|
if len(store.heard) != 1 {
|
|
t.Errorf("%s was set aside as stale, and the mesh will never hear it again: %+v",
|
|
c.what, *to)
|
|
}
|
|
}
|
|
}
|
|
|
|
// A heartbeat is not held for the store: the next one is a minute away, and one kept for two
|
|
// minutes to be written late says nothing the one after it will not say better.
|
|
func TestAHeartbeatIsNotHeldForTheStore(t *testing.T) {
|
|
s, in := serving()
|
|
s.listener = heardWith{err: errors.Join(ErrTryAgain, errors.New("starting up"))}
|
|
to := &settled{}
|
|
s.act(context.Background(), in.sends(t, to, KindHeartbeat, Alive{Node: "anchor"}))
|
|
if !to.acked || len(in.held) != 0 {
|
|
t.Fatalf("a heartbeat was held for the store: %+v, %d held", *to, len(in.held))
|
|
}
|
|
}
|