Files
mesh-catalog/modules/mesh-issues/cmd/mesh-issues/claims.go
T
jochen 983d434623
mesh/merge-gate fail: builds mesh-issues → novox; no bus step; a manifest the change touches fails the module check: modules/mesh-issues/module.json: this …
mesh/repo-check fail: its merge-check.sh failed: files asking for a setting without saying whether it is trusted: 25
mesh/delivery superseded: a newer head of the same pull request
mesh-issues: keep every issue in the log changes and the bucket as its fold, so the record outlives the bucket's cap and a week of events (hq ADR 0300, issues 501, 502)
Every write appends one operation at the issue's last sequence before the
bucket is written; a refused append writes nothing. The start carries what
the bucket holds, refolds what is behind and indexes every issue in memory,
so list answers from memory with a ranked search and a before cursor.
2026-10-12 02:41:54 +02:00

191 lines
7.3 KiB
Go

package main
// A fix delivered and an issue left in fixing, and an issue its worker abandoned (novox/hq issue 483). The operator found
// 13 of 23 issues in fixing whose fix was merged and delivered hours before, and two claimed by builders that had
// stopped: the tracker measured the delivery and said "ready to verify", but nothing said an issue left there, and
// nothing told a worked issue from a forgotten one. Here:
//
// - an issue in fixing whose every fix (fixed-by and the pull requests linked) is delivered is
// delivered-not-resolved; past `delivered-not-resolved-after` (1h) it is said to the operator's channel as a
// warning, key `issue-tracker.delivered-not-resolved.<number>`, naming the issue and its worker, cleared there
// when it leaves fixing or names a fix not yet delivered;
// - an issue in diagnosing or fixing whose worker did nothing on it for `worker-stale-after` (24h) and that names
// no pull request still open is abandoned (the glossary keeps "claim" for a module's on a seat): get and list
// say so, and a move by someone else takes it over without a reason, saying it was abandoned.
//
// A module that runs only its bundle cannot raise a controller condition (hq ADR 0240 rule 8, ADR 0256), so the
// warning is the operator's channel's, as the stuck fix's is (ADR 0281 §4).
import (
"fmt"
"strings"
"time"
)
// DefaultDeliveredNotResolvedAfter is how long an issue may stay in fixing once its fix is delivered before the
// operator hears of it: proving an acceptance live takes minutes, and a worker that went away would otherwise
// leave the issue there for good.
const DefaultDeliveredNotResolvedAfter = time.Hour
// MostSweepAsks is the most deliveries one sweep asks of mesh-delivery, each up to the bus's 30 seconds: a fix
// linked after its delivery moved is measured within a few sweeps, and a sweep stays short.
const MostSweepAsks = 5
// The flags list filters on (novox/hq issue 483).
const (
FlagDeliveredNotResolved = "delivered-not-resolved"
FlagAbandoned = "abandoned"
)
// Flags is every flag list takes.
var Flags = []string{FlagDeliveredNotResolved, FlagAbandoned}
// deliveredNotResolvedAfter is the setting's bound, or the default.
func (t *Tracker) deliveredNotResolvedAfter() time.Duration {
if s, err := t.Settings(); err == nil && strings.TrimSpace(s.DeliveredNotResolvedAfter) != "" {
if d, err := time.ParseDuration(strings.TrimSpace(s.DeliveredNotResolvedAfter)); err == nil && d > 0 {
return d
}
}
return DefaultDeliveredNotResolvedAfter
}
// deliveredNotResolved says whether an issue is in fixing and every fix it names is delivered.
func deliveredNotResolved(is *Issue) bool { return is.Status == StatusFixing && allDelivered(is) }
// deliveredAt is when the last of an issue's delivered fixes was delivered, as the delivery said it.
func deliveredAt(is *Issue) time.Time {
var last time.Time
for _, pr := range liveFixes(is) {
if f := fixOf(is, pr); f != nil && contains(is.Delivered, pr) && f.Since.After(last) {
last = f.Since
}
}
return last
}
func notResolvedKey(n uint64) string { return "issue-tracker.delivered-not-resolved." + keyOf(n) }
// sayNotResolved warns the operator's channel, once per spell, of an issue delivered and not resolved past the
// bound, and answers whether it did; an issue no longer so has its warning cleared.
func (t *Tracker) sayNotResolved(is *Issue, now time.Time) (bool, error) {
if !deliveredNotResolved(is) {
return false, t.clearNotResolved(is)
}
at := deliveredAt(is)
if is.NotResolvedOpen || t.Notify == nil || now.Sub(at) < t.deliveredNotResolvedAfter() {
return false, nil
}
worker := "nobody"
if is.Worker != nil {
worker = is.Worker.By
}
ago := now.Sub(at).Round(time.Minute)
prs := strings.Join(liveFixes(is), ", ")
if err := t.Notify(notResolvedKey(is.Number), "warning",
fmt.Sprintf("issue %d: fixed and delivered, not resolved; %s works on it", is.Number, worker),
fmt.Sprintf("Fixed and delivered, not resolved (issue %d)", is.Number),
fmt.Sprintf("Every fix of issue %d (%s) was delivered %s ago, at %s, and the issue is still in fixing; %s works "+
"on it. Its worker proves the acceptance live and resolves it. When the worker is gone, another agent "+
"takes it over with a move to fixing and resolves it; nothing is needed from you otherwise.", is.Number,
prs, ago, at.Format(time.RFC3339), worker), "", false); err != nil {
return false, err
}
_, err := t.change(is.Number, Act{Op: "warned", By: Seat}, func(i *Issue) error { i.NotResolvedOpen = true; return nil })
return err == nil, err
}
// clearNotResolved clears the warning once the issue is no longer delivered and in fixing: resolved, moved on, or
// naming a fix not delivered yet.
func (t *Tracker) clearNotResolved(is *Issue) error {
if !is.NotResolvedOpen || deliveredNotResolved(is) || t.Notify == nil {
return nil
}
if err := t.Notify(notResolvedKey(is.Number), "", "", "", "", fmt.Sprintf("issue %d left fixing (%s), or names a "+
"fix not delivered yet", is.Number, is.Status), true); err != nil {
return err
}
_, err := t.change(is.Number, Act{Op: "warned", By: Seat}, func(i *Issue) error { i.NotResolvedOpen = false; return nil })
return err
}
// askUnheard asks mesh-delivery for a few fixes of issues not yet resolved that the tracker never heard of: a pull
// request linked after its delivery moved hears no transition again, and OnMoved asks only at a move.
func (t *Tracker) askUnheard() error {
if t.Deliveries == nil {
return nil
}
all, err := t.allIssues()
if err != nil {
return err
}
asked := 0
for n, is := range all {
if !unresolved(is) {
continue
}
heard := false
for _, pr := range fixPullRequests(is) {
if fixOf(is, pr) != nil {
continue
}
if asked == MostSweepAsks {
return nil
}
asked++
repo, number, _ := strings.Cut(pr, "#")
d, err := t.Deliveries(repo, number)
if err != nil {
return fmt.Errorf("the delivery of %s (issue %d): %v", pr, n, err)
}
if d.State == "" {
continue
}
since := d.Since
if since.IsZero() {
since = t.now()
}
if _, err := t.recordFix(pr, d.State, d.Why, d.Merged, since, n); err != nil {
return err
}
heard = true
}
if heard {
if err := t.afterFix(n); err != nil {
return err
}
}
}
return nil
}
// openPullRequest says whether an issue names a pull request not merged yet, or one whose delivery the tracker
// never heard of: work in review is work, and a fix unheard of is not taken for none. One closed without merging
// is not open.
func openPullRequest(is *Issue) bool {
for _, pr := range liveFixes(is) {
f := fixOf(is, pr)
if f == nil || (!f.Merged && !contains(is.Delivered, pr)) {
return true
}
}
return false
}
// abandoned says whether an issue's worker went quiet with no pull request open: nothing done on it for the bound,
// and nothing in review.
func abandoned(is *Issue, now time.Time, after time.Duration) bool {
return worked(is.Status) && is.Worker != nil && is.Worker.stale(now, after) && !openPullRequest(is)
}
// hasFlag says whether an issue carries a list flag.
func hasFlag(is *Issue, flag string, now time.Time, staleAfter time.Duration) bool {
switch flag {
case FlagDeliveredNotResolved:
return deliveredNotResolved(is)
case FlagAbandoned:
return abandoned(is, now, staleAfter)
}
return false
}