E-5 marked waits: a wait on a marked event runs only on the operator's warrant for that one wait
mesh_wait goes through the runner's approval gate (node-tools/internal/approval, Gate.Waited): a wait whose
subjects overlap a marked subject of any module the catalog half lists (the newest entry of every followed
branch, with the served entry of each module this runner serves) is an act of the runner's own module and
build (MESH_FINGERPRINT at its start), asked once on operator-channel as "Wait on <names> on <node>?", and
held, subscribing nothing but its ask's decided, until its timeout, its caller's going or the choice. On a
warrant it subscribes, its timeout running from then, and answers {answer, approval}. Before the marks are
read nothing is asked; a wait from a seat is never marked. The state fixture moves to meshtest so the
console's table can hold a gate.
Builds: design 34 every-event §34, §35, §36, §37; design 47 G lead answer A-168
Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
This commit is contained in:
@@ -3,7 +3,6 @@ package approval
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
@@ -15,83 +14,14 @@ import (
|
||||
stdio "git.novox.be/novox/mesh-sdk/go"
|
||||
"git.novox.be/novox/mesh-sdk/go/keys"
|
||||
"git.novox.be/novox/mesh-sdk/go/seatdef"
|
||||
|
||||
mt "github.com/novox/mesh-tools/node-tools/internal/meshtest"
|
||||
)
|
||||
|
||||
// mem is the runner's state approvals as a fixture; fail makes every read and write fail, as a bus refusing them.
|
||||
type mem struct {
|
||||
mu sync.Mutex
|
||||
vals map[string]json.RawMessage
|
||||
revs map[string]uint64
|
||||
n uint64
|
||||
fail error
|
||||
}
|
||||
// mem is the runner's state approvals as a fixture (meshtest.State).
|
||||
type mem = mt.State
|
||||
|
||||
func newMem() *mem { return &mem{vals: map[string]json.RawMessage{}, revs: map[string]uint64{}} }
|
||||
|
||||
func (m *mem) Get(k string) (*stdio.StateEntry, error) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if m.fail != nil {
|
||||
return nil, m.fail
|
||||
}
|
||||
v, ok := m.vals[k]
|
||||
if !ok {
|
||||
return nil, nil
|
||||
}
|
||||
return &stdio.StateEntry{Key: k, Value: v, Revision: m.revs[k]}, nil
|
||||
}
|
||||
func (m *mem) Create(k string, v any) (uint64, error) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if m.fail != nil {
|
||||
return 0, m.fail
|
||||
}
|
||||
if _, ok := m.vals[k]; ok {
|
||||
return 0, stdio.ErrChanged
|
||||
}
|
||||
return m.put(k, v)
|
||||
}
|
||||
func (m *mem) Update(k string, v any, rev uint64) (uint64, error) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if m.fail != nil {
|
||||
return 0, m.fail
|
||||
}
|
||||
if rev == 0 || m.revs[k] != rev {
|
||||
return 0, stdio.ErrChanged
|
||||
}
|
||||
return m.put(k, v)
|
||||
}
|
||||
func (m *mem) Put(k string, v any) (uint64, error) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if m.fail != nil {
|
||||
return 0, m.fail
|
||||
}
|
||||
return m.put(k, v)
|
||||
}
|
||||
func (m *mem) Keys() ([]string, error) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if m.fail != nil {
|
||||
return nil, m.fail
|
||||
}
|
||||
var out []string
|
||||
for k := range m.vals {
|
||||
out = append(out, k)
|
||||
}
|
||||
sort.Strings(out)
|
||||
return out, nil
|
||||
}
|
||||
func (m *mem) put(k string, v any) (uint64, error) {
|
||||
raw, ok := v.(json.RawMessage)
|
||||
if !ok {
|
||||
raw, _ = json.Marshal(v)
|
||||
}
|
||||
m.n++
|
||||
m.vals[k], m.revs[k] = raw, m.n
|
||||
return m.n, nil
|
||||
}
|
||||
func newMem() *mem { return mt.NewState() }
|
||||
|
||||
// world is a gate on a fixture mesh: the catalog half, the router's records as the router writes them (mesh-catalog's
|
||||
// messenger, asks.go), the asks published, the clock.
|
||||
@@ -262,11 +192,11 @@ func theActAndItsAsk(t *testing.T) {
|
||||
{strings.Repeat("x", 50), "n1", "Run " + strings.Repeat("x", 50) + "?"},
|
||||
{strings.Repeat("y", 80), "n1", "Run " + strings.Repeat("y", 54) + "…?"},
|
||||
} {
|
||||
if got := headlineOf(tc.name, tc.node, true); got != tc.want || len([]rune(got)) > 60 {
|
||||
if got := headlineOf("Run", tc.name, tc.node, true); got != tc.want || len([]rune(got)) > 60 {
|
||||
t.Errorf("headline %q, want %q", got, tc.want)
|
||||
}
|
||||
}
|
||||
if got := headlineOf("wipe", "n1", false); got != "Run wipe on n1? Part shown" {
|
||||
if got := headlineOf("Run", "wipe", "n1", false); got != "Run wipe on n1? Part shown" {
|
||||
t.Errorf("headline %q", got)
|
||||
}
|
||||
}
|
||||
@@ -314,9 +244,9 @@ func TestTheRefusalsAndAnswers(t *testing.T) {
|
||||
t.Errorf("explanation %q", a.Explanation)
|
||||
}
|
||||
var k operatorchannelclient.Kept
|
||||
_ = json.Unmarshal(w.state.vals["n1.ask."+a.ID], &k)
|
||||
if k.Act["arg.dir"] != "scratch" || string(k.Arguments) != `{"dir":"scratch"}` || string(w.state.vals["n1.act."+d]) != `"`+a.ID+`"` {
|
||||
t.Errorf("kept %+v, act key %s", k, w.state.vals["n1.act."+d])
|
||||
_ = json.Unmarshal(w.state.Vals["n1.ask."+a.ID], &k)
|
||||
if k.Act["arg.dir"] != "scratch" || string(k.Arguments) != `{"dir":"scratch"}` || string(w.state.Vals["n1.act."+d]) != `"`+a.ID+`"` {
|
||||
t.Errorf("kept %+v, act key %s", k, w.state.Vals["n1.act."+d])
|
||||
}
|
||||
}},
|
||||
{"§12: with no record within 5 seconds, Not run, asked; not yet taken by the router", func(t *testing.T, w *world) {
|
||||
@@ -385,10 +315,10 @@ func TestTheRefusalsAndAnswers(t *testing.T) {
|
||||
rec.Warrant.AskDigest = operatorchannelclient.Digest(a)
|
||||
w.records["shell."+a.ID] = rec
|
||||
var k operatorchannelclient.Kept
|
||||
_ = json.Unmarshal(w.state.vals["n1.ask."+a.ID], &k)
|
||||
_ = json.Unmarshal(w.state.Vals["n1.ask."+a.ID], &k)
|
||||
k.Ask = a
|
||||
raw, _ := json.Marshal(k)
|
||||
w.state.vals["n1.ask."+a.ID] = raw
|
||||
w.state.Vals["n1.ask."+a.ID] = raw
|
||||
if _, err := w.do(call("wipe", `{"dir":"scratch"}`)); err == nil || w.runs != 0 || !strings.Contains(err.Error(), "nothing is done") {
|
||||
t.Fatalf("%v, runs %d", err, w.runs)
|
||||
}
|
||||
@@ -495,7 +425,7 @@ func TestTheRefusalsAndAnswers(t *testing.T) {
|
||||
t.Fatalf("recorded %v, findings %v, bounds %v", w.recorded, w.findings.raised, w.bounds.keys)
|
||||
}
|
||||
var k operatorchannelclient.Kept
|
||||
_ = json.Unmarshal(w.state.vals["n1.ask."+a.ID], &k)
|
||||
_ = json.Unmarshal(w.state.Vals["n1.ask."+a.ID], &k)
|
||||
if !k.Recorded || !k.Acted {
|
||||
t.Fatalf("kept %+v", k)
|
||||
}
|
||||
@@ -517,7 +447,7 @@ func TestTheRefusalsAndAnswers(t *testing.T) {
|
||||
t.Fatalf("%v", err)
|
||||
}
|
||||
w.g.d.Publish = pub
|
||||
w.state.vals = map[string]json.RawMessage{}
|
||||
w.state.Vals = map[string]json.RawMessage{}
|
||||
_, _ = w.do(call("wipe", `{}`))
|
||||
w.g.d.Record = func(string, string) (*stdio.StateEntry, error) { return nil, errors.New("timeout") }
|
||||
if _, err := w.do(call("wipe", `{}`)); err == nil || !strings.Contains(err.Error(), "the router's record of its ask") || w.runs != 0 {
|
||||
@@ -546,7 +476,7 @@ func TestTheRefusalsAndAnswers(t *testing.T) {
|
||||
{"§19: the state failing refuses, raises approvals-unreadable after five minutes, and clears when read again", func(t *testing.T, w *world) {
|
||||
stop, _ := w.g.WatchBounds()
|
||||
defer stop()
|
||||
w.state.fail = errors.New("the bus is unreachable")
|
||||
w.state.Fail = errors.New("the bus is unreachable")
|
||||
if _, err := w.do(call("wipe", `{}`)); err == nil || !strings.Contains(err.Error(), "cannot be read") {
|
||||
t.Fatalf("%v", err)
|
||||
}
|
||||
@@ -557,7 +487,7 @@ func TestTheRefusalsAndAnswers(t *testing.T) {
|
||||
if w.findings.raised[keys.EscapeName("n1.approvals")] != KindApprovalsUnreadable {
|
||||
t.Fatalf("findings %v", w.findings.raised)
|
||||
}
|
||||
w.state.fail = nil
|
||||
w.state.Fail = nil
|
||||
w.g.StateAgain() // a membership change heard, no further call
|
||||
if len(w.findings.raised) != 0 || len(w.bounds.keys) != 0 {
|
||||
t.Fatalf("findings %v, bounds %v", w.findings.raised, w.bounds.keys)
|
||||
|
||||
@@ -70,7 +70,7 @@ func (g *Gate) askFor(c Call, digest, id string) seat.Ask {
|
||||
if c.Seat != "" {
|
||||
name = c.Seat + "." + c.Name
|
||||
}
|
||||
headline := headlineOf(name, g.d.Node, whole)
|
||||
headline := headlineOf("Run", name, g.d.Node, whole)
|
||||
first, _, _ := strings.Cut(strings.TrimSpace(c.Description), "\n")
|
||||
if first == "" {
|
||||
first = name
|
||||
@@ -92,22 +92,23 @@ func (g *Gate) askFor(c Call, digest, id string) seat.Ask {
|
||||
Level: operatorchannelclient.Approve, Binds: digest}}}
|
||||
}
|
||||
|
||||
// headlineOf is "Run <tool> on <node>?", "Run <tool>?" when that is longer than the router allows, and the names cut with
|
||||
// "…" when that too is longer; ending "Part shown" whenever an argument is not shown whole (§10, Q-HL).
|
||||
func headlineOf(name, node string, whole bool) string {
|
||||
// headlineOf is "<verb> <names> on <node>?", "<verb> <names>?" when that is longer than the router allows, and the names cut
|
||||
// with "…" when that too is longer; ending "Part shown" whenever an argument is not shown whole (§10, Q-HL). verb is "Run"
|
||||
// for a call and "Wait on" for a wait (design 34 every-event §35).
|
||||
func headlineOf(verb, name, node string, whole bool) string {
|
||||
suffix := ""
|
||||
if !whole {
|
||||
suffix = " Part shown"
|
||||
}
|
||||
max := operatorchannelclient.HeadlineLength
|
||||
fits := func(s string) bool { return len([]rune(s)) <= max }
|
||||
if h := "Run " + name + " on " + node + "?" + suffix; fits(h) {
|
||||
if h := verb + " " + name + " on " + node + "?" + suffix; fits(h) {
|
||||
return h
|
||||
}
|
||||
if h := "Run " + name + "?" + suffix; fits(h) {
|
||||
if h := verb + " " + name + "?" + suffix; fits(h) {
|
||||
return h
|
||||
}
|
||||
room := max - len([]rune("Run …?"+suffix))
|
||||
room := max - len([]rune(verb+" …?"+suffix))
|
||||
r := []rune(name)
|
||||
if room < 1 {
|
||||
room = 1
|
||||
@@ -115,7 +116,7 @@ func headlineOf(name, node string, whole bool) string {
|
||||
if len(r) > room {
|
||||
r = r[:room]
|
||||
}
|
||||
return "Run " + string(r) + "…?" + suffix
|
||||
return verb + " " + string(r) + "…?" + suffix
|
||||
}
|
||||
|
||||
// firstCall is a marked call whose act has no ask open: refused at once when its arguments may not be asked (§9), else
|
||||
@@ -140,7 +141,15 @@ func (g *Gate) firstCall(c Call) (any, error) {
|
||||
// the first answer (§12), always an error, never a result.
|
||||
func (g *Gate) raise(c Call, act operatorchannelclient.Act, digest, markedSince string) error {
|
||||
id := newID()
|
||||
a := g.askFor(c, digest, id)
|
||||
if err := g.keepAsk(c, act, digest, g.askFor(c, digest, id)); err != nil {
|
||||
return err
|
||||
}
|
||||
return g.firstAnswer(c, id, digest, markedSince)
|
||||
}
|
||||
|
||||
// keepAsk raises an ask and keeps it in the runner's state with the act and the whole arguments, its act's key naming it
|
||||
// (§10, §13); refused, as an error, when it cannot be.
|
||||
func (g *Gate) keepAsk(c Call, act operatorchannelclient.Act, digest string, a seat.Ask) error {
|
||||
cl := g.client(c.Module)
|
||||
if size := keptSize(a, act, c.Args); size > MostKeptValue {
|
||||
return g.refused(c, digest, fmt.Sprintf("its ask would keep %d bytes, more than the %d a value of the state %s holds", size, MostKeptValue, StateName))
|
||||
@@ -148,15 +157,15 @@ func (g *Gate) raise(c Call, act operatorchannelclient.Act, digest, markedSince
|
||||
if err := cl.Ask(a); err != nil {
|
||||
return g.refused(c, digest, "the ask could not be raised: "+err.Error())
|
||||
}
|
||||
if err := g.keepCall(id, act, c.Args); err != nil {
|
||||
if err := g.keepCall(a.ID, act, c.Args); err != nil {
|
||||
g.stateFailed(err)
|
||||
return g.refused(c, digest, "the ask was raised and its call could not be kept: "+err.Error())
|
||||
}
|
||||
if _, err := g.d.State.Put(g.actKey(digest), id); err != nil {
|
||||
if _, err := g.d.State.Put(g.actKey(digest), a.ID); err != nil {
|
||||
g.stateFailed(err)
|
||||
return g.refused(c, digest, "the ask was raised and its act could not be kept: "+err.Error())
|
||||
}
|
||||
return g.firstAnswer(c, id, digest, markedSince)
|
||||
return nil
|
||||
}
|
||||
|
||||
// keptSize is the bytes the kept ask would take.
|
||||
|
||||
@@ -2,6 +2,7 @@ package approval
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
@@ -55,6 +56,12 @@ type Deps struct {
|
||||
Bounds Bounds
|
||||
// Holder says whether a module holds a seat, from the assigner's view of the seats' holders the runner follows (§19).
|
||||
Holder func(seat string) (bool, error)
|
||||
// Build is the tool runner's own fingerprint, read from its MESH_FINGERPRINT at its start: the build a marked wait's
|
||||
// act binds; empty while unknown, when every marked wait is refused (design 34 every-event §35).
|
||||
Build string
|
||||
// Hear waits within the bound for the first event on the subjects whose body match takes, or the caller's going: a
|
||||
// marked wait's ask's `decided` (every-event §36); false when none came.
|
||||
Hear func(ctx context.Context, subjects []string, within time.Duration, match func(json.RawMessage) bool) (bool, error)
|
||||
// Go runs what goes on beside the call (a run's recording); nil is a goroutine.
|
||||
Go func(func())
|
||||
}
|
||||
|
||||
@@ -29,6 +29,9 @@ type Catalog interface {
|
||||
Newest(module string) (catalogseat.CatalogEntry, bool)
|
||||
// Definitions are the seat definitions the catalog half carries, by seat.
|
||||
Definitions() map[string]seatdef.Definition
|
||||
// Entries are the newest entry of every module in every followed branch's half, a marked wait's marks (lead answer
|
||||
// A-168).
|
||||
Entries() []catalogseat.CatalogEntry
|
||||
}
|
||||
|
||||
// Marks is what one module's calls are gated by: its marked tools and seat verbs, and the subjects of its marked events and
|
||||
@@ -89,61 +92,67 @@ func Derive(c Catalog, module, fingerprint string) Marks {
|
||||
entries = append(entries, e)
|
||||
}
|
||||
for _, e := range entries {
|
||||
source := "entry " + e.Module + " " + orNone(e.Fp)
|
||||
var said marked
|
||||
if raw, err := json.Marshal(e.Manifest); err == nil {
|
||||
_ = json.Unmarshal(raw, &said)
|
||||
}
|
||||
serves := map[string]bool{}
|
||||
for _, sv := range e.Serves {
|
||||
serves[sv] = true
|
||||
seat, verb, _ := strings.Cut(sv, ".")
|
||||
for _, v := range defs[seat].Verbs {
|
||||
if v.Name == verb && v.NeedsApproval {
|
||||
m.Verbs[sv] = "seat " + seat
|
||||
}
|
||||
m.add(e, defs)
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// add takes one entry's marks into m: its `needs-approval` (a tool, a verb it serves or an event it declares), the seat
|
||||
// definitions' marks of every verb it serves, and every stream it declares with a `grant`, each with its source.
|
||||
func (m *Marks) add(e catalogseat.CatalogEntry, defs map[string]seatdef.Definition) {
|
||||
source := "entry " + e.Module + " " + orNone(e.Fp)
|
||||
var said marked
|
||||
if raw, err := json.Marshal(e.Manifest); err == nil {
|
||||
_ = json.Unmarshal(raw, &said)
|
||||
}
|
||||
serves := map[string]bool{}
|
||||
for _, sv := range e.Serves {
|
||||
serves[sv] = true
|
||||
seat, verb, _ := strings.Cut(sv, ".")
|
||||
for _, v := range defs[seat].Verbs {
|
||||
if v.Name == verb && v.NeedsApproval {
|
||||
m.Verbs[sv] = "seat " + seat
|
||||
}
|
||||
}
|
||||
events := map[string]bool{}
|
||||
for _, ev := range said.Emits {
|
||||
}
|
||||
events := map[string]bool{}
|
||||
for _, ev := range said.Emits {
|
||||
events[ev] = true
|
||||
}
|
||||
for _, s := range said.Streams {
|
||||
for _, ev := range s.Events {
|
||||
events[ev] = true
|
||||
}
|
||||
for _, s := range said.Streams {
|
||||
for _, ev := range s.Events {
|
||||
events[ev] = true
|
||||
}
|
||||
for _, name := range said.NeedsApproval {
|
||||
switch {
|
||||
case serves[name]:
|
||||
if _, bySeat := m.Verbs[name]; !bySeat {
|
||||
m.Verbs[name] = source
|
||||
}
|
||||
case events[name]:
|
||||
for _, s := range eventSubjects(e.Module, name) {
|
||||
m.Subjects[s] = source
|
||||
}
|
||||
default:
|
||||
m.Tools[name] = source
|
||||
}
|
||||
}
|
||||
for _, s := range said.Streams {
|
||||
if len(s.Grant) == 0 || string(s.Grant) == "null" {
|
||||
continue
|
||||
}
|
||||
for _, name := range said.NeedsApproval {
|
||||
switch {
|
||||
case serves[name]:
|
||||
if _, bySeat := m.Verbs[name]; !bySeat {
|
||||
m.Verbs[name] = source
|
||||
}
|
||||
case events[name]:
|
||||
for _, s := range eventSubjects(e.Module, name) {
|
||||
m.Subjects[s] = source
|
||||
}
|
||||
default:
|
||||
m.Tools[name] = source
|
||||
}
|
||||
wire := make([]string, 0, len(s.Events))
|
||||
for _, ev := range s.Events {
|
||||
wire = append(wire, buscompose.WirePattern(ev))
|
||||
}
|
||||
for _, s := range said.Streams {
|
||||
if len(s.Grant) == 0 || string(s.Grant) == "null" {
|
||||
continue
|
||||
}
|
||||
wire := make([]string, 0, len(s.Events))
|
||||
for _, ev := range s.Events {
|
||||
wire = append(wire, buscompose.WirePattern(ev))
|
||||
}
|
||||
for _, sub := range (buscompose.ModuleStream{Module: e.Module, Events: wire}).Subjects() {
|
||||
m.Subjects[sub] = source
|
||||
if !strings.HasSuffix(sub, ">") {
|
||||
m.Subjects[sub+".>"] = source
|
||||
}
|
||||
for _, sub := range (buscompose.ModuleStream{Module: e.Module, Events: wire}).Subjects() {
|
||||
m.Subjects[sub] = source
|
||||
if !strings.HasSuffix(sub, ">") {
|
||||
m.Subjects[sub+".>"] = source
|
||||
}
|
||||
}
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// eventSubjects are a marked event's subjects on the bus (design 34 every-event §33): said on the module's events and on its
|
||||
|
||||
@@ -32,6 +32,13 @@ func (c *fakeCatalog) Newest(m string) (catalogseat.CatalogEntry, bool) {
|
||||
return e, ok
|
||||
}
|
||||
func (c *fakeCatalog) Definitions() map[string]seatdef.Definition { return c.defs }
|
||||
func (c *fakeCatalog) Entries() []catalogseat.CatalogEntry {
|
||||
var out []catalogseat.CatalogEntry
|
||||
for _, e := range c.newest {
|
||||
out = append(out, e)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func entry(fp string, serves []string, manifest map[string]any) catalogseat.CatalogEntry {
|
||||
return catalogseat.CatalogEntry{Module: "shell", Fp: fp, Serves: serves, Manifest: manifest}
|
||||
|
||||
@@ -0,0 +1,296 @@
|
||||
package approval
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient"
|
||||
seat "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel"
|
||||
"git.novox.be/novox/mesh-sdk/go/buscompose"
|
||||
"git.novox.be/novox/mesh-sdk/go/outward"
|
||||
)
|
||||
|
||||
// Wait is one mesh_wait call as the gate judges it (design 34 every-event §34 to §36): the event names given, the subjects
|
||||
// they became (after `from`), the filter and the timeout.
|
||||
type Wait struct {
|
||||
Names []string
|
||||
From string
|
||||
Subjects []string
|
||||
Filter map[string]any
|
||||
Timeout time.Duration
|
||||
}
|
||||
|
||||
// Waited is a wait through the gate (every-event §34 to §36). An unmarked wait is heard at once by hear, given how long it
|
||||
// may wait. A marked one is heard only on the operator's warrant for that one wait, its timeout running from then; with
|
||||
// none it subscribes to nothing, its ask raised once under the runner's own module's name, and is answered as an error at
|
||||
// its timeout, when its caller goes, or as soon as the router's record says the ask was chosen, never in a hot loop. c is
|
||||
// the call as the console makes it: its address, its tool's name, its caller and its arguments as given.
|
||||
func (g *Gate) Waited(ctx context.Context, c Call, w Wait, hear func(within time.Duration) (any, error)) (any, error) {
|
||||
deadline := g.d.Now().Add(w.Timeout)
|
||||
// A wait from a seat is never marked, and a seat's event never is (§32, §34).
|
||||
if w.From == "seat" || !outsideSeats(w.Subjects) {
|
||||
return hear(w.Timeout)
|
||||
}
|
||||
if g.d.Catalog == nil {
|
||||
return nil, g.holdRefused(ctx, deadline, "this wait is refused: the tool runner reads no catalog half, so the marks are not read")
|
||||
}
|
||||
if err := g.d.Catalog.Ready(); err != nil {
|
||||
// Before the marks are read nothing is asked: the wait waits for the first view within its own timeout and is
|
||||
// then judged, and is refused at its timeout when it does not come (§34).
|
||||
if g.d.Wait != nil {
|
||||
err = g.waitCatalog(ctx, w.Timeout)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, g.holdRefused(ctx, deadline, fmt.Sprintf("this wait is refused at its timeout: the marks are not yet read (%v); "+
|
||||
"mesh_first_views says what the tool runner waits for", err))
|
||||
}
|
||||
}
|
||||
if !overlapsAny(w.Subjects, g.waitMarks()) {
|
||||
return hear(deadline.Sub(g.d.Now()))
|
||||
}
|
||||
c.Module, c.Seat, c.Fingerprint = g.d.Runner, "", g.d.Build
|
||||
if c.Fingerprint == "" {
|
||||
return nil, g.holdRefused(ctx, deadline, "this wait needs the operator's approval, and the tool runner's own fingerprint is "+
|
||||
"unknown (its MESH_FINGERPRINT was not set at its start), so no warrant can bind its build")
|
||||
}
|
||||
if b, ok := g.d.State.(Binding); g.d.State == nil || ok && !b.Bound() {
|
||||
return nil, g.holdRefused(ctx, deadline, g.refused(c, "", "the tool runner's membership binds no state "+StateName+" to keep its asks in").Error())
|
||||
}
|
||||
if why := refusedUnasked(c.Args); why != "" {
|
||||
return nil, g.holdRefused(ctx, deadline, g.refused(c, "", why).Error())
|
||||
}
|
||||
if why := g.held(); why != "" {
|
||||
return nil, g.holdRefused(ctx, deadline, g.refused(c, "", why).Error())
|
||||
}
|
||||
act, err := ActOf(c, g.d.Node)
|
||||
if err != nil {
|
||||
return nil, g.holdRefused(ctx, deadline, g.refused(c, "", err.Error()).Error())
|
||||
}
|
||||
digest, err := operatorchannelclient.ActDigest(act)
|
||||
if err != nil {
|
||||
return nil, g.holdRefused(ctx, deadline, g.refused(c, "", err.Error()).Error())
|
||||
}
|
||||
named, err := g.d.State.Get(g.actKey(digest))
|
||||
if err != nil {
|
||||
g.stateFailed(err)
|
||||
return nil, g.holdRefused(ctx, deadline, g.refused(c, digest, "its state "+StateName+" cannot be read: "+err.Error()).Error())
|
||||
}
|
||||
g.stateRead()
|
||||
anew := func() (any, error) {
|
||||
id := newID()
|
||||
if err := g.keepAsk(c, act, digest, g.askForWait(c, w, digest, id)); err != nil {
|
||||
return nil, g.holdRefused(ctx, deadline, err.Error())
|
||||
}
|
||||
return nil, g.holdAsk(ctx, deadline, c.Module, id, digest, true)
|
||||
}
|
||||
if named == nil {
|
||||
return anew()
|
||||
}
|
||||
var id string
|
||||
if err := json.Unmarshal(named.Value, &id); err != nil || id == "" {
|
||||
return nil, g.holdRefused(ctx, deadline, g.refused(c, digest, fmt.Sprintf("its act's key in %s names no ask", StateName)).Error())
|
||||
}
|
||||
rec, err := g.record(c.Module, id)
|
||||
switch {
|
||||
case err != nil:
|
||||
return nil, g.holdRefused(ctx, deadline, g.refused(c, digest, "the router's record of its ask "+id+" cannot be read: "+err.Error()).Error())
|
||||
case rec == nil, rec.State == "open", rec.State == "held-back":
|
||||
return nil, g.holdAsk(ctx, deadline, c.Module, id, digest, false)
|
||||
case rec.State == "refused":
|
||||
return nil, g.holdRefused(ctx, deadline, g.refused(c, digest, "the router refused its ask "+id+": "+orNone(rec.Answer)).Error())
|
||||
case rec.State != "chosen":
|
||||
return anew() // expired, cancelled, replaced: asked again (§15, §17)
|
||||
}
|
||||
wt := rec.Warrant
|
||||
if wt == nil {
|
||||
return nil, g.holdRefused(ctx, deadline, g.refused(c, digest, "the router's record of its ask "+id+" is chosen and carries no warrant").Error())
|
||||
}
|
||||
at, _ := operatorchannelclient.Time(wt.At)
|
||||
if wt.Option == OptionDecline {
|
||||
if g.d.Now().Before(at.Add(DeclinedFor)) {
|
||||
return nil, g.holdRefused(ctx, deadline, declinedWords(id, at))
|
||||
}
|
||||
return anew()
|
||||
}
|
||||
if g.d.Now().After(at.Add(RunWithin)) {
|
||||
return anew()
|
||||
}
|
||||
var answer any
|
||||
var ran error
|
||||
h, err := g.client(c.Module).Take(*wt, func(o seat.Option, _ seat.Warrant) error {
|
||||
if err := operatorchannelclient.Performs(o, act); err != nil {
|
||||
ran = err
|
||||
return err
|
||||
}
|
||||
// On the warrant it subscribes, and the wait's timeout runs from then (§35).
|
||||
answer, ran = hear(w.Timeout)
|
||||
return nil
|
||||
})
|
||||
switch {
|
||||
case h == operatorchannelclient.Done:
|
||||
return anew() // already run: one warrant is one wait (§35)
|
||||
case h == operatorchannelclient.Refused:
|
||||
return nil, g.holdRefused(ctx, deadline, g.refused(c, digest, "its warrant does not hold: "+errText(err)).Error())
|
||||
case h == "" && err != nil:
|
||||
g.stateFailed(err)
|
||||
return nil, g.holdRefused(ctx, deadline, g.refused(c, digest, "its state "+StateName+" cannot be written: "+err.Error()).Error())
|
||||
case err != nil:
|
||||
return nil, g.holdRefused(ctx, deadline, g.refused(c, digest, err.Error()).Error())
|
||||
}
|
||||
return g.ran(c, id, rec, answer, ran)
|
||||
}
|
||||
|
||||
// outsideSeats says a subject is not a seat's: only such a subject can be marked (§32, §34).
|
||||
func outsideSeats(subjects []string) bool {
|
||||
for _, s := range subjects {
|
||||
if !strings.HasPrefix(s, "mesh.seat.") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// waitMarks are the marked subjects of every module the catalog half lists (§33): the newest entry of every followed
|
||||
// branch, and the served entry of each module this runner serves (lead answer A-168), so a mark is never lost.
|
||||
func (g *Gate) waitMarks() map[string]string {
|
||||
m := Marks{Tools: map[string]string{}, Verbs: map[string]string{}, Subjects: map[string]string{}}
|
||||
defs := g.d.Catalog.Definitions()
|
||||
for _, e := range g.d.Catalog.Entries() {
|
||||
m.add(e, defs)
|
||||
}
|
||||
g.mu.Lock()
|
||||
served := make(map[string]string, len(g.served))
|
||||
for module, fp := range g.served {
|
||||
served[module] = fp
|
||||
}
|
||||
g.mu.Unlock()
|
||||
for _, fp := range served {
|
||||
if e, ok, _ := g.d.Catalog.Served(fp); ok {
|
||||
m.add(e, defs)
|
||||
}
|
||||
}
|
||||
return m.Subjects
|
||||
}
|
||||
|
||||
// overlapsAny says a subject a wait subscribes overlaps a marked subject (§34).
|
||||
func overlapsAny(subjects []string, marked map[string]string) bool {
|
||||
for _, s := range subjects {
|
||||
for mk := range marked {
|
||||
if buscompose.Overlaps(s, mk) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// waitCatalog waits for the catalog half's first view within the bound, or until the caller goes.
|
||||
func (g *Gate) waitCatalog(ctx context.Context, within time.Duration) error {
|
||||
done := make(chan error, 1)
|
||||
go func() { done <- g.d.Wait(within) }()
|
||||
select {
|
||||
case err := <-done:
|
||||
return err
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
// pause waits until the deadline or the caller's going.
|
||||
func (g *Gate) pause(ctx context.Context, deadline time.Time) {
|
||||
if left := deadline.Sub(g.d.Now()); left > 0 {
|
||||
select {
|
||||
case <-g.d.After(left):
|
||||
case <-ctx.Done():
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// holdRefused is a refused marked wait, said at its timeout or when its caller goes, never at once, so a watcher's loop
|
||||
// is never a hot one (§36).
|
||||
func (g *Gate) holdRefused(ctx context.Context, deadline time.Time, words string) error {
|
||||
g.pause(ctx, deadline)
|
||||
if !strings.HasPrefix(words, "Not run") {
|
||||
words = "Not run: " + words
|
||||
}
|
||||
return fmt.Errorf("%s", words)
|
||||
}
|
||||
|
||||
// holdAsk holds a marked wait whose ask is open, subscribing to nothing but its ask's `decided` (§36): it answers, as an
|
||||
// error, at its timeout, when its caller goes, or once the router's record says the ask was chosen, the event only waking it.
|
||||
func (g *Gate) holdAsk(ctx context.Context, deadline time.Time, module, id, digest string, first bool) error {
|
||||
for {
|
||||
left := deadline.Sub(g.d.Now())
|
||||
heard := false
|
||||
if left > 0 && ctx.Err() == nil {
|
||||
var err error
|
||||
if g.d.Hear != nil {
|
||||
heard, err = g.d.Hear(ctx, []string{seat.SubjectDecided(module)}, left, func(body json.RawMessage) bool {
|
||||
var w seat.Warrant
|
||||
return json.Unmarshal(body, &w) == nil && w.Ask == id
|
||||
})
|
||||
}
|
||||
if g.d.Hear == nil || err != nil {
|
||||
g.pause(ctx, deadline)
|
||||
}
|
||||
}
|
||||
rec, _ := g.record(module, id)
|
||||
if heard && (rec == nil || rec.State != "chosen") {
|
||||
continue // the event only wakes it: the record says nothing chosen yet
|
||||
}
|
||||
return g.waitWords(rec, id, digest, first)
|
||||
}
|
||||
}
|
||||
|
||||
// waitWords are §12's and §17's words for a marked wait not run, "this wait" for "the call" (§36).
|
||||
func (g *Gate) waitWords(rec *seat.AsksByAskerIDValue, id, digest string, first bool) error {
|
||||
switch {
|
||||
case rec == nil:
|
||||
return fmt.Errorf("Not run, asked; not yet taken by the router (ask %s, fingerprint %s); make this wait again later", id, Fingerprint(digest))
|
||||
case rec.State == "chosen" && rec.Warrant != nil && rec.Warrant.Option == OptionDecline:
|
||||
at, _ := operatorchannelclient.Time(rec.Warrant.At)
|
||||
return fmt.Errorf("%s", declinedWords(id, at))
|
||||
case rec.State == "chosen", first:
|
||||
return fmt.Errorf("Not run: this wait needs the operator's approval. Asked on %s at %s (ask %s, fingerprint %s); make this wait "+
|
||||
"again within 30 minutes of the approval; the ask expires at %s", orNone(strings.Join(rec.OfferedOn, ", ")), rec.Opened,
|
||||
id, Fingerprint(digest), rec.Expires)
|
||||
case rec.State == "open", rec.State == "held-back":
|
||||
return fmt.Errorf("Not run: still waiting for the operator's answer to ask %s (fingerprint %s); it expires at %s",
|
||||
id, Fingerprint(digest), rec.Expires)
|
||||
}
|
||||
return fmt.Errorf("Not run: the ask %s ended %s; make this wait again to ask anew", id, rec.State)
|
||||
}
|
||||
|
||||
func declinedWords(id string, at time.Time) string {
|
||||
return fmt.Sprintf("Not run: the operator declined ask %s; this wait is not asked again until %s", id,
|
||||
operatorchannelclient.FormatTime(at.Add(DeclinedFor)))
|
||||
}
|
||||
|
||||
// askForWait is a marked wait's ask (§35): "Wait on <names> on <node>?", shortened as §10 says, and an explanation naming
|
||||
// every event name one per line, the filter, and the bound as the timeout in minutes, then the act's fingerprint.
|
||||
func (g *Gate) askForWait(c Call, w Wait, digest, id string) seat.Ask {
|
||||
a := g.askFor(c, digest, id)
|
||||
filter, whole := "none", true
|
||||
if len(w.Filter) > 0 {
|
||||
raw, _ := json.Marshal(w.Filter)
|
||||
filter = string(raw)
|
||||
if r, ok := outward.Check(filter); !ok {
|
||||
filter, whole = shapeOf(r), false
|
||||
}
|
||||
}
|
||||
a.Headline = headlineOf("Wait on", strings.Join(w.Names, ", "), g.d.Node, whole)
|
||||
lines := append([]string{"Wait on " + g.d.Node + ", called through the mesh's tools, which cannot tell who called, for:"}, w.Names...)
|
||||
lines = append(lines, "Filter: "+filter, fmt.Sprintf("Timeout: %s minutes", minutes(w.Timeout)), "Fingerprint "+Fingerprint(digest))
|
||||
a.Explanation = strings.Join(lines, "\n")
|
||||
a.Options[0].Does = "the same wait, made again within 30 minutes, subscribes once, its timeout running from then"
|
||||
a.Options[1].Does = "nothing is heard; the same wait is not asked again for 10 minutes"
|
||||
return a
|
||||
}
|
||||
|
||||
// minutes is a timeout in minutes, as few digits as say it.
|
||||
func minutes(d time.Duration) string {
|
||||
return strings.TrimSuffix(strings.TrimRight(fmt.Sprintf("%.2f", d.Minutes()), "0"), ".")
|
||||
}
|
||||
Reference in New Issue
Block a user