The registration holds the controller's one receive loop. Checking the bound only between asks let an ask in flight hold it near two minutes, the deaf-controller limit. The whole wait now runs under a 60 s deadline, still says it waited when that deadline ends it, and a tool that did not answer in time is not asked again: it has already held the loop its whole wait.
149 lines
5.6 KiB
Go
149 lines
5.6 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/novox/mesh-controller/internal/link"
|
|
)
|
|
|
|
// A wait on a clock of its own: each pause moves it on, and nothing waits in real time.
|
|
func aFakeWait(bound time.Duration) (waitOut, *[]time.Duration) {
|
|
clock := time.Date(2026, 10, 11, 11, 52, 2, 0, time.UTC)
|
|
var paused []time.Duration
|
|
return waitOut{bound: bound, first: time.Second, most: 10 * time.Second,
|
|
now: func() time.Time { return clock },
|
|
sleep: func(_ context.Context, d time.Duration) error {
|
|
paused = append(paused, d)
|
|
clock = clock.Add(d)
|
|
return nil
|
|
}}, &paused
|
|
}
|
|
|
|
// notServed is what the bus says while node-tools re-serves, in the words seen on 2026-10-11.
|
|
func notServed() error {
|
|
return fmt.Errorf("cannot ask gitea.gitea_api: %w", link.ErrNothingServes)
|
|
}
|
|
|
|
// A registration's forge question that lands while the tool runner re-serves is asked again, and the forge's
|
|
// answer, seconds later, is the registration's (novox/hq issue 486).
|
|
func TestTheForgeNotServedForAMomentIsWaitedOut(t *testing.T) {
|
|
w, paused := aFakeWait(60 * time.Second)
|
|
asked := 0
|
|
facts, err := forgeWaitedOut(t.Context(), w, "novox/mesh-catalog", func(context.Context) (forgeFacts, error) {
|
|
asked++
|
|
if asked <= 2 {
|
|
return forgeFacts{}, notServed()
|
|
}
|
|
return forgeFacts{ID: 42, Guarded: true}, nil
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("a forge not served for two asks failed the registration: %v", err)
|
|
}
|
|
if facts.ID != 42 || !facts.Guarded || asked != 3 {
|
|
t.Fatalf("the forge's answer was %+v after %d asks, want its answer on the third", facts, asked)
|
|
}
|
|
if want := []time.Duration{time.Second, 2 * time.Second}; fmt.Sprint(*paused) != fmt.Sprint(want) {
|
|
t.Fatalf("paused %v, want %v", *paused, want)
|
|
}
|
|
}
|
|
|
|
// A tool that did not answer within its wait has held the receive loop that long already: not asked again.
|
|
func TestTheForgeNotAnsweringInTimeIsNotAskedAgain(t *testing.T) {
|
|
w, paused := aFakeWait(60 * time.Second)
|
|
asked := 0
|
|
_, err := forgeWaitedOut(t.Context(), w, "novox/x", func(context.Context) (forgeFacts, error) {
|
|
asked++
|
|
return forgeFacts{}, fmt.Errorf("gitea did not answer: %w", link.ErrNoAnswer)
|
|
})
|
|
if !errors.Is(err, link.ErrNoAnswer) || asked != 1 || len(*paused) != 0 {
|
|
t.Fatalf("a forge that did not answer in time was asked %d times, paused %v: %v", asked, *paused, err)
|
|
}
|
|
}
|
|
|
|
// An ask that hangs past the bound is cut off at the bound, not at its own wait, and the failure still says it
|
|
// waited: the receive loop is held no longer than the bound (novox/hq issue 486).
|
|
func TestAnAskHangingPastTheBoundIsCutOffAtIt(t *testing.T) {
|
|
w := waitOut{bound: 200 * time.Millisecond, first: 10 * time.Millisecond, most: 50 * time.Millisecond,
|
|
now: time.Now, sleep: sleepOrDone}
|
|
asked := 0
|
|
began := time.Now()
|
|
_, err := forgeWaitedOut(t.Context(), w, "novox/mesh-catalog", func(ctx context.Context) (forgeFacts, error) {
|
|
asked++
|
|
if asked == 1 {
|
|
return forgeFacts{}, notServed()
|
|
}
|
|
select { // link.Ask's own wait is 30 s; the bound must end it first
|
|
case <-ctx.Done():
|
|
return forgeFacts{}, fmt.Errorf("gitea did not answer: %w", link.ErrNoAnswer)
|
|
case <-time.After(30 * time.Second):
|
|
return forgeFacts{ID: 1, Guarded: true}, nil
|
|
}
|
|
})
|
|
if took := time.Since(began); took > 2*time.Second {
|
|
t.Fatalf("the wait took %s, past its bound of %s", took, w.bound)
|
|
}
|
|
if err == nil || !strings.Contains(err.Error(), "waited") || !strings.Contains(err.Error(), "asking 2 times") ||
|
|
!strings.Contains(err.Error(), "issue 486") {
|
|
t.Fatalf("the bound's end does not say it waited: %v", err)
|
|
}
|
|
}
|
|
|
|
// Past the bound the registration fails, saying it waited, for how long, and what the forge last said.
|
|
func TestTheForgeNotServedPastTheBoundFailsSayingItWaited(t *testing.T) {
|
|
w, paused := aFakeWait(60 * time.Second)
|
|
_, err := forgeWaitedOut(t.Context(), w, "novox/mesh-catalog", func(context.Context) (forgeFacts, error) {
|
|
return forgeFacts{}, notServed()
|
|
})
|
|
if err == nil {
|
|
t.Fatal("a forge never served was read as an answer")
|
|
}
|
|
var waited time.Duration
|
|
for _, p := range *paused {
|
|
waited += p
|
|
}
|
|
if waited > 60*time.Second || waited < 40*time.Second {
|
|
t.Fatalf("waited %s in all, want close to and within the bound of 60s", waited)
|
|
}
|
|
for _, want := range []string{"waited " + waited.String(), "novox/mesh-catalog", "nothing serves", "issue 486"} {
|
|
if !strings.Contains(err.Error(), want) {
|
|
t.Errorf("the failure does not say %q: %v", want, err)
|
|
}
|
|
}
|
|
if !errors.Is(err, link.ErrNothingServes) {
|
|
t.Errorf("the failure lost what the forge last said: %v", err)
|
|
}
|
|
}
|
|
|
|
// A refusal is the forge's word: never asked again.
|
|
func TestARefusalFromTheForgeIsNeverAskedAgain(t *testing.T) {
|
|
w, paused := aFakeWait(60 * time.Second)
|
|
asked := 0
|
|
_, err := forgeWaitedOut(t.Context(), w, "novox/x", func(context.Context) (forgeFacts, error) {
|
|
asked++
|
|
return forgeFacts{}, errors.New("gitea.gitea_api: 404 Not Found")
|
|
})
|
|
if err == nil || asked != 1 || len(*paused) != 0 {
|
|
t.Fatalf("a refusal was asked %d times, paused %v: %v", asked, *paused, err)
|
|
}
|
|
}
|
|
|
|
// A registration whose own context ends stops waiting at once.
|
|
func TestAWaitForTheForgeEndsWithItsContext(t *testing.T) {
|
|
w, _ := aFakeWait(60 * time.Second)
|
|
ctx, cancel := context.WithCancel(t.Context())
|
|
w.sleep = func(ctx context.Context, d time.Duration) error { cancel(); return sleepOrDone(ctx, d) }
|
|
asked := 0
|
|
_, err := forgeWaitedOut(ctx, w, "novox/x", func(context.Context) (forgeFacts, error) {
|
|
asked++
|
|
return forgeFacts{}, notServed()
|
|
})
|
|
if !errors.Is(err, context.Canceled) || asked != 1 {
|
|
t.Fatalf("asked %d times after the context ended: %v", asked, err)
|
|
}
|
|
}
|