diff --git a/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/approval.go b/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/approval.go index 32e42916f..9b4c5718f 100644 --- a/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/approval.go +++ b/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/approval.go @@ -313,7 +313,6 @@ func (h *Holder) headsHold(d *Delivery, c *ReadyRun) bool { // A Decline stands, and is not ended. An ask none of whose deliveries can still merge by it is taken away after // (endAsksOfFinished). Under the lock; the cancels run with the look's asks. func (h *Holder) endStaleAsks() { - h.endStaleSettles() ending := map[string]bool{} for _, d := range h.deliveries { r := d.Ready @@ -674,7 +673,7 @@ func (h *Holder) hearAsks() { return } h.mu.Lock() - keys := append(h.pendingAsks(), h.settleKeys()...) + keys := h.pendingAsks() h.mu.Unlock() for _, key := range keys { answer, err := h.Operator.Where(key) @@ -686,12 +685,7 @@ func (h *Holder) hearAsks() { continue } h.mu.Lock() - var said []func() - if h.isSettleKey(key) { - h.settled(key, *answer) // a person's settle ask (settle.go, novox/hq ADR 0556 clause 5) - } else { - said = h.answered(key, *answer) - } + said := h.answered(key, *answer) h.mu.Unlock() for _, s := range said { s() diff --git a/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/main.go b/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/main.go index 6430fa230..5b73dbf71 100644 --- a/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/main.go +++ b/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/main.go @@ -397,30 +397,6 @@ func tools(h *Holder, l *listening) []stdio.Tool { } return h.Close(strArg(a, "id"), strArg(a, "why")) }}, - {Name: seat + "settle", - Description: "Ask the operator to settle deliveries held because their merge reached the trunk without a passing check: " + - "one ask on the operator's channel for all of them, as an Approve is asked. The verb moves nothing; on the operator's " + - "answer each goes to merged, and whether it is on the machines is derived as for any merge (novox/hq ADR 0556). " + - "Refused, nothing asked, when any delivery named is not held or is held for another reason, naming why.", - Input: map[string]any{"type": "object", "additionalProperties": false, "required": []string{"ids", "why"}, - "properties": map[string]any{ - "ids": map[string]any{"type": "array", "items": map[string]any{"type": "string"}, "minItems": 1, - "description": "the held deliveries' ids (owner/repository@commit)"}, - "why": str("why they may be settled, shown to the operator")}}, - Run: func(a map[string]any) (any, error) { - var ids []string - switch v := a["ids"].(type) { - case []any: - for _, x := range v { - if s, ok := x.(string); ok { - ids = append(ids, s) - } - } - case []string: - ids = v - } - return h.Settle(ids, strArg(a, "why"), aPerson) - }}, {Name: "delivery_merge_again", Description: "On the operator's word, ask again at once the merge of an approved delivery the forge refused, or of an approved " + "group held or half merged, at the heads the Approve named (novox/hq issue 1275): every member is read first and " + diff --git a/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/manifest_test.go b/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/manifest_test.go index 141ad05c0..2a340989e 100644 --- a/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/manifest_test.go +++ b/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/manifest_test.go @@ -11,7 +11,6 @@ import ( "testing" "time" - "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient" "git.novox.be/novox/mesh-sdk/go/buscalls" "git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/seatlist/seatlisttest" @@ -76,9 +75,9 @@ func TestItClaimsTheDeliverySeatAndFollowsWhatItHandles(t *testing.T) { if len(m.Seats) != 0 { t.Fatal("the manifest declares a seat; mesh-delivery is the mesh's own") } - // The eight verbs the seat keeps (ADR 0539 §17): times, what-if, release, stop and retire-history went with the plans; and - // settle, a person's ask to settle merges held without a passing check (ADR 0556 clause 5). - if want := []string{"deliveries", "show", "groups", "source", "checks", "table", "stalled", "recheck", "close", "settle"}; !reflect.DeepEqual(m.Claims[0].Serves, want) { + // The verbs the seat keeps: times, what-if, release, stop and retire-history went with the plans (ADR 0539 §17), and + // settle with the merge states (design 47 G §431). + if want := []string{"deliveries", "show", "groups", "source", "checks", "table", "stalled", "recheck", "close"}; !reflect.DeepEqual(m.Claims[0].Serves, want) { t.Fatalf("serves %v, wanted %v", m.Claims[0].Serves, want) } // The forge messages' layout is the bridge's published package forgemsg, taken by its series (ADR 0634 §3, design 32). @@ -138,9 +137,9 @@ func TestItClaimsTheDeliverySeatAndFollowsWhatItHandles(t *testing.T) { t.Fatalf("the manifest still names check-gates-merge (%v)", err) } // W11.20: the gate keeps its records in a ledger and reads its fill before it opens one, so it is held off a machine whose - // tool runner answers neither (the fill a capability of its own, the lead's ruling). - if !reflect.DeepEqual(m.Capabilities, []string{buscalls.LedgerCalls, buscalls.LedgerFillCalls}) { - t.Fatalf("capabilities %v, want %s and %s", m.Capabilities, buscalls.LedgerCalls, buscalls.LedgerFillCalls) + // tool runner answers neither (the fill and the removal one capability, ledger-upkeep-calls, design 47 G §587). + if !reflect.DeepEqual(m.Capabilities, []string{buscalls.LedgerCalls, buscalls.LedgerUpkeepCalls}) { + t.Fatalf("capabilities %v, want %s and %s", m.Capabilities, buscalls.LedgerCalls, buscalls.LedgerUpkeepCalls) } // What it reads: the checker's verdicts alone (issue 1302; they go with the checker). The teller's answers and the delivery // view's reach, which `deliveries` and `show` derive whether a merge is on the machines from (ADR 0556 clause 4), are no @@ -286,12 +285,12 @@ func TestTheGateHoldsTheSeatAsTheSDKsTableOfRelease6HasIt(t *testing.T) { served[verb] = true } } - for removed := range meshdeliveryclient.RemovedVerbs() { + for _, removed := range []string{"settle"} { // design 47 G §431 if served[removed] { - t.Errorf("the gate serves %s, which release 6 removed: the SDK answers it by name", removed) + t.Errorf("the gate serves %s, which G removed with the merge states", removed) } if slices.Contains(manifest.Claims[0].Serves, removed) { - t.Errorf("the manifest serves %s, which release 6 removed", removed) + t.Errorf("the manifest serves %s, which G removed with the merge states", removed) } } for _, verb := range []string{"deliveries", "show", "groups", "checks", "table", "stalled", "recheck", "close"} { diff --git a/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/model.go b/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/model.go index 340bd7dac..6f275fb34 100644 --- a/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/model.go +++ b/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/model.go @@ -106,10 +106,6 @@ type Delivery struct { // trunk's head, before the operator is asked (novox/hq ADR 0421 §3, issue 1022). Cleared when it is asked. FreshWanted bool `json:"fresh_wanted,omitempty"` - // Settle is the settle ask a person had written for it while it is held for a merge without a passing check (settle.go, - // novox/hq ADR 0556 clause 5): its key in the gate's asks, why, who asked and when, and the operator's answer once read. - Settle *SettleAsk `json:"settle,omitempty"` - Transitions []Transition `json:"transitions,omitempty"` // Owed are the forge's and the bus's effects of its transitions not yet done: said only after the // delivery is kept, retried until done. diff --git a/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/record.go b/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/record.go index f99abaa03..1d5123b51 100644 --- a/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/record.go +++ b/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/record.go @@ -275,7 +275,7 @@ type RecordLedger interface { Apply(instance, operation string, body any) (uint64, error) // Get is the instance's state and the sequence of its last operation; a nil state when it has none. Get(instance string) (json.RawMessage, uint64, error) - // Remove removes a record with its operations and its snapshot (OpRemove); an error when any of them stays. + // Remove removes a record with its operations and its snapshot through the tool runner's ledger removal. Remove(instance string) error } @@ -312,15 +312,16 @@ func applyCall(instance, operation string, body any) (buscalls.Applied, error) { return got, nil } -// Remove applies OpRemove and reads the ledger's answer: a removal that left the record's operations, its snapshot or its -// state is an error, so the retention says it rather than leaving a record's history to grow unseen. +// Remove asks the tool runner to remove the record's operations, its snapshot and its keys (buscalls.LedgerRemove, design +// 47 G §587); an instance the ledger does not hold answers none removed, so a removal said twice changes nothing. func (sdkLedger) Remove(instance string) error { - got, err := applyCall(instance, OpRemove, map[string]any{}) + out, err := ledgerCall(buscalls.LedgerRemove, buscalls.LedgerRemoveRequest{Ledger: RecordsLedger, Instance: instance}) if err != nil { return err } - if !got.Removed { - return fmt.Errorf("the ledger removed %s but kept its operations or its snapshot (removed false at sequence %d)", instance, got.Sequence) + var got buscalls.LedgerRemoved + if err := json.Unmarshal(out, &got); err != nil { + return fmt.Errorf("the ledger's answer to the removal of %s is not readable: %w", instance, err) } return nil } diff --git a/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/record_test.go b/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/record_test.go index a5d091560..d1a7f09e4 100644 --- a/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/record_test.go +++ b/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/record_test.go @@ -787,19 +787,33 @@ func TestTheRecordsToolsReadThemAndEndOneByHand(t *testing.T) { t.Fatalf("a second repair did something: %+v", lines) } }) - t.Run("a removal that keeps the record's operations is an error", func(t *testing.T) { + t.Run("a removal asks the runtime's ledger removal of the instance, and one said twice changes nothing", func(t *testing.T) { was := ledgerCall t.Cleanup(func() { ledgerCall = was }) - removed := false + var asked []buscalls.LedgerRemoveRequest ledgerCall = func(method string, body any) (json.RawMessage, error) { - return json.Marshal(buscalls.Applied{Taken: true, Sequence: 4, Removed: removed}) + in, ok := body.(buscalls.LedgerRemoveRequest) + if method != buscalls.LedgerRemove || !ok { + t.Fatalf("the removal called %s with %T", method, body) + } + asked = append(asked, in) + if len(asked) == 1 { + return json.Marshal(buscalls.LedgerRemoved{Instance: in.Instance, Operations: 3, Bytes: 900}) + } + return json.Marshal(buscalls.LedgerRemoved{Instance: in.Instance}) } - if err := (sdkLedger{}).Remove("record/47_G/0011223344556677"); err == nil || !strings.Contains(err.Error(), "kept its operations") { - t.Fatalf("a removal that kept its operations: %v", err) + const inst = "record/47_G/0011223344556677" + for range 2 { + if err := (sdkLedger{}).Remove(inst); err != nil { + t.Fatal(err) + } } - removed = true - if err := (sdkLedger{}).Remove("record/47_G/0011223344556677"); err != nil { - t.Fatal(err) + if len(asked) != 2 || asked[0].Ledger != RecordsLedger || asked[0].Instance != inst { + t.Fatalf("asked %+v", asked) + } + ledgerCall = func(string, any) (json.RawMessage, error) { return nil, errors.New("the runtime is away") } + if err := (sdkLedger{}).Remove(inst); err == nil { + t.Fatal("a removal the runtime did not answer was taken as done") } }) } @@ -810,8 +824,7 @@ func TestTheRecordsToolsReadThemAndEndOneByHand(t *testing.T) { func TestTheRecordsLedgerSaysFillingFullAndRoom(t *testing.T) { const capBytes = 10 << 20 filled := func(bytes uint64) *buscalls.Fill { - return &buscalls.Fill{History: buscalls.Fill{Count: 1, Bytes: bytes, MaxBytes: capBytes}, - State: buscalls.Fill{Bytes: 1, MaxBytes: 64 << 20}} + return &buscalls.Fill{Count: 1, Bytes: bytes, MaxBytes: capBytes} } heads := map[string]string{"novox/app@111111111111": strings.Repeat("1", 40)} at := time.Date(2026, 11, 9, 20, 0, 0, 0, time.UTC) diff --git a/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/recordfill.go b/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/recordfill.go index 0154b4317..22565c04e 100644 --- a/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/recordfill.go +++ b/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/recordfill.go @@ -44,7 +44,7 @@ func (sdkLedger) Fill() (buscalls.Fill, error) { return got, nil } -// Fill is the records' ledger's store nearest its cap. +// Fill is the records' ledger's history against its cap (design 47 G §587). func (rs *Records) Fill() (buscalls.Fill, error) { fr, ok := rs.Ledger.(fillReader) if !ok { @@ -54,7 +54,7 @@ func (rs *Records) Fill() (buscalls.Fill, error) { if err != nil { return buscalls.Fill{}, fmt.Errorf("the records' ledger cannot say its fill: %w", err) } - return f.Fullest(), nil + return f, nil } // pastFilling is a fill at or past FillingPercent of its cap. diff --git a/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/settle.go b/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/settle.go deleted file mode 100644 index b43aa67c5..000000000 --- a/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/settle.go +++ /dev/null @@ -1,277 +0,0 @@ -package main - -import ( - "fmt" - "sort" - "strings" - "time" - - occ "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient" - oc "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel" - mdc "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient" -) - -// 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 (mdc.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 []mdc.Head - for _, d := range covered { - names = append(names, d.ID) - heads = append(heads, mdc.Head{Repository: d.Repository, Number: d.Number, Head: d.Commit}) - } - group := mdc.SettleGroup(names) - key, err := mdc.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 (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, err := gateAsk(group, heads, asking, composeSettleAsk(mdc.AskingID(key, asking), covered, why, now)) - if err != nil { - h.mu.Unlock() - return nil, fmt.Errorf("the settle ask is not one the operator can be asked: %w", err) - } - 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) occ.Act { - return occ.Act{"verb": mdc.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) oc.Ask { - var a oc.Ask - a.ID, a.Who, a.Expires = id, occ.Operator, occ.FormatTime(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)), occ.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, ", ")), occ.SummaryLength), - phone("Why: "+why, occ.PointLength), - }, "\n") - a.Details = strings.Join(lines, "\n") - group := mdc.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, _ := occ.ActDigest(settleAct(group, id, o.id, heads)) - a.Options = append(a.Options, oc.Option{ID: o.id, Label: o.label, Does: o.does, Level: occ.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 := mdc.ParseAskKey(key) - return err == nil && strings.HasPrefix(group, mdc.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 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 != mdc.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) - } - } -} diff --git a/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/settle_test.go b/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/settle_test.go deleted file mode 100644 index ee06dcc21..000000000 --- a/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/settle_test.go +++ /dev/null @@ -1,127 +0,0 @@ -package main - -import ( - "slices" - "strings" - "testing" - "time" - - occ "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient" - mdc "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient" -) - -// 1280 (novox/hq ADR 0556 clause 5): settle writes one ask for several deliveries held for a merge without a passing check and -// moves none of them; it refuses, asking nothing, a delivery held for another reason and one not held, naming why; the -// operator's Leave keeps them held, and Settle, asked again under a new asking, moves each to merged with the operator's words. -func TestSettleAsksTheOperatorOnceAndMovesOnlyOnTheAnswer(t *testing.T) { - w := newWorld(t) - w.op.auto = false - merged := w.now.Add(-time.Hour) - put := func(id string, state State, why string) *Delivery { - repo, commit, _ := strings.Cut(id, "@") - d := &Delivery{ID: id, Repository: repo, Commit: commit, Number: len(id), Title: "a fix", State: state, Since: merged, - MergedAs: "m" + commit, MergedAt: merged, HeldWhy: why} - w.h.mu.Lock() - w.h.keep(d) - w.h.mu.Unlock() - return d - } - a := put("novox/web@aaaaaaaaaaaa", Held, HeldUnchecked) - b := put("novox/sdk@bbbbbbbbbbbb", Held, HeldUnchecked) - put("novox/web@cccccccccccc", Held, "a conflict on the trunk") - put("novox/web@dddddddddddd", Merged, "") - for ids, words := range map[string]string{ - "novox/web@aaaaaaaaaaaa novox/web@cccccccccccc": "held for another reason", - "novox/web@aaaaaaaaaaaa novox/web@dddddddddddd": "not held", - "novox/web@aaaaaaaaaaaa novox/web@eeeeeeeeeeee": "no such delivery", - } { - if _, err := w.h.Settle(strings.Fields(ids), "on every machine since 10-25", aPerson); err == nil || !strings.Contains(err.Error(), words) { - t.Errorf("settle %s: %v, want refused naming %q", ids, err, words) - } - } - if _, err := w.h.Settle([]string{a.ID}, " ", aPerson); err == nil { - t.Error("a settle with no why was asked") - } - if w.op.count() != 0 { - t.Fatalf("a refused settle asked the operator: %d asks", w.op.count()) - } - out, err := w.h.Settle([]string{b.ID, a.ID}, "on every machine since 10-25", aPerson) - if err != nil { - t.Fatal(err) - } - key, _ := out["asked"].(string) - if open := w.op.open(); len(open) != 1 || open[0] != key || w.op.count() != 1 { - t.Fatalf("asks open %v (asked %d), want the one key %s", open, w.op.count(), key) - } - ask := w.op.ask(key) - heads := map[string]string{a.ID: a.Commit, b.ID: b.Commit} - group := mdc.SettleGroup([]string{a.ID, b.ID}) - for _, o := range ask.Options { - if want, _ := occ.ActDigest(settleAct(group, ask.ID, o.ID, heads)); o.Binds != want { - t.Errorf("the option %s binds %s, not mesh-delivery.settle of these heads", o.ID, o.Binds) - } - } - w.h.hearAsks() - if a.State != Held || b.State != Held { - t.Fatalf("moved before an answer: %s, %s", a.State, b.State) - } - w.op.answer(key, OptionLeave) - w.h.hearAsks() - if a.State != Held || a.Settle == nil || a.Settle.Answer != OptionLeave { - t.Fatalf("after Leave: %s %+v", a.State, a.Settle) - } - // Asked again: a new asking of the same key, under its own id. - if _, err := w.h.Settle([]string{a.ID, b.ID}, "asked again", aPerson); err != nil { - t.Fatal(err) - } - if again := w.op.ask(key); again.ID == ask.ID || a.Settle.Asking != 2 { - t.Fatalf("asked again under %s (asking %d), want a new id", again.ID, a.Settle.Asking) - } - // An answer under a settle key no delivery waits for moves nothing. - other, _ := mdc.MergeAskKey(mdc.SettleGroup([]string{"novox/web@ffffffffffff"}), []mdc.Head{{Repository: "novox/web", Number: 1, Head: "ffffffffffff"}}) - w.h.mu.Lock() - w.h.settled(other, Answer{Option: OptionSettle, Warrant: "x@now", Words: "chose Settle"}) - w.h.mu.Unlock() - if a.State != Held || b.State != Held || !strings.Contains(w.logbuf.String(), "covers no delivery") { - t.Fatalf("an answer for other ids moved %s, %s", a.State, b.State) - } - w.op.answer(key, OptionSettle) - w.h.hearAsks() - w.h.mu.Lock() - cancels := len(w.h.cancels) - w.h.mu.Unlock() - if cancels == 0 { - t.Error("the answered settle ask is not taken away") - } - for _, d := range []*Delivery{a, b} { - last := d.Transitions[len(d.Transitions)-1] - if d.State != Merged || last.Event != EvSettled || last.By != "the operator" || !strings.Contains(last.Why, "chose Settle") { - t.Errorf("%s after Settle: %s, %+v", d.ID, d.State, last) - } - } - // A settle ask nobody answers within its bound is taken away; its delivery stays held. - c := put("novox/web@999999999999", Held, HeldUnchecked) - if _, err := w.h.Settle([]string{c.ID}, "once more", aPerson); err != nil { - t.Fatal(err) - } - w.later(AskLasts + time.Minute) - w.h.mu.Lock() - w.h.endStaleSettles() - w.h.mu.Unlock() - if c.State != Held || c.Settle.Answer != "lapsed" { - t.Errorf("a lapsed settle ask: %s %+v", c.State, c.Settle) - } - // Issue 774: a settle asked again for another set of deliveries is another key; the earlier ask, which no delivery waits - // for any more, is taken away at once, not left to hold the teller's room at the router until it lapses. - e, f := put("novox/web@eeeeeeeeeeee", Held, HeldUnchecked), put("novox/web@ffffffffffff", Held, HeldUnchecked) - first, err := w.h.Settle([]string{e.ID}, "one", aPerson) - if err != nil { - t.Fatal(err) - } - if _, err := w.h.Settle([]string{e.ID, f.ID}, "both", aPerson); err != nil { - t.Fatal(err) - } - if k := first["asked"].(string); !slices.Contains(w.op.cancelled, k) || slices.Contains(w.op.open(), k) { - t.Errorf("the earlier settle ask %s stands once no delivery waits for it: open %v, taken away %v", k, w.op.open(), w.op.cancelled) - } -} diff --git a/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/table.go b/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/table.go index 771116f70..e79102a52 100644 --- a/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/table.go +++ b/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/table.go @@ -77,7 +77,6 @@ const ( EvMerged Event = "merged" // it reached the trunk EvMergedUnchecked Event = "merged-unchecked" // it reached the trunk without a passing check EvTrunkMoved Event = "trunk moved" // another merge moved its trunk, and its verdict names the trunk commit before (novox/hq ADR 0323) - EvSettled Event = "settled" // the operator settled a merge held without a passing check (novox/hq ADR 0556 clause 5) ) // HeldUnchecked is the why of a delivery held because its merge reached the trunk without a passing check: the one hold a @@ -186,20 +185,6 @@ var Table = []Row{ {From: []State{Proposed, Rejected}, Event: EvMergedUnchecked, To: Held, Guard: "merged without a passing check — a verdict known with it is taken first: only a person decides what follows", Holds: func(d *Delivery, f Facts) error { return unless(d.MergedAs != "", "not merged") }}, - // **A person settles a merge held without a passing check** (novox/hq ADR 0556 clause 5): only on the operator's answer to - // the settle ask, with its words; a hold of another kind is not taken here. Whether it is on the machines is then derived. - {From: []State{Held}, Event: EvSettled, To: Merged, Act: true, - Guard: "held because its merge reached the trunk without a passing check, and the operator answered settle to the ask " + - "`settle` wrote, with the words of that answer", - Holds: func(d *Delivery, f Facts) error { - switch { - case !heldUnchecked(d): - return errors.New("it is not held for a merge without a passing check: " + d.HeldWhy) - case f.By == "" || f.Why == "": - return errors.New("a settle names the operator's answer and its words") - } - return nil - }}, {From: []State{Rejected, Ready}, Event: EvRecheck, To: Proposed, Act: true, Guard: "a person asked, with why, or its group changed", Holds: func(d *Delivery, f Facts) error { return unless(f.Why != "", "a recheck says why") }}, diff --git a/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/table_test.go b/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/table_test.go index 1469a95a5..a9a034cec 100644 --- a/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/table_test.go +++ b/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/table_test.go @@ -34,9 +34,6 @@ func aDeliveryFor(r Row) (*Delivery, Facts) { case EvTrunkMoved: d.Check = &Verdict{Gate: "pass", Head: "c0ffee000000", Trunk: "d28ca95bbbbb"} d.TrunkNow = "690966dccccccccc" - case EvSettled: - d.MergedAs, d.HeldWhy = "aaaa1111", HeldUnchecked - f.Why, f.By = "settled: the operator chose Settle", "the operator" } return d, f } @@ -106,7 +103,7 @@ func TestEveryRowOfTheTableIsTakenWhenItsGuardHoldsAndRefusedWhenNot(t *testing. func TestEveryPairTheTableDoesNotHoldIsRefusedByName(t *testing.T) { events := []Event{EvAnnounced, EvAccepted, EvRefused, EvRecheck, EvReask, EvLost, EvNewHead, EvClosed, EvMerged, - EvMergedUnchecked, EvTrunkMoved, EvSettled} + EvMergedUnchecked, EvTrunkMoved} for _, from := range append([]State{None}, AllStates...) { for _, ev := range events { for _, act := range []bool{false, true} { @@ -131,17 +128,14 @@ func TestEveryPairTheTableDoesNotHoldIsRefusedByName(t *testing.T) { // The rules the table is for, read off the table. func TestTheTableKeepsItsRules(t *testing.T) { for _, r := range Table { - // The one row out of a final state is a person's settle of a merge held without a passing check, taken on the - // operator's answer (novox/hq ADR 0556 clause 5): it is already on the trunk, and moves from held to merged. - settle := r.Event == EvSettled && r.Act && len(r.From) == 1 && r.From[0] == Held && r.To == Merged for _, from := range r.From { // Nothing else leaves a final state. - if from.Final() && !settle { + if from.Final() { t.Errorf("%s is final and the table leaves it by %s", from, r.Event) } } // Only a ready delivery is merged, and nothing but its merge, or a merge without a passing check, puts one on the trunk. - if r.To == Merged && !settle && !(r.Event == EvMerged && len(r.From) == 1 && r.From[0] == Ready) { + if r.To == Merged && !(r.Event == EvMerged && len(r.From) == 1 && r.From[0] == Ready) { t.Errorf("%v —%s→ merged: only a ready delivery's merge merges", r.From, r.Event) } if r.To == Held && r.Event != EvMergedUnchecked { diff --git a/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/verbs.go b/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/verbs.go index 18378d182..7c0ea4fe6 100644 --- a/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/verbs.go +++ b/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/verbs.go @@ -351,8 +351,5 @@ func (h *Holder) Status(listening string) map[string]any { // read; the gate moves nothing after the merge. func HeldLine(d *Delivery) string { line := fmt.Sprintf("%s held since %s: %s", d.ID, d.Since.UTC().Format(time.RFC3339), d.HeldWhy) - if heldUnchecked(d) { - line += " (mesh-delivery.settle asks the operator to settle it, ADR 0556)" - } return line } diff --git a/modules/mesh-delivery-gate/module.json b/modules/mesh-delivery-gate/module.json index 141daa397..514b72be9 100644 --- a/modules/mesh-delivery-gate/module.json +++ b/modules/mesh-delivery-gate/module.json @@ -15,8 +15,7 @@ "table", "stalled", "recheck", - "close", - "settle" + "close" ] } ], @@ -327,7 +326,7 @@ ], "capabilities": [ "ledger-calls", - "ledger-fill" + "ledger-upkeep-calls" ], "reads": [ "mesh-delivery-checker.verdicts"