Files
mesh-catalog/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/lostcheck.go
T
jschoubben fbbf28d21c
mesh/merge-gate head fbbf28d21c37 merged into trunk c3d3194bc8d4 — pass: builds mesh-delivery-gate → novox; no bus step; every machine composes with the ch…
mesh/repo-check head fbbf28d21c37 merged into trunk c3d3194bc8d4 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
mesh/check failed: unit: modules/gitea.test: [gitea] where the poll of merges last looked from could not be read, so none is kept — no runtime | fbbf2…
mesh-delivery-gate: read the queue's state word as the controller says it, in flight (issue 1274)
2026-10-22 16:36:23 +02:00

212 lines
7.7 KiB
Go

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
}