Merge pull request 'Ask a check the build queue lost again, and take a recheck from proposed when no check of its head is held (issue 1274)' (#447) from fix/1274-ask-a-lost-check-again into main

This commit was merged in pull request #447.
This commit is contained in:
2026-10-22 15:27:42 +00:00
11 changed files with 444 additions and 4 deletions
@@ -128,6 +128,19 @@ type fakeController struct {
planned *DeliveryPlan
// forgot are the pull requests whose check builds were let go of (novox/hq ADR 0350 §5), "owner/repo#n".
forgot []string
// queue is the build queue as Check fills it: every ask made is held, waiting, until a test takes it out (a lost
// ask, issue 1274); queueDown makes the queue unreadable.
queue []QueuedAsk
queueDown bool
}
func (c *fakeController) Queue() ([]QueuedAsk, error) {
c.mu.Lock()
defer c.mu.Unlock()
if c.queueDown {
return nil, errors.New("the queue cannot be read")
}
return append([]QueuedAsk(nil), c.queue...), nil
}
// BackupsAgain records the word, and clears the walk's failed backups as the controller does.
@@ -199,6 +212,7 @@ func (c *fakeController) Check(group string, ms []Member) (string, error) {
}
c.checks = append(c.checks, group+":"+strings.Join(heads, ","))
for _, m := range ms {
c.queue = append(c.queue, QueuedAsk{ID: id, State: "waiting", Kind: "check", Repository: m.Repository, Commit: m.Head})
c.checkReady = append(c.checkReady, m.Ready)
c.baseSHAs = append(c.baseSHAs, m.BaseSHA)
}
@@ -71,6 +71,8 @@ type Holder struct {
unreached string
// askTried is when a check this owner asks was last tried and could not be asked: tried again a minute on.
askTried map[string]time.Time
// lostRead is when the build queue was last read for a proposed delivery's asked check (issue 1274).
lostRead map[string]time.Time
// walkAsked is when a walk that carried a step still judging was last asked for by its id: once an hour.
walkAsked map[string]time.Time
refusals []string
@@ -165,6 +167,7 @@ func (h *Holder) init() {
h.unmatched = map[string]unmatched{}
h.waitsWhy = map[string]string{}
h.askTried = map[string]time.Time{}
h.lostRead = map[string]time.Time{}
h.walkAsked = map[string]time.Time{}
h.unchecked = map[string]unchecked{}
h.walks = map[string]seenWalk{}
@@ -1392,6 +1395,10 @@ func (h *Holder) plan() []func() {
if a := h.askChecks(now); a != nil {
asks = append(asks, a)
}
// Proposed, its check asked and held by no entry of the build queue past the bound: asked again (issue 1274).
if a := h.lostChecks(now); a != nil {
asks = append(asks, a)
}
// Merged with no walk yet: tied to the newest walk heard that answers it — by name, by its own commit, or by
// the forge's word that the walk's commit contains it, asked here (novox/hq issue 362).
@@ -0,0 +1,153 @@
package main
import (
"strings"
"testing"
"time"
)
// Issue 1274: a proposed delivery whose asked check the build queue no longer holds is asked again, and a recheck from
// proposed is taken when no check of its head is waiting or in flight.
// 1274/AC1: the queue holds nothing for the ask, the sweep runs after the bound: the check is asked once more and the
// transition says the earlier ask was lost.
func TestACheckTheQueueLostIsAskedAgainBySweepAfterTheBound(t *testing.T) {
w := newWorld(t)
id := IDOf("novox/app", head)
w.h.PullUpdated(pr("novox/app", 7, head, "feat/x", "src/a.go"))
w.settleAll()
first := w.delivery(id).CheckAsk
if len(w.ctl.checks) != 1 || first == nil {
t.Fatalf("asked %v, ask %+v", w.ctl.checks, first)
}
w.ctl.queue = nil // the queue lost it
w.later(9 * time.Minute)
w.settleAll()
if len(w.ctl.checks) != 1 {
t.Fatalf("asked again inside the bound: %v", w.ctl.checks)
}
w.later(2 * time.Minute)
w.settleAll()
d := w.delivery(id)
last := d.Transitions[len(d.Transitions)-1]
if len(w.ctl.checks) != 2 || d.CheckAsk == nil || d.CheckAsk.ID == first.ID || last.Event != EvLost ||
!strings.Contains(last.Why, first.ID) || !strings.Contains(last.Why, "lost") {
t.Fatalf("asked %v; ask %+v; last transition %+v", w.ctl.checks, d.CheckAsk, last)
}
w.later(time.Hour)
w.settleAll()
if len(w.ctl.checks) != 2 {
t.Fatalf("an ask the queue holds was asked again: %v", w.ctl.checks)
}
}
// 1274/AC1: a verdict that went out of date, its check against the trunk's head asked and lost, is asked again too (the
// case of novox/mesh-controller@28a278c1462a), and the ask's bound runs from the new ask.
func TestACheckAgainstTheTrunkTheQueueLostIsAskedAgain(t *testing.T) {
w := approvedGroup(t)
id := IDOf(gCtl, gHc)
w.h.PullMerged(merged(gCtl, 5, "5555555555555555", gT1))
w.settleAll()
d := w.delivery(id)
if d.State != Proposed || d.AskedAgain == nil {
t.Fatalf("it is %s, asked again %v", d.State, d.AskedAgain)
}
asked := len(w.ctl.checks)
w.ctl.queue = nil
w.later(11 * time.Minute)
w.settleAll()
d = w.delivery(id)
last := d.Transitions[len(d.Transitions)-1]
if len(w.ctl.checks) != asked+1 || last.Event != EvLost || d.AskedAgain == nil || !d.AskedAgain.After(w.now.Add(-time.Minute)) {
t.Fatalf("asked %v; last transition %+v; asked again %v", w.ctl.checks[asked:], last, d.AskedAgain)
}
}
// 1274/AC2: the queue still holds the ask, waiting or in flight, or cannot be read: nothing is asked again.
func TestACheckTheQueueStillHoldsIsNotAskedAgain(t *testing.T) {
w := newWorld(t)
id := IDOf("novox/app", head)
w.h.PullUpdated(pr("novox/app", 7, head, "feat/x", "src/a.go"))
w.settleAll()
w.ctl.queue[0].State = "in flight"
w.later(3 * time.Hour)
w.settleAll()
if len(w.ctl.checks) != 1 {
t.Fatalf("an ask in flight was asked again: %v", w.ctl.checks)
}
w.ctl.queue = nil
w.ctl.queueDown = true
w.later(time.Hour)
w.settleAll()
if len(w.ctl.checks) != 1 || w.delivery(id).State != Proposed {
t.Fatalf("an unreadable queue asked again: %v", w.ctl.checks)
}
w.ctl.queueDown = false
w.later(3 * time.Minute)
w.ctl.queue = []QueuedAsk{{ID: "elsewhere", State: "dead", Kind: "check", Repository: "novox/app", Commit: head}}
w.settleAll()
if len(w.ctl.checks) != 2 {
t.Fatalf("a dead ask counted as held: %v", w.ctl.checks)
}
}
// 1274/AC3: recheck of a proposed delivery with no check waiting or in flight is taken and asks its check; with one
// held it is refused naming it.
func TestARecheckFromProposedIsTakenOnlyWhenNoCheckOfItsHeadIsHeld(t *testing.T) {
w := newWorld(t)
id := IDOf("novox/app", head)
w.h.PullUpdated(pr("novox/app", 7, head, "feat/x", "src/a.go"))
w.settleAll()
held := w.delivery(id).CheckAsk.ID
if _, err := w.h.Recheck(id, "nothing came", "jochen"); err == nil || !strings.Contains(err.Error(), held) {
t.Fatalf("a recheck with a check waiting: %v", err)
}
if _, err := w.h.Recheck(id, "", "jochen"); err == nil {
t.Fatal("a recheck with no why was taken")
}
if len(w.ctl.checks) != 1 {
t.Fatalf("a refused recheck asked: %v", w.ctl.checks)
}
w.ctl.queue = nil
said, err := w.h.Recheck(id, "nothing came", "jochen")
d := w.delivery(id)
last := d.Transitions[len(d.Transitions)-1]
if err != nil || len(w.ctl.checks) != 2 || d.State != Proposed || last.Event != EvLost || last.By != "jochen" ||
!strings.Contains(last.Why, "nothing came") || d.CheckAsk.ID != "check-2" || !strings.Contains(said, "check-2") {
t.Fatalf("%q %v; asked %v; last transition %+v", said, err, w.ctl.checks, last)
}
// Taken, it is now held: a second recheck is refused.
if _, err := w.h.Recheck(id, "again", "jochen"); err == nil || !strings.Contains(err.Error(), "check-2") {
t.Fatalf("a second recheck: %v", err)
}
}
// 1274/AC4: a group member re-asked this way has its group's composed check asked again at the members' heads once its
// own check answers.
func TestAGroupMemberReaskedAfterALostCheckHasTheComposedCheckAskedAgain(t *testing.T) {
w := approvedGroup(t)
id := IDOf(gCat, gHk)
if _, err := w.h.Recheck(id, "its verdict is wanted again", "jochen"); err != nil {
t.Fatal(err)
}
w.settleAll() // its own check is recorded as asked
w.h.mu.Lock()
w.h.groups["feat/g"].Check.OutOfDate = "" // as if the composed check stood: only the lost ask's own act marks it
w.h.mu.Unlock()
w.ctl.queue = nil
w.later(11 * time.Minute)
w.settleAll()
g := w.h.groups["feat/g"]
if g.Check.OutOfDate == "" || !strings.Contains(g.Check.OutOfDate, "lost") {
t.Fatalf("the composed check stands: %+v", g.Check)
}
first := g.Check.Asked
asked := len(w.ctl.checks)
w.later(time.Minute)
w.h.Checked(judged(gCat, 2, gHk, "pass", gTrunk0))
w.settleAll()
last := w.ctl.checks[len(w.ctl.checks)-1]
if len(w.ctl.checks) <= asked || !strings.HasPrefix(last, "feat/g:") || w.h.groups["feat/g"].Check.Asked == first {
t.Fatalf("the composed check was not asked again: %v", w.ctl.checks[asked:])
}
}
@@ -97,7 +97,7 @@ func TestTheGatesCodeCallsNoForgeWriteToolAndNoDeliveryPlan(t *testing.T) {
forbidden := []string{"delivery-plan", "gitea_commit_status\"", "gitea_delivery_view", "gitea_note_append", "gitea_add_comment",
"gitea_merge_pull_request\"", "gitea_open_pulls_write"}
// The controller's verbs the gate calls: ADR 0450 §7.
allowed := []string{"deliver", "delivery-stop", "delivery-backups-again", "delivery-walks", "delivery-check", "forget-checks", "plans"}
allowed := []string{"deliver", "delivery-stop", "delivery-backups-again", "delivery-walks", "delivery-check", "forget-checks", "plans", "queue"}
called := map[string]bool{}
fset := token.NewFileSet()
for _, f := range files {
@@ -0,0 +1,211 @@
package main
import (
"fmt"
"strings"
"time"
)
// A check the build queue lost (novox/hq issue 1274).
//
// A proposed delivery waits for a check it asked of the controller. When the build queue loses that ask — taken by a
// build agent that was replaced, or dropped with the queue's own state — nothing answers and nothing asks again: the
// delivery stays proposed until a new head is pushed, and a recheck from proposed was refused. The sweep now reads the
// controller's `queue` and asks again a check that no entry holds (neither waiting nor in flight; a dead one is not
// held) for longer than lostCheckBound. A recheck of a proposed delivery is taken on the same fact. Both are the
// table's EvLost, said in the delivery's transitions; neither asks while the queue holds an ask of the head, so no
// ask is made twice. A queue that cannot be read asks nothing: a guess would risk a duplicate.
// lostReadEvery is how often the queue is read for one proposed delivery's ask: the read is one bus call, and a check
// the queue holds is looked at again no sooner.
const lostReadEvery = 2 * time.Minute
// lostCheckBound is how long a proposed delivery's asked check may go with no entry in the build queue before it is
// asked again: the repository's last measured check time (ADR 0505 §4, lastCheckTime), at least UncheckedAfter (ten
// minutes, the bound a check never asked is said at). Under the lock.
func (h *Holder) lostCheckBound(repository string) time.Duration {
return max(UncheckedAfter, h.lastCheckTime(repository))
}
// awaitedSince is when a proposed delivery's check was asked, when it waits for one this owner asked: no verdict yet,
// or a verdict out of date with its check against the trunk's head asked. False for a delivery waiting for none.
func awaitedSince(d *Delivery) (time.Time, bool) {
if d.State != Proposed || d.MergedAs != "" || d.Number == 0 {
return time.Time{}, false
}
switch {
case d.Check == nil && askedCheck(d):
return d.CheckAsk.At, true
case d.Check != nil && d.outOfDate() != "" && d.AskedAgain != nil:
return *d.AskedAgain, true
}
return time.Time{}, false
}
// heldBy is the queue's entry that holds a check of the delivery's head: its own ask by id, or any check of its
// repository at its head — a group's composed check included, which a second ask would only repeat.
func heldBy(d *Delivery, asks []QueuedAsk) *QueuedAsk {
for i, a := range asks {
if !a.Holds() {
continue
}
if d.CheckAsk != nil && d.CheckAsk.ID != "" && a.ID == d.CheckAsk.ID {
return &asks[i]
}
if a.Kind == "check" && a.Commit != "" && sameCommit(a.Commit, d.Commit) &&
strings.Contains(strings.ToLower(a.Repository), strings.ToLower(d.Repository)) {
return &asks[i]
}
}
return nil
}
func (a QueuedAsk) words() string {
// The controller's `queue` says the state "in flight", its table's word.
state := strings.ReplaceAll(a.State, "-", " ")
if state == "in flight" && a.Machine != "" {
return a.ID + " is in flight on " + a.Machine
}
return a.ID + " is " + state + " in the build queue"
}
// lostChecks are the asks again of proposed deliveries whose asked check the build queue holds no entry of, past the
// bound, at each look. Under the lock; the queue is read, and the asks made, outside it.
func (h *Holder) lostChecks(now time.Time) func() {
var ids []string
for id, d := range h.deliveries {
since, ok := awaitedSince(d)
if !ok {
delete(h.lostRead, id)
continue
}
due := since.Add(h.lostCheckBound(d.Repository))
if now.Before(due) {
h.lookAt(due, "a proposed delivery's asked check, read in the build queue once its bound passed")
continue
}
if read, was := h.lostRead[id]; was && now.Sub(read) < lostReadEvery {
h.lookAt(read.Add(lostReadEvery), "a proposed delivery's asked check, looked for in the build queue again")
continue
}
h.lostRead[id] = now
h.lookAt(now.Add(lostReadEvery), "a proposed delivery's asked check, looked for in the build queue again")
ids = append(ids, id)
}
if len(ids) == 0 {
return nil
}
return func() {
asks, err := h.Controller.Queue()
if err != nil {
h.Logf("[mesh-delivery] the build queue cannot be read, so no check is asked again for a lost ask: %v", err)
return
}
for _, id := range ids {
h.mu.Lock()
d := h.deliveries[id]
since, ok := time.Time{}, false
if d != nil {
since, ok = awaitedSince(d)
}
var held *QueuedAsk
if ok {
held = heldBy(d, asks)
}
var why string
if ok && held == nil {
what := "its check against the trunk's head"
if d.Check == nil {
what = "its check " + d.CheckAsk.ID
}
why = fmt.Sprintf("%s, asked %s ago, is held by no entry of the build queue (neither waiting nor in flight): "+
"lost, asked again", what, h.Now().Sub(since).Round(time.Second))
}
h.mu.Unlock()
if why == "" {
continue
}
if _, err := h.askLost(id, byOwner, why, asks); err != nil {
h.Logf("[mesh-delivery] %s: its lost check could not be asked again: %v", id, err)
h.again("a proposed delivery's lost check that could not be asked again")
}
}
}
}
// recheckProposed is `recheck` of a proposed delivery (issue 1274): taken, with why, when no check of its head is
// waiting or in flight in the build queue, and refused naming the one that is. A queue that cannot be read refuses it.
func (h *Holder) recheckProposed(id, why, by string) (string, error) {
if why == "" {
return "", fmt.Errorf("a recheck says why")
}
asks, err := h.Controller.Queue()
if err != nil {
return "", fmt.Errorf("%s is not rechecked: the build queue cannot be read (%v), so whether its check is still held is not known", id, err)
}
h.mu.Lock()
d := h.deliveries[id]
var held *QueuedAsk
if d != nil {
held = heldBy(d, asks)
}
h.mu.Unlock()
if held != nil {
return "", fmt.Errorf("%s is not rechecked: its check is still held, %s", id, held.words())
}
return h.askLost(id, by, why, asks)
}
// askLost asks a proposed delivery's check again, the table's EvLost: tried first on a copy, so an ask the controller
// does not take spends nothing. Its group's composed check goes out of date, and is asked once the member answers.
func (h *Holder) askLost(id, by, why string, asks []QueuedAsk) (string, error) {
h.mu.Lock()
d := h.deliveries[id]
if d == nil {
h.mu.Unlock()
return "", fmt.Errorf("no delivery %q", id)
}
if held := heldBy(d, asks); held != nil {
h.mu.Unlock()
return "", fmt.Errorf("%s is not asked again: its check is still held, %s", id, held.words())
}
probe := *d
probe.Transitions = nil
if _, err := Apply(&probe, EvLost, true, Facts{Now: h.Now(), Why: why, By: by}); err != nil {
h.mu.Unlock()
return "", err
}
m := MemberOf(d)
if d.Check != nil && d.outOfDate() != "" && d.TrunkNow != "" {
m.BaseSHA = d.TrunkNow
}
h.mu.Unlock()
asked, err := h.Controller.Check("", []Member{m})
if err != nil {
return "", fmt.Errorf("%s's check could not be asked again yet: %w", id, err)
}
h.mu.Lock()
defer h.mu.Unlock()
d = h.deliveries[id]
if d == nil || d.State != Proposed {
return "", fmt.Errorf("%s moved while its check was asked again", id)
}
now := h.Now()
t, err := Apply(d, EvLost, true, Facts{Now: now, Why: why + " — asked as " + asked, By: by})
if err != nil {
return "", err
}
d.CheckAsk = &CheckAsk{ID: asked, At: now, By: by, Why: why}
if d.Check != nil {
d.AskedAgain, d.FreshWanted = &now, false
}
delete(h.askTried, id)
if g := h.groups[d.Group]; g != nil && g.Check != nil {
g.Check.OutOfDate = fmt.Sprintf("composed check out of date: %s's check was lost in the build queue and asked again", d.ID)
g.Check.OutOfDateAt = now
g.Updated = now
h.keepGroup(g)
}
h.moved(d, []Transition{t})
return fmt.Sprintf("%s: its check was held by no entry of the build queue and is asked again as %s (%s)", id, asked, why), nil
}
@@ -92,7 +92,7 @@ func TestItClaimsTheDeliverySeatAndFollowsWhatItHandles(t *testing.T) {
// It calls exactly the controller's seven verbs (ADR 0450 §7) and reads the forge with three tools; the forge's
// writes are the bridge's (ADR 0437 §8). It uses no operator-channel: the teller asks (ADR 0437 §6).
want := []string{}
for _, v := range []string{"deliver", "delivery-stop", "delivery-backups-again", "delivery-walks", "delivery-check", "forget-checks", "plans"} {
for _, v := range []string{"deliver", "delivery-stop", "delivery-backups-again", "delivery-walks", "delivery-check", "forget-checks", "queue", "plans"} {
want = append(want, "seat:"+ControllerSeat+"."+v)
}
want = append(want, "gitea.gitea_commit_statuses", "gitea.gitea_contains", "gitea.gitea_open_pulls")
@@ -67,8 +67,24 @@ type Controller interface {
Walks(walk string) (bool, []Walk, error)
// ForgetChecks lets go of the builds a pull request's merge checks made for its merge (novox/hq ADR 0350 §5).
ForgetChecks(owner, repo string, number int) error
// Queue is every ask the build queue holds, waiting or in flight (the controller's `queue`, novox/hq issue 455): what
// tells a check still on its way from one the queue lost (issue 1274). A dead ask is not held.
Queue() ([]QueuedAsk, error)
}
// QueuedAsk is one ask of the build queue as the controller's `queue` answers it as data.
type QueuedAsk struct {
ID string `json:"id"`
State string `json:"state"` // waiting, in flight or dead
Kind string `json:"kind"` // check or delivery
Repository string `json:"repository"`
Commit string `json:"commit,omitempty"`
Machine string `json:"machine,omitempty"`
}
// Holds is whether the ask is still the queue's to run: waiting or in flight, never dead.
func (q QueuedAsk) Holds() bool { return q.State != "dead" }
// Forge is what the forge's holder does for a delivery.
type Forge interface {
// Statuses reads a commit's statuses — a pull request's head's, given its number — and whether the branch
@@ -250,6 +266,20 @@ func (c seatController) Check(group string, members []Member) (string, error) {
return "", fmt.Errorf("the controller's delivery-check answered no check id (%s): it is not taken as asked", clip(string(raw), 200))
}
func (c seatController) Queue() ([]QueuedAsk, error) {
raw, err := c.verb("queue", map[string]any{})
if err != nil {
return nil, err
}
var a struct {
Asks []QueuedAsk `json:"asks"`
}
if err := json.Unmarshal(raw, &a); err != nil {
return nil, fmt.Errorf("the controller's queue is not readable: %w", err)
}
return a.Asks, nil
}
func (c seatController) Deliver(walk, why string) error {
_, err := c.verb("deliver", map[string]any{"plan": walk, "why": why})
return err
@@ -69,6 +69,7 @@ const (
EvRefused Event = "refused" // the verdict fails, or the check could not run
EvRecheck Event = "recheck" // a person asked for it again, or its group changed
EvReask Event = "re-ask" // healer H2 asked a check asked and not answered once more
EvLost Event = "check lost" // the build queue holds no check of its head and it was asked: asked again (issue 1274)
EvNewHead Event = "new-head" // a newer head of the same pull request
EvClosed Event = "closed" // the pull request closed unmerged
EvMerged Event = "merged" // it reached the trunk
@@ -240,6 +241,24 @@ var Table = []Row{
return nil
}},
// **A check the build queue lost is asked again** (novox/hq issue 1274): a proposed delivery waits for a check, and
// the queue, which the table cannot read, holds none of its head. The act (the sweep's or a person's recheck) reads
// the queue first and says why; the table keeps it to a delivery that waits for a verdict.
{From: []State{Proposed}, Event: EvLost, To: Proposed, Act: true,
Guard: "it waits for a check — no verdict yet, or one out of date — nothing of its head is waiting or in flight in " +
"the build queue, and who asks says why",
Holds: func(d *Delivery, f Facts) error {
switch {
case f.Why == "" || f.By == "":
return errors.New("a lost check's ask says who and why")
case d.MergedAs != "":
return errors.New("it is merged")
case d.Check != nil && d.outOfDate() == "":
return errors.New("its verdict is known")
}
return nil
}},
{From: []State{Published}, Event: EvSuperseded, To: Superseded,
Guard: "its walk was superseded and names no walk that took it over: a walk that took it over answers its merge, and it follows that walk (novox/hq ADR 0276)",
Holds: func(d *Delivery, f Facts) error {
@@ -43,7 +43,7 @@ func aDeliveryFor(r Row) (*Delivery, Facts) {
d.Check = &Verdict{Gate: "pass"}
case EvRefused:
d.Check = &Verdict{Gate: "fail"}
case EvRecheck, EvRelease, EvStop:
case EvRecheck, EvRelease, EvStop, EvLost:
f.Why, f.By = "a person's reason", "jochen"
if r.Event == EvStop && !r.Act {
f = Facts{Now: now}
@@ -133,7 +133,7 @@ func TestEveryRowOfTheTableIsTakenWhenItsGuardHoldsAndRefusedWhenNot(t *testing.
}
func TestEveryPairTheTableDoesNotHoldIsRefusedByName(t *testing.T) {
events := []Event{EvAnnounced, EvAppeared, EvAdopted, EvChecked, EvAccepted, EvRefused, EvRecheck, EvReask, EvNewHead,
events := []Event{EvAnnounced, EvAppeared, EvAdopted, EvChecked, EvAccepted, EvRefused, EvRecheck, EvReask, EvLost, EvNewHead,
EvClosed, EvMerged, EvMergedUnchecked, EvGo, EvHold, EvRelease, EvDone, EvFailed, EvSuperseded, EvStop,
EvHistory, EvRetire, EvCovered, EvLifted, EvRewalk, EvTrunkMoved, EvBackupFailed}
for _, from := range append([]State{None}, AllStates...) {
@@ -303,6 +303,11 @@ func (h *Holder) Recheck(id, why, by string) (string, error) {
h.mu.Unlock()
return "", fmt.Errorf("no delivery %q", id)
}
if d.State == Proposed {
// A proposed delivery's check is asked again when the build queue holds none of its head (issue 1274).
h.mu.Unlock()
return h.recheckProposed(id, why, by)
}
// Whether the table takes it, tried on a copy: nothing is said on the forge for a recheck it refuses.
probe := *d
probe.Transitions = append([]Transition(nil), d.Transitions...)
+1
View File
@@ -47,6 +47,7 @@
"seat:mesh-controller.delivery-walks",
"seat:mesh-controller.delivery-check",
"seat:mesh-controller.forget-checks",
"seat:mesh-controller.queue",
"seat:mesh-controller.plans",
"gitea.gitea_commit_statuses",
"gitea.gitea_contains",