Files
mesh-catalog/modules/mesh-delivery/cmd/mesh-delivery/effects.go
T
jochen 13ffae7706
mesh/merge-gate pass: builds mesh-delivery → novox; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)
mesh/delivery delivered
mesh-delivery: a walk answers every merge it carries, never a delivery that ended (hq issue 362, ADR 0276)
Two catalogue merges 18 s apart were walked by one walk at the later commit,
which superseded the first; this owner tied that walk to the dunst delivery
the supersession had ended and to no other, so nothing gave it its word and
claude-code was held for a person with a reason naming neither cause.

A delivery now follows the newest walk that answers its merge: one naming it
(delivery.merges, the controller's batches), its own commit, the walk that
took its own over (taken_over_by, or the superseded note), or, from a
controller that names no merges, an open walk of its branch made for a later
merge, moving all it moves, whose commit the forge says contains it. Only its
own walk's record moves an ended delivery. The word is asked once per walk,
when every delivery it carries is at its turn, a group's members on one walk
together. An earlier merge carried by a later commit ends covered with the
walk, and waits for a walk of its own when it fails. A merge no walk answers
is held with the true reason and listed at once; a waiting walk no live
delivery holds is listed as unheld; a merge in an assembling or queued batch
is shown, not held. Replay R362 fails before.
2026-10-10 13:05:50 +02:00

356 lines
11 KiB
Go

package main
import (
"encoding/json"
"fmt"
"regexp"
"sort"
"strings"
"time"
)
// What a transition owes outside the state (novox/hq ADR 0239 decision 5), each by its owner: the event on
// the bus (said by this module), a line on the commit's note and the pull request's view and statuses (by
// the forge's holder, asked through its tools). Owed, never lost: kept with the delivery before anything is
// tried, and tried again until done.
// transitionEvent is what `mesh-delivery.transition` says: no secret, no address.
func transitionEvent(d *Delivery, t Transition) map[string]any {
e := map[string]any{"id": d.ID, "repository": d.Repository, "commit": d.Commit, "from": t.From, "to": t.To,
"event": t.Event, "why": t.Why, "at": t.At}
if t.By != "" {
e["by"] = t.By
}
if d.Number > 0 {
e["number"] = d.Number
}
if d.Group != "" {
e["group"] = d.Group
}
if d.MergedAs != "" {
e["merged_as"] = d.MergedAs
}
if d.Walk != nil {
e["walk"] = d.Walk.ID
}
if d.Plan != nil {
e["plan"] = d.Plan.Summary
}
return e
}
// noteLine is one transition as the commit's note keeps it.
func noteLine(d *Delivery, t Transition) string {
by := ""
if t.By != "" {
by = " by " + t.By
}
line := fmt.Sprintf("%s delivery %s: %s -> %s (%s%s): %s", t.At.UTC().Format(time.RFC3339), d.ID, orNothing(t.From),
t.To, t.Event, by, t.Why)
if t.To == Proposed && d.Plan != nil {
line += "; predicted: " + d.Plan.Summary
}
if d.Group != "" && (t.To == Proposed || t.To == Published) {
line += "; group " + d.Group
}
return oneLine(line)
}
// executedLine is what a delivery did, written on the commit it landed as when it ends.
func executedLine(d *Delivery) string {
var parts []string
for _, s := range d.Steps {
parts = append(parts, fmt.Sprintf("%s on %s %s", s.Module, s.Machine, s.State))
}
walk := ""
if d.Walk != nil {
walk = " (walk " + d.Walk.ID + ")"
}
return oneLine(fmt.Sprintf("%s delivery %s executed%s: %s", time.Now().UTC().Format(time.RFC3339), d.ID, walk,
strings.Join(parts, "; ")))
}
func oneLine(s string) string { return strings.Join(strings.Fields(s), " ") }
// statusOf is the commit status `mesh/delivery`: where the delivery stands, in the forge's words.
func statusOf(d *Delivery) (string, string) {
why := ""
if len(d.Transitions) > 0 {
why = d.Transitions[len(d.Transitions)-1].Why
}
switch d.State {
case Proposed, Checked:
return "pending", "checking"
case Ready:
return "success", "ready: it delivers once merged"
case Rejected:
return "failure", clip("rejected: "+why, 140)
case Published:
return "pending", "published: its walk waits for its turn"
case Held:
return "pending", clip("held for a person: "+d.HeldWhy, 140)
case Delivering:
passed := 0
for _, s := range d.Steps {
if s.State == StepPassed {
passed++
}
}
return "pending", fmt.Sprintf("delivering: %d machine step(s) passed", passed)
case Delivered:
return "success", "delivered"
case Failed:
return "failure", clip("failed: "+why, 140)
case Superseded:
return "warning", clip("superseded: "+why, 140)
case Stopped:
return "error", clip("stopped: "+why, 140)
case Retired:
return "success", "history: merged before this owner delivered anything, never walked"
case Covered:
by := d.CoveredBy
if by == "" {
by = d.Carrier
}
return "success", clip("covered: a later merge that contains it was delivered: "+by, 140)
}
return "pending", string(d.State)
}
// viewMarker is how the forge's holder finds the one view comment of a pull request.
const viewMarker = "<!-- mesh-delivery:view -->"
// ViewBody is a delivery's view on its pull request, written from the delivery whole.
func ViewBody(d *Delivery, g *Group) string {
var b strings.Builder
fmt.Fprintf(&b, "%s\n**Delivery** `%s` — **%s** since %s\n\n", viewMarker, d.ID, d.State, d.Since.UTC().Format(time.RFC3339))
if d.HeldWhy != "" && d.State == Held {
fmt.Fprintf(&b, "Held for a person: %s — `mesh-delivery.release` with why lets it go on.\n\n", d.HeldWhy)
}
if d.Plan != nil {
fmt.Fprintf(&b, "**Delivery plan** — %s\n", d.Plan.Summary)
for i, tier := range d.Plan.Tiers {
fmt.Fprintf(&b, "- tier %d: %s\n", i, strings.Join(tier, ", "))
}
for _, m := range d.Plan.Machines {
var parts []string
if len(m.Receives) > 0 {
parts = append(parts, "receives "+strings.Join(m.Receives, ", "))
}
if len(m.Waits) > 0 {
parts = append(parts, "waits for a person: "+strings.Join(m.Waits, ", "))
}
fmt.Fprintf(&b, "- %s: %s\n", m.Machine, strings.Join(parts, "; "))
}
for _, s := range d.Plan.Steps {
fmt.Fprintf(&b, "- %s\n", s)
}
b.WriteString("\n")
}
if g != nil {
fmt.Fprintf(&b, "**Group** `%s`, in order: %s\n", g.ID, strings.Join(g.Order, " → "))
for _, p := range g.Pairs {
fmt.Fprintf(&b, "- %s\n", p.orderSaid())
}
for _, c := range g.Contradictions {
fmt.Fprintf(&b, "- **refused**: %s\n", c.Said)
}
if g.Check != nil && g.Check.Verdict != "" {
fmt.Fprintf(&b, "- composed together: %s — %s\n", g.Check.Verdict, g.Check.Summary)
}
b.WriteString("\n")
}
if len(d.Steps) > 0 {
b.WriteString("**Machines**\n")
for _, s := range d.Steps {
fmt.Fprintf(&b, "- %s on %s: %s", s.Module, s.Machine, s.State)
if s.Why != "" {
fmt.Fprintf(&b, " — %s", s.Why)
}
b.WriteString("\n")
}
b.WriteString("\n")
}
b.WriteString("**Transitions**\n")
from := 0
if len(d.Transitions) > 12 {
from = len(d.Transitions) - 12
}
for _, t := range d.Transitions[from:] {
fmt.Fprintf(&b, "- %s %s → %s (%s): %s\n", t.At.UTC().Format("2006-01-02 15:04"), orNothing(t.From), t.To, t.Event, t.Why)
}
b.WriteString("\nThe commit's note under `refs/notes/mesh-plan` keeps every transition: `git log --notes=mesh-plan`.\n")
return b.String()
}
// Flush does what is owed, kept deliveries first: a delivery not yet kept says nothing.
func (h *Holder) Flush() {
type job struct {
id string
group bool
e Effect
body string
repo [2]string
num int
url string
}
h.mu.Lock()
now := h.Now()
for id := range h.dirty {
if d := h.deliveries[id]; d != nil {
h.keep(d)
}
}
for id := range h.dirtyGroup {
if g := h.groups[id]; g != nil {
h.keepGroup(g)
}
}
var jobs []job
ids := make([]string, 0, len(h.deliveries))
for id := range h.deliveries {
ids = append(ids, id)
}
sort.Strings(ids)
for _, id := range ids {
d := h.deliveries[id]
if h.dirty[id] {
continue
}
for _, e := range d.Owed {
if e.Next != nil && now.Before(*e.Next) {
continue
}
j := job{id: id, e: e, repo: [2]string{d.Owner(), d.Repo()}, num: d.Number, url: d.HTMLURL}
if e.Kind == EffectView {
j.body = ViewBody(d, h.groups[d.Group])
}
jobs = append(jobs, j)
}
}
for id, g := range h.groups {
if h.dirtyGroup[id] {
continue
}
for _, e := range g.Owed {
if e.Next != nil && now.Before(*e.Next) {
continue
}
jobs = append(jobs, job{id: id, group: true, e: e})
}
}
h.mu.Unlock()
for _, j := range jobs {
err := h.do(j.e, j.repo, j.num, j.body, j.url, j.group)
h.mu.Lock()
// Why it is given up, when it is: the forge's word that what it is for does not exist is final, and
// so is a failure tried retryLimit times, the last ones retryCap apart.
dropped := ""
switch {
case err != nil && forgeKinds[j.e.Kind] && doesNotExist(err):
dropped = "the forge answers that what it is for does not exist"
case err != nil && j.e.Tries+1 >= retryLimit:
dropped = fmt.Sprintf("tried %d times since %s", j.e.Tries+1, j.e.Since.UTC().Format(time.RFC3339))
}
owed := func(list []Effect) []Effect {
for i, o := range list {
if o.Kind == j.e.Kind && o.Commit == j.e.Commit && o.Context == j.e.Context && o.Line == j.e.Line &&
o.Since.Equal(j.e.Since) {
if err == nil || dropped != "" {
return append(list[:i:i], list[i+1:]...)
}
list[i].Tries++
list[i].Last = err.Error()
next := h.Now().Add(retryAfter(list[i].Tries))
list[i].Next = &next
return list
}
}
return list
}
if j.group {
if g := h.groups[j.id]; g != nil {
g.Owed = owed(g.Owed)
h.keepGroup(g)
}
} else if d := h.deliveries[j.id]; d != nil {
d.Owed = owed(d.Owed)
h.keep(d)
}
if dropped != "" {
h.refuse(fmt.Errorf("%s: its owed %s given up — %s: %v", j.id, j.e.Kind, dropped, err))
}
h.mu.Unlock()
if err != nil && dropped == "" && j.e.Tries%10 == 0 {
h.Logf("[mesh-delivery] %s: %s not done yet (%v); tried again in %s", j.id, j.e.Kind, err,
retryAfter(j.e.Tries+1))
}
}
}
// An owed effect that fails is tried again later each time: retryFirst after the first failure, doubling up
// to retryCap, and given up after retryLimit tries — a little over two days — saying so (novox/hq issue 313).
const (
retryFirst = 10 * time.Second
retryCap = time.Hour
retryLimit = 60
)
// retryAfter is how long after its tries-th failure an effect is tried again.
func retryAfter(tries int) time.Duration {
wait := retryFirst
for i := 1; i < tries && wait < retryCap; i++ {
wait *= 2
}
return min(wait, retryCap)
}
// forgeKinds are the effects done by the forge's holder.
var forgeKinds = map[string]bool{EffectNote: true, EffectView: true, EffectStatus: true}
// notThere is the forge's own answer that what was asked of does not exist: a pull request whose number the
// forge no longer holds, a commit it does not know. Tried again it answers the same, for ever.
var notThere = regexp.MustCompile(`(?i)Gitea API \S+: (404\b|\d{3}\b.*\bdoes not exist\b)`)
func doesNotExist(err error) bool { return err != nil && notThere.MatchString(err.Error()) }
// do is one owed effect.
func (h *Holder) do(e Effect, repo [2]string, number int, body, url string, group bool) error {
switch e.Kind {
case EffectEmit:
var payload any
if err := json.Unmarshal([]byte(e.Line), &payload); err != nil {
return nil // nothing sayable: dropped
}
event := "transition"
if group {
event = "group"
}
return h.Emit(event, payload)
case EffectNote:
if h.Forge == nil {
return fmt.Errorf("no forge")
}
return h.Forge.Note(repo[0], repo[1], e.Commit, e.Line)
case EffectView:
if h.Forge == nil || number == 0 {
return nil
}
return h.Forge.View(repo[0], repo[1], number, body)
case EffectStatus:
if h.Forge == nil {
return fmt.Errorf("no forge")
}
if group {
// A group's status names its member's repository and page in Last.
where, page, _ := strings.Cut(e.Last, " ")
ownerRepo, _, _ := strings.Cut(where, "#")
owner, name, _ := strings.Cut(ownerRepo, "/")
return h.Forge.Status(owner, name, e.Commit, e.Context, e.State, e.Line, page)
}
return h.Forge.Status(repo[0], repo[1], e.Commit, e.Context, e.State, e.Line, url)
}
return nil
}