An agent following a rollout had plans answer once, in prose, and polled the console with a curl loop of its own to be told a plan moved. plans now takes wait (a plan's id), until (moved, tier or done) and bound, and answers when the plan moves or the bound passes, saying which, with the plan as data and its line as the output. A call's one answer has to reach the console inside its thirty seconds, so link.AnswersWithin lets a handler that waits on purpose hold its answer up to LongestAnswer (25s) instead of AnswerWithin; the bound is at most 20s, and a caller following a plan further asks again.
56 lines
2.0 KiB
Go
56 lines
2.0 KiB
Go
package link
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// **A handler that waits on purpose is answered in full when it ends inside the window it asked for**
|
|
// (novox/hq issue 229): `plans` asked to wait for a plan holds its caller's one answer until the plan
|
|
// moves or its bound passes, rather than being answered "still running" at AnswerWithin.
|
|
func TestAHandlerThatWaitsOnPurposeIsAnsweredInFullInsideItsWindow(t *testing.T) {
|
|
shortWindow(t, 20*time.Millisecond)
|
|
l, a := NewCallLog(), newAnswers(t)
|
|
l.serveCall("mesh-controller", "plans", json.RawMessage(`{"wait":"plan-1"}`), "_INBOX.x.3",
|
|
func(ctx context.Context, _ json.RawMessage) (any, error) {
|
|
AnswersWithin(ctx, 200*time.Millisecond)
|
|
time.Sleep(80 * time.Millisecond) // past AnswerWithin, inside what it asked for
|
|
return "plan-1 moved", nil
|
|
}, a.respond, nil)
|
|
if got := a.only()["result"]; got != "plan-1 moved" {
|
|
t.Fatalf("a waiting handler inside its window was answered %v, not in full", got)
|
|
}
|
|
}
|
|
|
|
// What a handler may ask for is bounded: past LongestAnswer its caller is told it is running, as any
|
|
// call is, so no answer is ever later than the console waits.
|
|
func TestAWaitIsNeverLongerThanTheLongestAnswer(t *testing.T) {
|
|
shortWindow(t, 10*time.Millisecond)
|
|
was := LongestAnswer
|
|
LongestAnswer = 30 * time.Millisecond
|
|
t.Cleanup(func() { LongestAnswer = was })
|
|
l, a := NewCallLog(), newAnswers(t)
|
|
release := make(chan struct{})
|
|
finished := make(chan struct{})
|
|
go func() {
|
|
l.serveCall("mesh-controller", "plans", nil, "_INBOX.x.4", func(ctx context.Context, _ json.RawMessage) (any, error) {
|
|
AnswersWithin(ctx, time.Hour)
|
|
<-release
|
|
return "late", nil
|
|
}, a.respond, nil)
|
|
close(finished)
|
|
}()
|
|
select {
|
|
case <-a.sent:
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("a handler asking for an hour held its caller past LongestAnswer")
|
|
}
|
|
if got, _ := a.only()["result"].(map[string]any); got["running"] != true {
|
|
t.Fatalf("past LongestAnswer the caller was answered %v, not that it is running", a.only())
|
|
}
|
|
close(release)
|
|
<-finished
|
|
}
|