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.
273 lines
10 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|