Record a drill in mesh-hand-acts and count S15 over both logs, so the healers no longer refuse a drill (issue 1953)
This commit is contained in:
@@ -25,7 +25,7 @@ again, `fortnight` ends when S15's oldest counted act leaves the fortnight and c
|
||||
each expiry; no timer runs in the process. A wait still running is on the bus across a restart; one that expired while
|
||||
no instance watched is judged at the start, once the lease is first read.
|
||||
|
||||
S15: it watches the controller's hand-act log and writes `healer-wanted` for a cause repaired by hand twice within a
|
||||
S15: it hears the seat mesh-hand-acts' `act-recorded` and watches the controller's hand-act log and writes `healer-wanted` for a cause repaired by hand twice within a
|
||||
fortnight; a drill and a person's decision by design never count.
|
||||
|
||||
## What it keeps
|
||||
@@ -41,5 +41,6 @@ fortnight; a drill and a person's decision by design never count.
|
||||
- `mesh-healers.healers`: the registry, the acts of the last days, the would-acts, the brake, whether it acts and why;
|
||||
`recount=true` counts the hand-act log again.
|
||||
- `mesh-healers.heal`: one act now on an open condition, by key and why.
|
||||
- `mesh-healers.drill`: refused until the hand-act log's home is delivered (issue 1953), naming `mesh-controller.drill`.
|
||||
- `mesh-healers.drill`: records a drill in the hand-act log of the seat `mesh-hand-acts` as `hand-act drill` (the
|
||||
controller's `drill` while that seat has no holder and the controller holds its lease). S15 never counts a drill.
|
||||
- `healers_findings`, `healers_would`, `healers_wakes`: the reads of its buckets.
|
||||
|
||||
@@ -27,6 +27,7 @@ import (
|
||||
|
||||
stdio "git.novox.be/novox/mesh-sdk/go"
|
||||
"git.novox.be/novox/mesh-sdk/go/buscompose"
|
||||
"git.novox.be/novox/mesh-sdk/go/handacts"
|
||||
"git.novox.be/novox/mesh-sdk/go/health"
|
||||
)
|
||||
|
||||
@@ -48,6 +49,8 @@ const (
|
||||
ActedEvent = health.EventHealerActed
|
||||
// ActedSubject is where the seat says it, which its consumers hear (catalogue.SeatSays).
|
||||
ActedSubject = "mesh.seat." + Seat + ".event." + ActedEvent
|
||||
// RecordedEvent is the hand-act log's seat event, each act recorded there (ADR 0565 §11).
|
||||
RecordedEvent = handacts.Seat + "." + handacts.EventRecorded
|
||||
// The events it hears: the keeper's, on the seat mesh-conditions.
|
||||
RaisedEvent = "mesh-conditions.condition-raised"
|
||||
ChangedEvent = "mesh-conditions.condition-changed"
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/handacts"
|
||||
"git.novox.be/novox/mesh-sdk/go/health"
|
||||
)
|
||||
|
||||
@@ -379,9 +380,14 @@ func TestACauseRepairedByHandTwiceIsAHealerWanted(t *testing.T) {
|
||||
if keys := w.store.keys(FindingsState); len(keys) != 0 {
|
||||
t.Fatalf("one act is a finding: %v", keys)
|
||||
}
|
||||
act("a2", "broker consumer-reset", "consumer-behind", 24*time.Hour)
|
||||
// The second of the cause is the seat's own act, heard as act-recorded: one in each log counts two (ADR 0565 §11).
|
||||
seatAct, _ := json.Marshal(HandAct{ID: "a2", At: w.now.Add(-24 * time.Hour), By: "person.jochen",
|
||||
Verb: "broker consumer-reset", Why: "x", Cause: "consumer-behind"})
|
||||
w.h.ActRecorded(seatAct)
|
||||
// A drill in each log counts none.
|
||||
act("a3", "hand-act drill", "drill", time.Hour)
|
||||
act("a4", "hand-act drill", "drill", 2*time.Hour)
|
||||
drill, _ := json.Marshal(HandAct{ID: "a4", At: w.now.Add(-2 * time.Hour), Verb: "hand-act drill", Why: "x", Cause: "drill"})
|
||||
w.h.ActRecorded(drill)
|
||||
act("a5", "conditions silence", "operator-answer", time.Hour)
|
||||
act("a6", "conditions silence", "operator-answer", time.Hour)
|
||||
act("a7", "hand-act record", "relogin", 20*24*time.Hour)
|
||||
@@ -402,7 +408,7 @@ func TestACauseRepairedByHandTwiceIsAHealerWanted(t *testing.T) {
|
||||
if keys := w.store.keys(FindingsState); len(keys) != 0 {
|
||||
t.Errorf("past the fortnight the finding stays: %v", keys)
|
||||
}
|
||||
w.h.HandActHeard("a2", "delete", nil)
|
||||
w.h.HandActHeard("a5", "delete", nil)
|
||||
if len(w.h.acts) != 7 {
|
||||
t.Errorf("a deleted entry is kept: %d", len(w.h.acts))
|
||||
}
|
||||
@@ -436,14 +442,53 @@ func TestTheVerbsAndReads(t *testing.T) {
|
||||
if a["acting"] != true || len(a["acts"].([]Attempt)) != 1 || len(a["healers"].([]map[string]any)) != 3 {
|
||||
t.Errorf("healers answered %v", a)
|
||||
}
|
||||
var drill error
|
||||
for _, tool := range Tools(w.h) {
|
||||
if tool.Name == DrillTool {
|
||||
_, drill = tool.Run(map[string]any{})
|
||||
}
|
||||
// drill records into mesh-hand-acts as a drill; while that seat has no holder, into the controller's drill until
|
||||
// the lease is handed away (ADR 0565 §11).
|
||||
drills := []struct {
|
||||
name string
|
||||
seatGone bool
|
||||
leaseAway bool
|
||||
args map[string]any
|
||||
log, err string
|
||||
}{
|
||||
{"into the seat", false, true, map[string]any{"what": "stopped records", "why": "does it raise"}, handacts.LogSeat, ""},
|
||||
{"no holder: the controller's", true, false, map[string]any{"what": "stopped records", "why": "x"}, handacts.LogController, ""},
|
||||
{"no holder, lease away", true, true, map[string]any{"what": "stopped records", "why": "x"}, "", "handed its lease away"},
|
||||
{"says nothing", false, true, map[string]any{}, "", "says what"},
|
||||
}
|
||||
if drill == nil || !strings.Contains(drill.Error(), "mesh-controller.drill") {
|
||||
t.Errorf("drill answered %v", drill)
|
||||
for _, d := range drills {
|
||||
x := newWorld()
|
||||
if d.leaseAway {
|
||||
x.handAway()
|
||||
} else {
|
||||
x.h.LeaseHeard([]byte(`{"instance":"novox/1","epoch":5}`), true, nil)
|
||||
}
|
||||
var asked []string
|
||||
x.h.Ask = func(key string, body any) (json.RawMessage, error) {
|
||||
raw, _ := json.Marshal(body)
|
||||
asked = append(asked, key+" "+string(raw))
|
||||
if d.seatGone && strings.HasPrefix(key, "seat:mesh-hand-acts.") {
|
||||
return nil, errors.New("nats: no responders available for request")
|
||||
}
|
||||
return json.RawMessage(`{}`), nil
|
||||
}
|
||||
var got any
|
||||
var err error
|
||||
for _, tool := range Tools(x.h) {
|
||||
if tool.Name == DrillTool {
|
||||
got, err = tool.Run(d.args)
|
||||
}
|
||||
}
|
||||
switch {
|
||||
case d.err != "":
|
||||
if err == nil || !strings.Contains(err.Error(), d.err) {
|
||||
t.Errorf("drill %s: %v", d.name, err)
|
||||
}
|
||||
case err != nil || got.(map[string]any)["log"] != d.log:
|
||||
t.Errorf("drill %s: %v, %v", d.name, got, err)
|
||||
case d.log == handacts.LogSeat && !strings.Contains(asked[0], `"verb":"hand-act drill"`):
|
||||
t.Errorf("drill %s asked %v", d.name, asked)
|
||||
}
|
||||
}
|
||||
if _, err := w.h.read(WouldState, ""); err != nil {
|
||||
t.Error(err)
|
||||
|
||||
@@ -84,6 +84,20 @@ func main() {
|
||||
h.HandActHeard(c.Key, c.Op, c.Value)
|
||||
return nil
|
||||
})
|
||||
// S15 over both logs: the seat's act-recorded on a durable consumer, beside the controller's bucket's watch.
|
||||
go func() {
|
||||
for wait := 2 * time.Second; ; wait = min(wait*2, time.Minute) {
|
||||
time.Sleep(wait)
|
||||
err := stdio.Subscribe(RecordedEvent, func(e stdio.Envelope) error {
|
||||
h.ActRecorded(e.Body)
|
||||
return nil
|
||||
})
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
logf("[%s] does not hear %s yet (%v); asking again in %s", Module, RecordedEvent, err, wait)
|
||||
}
|
||||
}()
|
||||
for _, event := range []string{RaisedEvent, ChangedEvent} {
|
||||
go func(event string) {
|
||||
for wait := 2 * time.Second; ; wait = min(wait*2, time.Minute) {
|
||||
|
||||
@@ -48,8 +48,8 @@ func TestTheManifestSaysWhatTheCodeDoes(t *testing.T) {
|
||||
got, want []string
|
||||
}{
|
||||
{"emits (healer-acted is the seat's, said on its subject)", m.Emits, nil},
|
||||
{"consumes", m.Consumes, []string{RaisedEvent, ChangedEvent}},
|
||||
{"invokes", m.Invokes, []string{AssertTool}},
|
||||
{"consumes", m.Consumes, []string{RaisedEvent, ChangedEvent, RecordedEvent}},
|
||||
{"invokes", m.Invokes, []string{AssertTool, "seat:mesh-hand-acts.hand-act", "seat:mesh-controller.drill"}},
|
||||
{"reads", m.Reads, []string{LeaseRead, ConditionsRead, HandActsRead}},
|
||||
} {
|
||||
if !slices.Equal(c.got, c.want) {
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"time"
|
||||
|
||||
stdio "git.novox.be/novox/mesh-sdk/go"
|
||||
"git.novox.be/novox/mesh-sdk/go/handacts"
|
||||
)
|
||||
|
||||
// Verbs are the seat's verbs, as the manifest claims them.
|
||||
@@ -55,12 +56,14 @@ func Tools(h *Healers) []stdio.Tool {
|
||||
return h.Heal(strArg(a, "key"), strArg(a, "why"), stdio.CallerOf(a))
|
||||
}},
|
||||
{Name: DrillTool,
|
||||
Description: "A drill: something broken on purpose to see the mesh raise and clear it, recorded in the hand-act " +
|
||||
"log. Refused until the log's home is delivered (issue 1953): the controller's drill records it meanwhile.",
|
||||
Input: obj(map[string]any{"why": map[string]any{"type": "string"}}),
|
||||
Run: func(map[string]any) (any, error) {
|
||||
return nil, errors.New("a drill is recorded in the hand-act log, whose one writer is the controller until its " +
|
||||
"home is delivered (issue 1953): use mesh-controller.drill, which records it and S15 never counts")
|
||||
Description: "Record a drill: something broken on purpose to see the mesh raise and clear it, with what it " +
|
||||
"tests, in the hand-act log of the seat mesh-hand-acts (the controller's drill while that seat has no " +
|
||||
"holder and the controller holds its lease). A drill is never a repair: S15 never counts it.",
|
||||
Input: obj(map[string]any{"what": map[string]any{"type": "string", "description": "what was done on purpose"},
|
||||
"why": map[string]any{"type": "string", "description": "what the drill tests"},
|
||||
"condition": map[string]any{"type": "string", "description": "the condition it is meant to raise, if any"}}),
|
||||
Run: func(a map[string]any) (any, error) {
|
||||
return h.Drill(strArg(a, "what"), strArg(a, "why"), strArg(a, "condition"))
|
||||
}},
|
||||
{Name: FindingsTool,
|
||||
Description: "What the healers find of themselves (their bucket findings): the brake, their blindness, and " +
|
||||
@@ -81,6 +84,40 @@ func Tools(h *Healers) []stdio.Tool {
|
||||
}
|
||||
}
|
||||
|
||||
// Drill records a drill (ADR 0565 §11, which ends ADR 0564 Limits 1's refusal): through the seat mesh-hand-acts as
|
||||
// the act `hand-act drill`, and while that seat has no holder that answers and the controller still holds its lease,
|
||||
// through the controller's own `drill`, which records it as a drill too. It answers which log took it.
|
||||
func (h *Healers) Drill(what, why, condition string) (any, error) {
|
||||
if what == "" || why == "" {
|
||||
return nil, errors.New("a drill says what was done on purpose and what it tests: nothing was recorded")
|
||||
}
|
||||
if h.Ask == nil {
|
||||
return nil, errors.New("the hand-act log cannot be reached from here: nothing was recorded")
|
||||
}
|
||||
body := map[string]any{"what": what, "why": why, "verb": handacts.VerbDrill}
|
||||
controller := map[string]any{"what": what, "why": why}
|
||||
if condition != "" {
|
||||
body["condition"], controller["condition"] = condition, condition
|
||||
}
|
||||
answer, err := h.Ask("seat:"+handacts.Seat+".hand-act", body)
|
||||
if err == nil {
|
||||
return map[string]any{"log": handacts.LogSeat, "answer": answer}, nil
|
||||
}
|
||||
if !handacts.NoResponder(err) {
|
||||
return nil, fmt.Errorf("%s.hand-act refused the drill: %w", handacts.Seat, err)
|
||||
}
|
||||
if acting, _ := h.Acting(); acting {
|
||||
return nil, fmt.Errorf("%s has no holder that answers, and the controller handed its lease away: nothing was "+
|
||||
"recorded (%w)", handacts.Seat, err)
|
||||
}
|
||||
answer, cerr := h.Ask("seat:mesh-controller.drill", controller)
|
||||
if cerr != nil {
|
||||
return nil, fmt.Errorf("%s has no holder that answers (%v), and the controller's drill refused too: %w", handacts.Seat,
|
||||
err, cerr)
|
||||
}
|
||||
return map[string]any{"log": handacts.LogController, "answer": answer}, nil
|
||||
}
|
||||
|
||||
// Heal is the verb heal: one condition judged now by its healer.
|
||||
func (h *Healers) Heal(key, why, caller string) (any, error) {
|
||||
if key == "" || why == "" {
|
||||
|
||||
@@ -15,6 +15,8 @@ import (
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/handacts"
|
||||
)
|
||||
|
||||
// Within is how far back a repeated cause counts.
|
||||
@@ -66,6 +68,16 @@ func personsDecision(a HandAct) bool {
|
||||
return len(causes) == 0 || slices.Contains(causes, a.Cause)
|
||||
}
|
||||
|
||||
// ActRecorded is one act the seat mesh-hand-acts recorded, heard as act-recorded: counted beside the controller's
|
||||
// log's, under a key of its own log, so an act is counted once whichever log holds it (ADR 0565 §11).
|
||||
func (h *Healers) ActRecorded(body []byte) {
|
||||
var a HandAct
|
||||
if json.Unmarshal(body, &a) != nil || a.ID == "" {
|
||||
return
|
||||
}
|
||||
h.HandActHeard(handacts.LogSeat+":"+a.ID, "put", body)
|
||||
}
|
||||
|
||||
// HandActHeard is one change of the hand-act log: kept or forgotten, and every cause counted again.
|
||||
func (h *Healers) HandActHeard(key, op string, value []byte) {
|
||||
h.mu.Lock()
|
||||
|
||||
@@ -15,10 +15,13 @@
|
||||
],
|
||||
"consumes": [
|
||||
"mesh-conditions.condition-raised",
|
||||
"mesh-conditions.condition-changed"
|
||||
"mesh-conditions.condition-changed",
|
||||
"mesh-hand-acts.act-recorded"
|
||||
],
|
||||
"invokes": [
|
||||
"mesh-grants.grants_assert"
|
||||
"mesh-grants.grants_assert",
|
||||
"seat:mesh-hand-acts.hand-act",
|
||||
"seat:mesh-controller.drill"
|
||||
],
|
||||
"reads": [
|
||||
"mesh-controller.lease",
|
||||
|
||||
Reference in New Issue
Block a user