Files
mesh-controller/internal/link/queue_test.go
T

130 lines
5.3 KiB
Go

package link
import (
"testing"
"time"
"github.com/nats-io/nats.go"
)
// The build queue read and changed by hand (novox/hq ADR 0219).
// Which ask is waiting, in flight and dead, from the stream, the worker and the seat's events.
func TestTheQueueTellsWaitingInFlightAndDeadApart(t *testing.T) {
at := func(m int) string {
return time.Date(2026, 10, 5, 12, m, 0, 0, time.UTC).Format(time.RFC3339Nano)
}
asks := []QueuedAsk{
{Seq: 1, ID: "dead-1"}, // handed out five times, its machine moved on: dead
{Seq: 2, ID: "running-g"}, // g14's latest start, no outcome: in flight
{Seq: 3, ID: "running-a"}, // ace's latest start, no outcome: in flight
{Seq: 4, ID: "unsaid"}, // taken, not yet said: in flight while the worker counts it
{Seq: 5, ID: "waiting-1"}, // after the worker's last delivery
{Seq: 6, ID: "waiting-2"},
}
heard := BuildHeard{
Started: map[string]BuildStart{
"dead-1": {ID: "dead-1", On: "g14", At: at(1)},
"running-g": {ID: "running-g", On: "g14", At: at(5)},
"running-a": {ID: "running-a", On: "ace", At: at(6)},
"done": {ID: "done", On: "novox", At: at(7)},
},
Outcomes: map[string]bool{"done": true},
}
got := ClassifyQueue(asks, 4, 2, heard)
want := map[string]string{"dead-1": AskDead, "running-g": AskInFlight, "running-a": AskInFlight,
"unsaid": AskDead, "waiting-1": AskWaiting, "waiting-2": AskWaiting}
for _, a := range got {
if a.State != want[a.ID] {
t.Errorf("%s is %s, want %s", a.ID, a.State, want[a.ID])
}
}
q := Queue{Asks: got}
if a, _ := q.Find("running-a"); a.On != "ace" || a.Started.IsZero() {
t.Errorf("an ask in flight does not say where: %+v", a)
}
// **The worker's count bounds it**: with one pending, the machine whose start is oldest died
// with its ask.
got = ClassifyQueue(asks, 4, 1, heard)
q = Queue{Asks: got}
if a, _ := q.Find("running-a"); a.State != AskInFlight {
t.Errorf("running-a is %s", a.State)
}
if a, _ := q.Find("running-g"); a.State != AskDead || a.On != "" {
t.Errorf("past the worker's count running-g is %s on %q", a.State, a.On)
}
// **Handed back after a holder restarted mid-build**: started on g14, g14 then started something
// else, and the worker still counts it — it waits to be handed out again, and is not dead. With
// no more asks behind the worker than it counts pending, none is dead.
restarted := []QueuedAsk{{Seq: 1, ID: "dead-1"}, {Seq: 2, ID: "running-g"}}
for _, a := range ClassifyQueue(restarted, 2, 2, heard) {
if a.State != AskInFlight {
t.Errorf("%s is %s with two behind the worker and two pending", a.ID, a.State)
}
if a.ID == "dead-1" && (a.On != "" || a.Was != "g14") {
t.Errorf("an ask handed back reads on %q, was on %q", a.On, a.Was)
}
}
// The handed-back one takes a leftover slot before an ask nobody said started.
got = ClassifyQueue(asks, 4, 4, heard)
q = Queue{Asks: got}
for _, id := range []string{"dead-1", "running-g", "running-a", "unsaid"} {
if a, _ := q.Find(id); a.State != AskInFlight {
t.Errorf("with four pending %s is %s", id, a.State)
}
}
got = ClassifyQueue(asks, 4, 3, heard)
q = Queue{Asks: got}
if a, _ := q.Find("dead-1"); a.State != AskInFlight {
t.Errorf("the handed-back ask lost its slot to one nobody said: %s", a.State)
}
if a, _ := q.Find("unsaid"); a.State != AskDead {
t.Errorf("unsaid is %s", a.State)
}
// None pending: everything behind the worker is dead, and names no machine.
for _, a := range ClassifyQueue(asks, 4, 0, heard) {
if a.Seq <= 4 && (a.State != AskDead || a.On != "") {
t.Errorf("%s with nothing pending is %s on %q", a.ID, a.State, a.On)
}
}
}
// What a direct read of the cancelled set answers.
func TestACancelledSetReadSaysWhatWasCancelled(t *testing.T) {
found := &nats.Msg{Header: nats.Header{"Nats-Subject": []string{"$KV.x.build-1"}}, Data: []byte(`{"why":"cancelled by hand"}`)}
if c, err := cancelledFrom(found); err != nil || !c {
t.Errorf("a value read as %v %v", c, err)
}
missing := &nats.Msg{Header: nats.Header{"Status": []string{"404"}}}
if c, err := cancelledFrom(missing); err != nil || c {
t.Errorf("not found read as %v %v", c, err)
}
deleted := &nats.Msg{Header: nats.Header{"KV-Operation": []string{"DEL"}}}
if c, err := cancelledFrom(deleted); err != nil || c {
t.Errorf("a deletion marker read as %v %v", c, err)
}
broken := &nats.Msg{Header: nats.Header{"Status": []string{"408"}, "Description": []string{"Request Timeout"}}}
if _, err := cancelledFrom(broken); err == nil {
t.Error("an error answer read as an answer")
}
if c, err := IsCancelled(nil, TheBuildMachine, "a.b.>"); err != nil || c {
t.Error("a pattern was looked up as a key")
}
}
// --- against a real server ----------------------------------------------------------------------
// **The race a delete alone leaves open**: an ask fetched in the moment it was cancelled is ended by
// the holder that took it — terminated, never built, its outcome said as failed.
// A paused holder takes nothing; resumed, it takes what waited.
// What a holder last said about taking work is read back per machine.
// A `build` waiting on its outcome hears a cancel — which publishes no outcome — from the cancelled
// set, and a kill from the outcome the holder announces (novox/hq ADR 0219).
// Paused in the moment a pull was answered: the ask is handed back, not built (ADR 0219).