issue 974: the checks are queued by pull request on two work-queue streams, taken after a delivery build, dropped when judged, stopped when their key moves; the reordering and the forge-asking end are gone
This commit is contained in:
@@ -27,6 +27,7 @@ import (
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
@@ -290,6 +291,26 @@ func answer(ctx context.Context, publisher builder.Publisher, on, workspace stri
|
||||
if err := work.Began(ctx); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "cannot say a build started: %v\n", err)
|
||||
}
|
||||
// **A running check watches its pull request's key** (novox/hq ADR 0406 §4, issue 974): when the head or the trunk
|
||||
// commit in it moves, the check is for a state nobody wants any more, and ends itself — nothing is asked of the forge
|
||||
// (issue 726). A group's check, a try and a build watch nothing.
|
||||
var superseded atomic.Bool
|
||||
if w, ok := work.(interface {
|
||||
WatchPull(context.Context, func(link.PullKey)) (func(), error)
|
||||
}); ok && request.Check != nil {
|
||||
stopWatching, err := w.WatchPull(building, func(k link.PullKey) {
|
||||
superseded.Store(true)
|
||||
say("superseded", fmt.Sprintf("the pull request's key moved to %.12s on %.12s while this checked %.12s on %.12s: stopped",
|
||||
k.Head, k.Trunk, request.Ref, request.Check.Trunk))
|
||||
cancel()
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "the key of %s/%s#%d could not be watched, so its check runs on if the key moves: %v\n",
|
||||
request.Check.Owner, request.Check.Repo, request.Check.Number, err)
|
||||
} else {
|
||||
defer stopWatching()
|
||||
}
|
||||
}
|
||||
|
||||
result := link.BuildResult{
|
||||
ID: request.ID, Repository: request.Repository, Path: request.Path,
|
||||
@@ -382,7 +403,11 @@ func answer(ctx context.Context, publisher builder.Publisher, on, workspace stri
|
||||
if mine != nil {
|
||||
killed = h.returned(mine) && err != nil
|
||||
}
|
||||
if killed {
|
||||
if superseded.Load() && err != nil {
|
||||
// Ended by its own key moving on (ADR 0406 §4): said as the check's failure, as a kill is.
|
||||
result.Failed = link.SupersededByKey
|
||||
say("failed", link.SupersededByKey)
|
||||
} else if killed {
|
||||
// **Killed by hand is the outcome, whatever the build was doing** (novox/hq ADR 0219): the
|
||||
// error it ended with is the kill's consequence, not a fault of the source.
|
||||
result.Failed = link.KilledByHand
|
||||
|
||||
@@ -191,8 +191,9 @@ func (f following) PullUpdated(ctx context.Context, p link.PullUpdated) error {
|
||||
if err := ask.Ask(ctx, request); err != nil {
|
||||
return err
|
||||
}
|
||||
// A new head ends the check of the old one (novox/hq issue 726).
|
||||
endingSupersededChecks(ctx, seat, p, request.ID, f.oneShot)
|
||||
// **A newer request replaces the one still waiting, and a newer key stops the one running** (novox/hq ADR 0406 §3, §4,
|
||||
// issue 974): the check queue keeps one request a pull request, and a running check watches its key. Nothing is asked
|
||||
// of the forge here (issue 726).
|
||||
what := "its own merge-check.sh alone: " + noModuleTouched
|
||||
if scope.gated() {
|
||||
what = fmt.Sprintf("the gate over %s (a merge would build %d module(s) in %d tier(s))",
|
||||
@@ -270,7 +271,7 @@ func checkRequestFor(ctx context.Context, open *stores, p link.PullUpdated, scop
|
||||
Held: heldBy(ctx),
|
||||
Seats: seatBases(ctx),
|
||||
Source: sourceOnSeat(source),
|
||||
Check: &link.CheckRequest{Owner: p.Owner, Repo: p.Repo, Number: p.Number, Base: p.Base,
|
||||
Check: &link.CheckRequest{Owner: p.Owner, Repo: p.Repo, Number: p.Number, Base: p.Base, Trunk: p.BaseSHA, Ready: p.Ready,
|
||||
Paths: p.Paths, Beside: beside, Modules: scope.Modules, Dependents: scope.Dependents, New: scope.New,
|
||||
Manifests: scope.Manifests, Judge: scope.Judge, Builds: builds, Named: namedForTheCheck(ctx, p, builds),
|
||||
Release: release, Running: runningBy(ctx)},
|
||||
|
||||
@@ -584,6 +584,9 @@ type orderMember struct {
|
||||
// BaseSHA is the base branch's head the forge last announced the head with (novox/hq ADR 0323): the trunk commit
|
||||
// a verdict no build judges names when the head is checked again through mesh-delivery.
|
||||
BaseSHA string `json:"base_sha,omitempty"`
|
||||
// Ready says the member's pull request is marked ready (novox/hq ADR 0406 §3, issue 974): its check is queued before
|
||||
// every other check. Said by mesh-delivery on a ready mark.
|
||||
Ready bool `json:"ready,omitempty"`
|
||||
}
|
||||
|
||||
// pull is the member as the forge announced it.
|
||||
@@ -595,7 +598,7 @@ func (m orderMember) pull() link.PullUpdated {
|
||||
}
|
||||
return link.PullUpdated{Owner: owner, Repo: repo, Number: m.Number, Base: base, Commit: m.Head, BaseSHA: m.BaseSHA,
|
||||
CloneURL: m.CloneURL, Paths: m.Paths, PathsTruncated: m.PathsTruncated, Removed: m.Removed,
|
||||
ModuleDirs: m.ModuleDirs, ModuleDirsSaid: m.ModuleDirsSaid}
|
||||
ModuleDirs: m.ModuleDirs, ModuleDirsSaid: m.ModuleDirsSaid, Ready: m.Ready}
|
||||
}
|
||||
|
||||
// orderPair is one "before" among a group's members, why, and the rules it won over.
|
||||
@@ -892,6 +895,8 @@ func deliveryCommand(ctx context.Context, args []string) error {
|
||||
removed := set.String("removed", "", "the files it deletes, comma-separated")
|
||||
membersJSON := set.String("members", "", "a group's members, as JSON")
|
||||
group := set.String("group", "", "a delivery group's id")
|
||||
ready := set.Bool("ready", false, "the head's pull request is marked ready: its check is queued before every other check "+
|
||||
"(novox/hq ADR 0406 §3)")
|
||||
by := set.String("by", catalogue.DeliverySeat, "who says it")
|
||||
why := set.String("why", "", "why")
|
||||
without := set.String("without", "", "merges a stop leaves out, <repository>@<commit>, comma-separated: the walk's "+
|
||||
@@ -907,6 +912,12 @@ func deliveryCommand(ctx context.Context, args []string) error {
|
||||
if err := json.Unmarshal([]byte(*membersJSON), &members); err != nil {
|
||||
return fmt.Errorf("the members are not readable: %w", err)
|
||||
}
|
||||
// Marked ready by the verb's word for every member it names, besides any member that says so itself.
|
||||
if *ready {
|
||||
for i := range members {
|
||||
members[i].Ready = true
|
||||
}
|
||||
}
|
||||
}
|
||||
open, err := openStores(ctx)
|
||||
if err != nil {
|
||||
@@ -1166,6 +1177,8 @@ func askGroupCheck(ctx context.Context, open *stores, group string, members []or
|
||||
return "", err
|
||||
}
|
||||
request.Check.Group = group
|
||||
// A group's check is queued as ready when any member's pull request is marked ready.
|
||||
request.Check.Ready = slices.ContainsFunc(members, func(m orderMember) bool { return m.Ready })
|
||||
world, err := theRestOfTheMesh(ctx, open.inventory, shelfOf(entries), "")
|
||||
if err != nil {
|
||||
return "", err
|
||||
|
||||
@@ -80,30 +80,6 @@ func listQueue(ctx context.Context, args []string, w io.Writer) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// keepingTheQueueInOrder runs the build queue's passes while this controller acts (novox/hq issue 558):
|
||||
// the one process that puts the queue in order, so no two passes pause and resume it over each other.
|
||||
// The build seat is read from the catalogue at most every half minute.
|
||||
func keepingTheQueueInOrder(ctx context.Context, open *stores, js *broker.JetStream) {
|
||||
if js == nil {
|
||||
return
|
||||
}
|
||||
seat, readAt := link.TheBuildMachine, time.Time{}
|
||||
seatNow := func() string {
|
||||
if time.Since(readAt) < watchEvery {
|
||||
return seat
|
||||
}
|
||||
if entries, err := open.inventory.Catalogued(ctx); err == nil {
|
||||
seat, readAt = buildSeatAmong(entries), time.Now()
|
||||
} else {
|
||||
fmt.Printf("the build seat is read as %s: the catalogue cannot be read: %v\n", seat, err)
|
||||
}
|
||||
return seat
|
||||
}
|
||||
link.KeepingInOrder(ctx, js, seatNow, link.Holding, func(format string, args ...any) {
|
||||
fmt.Printf(format+"\n", args...)
|
||||
})
|
||||
}
|
||||
|
||||
// queueAnswer is the queue as data (novox/hq issue 455): what a caller reads to say how far back an ask
|
||||
// waits — "waiting in the build queue behind 2" — which the table alone could not tell it. Text is the
|
||||
// table, written from the same read: the verb answers it as its output and the rest as its answer, so the
|
||||
@@ -303,16 +279,8 @@ func cancelAsk(ctx context.Context, js *broker.JetStream, open *stores, seat str
|
||||
fmt.Fprintf(os.Stderr, "could not withdraw the cancel of %s: %v — a holder taking it ends it as cancelled\n", ask.ID, err)
|
||||
}
|
||||
}
|
||||
worker, _ := broker.HolderConsumerFor("", "", broker.DeclaredSeat{Name: seat, Accepts: []string{"build"}})
|
||||
// **Where it waits now** (novox/hq issue 558): moved into order since it was read, a waiting ask has a
|
||||
// later sequence, and the worker handing out what was behind its old one is not it being taken.
|
||||
if ask.State == link.AskWaiting {
|
||||
if q, err := link.ReadQueue(ctx, js, seat); err == nil {
|
||||
if now, ok := q.Find(ask.ID); ok && now.State == link.AskWaiting {
|
||||
ask.Seq = now.Seq
|
||||
}
|
||||
}
|
||||
}
|
||||
// The worker of the queue it waits in: the seat's own, or one of the two check queues' (novox/hq ADR 0406 §3).
|
||||
worker := link.WorkerOf(seat, ask)
|
||||
switch ask.State {
|
||||
case link.AskWaiting:
|
||||
if taken, err := takenSince(js, worker, ask.Seq); err != nil {
|
||||
@@ -334,7 +302,6 @@ func cancelAsk(ctx context.Context, js *broker.JetStream, open *stores, seat str
|
||||
"ends it there", ask.ID, started, ask.ID)
|
||||
}
|
||||
}
|
||||
// Found by its id when it was moved into order since it was read (novox/hq issue 558).
|
||||
removed, err := link.DeleteAsk(ctx, js, seat, ask)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("%s is marked cancelled and could not be deleted from the queue (seq %d): %w — a "+
|
||||
|
||||
@@ -413,10 +413,15 @@ func (a *verbArguments) commandLine() ([]string, error) {
|
||||
if err := need("members"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
argv := []string{"delivery", "check", "--members", str("members")}
|
||||
if g := str("group"); g != "" {
|
||||
return []string{"delivery", "check", "--group", g, "--members", str("members")}, nil
|
||||
argv = []string{"delivery", "check", "--group", g, "--members", str("members")}
|
||||
}
|
||||
return []string{"delivery", "check", "--members", str("members")}, nil
|
||||
// Marked ready (novox/hq ADR 0406 §3): the check is queued before every other check.
|
||||
if r := strings.ToLower(str("ready")); r != "" && r != "false" && r != "no" && r != "0" {
|
||||
argv = append(argv, "--ready")
|
||||
}
|
||||
return argv, nil
|
||||
case "deliver":
|
||||
if err := need("plan"); err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -1,157 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/novox/mesh-controller/internal/broker"
|
||||
"github.com/novox/mesh-controller/internal/link"
|
||||
)
|
||||
|
||||
// A new push to a pull request ends the check of its old head (novox/hq issue 726).
|
||||
//
|
||||
// A check judges one head; once the pull request has another, its verdict answers nobody and its run holds a build
|
||||
// machine for minutes the new head waits behind. So when a check is asked for pull request N of a repository at a
|
||||
// head, **every check of that pull request at another head ends**: a waiting one is dropped from the queue as
|
||||
// `cancel` drops it, one in flight is ended on its machine as `kill` ends it. Each is said in the log.
|
||||
//
|
||||
// **Never anything else**: a try (`try`, asked for no pull request), a walk's build (no check), a delivery group's
|
||||
// composed check (it answers several pull requests), another pull request's check, a dead ask (left for `clear
|
||||
// --dead`), and a check of the same head — a recheck of that head is not another head.
|
||||
|
||||
// **Only for the pull request's current head.** Announcements of a pull request are not ordered, one is heard again
|
||||
// after a restart, and a recheck asks for the head it recorded: a check asked for a head the pull request no longer
|
||||
// has must never end the check of the head it has. So the forge is asked first which head the pull request has; only
|
||||
// when it names this one does anything end, and when it cannot be asked, or names another, nothing ends and that is
|
||||
// said.
|
||||
|
||||
// forgePullHead asks the forge, through its module's tools on the bus, which commit a pull request's head is. A
|
||||
// variable so a test needs no forge.
|
||||
var forgePullHead = func(ctx context.Context, js *broker.JetStream, owner, repo string, number int) (string, error) {
|
||||
raw, _ := json.Marshal(map[string]any{"path": fmt.Sprintf("/repos/%s/%s/pulls/%d", owner, repo, number)})
|
||||
answer, err := link.Ask(ctx, link.OverNATS{Conn: js.Conn()}, "gitea", "gitea_api", raw, 30*time.Second)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if answer.Error != "" {
|
||||
return "", errors.New(answer.Error)
|
||||
}
|
||||
var pull struct {
|
||||
Result struct {
|
||||
Head struct {
|
||||
SHA string `json:"sha"`
|
||||
} `json:"head"`
|
||||
} `json:"result"`
|
||||
}
|
||||
if err := json.Unmarshal(answer.Result, &pull); err != nil {
|
||||
return "", fmt.Errorf("the forge's answer about the pull request cannot be read: %w", err)
|
||||
}
|
||||
if pull.Result.Head.SHA == "" {
|
||||
return "", errors.New("the forge named no head for the pull request")
|
||||
}
|
||||
return pull.Result.Head.SHA, nil
|
||||
}
|
||||
|
||||
// supersededChecks are the checks in the queue that a check of owner/repo#number at head ends: of the same pull
|
||||
// request, at another head, waiting or in flight, and not the ask keep.
|
||||
func supersededChecks(q link.Queue, owner, repo string, number int, head, keep string) []link.QueuedAsk {
|
||||
if number <= 0 || head == "" {
|
||||
return nil
|
||||
}
|
||||
var out []link.QueuedAsk
|
||||
for _, a := range q.Asks {
|
||||
c := a.Request.Check
|
||||
switch {
|
||||
case c == nil, link.IsTry(a.Request), c.Group != "", c.Number != number, a.ID == keep,
|
||||
!strings.EqualFold(c.Owner, owner), !strings.EqualFold(c.Repo, repo),
|
||||
a.State != link.AskWaiting && a.State != link.AskInFlight:
|
||||
continue
|
||||
}
|
||||
ref := a.Request.Ref
|
||||
if ref == "" {
|
||||
ref = a.Ref
|
||||
}
|
||||
if ref == "" || ref == head || link.SameHead(ref, head) {
|
||||
continue
|
||||
}
|
||||
out = append(out, a)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// endSupersededChecks ends the checks a check of p at its head supersedes, keep being the new check's own ask, and
|
||||
// says each in the log. A waiting one is cancelled — nothing recorded: a check builds nothing, and the forge's status
|
||||
// of the old head is no longer the pull request's — and one a machine builds is killed there.
|
||||
func endSupersededChecks(ctx context.Context, js *broker.JetStream, seat string, p link.PullUpdated, keep string) []string {
|
||||
if p.Number <= 0 || p.Commit == "" {
|
||||
return nil
|
||||
}
|
||||
current, err := forgePullHead(ctx, js, p.Owner, p.Repo, p.Number)
|
||||
if err != nil {
|
||||
return []string{fmt.Sprintf("%s/%s#%d (%.8s): nothing ended — the forge could not say the pull request's "+
|
||||
"current head, so checks of its other heads keep running: %v", p.Owner, p.Repo, p.Number, p.Commit, err)}
|
||||
}
|
||||
if current != p.Commit && !link.SameHead(current, p.Commit) {
|
||||
return []string{fmt.Sprintf("%s/%s#%d (%.8s): nothing ended — the pull request's head is %.12s now, not "+
|
||||
"this one", p.Owner, p.Repo, p.Number, p.Commit, current)}
|
||||
}
|
||||
q, err := link.ReadQueue(ctx, js, seat)
|
||||
if err != nil {
|
||||
return []string{fmt.Sprintf("%s/%s#%d (%.8s): could not read the %s queue for checks of its earlier heads: %v",
|
||||
p.Owner, p.Repo, p.Number, p.Commit, seat, err)}
|
||||
}
|
||||
var said []string
|
||||
for _, a := range supersededChecks(q, p.Owner, p.Repo, p.Number, p.Commit, keep) {
|
||||
old := a.Request.Ref
|
||||
if old == "" {
|
||||
old = a.Ref
|
||||
}
|
||||
what := fmt.Sprintf("check of %s/%s#%d at %.12s", p.Owner, p.Repo, p.Number, old)
|
||||
var err error
|
||||
if a.State == link.AskInFlight && a.On != "" {
|
||||
var answer link.Answer
|
||||
answer, err = link.AskSeatTool(ctx, js.Conn(), seat, "kill", a.On, map[string]string{"id": a.ID}, killAnswer)
|
||||
if err == nil && answer.Error != "" {
|
||||
err = fmt.Errorf("%s refused: %s", a.On, answer.Error)
|
||||
}
|
||||
} else {
|
||||
_, err = cancelAsk(ctx, js, nil, seat, a)
|
||||
}
|
||||
if err != nil {
|
||||
said = append(said, fmt.Sprintf("not ended: %s (%s) — superseded by %.12s, but: %v", what, a.ID, p.Commit, err))
|
||||
continue
|
||||
}
|
||||
said = append(said, fmt.Sprintf("ended: %s — superseded by %.12s (%s, %s)", what, p.Commit, a.ID, a.State))
|
||||
}
|
||||
return said
|
||||
}
|
||||
|
||||
// endingSupersededChecks ends them beside the announcement, so a kill waiting on its machine never holds the
|
||||
// handler: on its own connection to the bus, within twice a kill's wait. A one-shot process (oneShot) ends them
|
||||
// within the call instead, as its exit would cut them off.
|
||||
func endingSupersededChecks(ctx context.Context, seat string, p link.PullUpdated, keep string, oneShot bool) {
|
||||
end := func() {
|
||||
ctx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 2*killAnswer)
|
||||
defer cancel()
|
||||
js, err := dialTheBus()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "%s/%s#%d (%.8s): could not reach the bus to end checks of its earlier heads: %v\n",
|
||||
p.Owner, p.Repo, p.Number, p.Commit, err)
|
||||
return
|
||||
}
|
||||
defer js.Close()
|
||||
for _, line := range endSupersededChecks(ctx, js, seat, p, keep) {
|
||||
fmt.Println(line)
|
||||
}
|
||||
}
|
||||
if oneShot {
|
||||
end()
|
||||
return
|
||||
}
|
||||
go end()
|
||||
}
|
||||
@@ -1,192 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/nats-io/nats.go"
|
||||
|
||||
"github.com/novox/mesh-controller/internal/broker"
|
||||
"github.com/novox/mesh-controller/internal/link"
|
||||
)
|
||||
|
||||
// A new push to a pull request ends the check of its old head (novox/hq issue 726): the waiting one is dropped from
|
||||
// the queue, the one in flight is killed on its machine, and each is said. A try, a walk's build, another pull
|
||||
// request's check, a group's check and a recheck of the new head are left alone. On the bus alone: no store.
|
||||
func TestANewHeadEndsTheChecksOfTheOldOnes(t *testing.T) {
|
||||
js, _ := aBuildQueue(t)
|
||||
ctx := t.Context()
|
||||
const (
|
||||
oldHead = "1111111111111111111111111111111111111111"
|
||||
olderHead = "2222222222222222222222222222222222222222"
|
||||
newHead = "3333333333333333333333333333333333333333"
|
||||
)
|
||||
publish := func(r link.BuildRequest) {
|
||||
t.Helper()
|
||||
body, _ := json.Marshal(r)
|
||||
if _, err := js.Context().Publish(link.BuildWork(), body); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
check := func(id string, number int, head string) link.BuildRequest {
|
||||
return link.BuildRequest{ID: id, Repository: "https://forge.example/novox/a.git", Ref: head,
|
||||
Check: &link.CheckRequest{Owner: "novox", Repo: "a", Number: number}}
|
||||
}
|
||||
// In flight first: taken by a holder, started on ace.
|
||||
publish(check("check-running", 7, olderHead))
|
||||
worker, _ := broker.HolderConsumerFor("", "", broker.DeclaredSeat{Name: link.TheBuildMachine, Accepts: []string{"build"}})
|
||||
sub, err := js.Context().PullSubscribe(worker.Filters[0], worker.Name, nats.Bind(worker.Stream, worker.Name), nats.ManualAck())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer func() { _ = sub.Unsubscribe() }()
|
||||
if _, err := sub.Fetch(1, nats.MaxWait(3*time.Second)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
started, _ := json.Marshal(link.BuildStart{ID: "check-running", On: "ace", At: time.Now().UTC().Format(time.RFC3339Nano)})
|
||||
if _, err := js.Context().Publish(link.BuildStarted(), started); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
publish(check("check-old", 7, oldHead))
|
||||
publish(check("check-other-pull", 8, oldHead))
|
||||
publish(check("check-recheck", 7, newHead))
|
||||
publish(check("check-new", 7, newHead))
|
||||
try := check("try-old", 7, oldHead)
|
||||
try.Check.Number, try.Check.Try = 0, true
|
||||
publish(try)
|
||||
group := check("group-old", 7, oldHead)
|
||||
group.Check.Group = "g1"
|
||||
publish(group)
|
||||
publish(link.BuildRequest{ID: "walk-build", Repository: "https://forge.example/novox/a.git", Ref: oldHead})
|
||||
|
||||
var mu sync.Mutex
|
||||
var killed []string
|
||||
stop, err := link.OverNATS{Conn: js.Conn()}.ServeNodeSeatTools(link.TheBuildMachine, "ace", map[string]link.ToolHandler{
|
||||
"kill": func(_ context.Context, raw json.RawMessage) (any, error) {
|
||||
var args struct{ ID string }
|
||||
_ = json.Unmarshal(raw, &args)
|
||||
mu.Lock()
|
||||
killed = append(killed, args.ID)
|
||||
mu.Unlock()
|
||||
return map[string]any{"said": "killed " + args.ID}, nil
|
||||
}}, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer stop()
|
||||
|
||||
forgeSays(t, newHead, nil)
|
||||
said := endSupersededChecks(ctx, js, link.TheBuildMachine,
|
||||
link.PullUpdated{Owner: "novox", Repo: "a", Number: 7, Commit: newHead}, "check-new")
|
||||
|
||||
mu.Lock()
|
||||
if len(killed) != 1 || killed[0] != "check-running" {
|
||||
t.Errorf("the holder was asked to kill %v, want only the in-flight check of the older head", killed)
|
||||
}
|
||||
mu.Unlock()
|
||||
q, err := link.ReadQueue(ctx, js, link.TheBuildMachine)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, found := q.Find("check-old"); found {
|
||||
t.Error("the waiting check of the old head is still in the queue")
|
||||
}
|
||||
for _, id := range []string{"check-other-pull", "check-recheck", "check-new", "try-old", "group-old", "walk-build"} {
|
||||
if _, found := q.Find(id); !found {
|
||||
t.Errorf("%s was ended; only checks of the same pull request at another head end", id)
|
||||
}
|
||||
}
|
||||
text := strings.Join(said, "\n")
|
||||
for _, want := range []string{
|
||||
"ended: check of novox/a#7 at " + oldHead[:12] + " — superseded by " + newHead[:12],
|
||||
"ended: check of novox/a#7 at " + olderHead[:12] + " — superseded by " + newHead[:12],
|
||||
} {
|
||||
if !strings.Contains(text, want) {
|
||||
t.Errorf("the log does not say %q:\n%s", want, text)
|
||||
}
|
||||
}
|
||||
if len(said) != 2 {
|
||||
t.Errorf("said %d lines, want 2:\n%s", len(said), text)
|
||||
}
|
||||
}
|
||||
|
||||
// Which asks a check supersedes, read from the queue alone.
|
||||
func TestSupersededChecksAreOnlyThePullRequestsOtherHeads(t *testing.T) {
|
||||
ask := func(id, state, ref string, c *link.CheckRequest) link.QueuedAsk {
|
||||
return link.QueuedAsk{ID: id, State: state, Ref: ref, Request: link.BuildRequest{ID: id, Ref: ref, Check: c}}
|
||||
}
|
||||
pr := func(n int) *link.CheckRequest { return &link.CheckRequest{Owner: "Novox", Repo: "A", Number: n} }
|
||||
head := "abcdefabcdefabcdef0123456789abcdef012345"
|
||||
q := link.Queue{Asks: []link.QueuedAsk{
|
||||
ask("old", link.AskWaiting, "0123456789ab0123456789ab0123456789ab0123", pr(7)),
|
||||
ask("short-same", link.AskWaiting, head[:12], pr(7)),
|
||||
ask("dead", link.AskDead, "0123456789ab0123456789ab0123456789ab0123", pr(7)),
|
||||
ask("other", link.AskWaiting, "0123456789ab0123456789ab0123456789ab0123", pr(8)),
|
||||
ask("build", link.AskWaiting, "0123456789ab0123456789ab0123456789ab0123", nil),
|
||||
ask("keep", link.AskWaiting, "0123456789ab0123456789ab0123456789ab0123", pr(7)),
|
||||
}}
|
||||
got := supersededChecks(q, "novox", "a", 7, head, "keep")
|
||||
if len(got) != 1 || got[0].ID != "old" {
|
||||
t.Fatalf("superseded %+v, want only old", got)
|
||||
}
|
||||
if got := supersededChecks(q, "novox", "a", 0, head, ""); got != nil {
|
||||
t.Errorf("a check of no pull request superseded %+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
// forgeSays makes the forge name head as every pull request's current head, or fail with err.
|
||||
func forgeSays(t *testing.T, head string, err error) {
|
||||
t.Helper()
|
||||
was := forgePullHead
|
||||
forgePullHead = func(context.Context, *broker.JetStream, string, string, int) (string, error) { return head, err }
|
||||
t.Cleanup(func() { forgePullHead = was })
|
||||
}
|
||||
|
||||
// An older head heard after a newer one — an announcement out of order, one heard again after a restart, a recheck
|
||||
// of a recorded head — ends nothing: the forge says the pull request's head is the newer one. Nor does a check whose
|
||||
// pull request's head the forge cannot say.
|
||||
func TestAnOlderHeadHeardLateEndsNothing(t *testing.T) {
|
||||
js, _ := aBuildQueue(t)
|
||||
ctx := t.Context()
|
||||
const (
|
||||
oldHead = "1111111111111111111111111111111111111111"
|
||||
newHead = "3333333333333333333333333333333333333333"
|
||||
)
|
||||
for _, id := range []string{"check-new", "check-old-late"} {
|
||||
head := newHead
|
||||
if id == "check-old-late" {
|
||||
head = oldHead
|
||||
}
|
||||
body, _ := json.Marshal(link.BuildRequest{ID: id, Repository: "https://forge.example/novox/a.git", Ref: head,
|
||||
Check: &link.CheckRequest{Owner: "novox", Repo: "a", Number: 7}})
|
||||
if _, err := js.Context().Publish(link.BuildWork(), body); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
late := link.PullUpdated{Owner: "novox", Repo: "a", Number: 7, Commit: oldHead}
|
||||
forgeSays(t, newHead, nil)
|
||||
said := endSupersededChecks(ctx, js, link.TheBuildMachine, late, "check-old-late")
|
||||
if len(said) != 1 || !strings.Contains(said[0], "nothing ended") {
|
||||
t.Errorf("an older head heard late said %q", said)
|
||||
}
|
||||
forgeSays(t, "", errors.New("nothing serves gitea.gitea_api"))
|
||||
said = endSupersededChecks(ctx, js, link.TheBuildMachine,
|
||||
link.PullUpdated{Owner: "novox", Repo: "a", Number: 7, Commit: newHead}, "check-new")
|
||||
if len(said) != 1 || !strings.Contains(said[0], "nothing ended") {
|
||||
t.Errorf("a head the forge could not confirm said %q", said)
|
||||
}
|
||||
q, err := link.ReadQueue(ctx, js, link.TheBuildMachine)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, id := range []string{"check-new", "check-old-late"} {
|
||||
if _, found := q.Find(id); !found {
|
||||
t.Errorf("%s was ended", id)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -759,9 +759,6 @@ func watchTheMesh(ctx context.Context, open *stores, server *link.Server, bus li
|
||||
// and the answer chosen is performed on its warrant.
|
||||
startAsking(watching, open, server, bus.Conn, keeper)
|
||||
go forgettingOldHeals(watching, open.inventory)
|
||||
// And the build queue's order (novox/hq issue 558): a check that has waited its while goes ahead of the
|
||||
// deliveries asked after it even when nothing new is asked.
|
||||
go keepingTheQueueInOrder(watching, open, server.JetStream())
|
||||
fmt.Printf("watching the mesh: %d signal(s) every %s, %d probe(s) every %s; what is wrong is kept in %s "+
|
||||
"and said as %s events\n", len(watchedRows()), watchEvery, len(runnableProbes()), doctorEvery,
|
||||
broker.ConditionsBucket, conditions.Seat)
|
||||
|
||||
Reference in New Issue
Block a user