Files
mesh-catalog/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/settle.go
T
jschoubben fdb8deea4c
mesh/delivery merged: source.main moved to novox/mesh-catalog@a68b39f7b512
mesh/delivery-group group fix/merge-ask-ends-on-merge merged: every member is merged
mesh/check checking fdb8deea4c9e on a68b39f7b512 | novox/mesh-catalog#577 in group fix/merge-ask-ends-on-merge
Gate: an ask ends once no delivery it covers can still merge by it, so an ask of a pull request merged another way cannot hold the teller's room for a day (issue 774)
endStaleAsks skipped every merged delivery, so a pull request merged without the
Approve kept its merge ask in the gate's asks until it lapsed: on 2026-10-31 two
such asks held two of the router's three places for the teller and a placement
ask was refused for want of room. Every look now takes away a standing ask none of
whose deliveries is still open, read from what the gate keeps, so a failed take
away is retried. A settle asked again for another set takes the earlier settle ask
away for the same reason.
2026-10-31 02:16:08 +01:00

273 lines
10 KiB
Go

package main
import (
"fmt"
"sort"
"strings"
"time"
"git.novox.be/novox/mesh-sdk/go/asks"
sdk "git.novox.be/novox/mesh-sdk/go/delivery"
)
// A person settles a delivery held because its merge reached the trunk without a passing check (novox/hq ADR 0556 clause 5).
// The verb `settle` moves nothing: it writes one ask, for every delivery named, into the gate's asks, which the teller puts to
// the operator as it puts an Approve, its options binding mesh-delivery.settle (sdk.AskVerb). On the answer settle each
// delivery takes the table's asked row held → merged with the operator's words; on leave they stay held. Whether a settled
// merge is on the machines is then derived as for any merge (reached.go). A hold of another kind, or a delivery not held, is
// refused, naming why.
// The settle ask's two answers, both at the level approve.
const (
OptionSettle = "settle"
OptionLeave = "leave"
)
// SettleAsk is the settle ask a delivery is covered by.
type SettleAsk struct {
Key string `json:"key"`
Asking int `json:"asking,omitempty"`
Why string `json:"why"`
By string `json:"by,omitempty"`
At time.Time `json:"at"`
Answered *time.Time `json:"answered,omitempty"`
Answer string `json:"answer,omitempty"`
Words string `json:"words,omitempty"`
}
// Settle is the `settle` verb: one ask of the operator for every delivery named, each held for a merge without a passing
// check. Nothing moves until the operator answers.
func (h *Holder) Settle(ids []string, why, by string) (map[string]any, error) {
why = strings.TrimSpace(why)
if why == "" {
return nil, fmt.Errorf("settle says why: it is shown to the operator")
}
if len(ids) == 0 {
return nil, fmt.Errorf("settle names at least one held delivery (ids)")
}
if h.Operator == nil {
return nil, fmt.Errorf("this owner has no way to ask the operator")
}
h.mu.Lock()
var covered []*Delivery
var refused []string
seen := map[string]bool{}
for _, id := range ids {
id = strings.TrimSpace(id)
if seen[id] {
continue
}
seen[id] = true
d := h.deliveries[id]
switch {
case d == nil:
refused = append(refused, id+": no such delivery")
case d.State != Held:
refused = append(refused, fmt.Sprintf("%s: it is %s, not held", id, d.State))
case !heldUnchecked(d):
refused = append(refused, fmt.Sprintf("%s: it is held for another reason, which settle does not take (%s)", id, d.HeldWhy))
default:
covered = append(covered, d)
}
}
if len(refused) > 0 {
h.mu.Unlock()
sort.Strings(refused)
return nil, fmt.Errorf("settle refused, nothing asked: %s", strings.Join(refused, "; "))
}
sort.Slice(covered, func(i, j int) bool { return covered[i].ID < covered[j].ID })
var names []string
var heads []sdk.Head
for _, d := range covered {
names = append(names, d.ID)
heads = append(heads, sdk.Head{Repository: d.Repository, Number: d.Number, Head: d.Commit})
}
group := sdk.SettleGroup(names)
key, err := sdk.MergeAskKey(group, heads)
if err != nil {
h.mu.Unlock()
return nil, fmt.Errorf("the settle ask has no key: %w", err)
}
now := h.Now()
// The same deliveries asked again (the ask lapsed, or was left unanswered) are a new asking of the same key, under its own
// router id: the router drops a second ask under an id it kept.
asking := 1 // the first asking (sdk.Ask.Asking: 0 or 1 is the first, n the n-th)
for _, d := range covered {
if d.Settle != nil && d.Settle.Key == key && max(d.Settle.Asking, 1) >= asking {
asking = max(d.Settle.Asking, 1) + 1
}
}
ask := sdk.Ask{Group: group, Heads: heads, Asking: asking, Ask: composeSettleAsk(sdk.AskingID(key, asking), covered, why, now)}
earlier := map[string]bool{}
for _, d := range covered {
if d.Settle != nil && d.Settle.Answered == nil && d.Settle.Key != key {
earlier[d.Settle.Key] = true
}
d.Settle = &SettleAsk{Key: key, Asking: asking, Why: why, By: by, At: now}
h.keep(d)
}
// An earlier settle ask of another set that no delivery waits for any more is taken away (issue 774): its question is
// gone, and left standing it would hold the teller's room at the router until it lapsed.
for _, d := range h.deliveries {
if d.Settle != nil {
delete(earlier, d.Settle.Key)
}
}
gone := make([]string, 0, len(earlier))
for k := range earlier {
gone = append(gone, k)
}
sort.Strings(gone)
h.mu.Unlock()
for _, k := range gone {
h.Logf("[mesh-delivery] the settle ask %s is taken away: settle asked again for another set, and no delivery waits for it", k)
h.cancelSettle(k)()
}
if err := h.Operator.Ask(key, ask); err != nil {
return nil, fmt.Errorf("the settle ask %s could not be written (call settle again): %w", key, err)
}
h.Logf("[mesh-delivery] asked the operator to settle %s: %s", strings.Join(names, ", "), why)
return map[string]any{"asked": key, "deliveries": names,
"said": fmt.Sprintf("The operator is asked once to settle %d deliveries; nothing moves until the answer.", len(covered))}, nil
}
// settleAct is what a settle ask's answer binds: its verb (mesh-delivery.settle), this ask, the answer and the heads.
func settleAct(group, ask, option string, heads map[string]string) asks.Act {
return asks.Act{"verb": sdk.AskVerb(group), "ask": ask, "option": option, "heads": headsString(heads)}
}
// composeSettleAsk is the operator's ask to settle these held deliveries.
func composeSettleAsk(id string, ds []*Delivery, why string, now time.Time) asks.Ask {
var a asks.Ask
a.ID, a.Who, a.Expires = id, asks.Operator, now.Add(AskLasts)
a.OnExpiry = "they stay held; settle asks again"
a.About = "delivery.settle"
var pulls, lines []string
heads := map[string]string{}
for _, d := range ds {
pulls = append(pulls, pullSaid(d))
heads[d.ID] = d.Commit
lines = append(lines, phone(fmt.Sprintf("%s %q merged at %s without a passing check", pullSaid(d), d.Title,
shortOf(d.MergedAs, 12)), 400))
}
a.Headline = phone(fmt.Sprintf("Settle %d merges made without a passing check", len(ds)), asks.HeadlineLength)
a.Explanation = strings.Join([]string{
phone(fmt.Sprintf("%s reached main without a passing check and are held. Settle ends the hold: each reads merged, and on the machines once every machine runs it. Leave keeps them held.",
strings.Join(pulls, ", ")), asks.SummaryLength),
phone("Why: "+why, asks.PointLength),
}, "\n")
a.Details = strings.Join(lines, "\n")
group := sdk.SettleGroup(func() []string {
var ids []string
for _, d := range ds {
ids = append(ids, d.ID)
}
return ids
}())
for _, o := range []struct{ id, label, does string }{
{OptionSettle, "Settle", "each delivery named ends its hold and reads merged"},
{OptionLeave, "Leave", "they stay held"},
} {
binds, _ := asks.ActDigest(settleAct(group, id, o.id, heads))
a.Options = append(a.Options, asks.Option{ID: o.id, Label: o.label, Does: o.does, Level: asks.Approve, Binds: binds})
}
return a
}
// isSettleKey is whether an ask key is a settle ask's; under the lock.
func (h *Holder) isSettleKey(key string) bool {
_, group, _, err := sdk.ParseAskKey(key)
return err == nil && strings.HasPrefix(group, sdk.SettleGroupPrefix)
}
// settleKeys are the settle asks waiting for an answer; under the lock.
func (h *Holder) settleKeys() []string {
seen := map[string]bool{}
var out []string
for _, d := range h.deliveries {
if s := d.Settle; s != nil && s.Answered == nil && d.State == Held && !seen[s.Key] {
seen[s.Key] = true
out = append(out, s.Key)
}
}
sort.Strings(out)
return out
}
// settled applies the teller's answer to a settle ask; under the lock. The teller held the warrant (the act it bound is
// mesh-delivery.settle); an answer to an earlier asking, or one choosing what the ask did not offer, moves nothing.
func (h *Holder) settled(key string, ans sdk.Answer) {
var covered []*Delivery
for _, d := range h.deliveries {
if d.Settle != nil && d.Settle.Key == key && d.Settle.Answered == nil {
covered = append(covered, d)
}
}
if len(covered) == 0 {
h.Logf("[mesh-delivery] the answer to the settle ask %s covers no delivery that waits for it: nothing settled", key)
return
}
sort.Slice(covered, func(i, j int) bool { return covered[i].ID < covered[j].ID })
if asked, at, ok := strings.Cut(ans.Warrant, "@"); ok && at != "" && asked != sdk.AskingID(key, covered[0].Settle.Asking) {
h.Logf("[mesh-delivery] the answer to the settle ask %s was given to another asking (%s): not taken", key, asked)
return
}
if ans.Option != OptionSettle && ans.Option != OptionLeave {
h.refuse(fmt.Errorf("the answer to the settle ask %s chose %s, which it did not offer: nothing settled", key, ans.Option))
return
}
now := h.Now()
for _, d := range covered {
d.Settle.Answered, d.Settle.Answer, d.Settle.Words = &now, ans.Option, ans.Words
if ans.Option == OptionLeave {
h.Logf("[mesh-delivery] %s: the operator left it held (%s)", d.ID, ans.Words)
h.keep(d)
continue
}
t, err := Apply(d, EvSettled, true, Facts{Now: now, By: "the operator", Why: "settled: " + ans.Words})
if err != nil {
h.refuse(err)
h.keep(d)
continue
}
d.HeldWhy = ""
h.moved(d, []Transition{t})
}
// Answered: the ask is taken away, as a merge ask is once acted on.
h.cancels = append(h.cancels, h.cancelSettle(key))
}
// cancelSettle takes a settle ask away from the gate's asks.
func (h *Holder) cancelSettle(key string) func() {
return func() {
if h.Operator == nil {
return
}
if err := h.Operator.Cancel(key); err != nil {
h.Logf("[mesh-delivery] the settle ask %s could not be taken away (taken away again at the next look): %v", key, err)
}
}
}
// endStaleSettles takes away every settle ask nobody answered within its bound; its deliveries stay held and settle may ask
// again. Under the lock; the cancels run with the look's asks.
func (h *Holder) endStaleSettles() {
done := map[string]bool{}
for _, d := range h.deliveries {
s := d.Settle
if s == nil || s.Answered != nil || done[s.Key] || h.Now().Before(s.At.Add(AskLasts)) {
continue
}
done[s.Key] = true
h.Logf("[mesh-delivery] the settle ask %s lapsed with no answer: taken away, %s stays held", s.Key, d.ID)
h.cancels = append(h.cancels, h.cancelSettle(s.Key))
}
for _, d := range h.deliveries {
if s := d.Settle; s != nil && done[s.Key] {
now := h.Now()
s.Answered, s.Answer = &now, "lapsed"
h.keep(d)
}
}
}