The consume side behind a seam, and the window wiring into the loop
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.
This commit is contained in:
@@ -2,15 +2,11 @@ package link
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"testing"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgconn"
|
||||
amqp "github.com/rabbitmq/amqp091-go"
|
||||
)
|
||||
|
||||
// What a store restarting under an adoption answers with (novox/hq issues 082, 083).
|
||||
@@ -28,29 +24,6 @@ type replaysWith struct{ err error }
|
||||
|
||||
func (r replaysWith) Announceable(context.Context) ([]Announcement, error) { return nil, r.err }
|
||||
|
||||
type settledAs struct{ acked, nacked, requeued, rejected bool }
|
||||
|
||||
func (a *settledAs) Ack(uint64, bool) error { a.acked = true; return nil }
|
||||
func (a *settledAs) Nack(_ uint64, _ bool, requeue bool) error {
|
||||
a.nacked, a.requeued = true, requeue
|
||||
return nil
|
||||
}
|
||||
func (a *settledAs) Reject(uint64, bool) error { a.rejected = true; return nil }
|
||||
|
||||
func a(t *testing.T, to *settledAs, key string, v any) amqp.Delivery {
|
||||
t.Helper()
|
||||
body, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tag++
|
||||
return amqp.Delivery{Acknowledger: to, RoutingKey: key, Body: body, DeliveryTag: tag}
|
||||
}
|
||||
|
||||
func quietServer() *Server { return &Server{log: log.New(io.Discard, "", 0)} }
|
||||
|
||||
func (a *settledAs) held() bool { return !a.acked && !a.nacked && !a.rejected }
|
||||
|
||||
// A build result the store could not take right now is handed back; one it refused is rejected,
|
||||
// as before; one it kept is acknowledged.
|
||||
func TestABuildResultWaitsOutARestartingStore(t *testing.T) {
|
||||
@@ -58,16 +31,16 @@ func TestABuildResultWaitsOutARestartingStore(t *testing.T) {
|
||||
for _, c := range []struct {
|
||||
what string
|
||||
err error
|
||||
want func(*settledAs) bool
|
||||
want func(*settled) bool
|
||||
}{
|
||||
{"restarting", restarting, func(s *settledAs) bool { return s.held() }},
|
||||
{"refused", errors.New("no such module"), func(s *settledAs) bool { return s.rejected && !s.nacked }},
|
||||
{"kept", nil, func(s *settledAs) bool { return s.acked && !s.nacked }},
|
||||
{"restarting", restarting, func(s *settled) bool { return s.unsettled() }},
|
||||
{"refused", errors.New("no such module"), func(s *settled) bool { return s.rejected && !s.nacked }},
|
||||
{"kept", nil, func(s *settled) bool { return s.acked && !s.nacked }},
|
||||
} {
|
||||
s := quietServer()
|
||||
s, in := serving()
|
||||
s.recorder = recordsWith{err: c.err}
|
||||
to := &settledAs{}
|
||||
s.handleBuilt(context.Background(), a(t, to, KeyBuilt, built))
|
||||
to := &settled{}
|
||||
s.act(context.Background(), in.sends(t, to, KindBuilt, built))
|
||||
if !c.want(to) {
|
||||
t.Errorf("%s: a build result was settled as %+v", c.what, *to)
|
||||
}
|
||||
@@ -81,17 +54,17 @@ func TestAnUpgradeWaitsOutARestartingStoreAndNothingElse(t *testing.T) {
|
||||
for _, c := range []struct {
|
||||
what string
|
||||
err error
|
||||
want func(*settledAs) bool
|
||||
want func(*settled) bool
|
||||
}{
|
||||
{"the store away, said by the upgrader", errors.Join(ErrTryAgain, restarting), func(s *settledAs) bool { return s.held() }},
|
||||
{"a push that timed out", context.DeadlineExceeded, func(s *settledAs) bool { return s.acked && !s.nacked }},
|
||||
{"cannot act", errors.New("anchor cannot be resolved"), func(s *settledAs) bool { return s.acked && !s.nacked }},
|
||||
{"acted", nil, func(s *settledAs) bool { return s.acked && !s.nacked }},
|
||||
{"the store away, said by the upgrader", errors.Join(ErrTryAgain, restarting), func(s *settled) bool { return s.unsettled() }},
|
||||
{"a push that timed out", context.DeadlineExceeded, func(s *settled) bool { return s.acked && !s.nacked }},
|
||||
{"cannot act", errors.New("anchor cannot be resolved"), func(s *settled) bool { return s.acked && !s.nacked }},
|
||||
{"acted", nil, func(s *settled) bool { return s.acked && !s.nacked }},
|
||||
} {
|
||||
s := quietServer()
|
||||
s, in := serving()
|
||||
s.upgrader = upgradesWith{err: c.err}
|
||||
to := &settledAs{}
|
||||
s.upgraded(context.Background(), a(t, to, "upgraded", moved))
|
||||
to := &settled{}
|
||||
s.act(context.Background(), in.sends(t, to, KindModuleMoved, moved))
|
||||
if !c.want(to) {
|
||||
t.Errorf("%s: an upgrade was settled as %+v", c.what, *to)
|
||||
}
|
||||
@@ -104,16 +77,16 @@ func TestACatchUpWaitsOutARestartingStore(t *testing.T) {
|
||||
for _, c := range []struct {
|
||||
what string
|
||||
err error
|
||||
want func(*settledAs) bool
|
||||
want func(*settled) bool
|
||||
}{
|
||||
{"restarting", restarting, func(s *settledAs) bool { return s.held() }},
|
||||
{"unreadable", errors.New("a build row is malformed"), func(s *settledAs) bool { return s.acked && !s.nacked }},
|
||||
{"nothing to replay", nil, func(s *settledAs) bool { return s.acked && !s.nacked }},
|
||||
{"restarting", restarting, func(s *settled) bool { return s.unsettled() }},
|
||||
{"unreadable", errors.New("a build row is malformed"), func(s *settled) bool { return s.acked && !s.nacked }},
|
||||
{"nothing to replay", nil, func(s *settled) bool { return s.acked && !s.nacked }},
|
||||
} {
|
||||
s := quietServer()
|
||||
s, in := serving()
|
||||
s.replayer = replaysWith{err: c.err}
|
||||
to := &settledAs{}
|
||||
s.catchingUp(context.Background(), a(t, to, "catch-up", map[string]string{}))
|
||||
to := &settled{}
|
||||
s.act(context.Background(), in.sends(t, to, KindCatchUp, map[string]string{}))
|
||||
if !c.want(to) {
|
||||
t.Errorf("%s: a catch-up request was settled as %+v", c.what, *to)
|
||||
}
|
||||
@@ -121,15 +94,15 @@ func TestACatchUpWaitsOutARestartingStore(t *testing.T) {
|
||||
}
|
||||
|
||||
// Shutting down is not an answer about a message: one handled with a cancelled context is left
|
||||
// unsettled, for the broker to hand to whatever consumes next (issue 083, review).
|
||||
func TestAMessageHandledDuringShutdownIsLeftForTheBroker(t *testing.T) {
|
||||
// unsettled, for the bus to hand to whatever consumes next (issue 083, review).
|
||||
func TestAMessageHandledDuringShutdownIsLeftForTheBus(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
s := quietServer()
|
||||
s, in := serving()
|
||||
s.recorder = recordsWith{err: context.Canceled}
|
||||
to := &settledAs{}
|
||||
s.handleBuilt(ctx, a(t, to, KeyBuilt, BuildResult{On: "anchor", Repository: "/r", Commit: "abc"}))
|
||||
if !to.held() {
|
||||
to := &settled{}
|
||||
s.act(ctx, in.sends(t, to, KindBuilt, BuildResult{On: "anchor", Repository: "/r", Commit: "abc"}))
|
||||
if !to.unsettled() {
|
||||
t.Fatalf("a build result handled during shutdown was settled, and so lost: %+v", *to)
|
||||
}
|
||||
}
|
||||
@@ -137,45 +110,45 @@ func TestAMessageHandledDuringShutdownIsLeftForTheBroker(t *testing.T) {
|
||||
// Two identical build results: the newer sets the older aside rather than leaving it unsettled
|
||||
// for ever, holding a place in the prefetch.
|
||||
func TestAnIdenticalBuildResultSetsTheHeldOneAside(t *testing.T) {
|
||||
s := quietServer()
|
||||
s, in := serving()
|
||||
s.recorder = recordsWith{err: restarting}
|
||||
built := BuildResult{On: "anchor", Repository: "/r", Commit: "abc"}
|
||||
first, second := &settledAs{}, &settledAs{}
|
||||
s.handleBuilt(context.Background(), a(t, first, KeyBuilt, built))
|
||||
s.handleBuilt(context.Background(), a(t, second, KeyBuilt, built))
|
||||
if !first.acked || !second.held() || len(s.parked) != 1 {
|
||||
first, second := &settled{}, &settled{}
|
||||
s.act(context.Background(), in.sends(t, first, KindBuilt, built))
|
||||
s.act(context.Background(), in.sends(t, second, KindBuilt, built))
|
||||
if !first.acked || !second.unsettled() || len(in.held) != 1 {
|
||||
t.Fatalf("an identical build result did not set the held one aside: first %+v second %+v, %d held",
|
||||
*first, *second, len(s.parked))
|
||||
*first, *second, len(in.held))
|
||||
}
|
||||
}
|
||||
|
||||
// What is held stops short of the prefetch, so the loop always has room to answer an enrolment.
|
||||
func TestWhatIsHeldLeavesRoomInThePrefetch(t *testing.T) {
|
||||
s := quietServer()
|
||||
s, in := serving()
|
||||
s.recorder = recordsWith{err: restarting}
|
||||
var last *settledAs
|
||||
var last *settled
|
||||
for i := 0; i < Prefetch; i++ {
|
||||
last = &settledAs{}
|
||||
s.handleBuilt(context.Background(), a(t, last, KeyBuilt, BuildResult{On: "anchor", Commit: fmt.Sprint(i)}))
|
||||
last = &settled{}
|
||||
s.act(context.Background(), in.sends(t, last, KindBuilt, BuildResult{On: "anchor", Commit: fmt.Sprint(i)}))
|
||||
}
|
||||
if len(s.parked) != Prefetch-PrefetchHeadroom {
|
||||
t.Fatalf("%d messages were held; the ceiling is %d", len(s.parked), Prefetch-PrefetchHeadroom)
|
||||
if len(in.held) != Prefetch-PrefetchHeadroom {
|
||||
t.Fatalf("%d messages were held; the ceiling is %d", len(in.held), Prefetch-PrefetchHeadroom)
|
||||
}
|
||||
if last.held() {
|
||||
if last.unsettled() {
|
||||
t.Fatalf("a message past the ceiling was held: %+v", *last)
|
||||
}
|
||||
}
|
||||
|
||||
// An upgrade handled during shutdown is left for the broker too — the upgrader's error is the
|
||||
// An upgrade handled during shutdown is left for the bus too — the upgrader's error is the
|
||||
// cancelled context, which is no answer about the announcement.
|
||||
func TestAnUpgradeHandledDuringShutdownIsLeftForTheBroker(t *testing.T) {
|
||||
func TestAnUpgradeHandledDuringShutdownIsLeftForTheBus(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
s := quietServer()
|
||||
s, in := serving()
|
||||
s.upgrader = upgradesWith{err: context.Canceled}
|
||||
to := &settledAs{}
|
||||
s.upgraded(ctx, a(t, to, "upgraded", Upgraded{Module: "gitea", Commit: "abcdef0123"}))
|
||||
if !to.held() {
|
||||
t.Fatalf("an upgrade handled during shutdown was settled, and so lost: %+v", *to)
|
||||
to := &settled{}
|
||||
s.act(ctx, in.sends(t, to, KindModuleMoved, Upgraded{Module: "gitea", Commit: "abcdef0123"}))
|
||||
if !to.unsettled() {
|
||||
t.Fatalf("an upgrade was settled during shutdown, and so lost: %+v", *to)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user