Merge remote-tracking branch 'origin/main' into fix/403-fix-time
mesh/merge-gate head daf52b3aeb79 merged into trunk 4dbd4066ca54 — pass: builds build-agent, mesh-controller → ace, g14, novox, shanks; no bus step; every …
mesh/repo-check head daf52b3aeb79 merged into trunk 4dbd4066ca54 — pass: its merge-check.sh passed
mesh/delivery covered after its failure: a later walk that contains it was delivered: plan-1791834103351744736 (novox/mesh-controller at c0c379af into ma…
mesh/delivery-group group fix/403-fix-time delivered: every member is delivered
mesh/merge-gate head daf52b3aeb79 merged into trunk 4dbd4066ca54 — pass: builds build-agent, mesh-controller → ace, g14, novox, shanks; no bus step; every …
mesh/repo-check head daf52b3aeb79 merged into trunk 4dbd4066ca54 — pass: its merge-check.sh passed
mesh/delivery covered after its failure: a later walk that contains it was delivered: plan-1791834103351744736 (novox/mesh-controller at c0c379af into ma…
mesh/delivery-group group fix/403-fix-time delivered: every member is delivered
# Conflicts: # internal/link/events.go
This commit is contained in:
@@ -138,6 +138,9 @@ type GroupMember struct {
|
||||
Repository string `json:"repository"`
|
||||
Ref string `json:"ref"`
|
||||
Paths []string `json:"paths,omitempty"`
|
||||
// Base is the branch it merges into: the build seat merges the head into that trunk's head before the
|
||||
// group is composed (novox/hq ADR 0323 §6). A build seat from before it composes the head alone.
|
||||
Base string `json:"base,omitempty"`
|
||||
}
|
||||
|
||||
// Who judges a merge check's gate.
|
||||
@@ -164,6 +167,14 @@ type CheckOutcome struct {
|
||||
// Gate and RepoCheck are its two layers; Verdict and Summary above are the gate's.
|
||||
Gate *CheckLayer `json:"gate,omitempty"`
|
||||
RepoCheck *CheckLayer `json:"repo-check,omitempty"`
|
||||
// Head and Trunk are what was judged (novox/hq ADR 0323 §3): the pull request's head merged into the trunk
|
||||
// commit, each as twelve characters. Trunk is empty from a build seat from before it, which judged the head
|
||||
// alone. Beside are the commits of the repositories read beside it, by directory, named and not judged
|
||||
// against (issue 568); Members a delivery group's other heads, each with the trunk commit it merged into.
|
||||
Head string `json:"head,omitempty"`
|
||||
Trunk string `json:"trunk,omitempty"`
|
||||
Beside map[string]string `json:"beside,omitempty"`
|
||||
Members []CheckedMember `json:"members,omitempty"`
|
||||
}
|
||||
|
||||
// SourceOnSeat names a repository by the seat whose holder serves it and its path there.
|
||||
|
||||
+47
-10
@@ -601,6 +601,24 @@ func Acknowledge(ctx context.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// LongestAnswer is the longest a handler that waits on purpose may hold its caller's one answer
|
||||
// (AnswersWithin, novox/hq issue 229): inside the console's thirty seconds with room for the trip back,
|
||||
// and inside the bus's window for an answer (broker.ResponseTTL). A variable so a test need not wait.
|
||||
var LongestAnswer = 25 * time.Second
|
||||
|
||||
type answerWithinKey struct{}
|
||||
|
||||
// AnswersWithin asks that the call's caller be answered in full if the handler ends within d of the
|
||||
// call's start, rather than within AnswerWithin: for a handler that waits on purpose, with a bound —
|
||||
// `plans` waiting for a plan to move (novox/hq issue 229). Past d, or past LongestAnswer whichever is
|
||||
// shorter, its caller is told it is running, as for any call. Never shortens the window; without effect
|
||||
// outside a served call, and once the caller was answered.
|
||||
func AnswersWithin(ctx context.Context, d time.Duration) {
|
||||
if f, ok := ctx.Value(answerWithinKey{}).(func(time.Duration)); ok {
|
||||
f(d)
|
||||
}
|
||||
}
|
||||
|
||||
// running is the interim answer: what a caller reads when the call has not finished.
|
||||
func running(c *Call, within time.Duration, acknowledged bool, follow string) []byte {
|
||||
why := fmt.Sprintf("it has not finished in %s", within)
|
||||
@@ -645,6 +663,13 @@ func (l *CallLog) serveCallWithin(seat, verb string, args json.RawMessage, reply
|
||||
acknowledged := make(chan struct{})
|
||||
var once sync.Once
|
||||
ctx = context.WithValue(ctx, answerNowKey{}, func() { once.Do(func() { close(acknowledged) }) })
|
||||
longer := make(chan time.Duration, 1)
|
||||
ctx = context.WithValue(ctx, answerWithinKey{}, func(d time.Duration) {
|
||||
select {
|
||||
case longer <- d:
|
||||
default:
|
||||
}
|
||||
})
|
||||
|
||||
type outcome struct {
|
||||
body []byte
|
||||
@@ -703,17 +728,29 @@ func (l *CallLog) serveCallWithin(seat, verb string, args json.RawMessage, reply
|
||||
}
|
||||
}
|
||||
}
|
||||
timer := time.NewTimer(AnswerWithin)
|
||||
within, start := AnswerWithin, time.Now()
|
||||
timer := time.NewTimer(within)
|
||||
defer timer.Stop()
|
||||
select {
|
||||
case o := <-done:
|
||||
l.finish(c, o.body, o.failed, false)
|
||||
say(o.body)
|
||||
return
|
||||
case <-acknowledged:
|
||||
say(running(c, AnswerWithin, true, l.Follow))
|
||||
case <-timer.C:
|
||||
say(running(c, AnswerWithin, false, l.Follow))
|
||||
waiting:
|
||||
for {
|
||||
select {
|
||||
case o := <-done:
|
||||
l.finish(c, o.body, o.failed, false)
|
||||
say(o.body)
|
||||
return
|
||||
case <-acknowledged:
|
||||
say(running(c, within, true, l.Follow))
|
||||
break waiting
|
||||
case d := <-longer:
|
||||
// A handler waiting on purpose (AnswersWithin): its window is stretched, never past LongestAnswer.
|
||||
if d = min(d, LongestAnswer); d > within {
|
||||
within = d
|
||||
timer.Reset(time.Until(start.Add(d)))
|
||||
}
|
||||
case <-timer.C:
|
||||
say(running(c, within, false, l.Follow))
|
||||
break waiting
|
||||
}
|
||||
}
|
||||
o := <-done
|
||||
l.finish(c, o.body, o.failed, true)
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
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
|
||||
}
|
||||
+25
-8
@@ -221,13 +221,17 @@ type SourceMoved struct {
|
||||
// PullUpdated is what the forge announces when an open pull request's head moves — opened, or pushed
|
||||
// to (novox/hq to-be 45 §9): what the controller asks the build seat to check before it merges.
|
||||
type PullUpdated struct {
|
||||
Owner string `json:"owner"`
|
||||
Repo string `json:"repo"`
|
||||
Number int `json:"number"`
|
||||
Title string `json:"title,omitempty"`
|
||||
Base string `json:"base"`
|
||||
Head string `json:"head"`
|
||||
Commit string `json:"head_sha"`
|
||||
Owner string `json:"owner"`
|
||||
Repo string `json:"repo"`
|
||||
Number int `json:"number"`
|
||||
Title string `json:"title,omitempty"`
|
||||
Base string `json:"base"`
|
||||
Head string `json:"head"`
|
||||
Commit string `json:"head_sha"`
|
||||
// BaseSHA is the base branch's head when the forge announced the head (novox/hq ADR 0323): the trunk commit a
|
||||
// verdict the controller gives without the build seat — a change that runs no build — is judged against. Empty
|
||||
// from a forge module from before it.
|
||||
BaseSHA string `json:"base_sha,omitempty"`
|
||||
CloneURL string `json:"clone_url"`
|
||||
HTMLURL string `json:"html_url,omitempty"`
|
||||
// Paths are the files the pull request changes; PathsTruncated says there were more.
|
||||
@@ -278,14 +282,24 @@ type Checked struct {
|
||||
// status from it — the group's verdict is its owner's, mesh-delivery, to say on each member's head.
|
||||
Group string `json:"group,omitempty"`
|
||||
Members []CheckedMember `json:"members,omitempty"`
|
||||
// Trunk is the trunk commit the head was merged into and judged as, twelve characters (novox/hq ADR 0323
|
||||
// §3): the verdict holds for that merge only, and goes out of date when the trunk moves. Empty from a build
|
||||
// seat that judged the head alone. Beside are the commits of the other repositories the check read, by
|
||||
// directory, named and not judged against (issue 568). Took is how long the check ran, for the merge tool
|
||||
// to say how long the next one may take (ADR 0323 §5).
|
||||
Trunk string `json:"trunk,omitempty"`
|
||||
Beside map[string]string `json:"beside,omitempty"`
|
||||
Took string `json:"took,omitempty"`
|
||||
}
|
||||
|
||||
// CheckedMember is one head a group's composed check judged.
|
||||
// CheckedMember is one head a group's composed check judged, and the trunk commit it was merged into (novox/hq
|
||||
// ADR 0323 §6), twelve characters; empty from a build seat that composed the head alone.
|
||||
type CheckedMember struct {
|
||||
Owner string `json:"owner"`
|
||||
Repo string `json:"repo"`
|
||||
Number int `json:"number,omitempty"`
|
||||
Commit string `json:"commit"`
|
||||
Trunk string `json:"trunk,omitempty"`
|
||||
}
|
||||
|
||||
// ChangePlan is what a change does to the mesh, computed from its diffset — a repository, the branch it
|
||||
@@ -314,6 +328,9 @@ type ChangePlan struct {
|
||||
// Class is the class of the slowest-budgeted module the change moves (novox/hq ADR 0282 §1, ADR 0313 §2):
|
||||
// core, leaf, or none when it moves no module at all — a fix that moves nothing has no fix budget.
|
||||
Class string `json:"class,omitempty"`
|
||||
// Trunk is the trunk commit the head was merged into when it was judged (novox/hq ADR 0323 §3), twelve
|
||||
// characters: set on the plan a verdict carries, which mesh-delivery keeps with its other inputs.
|
||||
Trunk string `json:"trunk,omitempty"`
|
||||
}
|
||||
|
||||
// MachinePlan is one machine's part of a change plan.
|
||||
|
||||
Reference in New Issue
Block a user