mesh-delivery-gate: ask a check the build queue lost again, and take a recheck from proposed when none is held (issue 1274)

A proposed delivery whose asked check no queue entry holds stayed proposed for good (met in 1311, 1336, 1726, 1792, 1795, 1833). The sweep reads the controller's queue and asks again past max(10 minutes, the repository's last check time); recheck from proposed is taken on the same fact; never while the queue holds one.
This commit is contained in:
2026-10-22 16:34:33 +02:00
parent 77578a882f
commit fb89990e2b
11 changed files with 442 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,209 @@
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 {
if a.State == "in-flight" && a.Machine != "" {
return a.ID + " is in flight on " + a.Machine
}
return a.ID + " is " + strings.ReplaceAll(a.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",