gate_record_repair asks the owners' read verbs for the outcomes a record's node entries lack and applies them as the consumer would (milestone 47/G, gate_record_repair)
Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ (Joined from 46d3053c.) Builds: design 47 G §159 (GATE13) Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
This commit is contained in:
@@ -161,7 +161,7 @@ func TestItClaimsTheDeliverySeatAndFollowsWhatItHandles(t *testing.T) {
|
||||
// bridge's (ADR 0437 §8). It uses operator-channel only to say its conditions (ADR 0437 §9, issue 1275): the teller asks
|
||||
// (ADR 0437 §6).
|
||||
want := []string{"seat:git.pull", "seat:git.statuses", "seat:git.open-pulls", "seat:git.contains", "seat:mesh-hand-acts.hand-act",
|
||||
TrackerListVerb, tellerAnswersTool, viewReachTool}
|
||||
TrackerListVerb, WantsEntryVerb, EngineCompositionVerb, tellerAnswersTool, viewReachTool}
|
||||
if !reflect.DeepEqual(m.Uses, []string{"operator-channel"}) {
|
||||
t.Fatalf("uses %v: operator-channel, for its conditions alone", m.Uses)
|
||||
}
|
||||
|
||||
@@ -742,6 +742,51 @@ func TestTheRecordsToolsReadThemAndEndOneByHand(t *testing.T) {
|
||||
t.Fatal("a record ended is ended again")
|
||||
}
|
||||
})
|
||||
t.Run("gate_record_repair asks the owners for the outcomes an entry lacks, applies what they answer, and ends the record", func(t *testing.T) {
|
||||
rs, r, said := setup(t)
|
||||
owners := &ownersFake{wanted: "src2:f1", fp: "src2:f1", phase: "running"}
|
||||
log := &handActsFake{}
|
||||
if _, err := rs.Repair(r.Instance, "", "jochen@shanks", owners, log); err == nil {
|
||||
t.Fatal("repaired with no reason")
|
||||
}
|
||||
log.refuse = errors.New("no responders")
|
||||
if _, err := rs.Repair(r.Instance, "the events were lost", "jochen@shanks", owners, log); err == nil || len(owners.asked) != 0 {
|
||||
t.Fatalf("repaired though the hand-act log refused it: %v, asked %v", err, owners.asked)
|
||||
}
|
||||
log.refuse = nil
|
||||
// The owner says the module runs another fingerprint: the entry is left.
|
||||
owners.fp = "src2:other"
|
||||
got, err := rs.Repair(r.Instance, "the events were lost", "jochen@shanks", owners, log)
|
||||
if err != nil || got["ended"] != false {
|
||||
t.Fatalf("%v %v", got, err)
|
||||
}
|
||||
if back, _ := rs.Read(r.Instance); back.Outcomes[OutcomeKey("mesh-x", "src2:f1", "ace", StepRunning)].Outcome != "" {
|
||||
t.Fatalf("an outcome the owner did not answer was applied: %+v", back.Outcomes)
|
||||
}
|
||||
// The owners answer: wanted and running are applied as the consumer would, the record ends, and the steps with no read
|
||||
// verb are named.
|
||||
owners.fp = "src2:f1"
|
||||
got, err = rs.Repair(r.Instance, "the events were lost", "jochen@shanks", owners, log)
|
||||
if err != nil || got["ended"] != true {
|
||||
t.Fatalf("%v %v", got, err)
|
||||
}
|
||||
back, _ := rs.Read(r.Instance)
|
||||
if back.Outcomes[OutcomeKey("mesh-x", "src2:f1", "ace", StepRunning)].Outcome != OutcomeRunning ||
|
||||
back.Outcomes[OutcomeKey("mesh-x", "src2:f1", "ace", StepWanted)].Outcome != OutcomeWanted || back.Ended == nil {
|
||||
t.Fatalf("outcomes %+v ended %+v", back.Outcomes, back.Ended)
|
||||
}
|
||||
if unread := got["not-readable-yet"].([]string); len(unread) != 4 || !strings.HasPrefix(unread[0], "built:") {
|
||||
t.Fatalf("not readable yet: %v", unread)
|
||||
}
|
||||
if !slices.Contains(*said, meshdelivery.SubjectRecordEnded+" record-ended."+r.Instance) || len(log.took) != 2 {
|
||||
t.Fatalf("said %v, hand acts %d", *said, len(log.took))
|
||||
}
|
||||
// A repair repeated changes nothing.
|
||||
again, _ := rs.Repair(r.Instance, "again", "jochen@shanks", owners, log)
|
||||
if lines := again["lines"].([]repairLine); len(lines) != 0 {
|
||||
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) {
|
||||
was := ledgerCall
|
||||
t.Cleanup(func() { ledgerCall = was })
|
||||
@@ -877,3 +922,19 @@ func TestTheRecordsLedgerSaysFillingFullAndRoom(t *testing.T) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ownersFake answers the owners' reads: the fingerprint the assignment graph names, and the fingerprint and phase of the have.
|
||||
type ownersFake struct {
|
||||
wanted, fp, phase string
|
||||
asked []string
|
||||
}
|
||||
|
||||
func (o *ownersFake) Wanted(node, module string) (string, error) {
|
||||
o.asked = append(o.asked, "wanted "+node+" "+module)
|
||||
return o.wanted, nil
|
||||
}
|
||||
|
||||
func (o *ownersFake) Running(node, module string) (string, string, error) {
|
||||
o.asked = append(o.asked, "running "+node+" "+module)
|
||||
return o.fp, o.phase, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
package main
|
||||
|
||||
// gate_record_repair (design 47 *The delivery record*, its tools against the four lines): for one record, asks each owner for
|
||||
// the outcome of each entry that lacks one, through that owner's read verb, and applies the answers as the consumer would. The
|
||||
// reads that exist as seat verbs are the seat mesh-wants' `entry` (is the node's entry at the fingerprint: wanted) and the seat
|
||||
// node-engine's `composition` of that node (does the module run at the fingerprint: running). The indexer's take-in read, the
|
||||
// builder's build read and the checker's check read are no seat verb yet: an entry missing one of those steps is said as
|
||||
// unreadable, naming the owner, and is left to its event.
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshhandactsclient"
|
||||
)
|
||||
|
||||
// The verbs the repair reads through (the manifest invokes them).
|
||||
const (
|
||||
WantsEntryVerb = "seat:mesh-wants.entry"
|
||||
EngineCompositionVerb = "seat:node-engine.composition"
|
||||
)
|
||||
|
||||
// StepWanted is the step whose outcome OutcomeWanted says the assignment graph names the entry on its node.
|
||||
const StepWanted = "wanted"
|
||||
|
||||
// ownerReads are the reads of the owners the repair asks.
|
||||
type ownerReads interface {
|
||||
// Wanted is the fingerprint the assignment graph names for the module on the node; empty when it names none.
|
||||
Wanted(node, module string) (fingerprint string, err error)
|
||||
// Running is the fingerprint and phase of the module's have on the node, from the node-engine seat's verb composition.
|
||||
Running(node, module string) (fingerprint, phase string, err error)
|
||||
}
|
||||
|
||||
// seatOwners reads the owners through their seats.
|
||||
type seatOwners struct{ ask asking }
|
||||
|
||||
func (s seatOwners) call(verb string, in map[string]any) (json.RawMessage, error) {
|
||||
raw, err := s.ask(verb, in)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
answer, output, ok := answerOf(raw)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%s refused: %s", verb, lastLine(output))
|
||||
}
|
||||
return answer, nil
|
||||
}
|
||||
|
||||
func (s seatOwners) Wanted(node, module string) (string, error) {
|
||||
answer, err := s.call(WantsEntryVerb, map[string]any{"node": node, "module": module})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
var a struct {
|
||||
Entry struct {
|
||||
Fp string `json:"fp"`
|
||||
} `json:"entry"`
|
||||
}
|
||||
if err := json.Unmarshal(answer, &a); err != nil {
|
||||
return "", fmt.Errorf("the entry of %s on %s is not readable: %s", module, node, clip(string(answer), 200))
|
||||
}
|
||||
return a.Entry.Fp, nil
|
||||
}
|
||||
|
||||
func (s seatOwners) Running(node, module string) (string, string, error) {
|
||||
answer, err := s.call(EngineCompositionVerb+"@"+node, map[string]any{"module": module})
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
var a struct {
|
||||
Have *struct {
|
||||
Fp string `json:"fp"`
|
||||
Phase string `json:"phase"`
|
||||
} `json:"have"`
|
||||
}
|
||||
if err := json.Unmarshal(answer, &a); err != nil {
|
||||
return "", "", fmt.Errorf("the composition of %s on %s is not readable: %s", module, node, clip(string(answer), 200))
|
||||
}
|
||||
if a.Have == nil {
|
||||
return "", "", nil
|
||||
}
|
||||
return a.Have.Fp, a.Have.Phase, nil
|
||||
}
|
||||
|
||||
// unreadable are the steps whose owners offer no read verb yet, with the owner and the read it lacks.
|
||||
var unreadable = map[string]string{
|
||||
OutcomeTakenIn: "the indexer has no read of what it took in",
|
||||
OutcomeBuilt: "the builder has no read of a build by entry",
|
||||
OutcomeChecked: "the checker has no read of a check by entry",
|
||||
OutcomeComposed: "the assigner has no read of a composition by entry",
|
||||
}
|
||||
|
||||
// repairLine is what the repair did for one entry and step.
|
||||
type repairLine struct {
|
||||
Entry string `json:"entry"`
|
||||
Step string `json:"step"`
|
||||
Did string `json:"did"`
|
||||
Because string `json:"because,omitempty"`
|
||||
}
|
||||
|
||||
// Repair asks the owners for each outcome the record's node entries lack, applies what they answer as the consumer would
|
||||
// (an outcome with its why naming the read), and ends the record if every present node then runs its entries. Entries on
|
||||
// no node (the catalog half's) are read for the steps no node-less owner offers: said unreadable.
|
||||
func (rs *Records) Repair(instance, why, by string, owners ownerReads, acts handActs) (map[string]any, error) {
|
||||
if strings.TrimSpace(why) == "" {
|
||||
return nil, errors.New("say why the record is repaired by hand")
|
||||
}
|
||||
r, err := rs.Read(instance)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if r == nil {
|
||||
return nil, fmt.Errorf("no delivery record %s is kept", instance)
|
||||
}
|
||||
if owners == nil {
|
||||
return nil, errors.New("the owners cannot be asked from here, so nothing was repaired")
|
||||
}
|
||||
if acts == nil {
|
||||
return nil, errors.New("the hand-act log cannot be reached here, so nothing was repaired")
|
||||
}
|
||||
if _, err := acts.Record(meshhandactsclient.Request{What: "asked the owners for the outcomes the delivery record " + instance + " lacks",
|
||||
Why: why, Cause: "gate_record_repair", Verb: "record-repair"}); err != nil {
|
||||
return nil, fmt.Errorf("the hand act was not recorded through mesh-hand-acts, so nothing was repaired: %w", err)
|
||||
}
|
||||
keys := make([]string, 0, len(r.Entries))
|
||||
for k := range r.Entries {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
var lines []repairLine
|
||||
apply := func(e RecordEntry, step, outcome, because string) {
|
||||
o := RecordOutcome{Module: e.Module, Fingerprint: e.Fingerprint, Node: e.Node, Step: step, Outcome: outcome,
|
||||
Why: because, At: rs.Now(), Event: "repair." + OutcomeKey(e.Module, e.Fingerprint, e.Node, step)}
|
||||
applied, err := rs.Outcome(r, o)
|
||||
switch {
|
||||
case err != nil:
|
||||
lines = append(lines, repairLine{Entry: e.Key, Step: step, Did: "failed", Because: err.Error()})
|
||||
case applied:
|
||||
lines = append(lines, repairLine{Entry: e.Key, Step: step, Did: "applied", Because: because})
|
||||
}
|
||||
}
|
||||
for _, k := range keys {
|
||||
e := r.Entries[k]
|
||||
if e.Node == "" {
|
||||
continue
|
||||
}
|
||||
for _, step := range []string{StepWanted, StepRunning} {
|
||||
if _, has := r.Outcomes[OutcomeKey(e.Module, e.Fingerprint, e.Node, step)]; has {
|
||||
continue
|
||||
}
|
||||
switch step {
|
||||
case StepWanted:
|
||||
fp, err := owners.Wanted(e.Node, e.Module)
|
||||
switch {
|
||||
case err != nil:
|
||||
lines = append(lines, repairLine{Entry: k, Step: step, Did: "unread", Because: err.Error()})
|
||||
case fp != e.Fingerprint:
|
||||
lines = append(lines, repairLine{Entry: k, Step: step, Did: "left", Because: fmt.Sprintf("the assignment graph names %q on %s", fp, e.Node)})
|
||||
default:
|
||||
apply(e, step, OutcomeWanted, "read from the seat mesh-wants: the entry of "+e.Module+" on "+e.Node+" names this fingerprint")
|
||||
}
|
||||
case StepRunning:
|
||||
fp, phase, err := owners.Running(e.Node, e.Module)
|
||||
switch {
|
||||
case err != nil:
|
||||
lines = append(lines, repairLine{Entry: k, Step: step, Did: "unread", Because: err.Error()})
|
||||
case fp != e.Fingerprint || phase != "running":
|
||||
lines = append(lines, repairLine{Entry: k, Step: step, Did: "left", Because: fmt.Sprintf("its have on %s is %q at %q", e.Node, phase, fp)})
|
||||
default:
|
||||
apply(e, step, OutcomeRunning, "read from the seat node-engine's composition on "+e.Node+": its have runs this fingerprint")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
steps := make([]string, 0, len(unreadable))
|
||||
for s := range unreadable {
|
||||
steps = append(steps, s)
|
||||
}
|
||||
sort.Strings(steps)
|
||||
var unread []string
|
||||
for _, s := range steps {
|
||||
unread = append(unread, s+": "+unreadable[s])
|
||||
}
|
||||
ended := false
|
||||
if r.Ended == nil {
|
||||
if ended, err = rs.EndIfRun(r); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
rs.keep(r)
|
||||
return map[string]any{"instance": instance, "by": by, "lines": lines, "ended": ended, "not-readable-yet": unread, "record": viewOf(r)}, nil
|
||||
}
|
||||
@@ -179,6 +179,21 @@ func recordTools(h *Holder, acts handActs) []stdio.Tool {
|
||||
}
|
||||
return map[string]any{"families": h.Consumer.Heard()}, nil
|
||||
}},
|
||||
{Name: "gate_record_repair",
|
||||
Description: "Do by hand what the consumer does itself, for one delivery record: ask the owners for the outcome of each " +
|
||||
"node entry that lacks one, through their read verbs (the seat mesh-wants' entry for the wanted step, the seat " +
|
||||
"node-engine's composition of that node for the running step), and apply the answers as the consumer would; an " +
|
||||
"entry whose owner says otherwise is left, a read that failed is said, and the record ends when every present node " +
|
||||
"runs its entries. The steps whose owners offer no read verb yet are named. Recorded in the hand-act log first.",
|
||||
Input: map[string]any{"type": "object", "properties": map[string]any{
|
||||
"instance": str("the record's instance"), "why": str("why it is repaired by hand, in plain words")},
|
||||
"required": []string{"instance", "why"}},
|
||||
Run: func(a map[string]any) (any, error) {
|
||||
if h.Records == nil {
|
||||
return none()
|
||||
}
|
||||
return h.Records.Repair(strArg(a, "instance"), strArg(a, "why"), stdio.CallerOf(a), seatOwners{ask: stdio.Ask}, acts)
|
||||
}},
|
||||
{Name: "gate_record_end",
|
||||
Description: "End a delivery record left open by hand (a node gone for good, an entry that never runs), with the " +
|
||||
"reason; recorded in the hand-act log of the seat mesh-hand-acts first, and said as record-ended.",
|
||||
|
||||
@@ -298,6 +298,8 @@
|
||||
"seat:git.contains",
|
||||
"seat:mesh-hand-acts.hand-act",
|
||||
"seat:issue-tracker.list",
|
||||
"seat:mesh-wants.entry",
|
||||
"seat:node-engine.composition",
|
||||
"mesh-delivery-teller.teller_answers",
|
||||
"mesh-delivery-view.view_reach"
|
||||
],
|
||||
@@ -336,6 +338,7 @@
|
||||
"gate_consumer",
|
||||
"gate_record",
|
||||
"gate_record_end",
|
||||
"gate_record_repair",
|
||||
"gate_records"
|
||||
],
|
||||
"config": {
|
||||
|
||||
Reference in New Issue
Block a user