Merge feat/47-G-fix9-routeproxy: the seat-holder helper (A-126) the recovery module reads the bus module with
This commit is contained in:
@@ -25,6 +25,9 @@ func SeatFromDefinition(d seatdef.Definition) Seat {
|
||||
Input: seatdef.ResolveRefs(v.Input, d.Defs), Output: seatdef.ResolveRefs(v.Answer, d.Defs),
|
||||
Optional: v.Optional, Replaces: v.Replaces})
|
||||
}
|
||||
for _, st := range d.States {
|
||||
s.States = append(s.States, st.Name)
|
||||
}
|
||||
for _, r := range d.Receives {
|
||||
rv := Receivable{Kind: r.Kind, Comment: r.Comment, Dirs: r.Dirs, Offered: r.Offered}
|
||||
if r.Shape != nil {
|
||||
|
||||
@@ -46,6 +46,9 @@ type Seat struct {
|
||||
// Serves carries each verb in full — name, description, schema — because a role's tools are the
|
||||
// mesh's to define and an agent's to call (novox/hq ADR 0132, design 33 §2).
|
||||
Serves []Verb
|
||||
// States are the names of the state the seat's holder keeps, which a module reads by `seat:<seat>.<state>` (design 47 G
|
||||
// A-116, A-128). Compiled from the definition's states.
|
||||
States []string
|
||||
// Receives is what other modules may contribute to the seat's holder, by kind (novox/hq ADR
|
||||
// 0212): each kind is text in the tool's own grammar, placed by the holder with
|
||||
// ${contribution:<seat>:<kind>}. Compiled, never stored: like the protocol, it is the mesh's
|
||||
|
||||
@@ -78,6 +78,25 @@ func (s StateDeclaration) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(plain(s))
|
||||
}
|
||||
|
||||
// SeatReadPrefix opens a read of the state a seat's holder keeps: `seat:<seat>.<state>` (design 47 G A-116, A-128).
|
||||
const SeatReadPrefix = "seat:"
|
||||
|
||||
// SeatRead splits a read of a seat holder's state, `seat:<seat>.<state>`, into the seat and the local name; ok is false for a
|
||||
// read that is not of this kind (a module's `<module>.<state>`, which ReadState splits), and err says why a `seat:` read is
|
||||
// malformed. The holder is not in it: it is resolved where the read is made (seatholder), so a new holder is read with no
|
||||
// change of the manifest.
|
||||
func SeatRead(read string) (seat, local string, ok bool, err error) {
|
||||
rest, isSeat := strings.CutPrefix(read, SeatReadPrefix)
|
||||
if !isSeat {
|
||||
return "", "", false, nil
|
||||
}
|
||||
at := strings.LastIndex(rest, ".")
|
||||
if at <= 0 || at == len(rest)-1 || !stateName.MatchString(rest[:at]) || !stateName.MatchString(rest[at+1:]) {
|
||||
return "", "", true, fmt.Errorf("%q does not name a seat and its state: a read of a seat holder's state is seat:<seat>.<name>", read)
|
||||
}
|
||||
return rest[:at], rest[at+1:], true, nil
|
||||
}
|
||||
|
||||
// ReadState splits a read into the owning module and the local name, or says why it is not one.
|
||||
func ReadState(read string) (module, local string, err error) {
|
||||
at := strings.LastIndex(read, ".")
|
||||
|
||||
@@ -4,6 +4,8 @@ import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/seatdef"
|
||||
)
|
||||
|
||||
// A module declares the state it keeps and the state it reads (novox/hq ADR 0201), a bucket by its
|
||||
@@ -67,3 +69,27 @@ func TestAStateMayHaveKeysThatExpireOneByOne(t *testing.T) {
|
||||
t.Fatalf("read-only given twice: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// A read of a seat holder's state is its own kind, `seat:<seat>.<state>` (design 47 G A-128), and a seat's compiled form
|
||||
// carries the names of the states its definition declares.
|
||||
func TestSeatRead(t *testing.T) {
|
||||
for _, row := range []struct {
|
||||
read, seat, local string
|
||||
ok, bad bool
|
||||
}{
|
||||
{"seat:mesh-provisioner.plans", "mesh-provisioner", "plans", true, false},
|
||||
{"mesh-provisioner.plans", "", "", false, false},
|
||||
{"seat:mesh-provisioner", "", "", true, true},
|
||||
{"seat:.plans", "", "", true, true},
|
||||
{"seat:Mesh.plans", "", "", true, true},
|
||||
} {
|
||||
seat, local, ok, err := SeatRead(row.read)
|
||||
if seat != row.seat || local != row.local || ok != row.ok || (err != nil) != row.bad {
|
||||
t.Errorf("SeatRead(%q) = %q, %q, %v, %v", row.read, seat, local, ok, err)
|
||||
}
|
||||
}
|
||||
s := SeatFromDefinition(seatdef.Definition{Seat: "mesh-provisioner", Scope: "mesh", States: []seatdef.State{{Name: "plans"}, {Name: "provisions"}}})
|
||||
if len(s.States) != 2 || s.States[0] != "plans" || s.States[1] != "provisions" {
|
||||
t.Errorf("the seat's states = %v", s.States)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Package operatorask is a module's asking of a person through the seat that routes asks to the operator (design 47 G
|
||||
// A-101, §700, A-74): the seat's holder is found on the seat list, an ask is published under the module's own name, the
|
||||
// A-101, §700, A-74): the seat's holder is read by seatlist.HolderOf, an ask is published under the module's own name, the
|
||||
// router's record of it is read for the warrant, and the router's `decided` is heard. It is shared code, so no module
|
||||
// keeps its own copy; it names no seat, verb or module: the generated contracts' names and types are its parameters.
|
||||
package operatorask
|
||||
@@ -10,45 +10,28 @@ import (
|
||||
"fmt"
|
||||
|
||||
stdio "git.novox.be/novox/mesh-sdk/go"
|
||||
"git.novox.be/novox/mesh-sdk/go/seatlist"
|
||||
)
|
||||
|
||||
// Caller calls a seat verb or module tool as `seat:<seat>.<verb>` or `<module>.<tool>` and returns its answer
|
||||
// (stdio.Ask is one; so is a process's calls).
|
||||
type Caller func(key string, body any) (json.RawMessage, error)
|
||||
type Caller = seatlist.HolderCaller
|
||||
|
||||
// Contract is what the asker takes from the generated contracts: the verb that answers the seat list with its seats
|
||||
// (`Lists` is its call key, `ListBody` its input) and the asking seat and its state of the router's records.
|
||||
// Contract is what the asker takes from the generated contracts: the asking seat and its state of the router's records.
|
||||
type Contract struct {
|
||||
Lists string
|
||||
ListBody any
|
||||
Seat string
|
||||
Seat string
|
||||
// StateAsks is the seat's state holding the router's record of each ask.
|
||||
StateAsks string
|
||||
}
|
||||
|
||||
// Bucket is the bucket of the router's records: the seat's state in the bucket of the seat's holder on the seat list.
|
||||
// Bucket is the bucket of the router's records: the seat's state in the bucket of the seat's holder, as seatlist reads it
|
||||
// (design 47 G A-126).
|
||||
func Bucket(call Caller, c Contract) (string, error) {
|
||||
raw, err := call(c.Lists, c.ListBody)
|
||||
module, err := seatlist.HolderOf(call, c.Seat)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("who holds the seat %s is not asked of %s: %w", c.Seat, c.Lists, err)
|
||||
return "", fmt.Errorf("the router's record of an ask is not read: %w", err)
|
||||
}
|
||||
var held struct {
|
||||
Seats []struct {
|
||||
Seat string `json:"seat"`
|
||||
Holders []struct {
|
||||
Module string `json:"module"`
|
||||
} `json:"holders"`
|
||||
} `json:"seats"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &held); err != nil {
|
||||
return "", fmt.Errorf("the answer of %s is not the seat list's: %w", c.Lists, err)
|
||||
}
|
||||
for _, s := range held.Seats {
|
||||
if s.Seat == c.Seat && len(s.Holders) > 0 {
|
||||
return s.Holders[0].Module + "_" + c.StateAsks, nil
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("no module holds the seat %s on the seat list, so the router's record of an ask is not read", c.Seat)
|
||||
return module + "_" + c.StateAsks, nil
|
||||
}
|
||||
|
||||
// Asker asks as one module. W is the generated contract's warrant type.
|
||||
|
||||
@@ -24,7 +24,7 @@ func TestTheAskerFindsTheHolderPublishesReadsTheWarrantAndHearsDecided(t *testin
|
||||
var published any
|
||||
var heard func(stdio.Envelope) error
|
||||
a := &Asker[warrant]{
|
||||
Module: "me", Contract: Contract{Lists: "seat:lister.list", ListBody: map[string]bool{"seats": true}, Seat: "router-seat", StateAsks: "asks"},
|
||||
Module: "me", Contract: Contract{Seat: "router-seat", StateAsks: "asks"},
|
||||
Call: call,
|
||||
Record: func(b, i string) (*stdio.StateEntry, error) {
|
||||
bucket, id = b, i
|
||||
@@ -39,7 +39,7 @@ func TestTheAskerFindsTheHolderPublishesReadsTheWarrantAndHearsDecided(t *testin
|
||||
t.Errorf("Ask published %v on %q: %v", published, subject, err)
|
||||
}
|
||||
state, w, err := a.Where("a1")
|
||||
if err != nil || state != "chosen" || w == nil || w.Choice != "go" || bucket != "holder_asks" || id != "a1" || len(asked) != 1 || asked[0] != "seat:lister.list" {
|
||||
if err != nil || state != "chosen" || w == nil || w.Choice != "go" || bucket != "holder_asks" || id != "a1" || len(asked) != 1 || asked[0] != "seat:mesh-module-assigner.assignments" {
|
||||
t.Errorf("Where = %q %+v %v, read %q %q after %v", state, w, err, bucket, id, asked)
|
||||
}
|
||||
var got []warrant
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
// Who holds a seat now (design 47 G A-126, beside the seat list): the module on record as the holder of a seat, asked of
|
||||
// the seat that places modules (`mesh-module-assigner`) through its verb `assignments` with `seats` true, which answers the
|
||||
// seat's holders. It is the one such reading: a module that reads a seat holder's state (a `reads` of `seat:<seat>.<state>`,
|
||||
// A-116) and the asker of the operator (operatorask) call it, and no part keeps its own copy. It names the one seat and
|
||||
// verb it asks, as this package names the seat that serves the seat list, and no module.
|
||||
|
||||
package seatlist
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
const (
|
||||
// HolderSeat is the seat whose verb answers who holds each seat.
|
||||
HolderSeat = "mesh-module-assigner"
|
||||
// HolderVerb is its verb, asked with `seats` true.
|
||||
HolderVerb = "assignments"
|
||||
)
|
||||
|
||||
// HolderCaller calls a seat verb or module tool as `seat:<seat>.<verb>` or `<module>.<tool>` and returns its answer (stdio.Ask is
|
||||
// one; so is a process's calls).
|
||||
type HolderCaller func(key string, body any) (json.RawMessage, error)
|
||||
|
||||
// HolderKey is the address asked, as stdio.Ask takes it; a host's manifest `invokes` lists it.
|
||||
func HolderKey() string { return "seat:" + HolderSeat + "." + HolderVerb }
|
||||
|
||||
// HolderOf is the module holding the seat now: the first holder the seat's answer lists. It is an error while the answer cannot be
|
||||
// read or no module holds the seat.
|
||||
func HolderOf(call HolderCaller, seat string) (string, error) {
|
||||
raw, err := call(HolderKey(), map[string]string{"seats": "true"})
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("who holds the seat %s is not asked of %s: %w", seat, HolderKey(), err)
|
||||
}
|
||||
var held struct {
|
||||
Seats []struct {
|
||||
Seat string `json:"seat"`
|
||||
Holders []struct {
|
||||
Module string `json:"module"`
|
||||
} `json:"holders"`
|
||||
} `json:"seats"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &held); err != nil {
|
||||
return "", fmt.Errorf("the answer of %s is not the seat holders': %w", HolderKey(), err)
|
||||
}
|
||||
for _, s := range held.Seats {
|
||||
if s.Seat == seat && len(s.Holders) > 0 {
|
||||
return s.Holders[0].Module, nil
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("no module holds the seat %s on record", seat)
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package seatlist
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestHolderOf(t *testing.T) {
|
||||
answer := `{"seats":[{"seat":"other","holders":[{"module":"x"}]},{"seat":"mesh-provisioner","holders":[{"module":"prov"},{"module":"second"}]}]}`
|
||||
for _, row := range []struct {
|
||||
name, seat, want, err string
|
||||
call HolderCaller
|
||||
}{
|
||||
{"the first holder on record", "mesh-provisioner", "prov", "", func(k string, b any) (json.RawMessage, error) {
|
||||
if k != "seat:mesh-module-assigner.assignments" || b.(map[string]string)["seats"] != "true" {
|
||||
t.Errorf("asked %q with %v", k, b)
|
||||
}
|
||||
return json.RawMessage(answer), nil
|
||||
}},
|
||||
{"no holder", "absent", "", "no module holds the seat absent", func(string, any) (json.RawMessage, error) { return json.RawMessage(answer), nil }},
|
||||
{"the ask fails", "x", "", "who holds the seat x is not asked", func(string, any) (json.RawMessage, error) { return nil, errors.New("down") }},
|
||||
{"not the holders' answer", "x", "", "is not the seat holders'", func(string, any) (json.RawMessage, error) { return json.RawMessage(`nope`), nil }},
|
||||
} {
|
||||
t.Run(row.name, func(t *testing.T) {
|
||||
got, err := HolderOf(row.call, row.seat)
|
||||
if row.err == "" && (err != nil || got != row.want) || row.err != "" && (err == nil || !strings.Contains(err.Error(), row.err)) {
|
||||
t.Errorf("HolderOf = %q, %v; want %q, %q", got, err, row.want, row.err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Vendored
+1
@@ -25,4 +25,5 @@ catalogue/seat_dependencies.go: node-service-manager
|
||||
catalogue/shares.go: node-mounts
|
||||
catalogue/shares.go: node-nfs-server
|
||||
internal/beside/beside.go: mesh-catalog
|
||||
seatlist/holder.go: mesh-module-assigner
|
||||
seatlist/seatlist.go: mesh-catalog
|
||||
|
||||
@@ -20,6 +20,9 @@ const (
|
||||
// ViewWaitsKind is a part's finding of a view it has waited for past WaitsBound (design 47 *A part with no view says it
|
||||
// waits*).
|
||||
ViewWaitsKind = "view-waits"
|
||||
// EntryNotFoundKind is a part's finding of an entry by fingerprint the seat mesh-catalog's `entry` answered it does not hold
|
||||
// (design 47 G §614, A-15, A-103): scope the part, id the fingerprint.
|
||||
EntryNotFoundKind = "entry-not-found"
|
||||
)
|
||||
|
||||
// viewFinding is what a views part says is wrong with a state it owns or follows, in plain fields: kind and id name it, summary
|
||||
@@ -99,3 +102,18 @@ func viewWaitsFinding(part string, s Status, at time.Time) viewFinding {
|
||||
part, s.Owner, s.State, s.WaitsFor, s.WaitingSince.UTC().Format(time.RFC3339), cmp.Or(s.View, "none"))
|
||||
return viewFinding{kind: ViewWaitsKind, id: part + "." + s.Owner + "." + s.State, summary: summary, headline: fmt.Sprintf("%s waits for a view of %s", part, s.Owner), at: at.UTC()}
|
||||
}
|
||||
|
||||
// WriteEntryNotFound writes the part's finding that the catalog holds no entry for fingerprint, asked since `since`
|
||||
// (design 47 G A-103); ClearEntryNotFound clears it, when nothing the part holds names the fingerprint or a later ask answers.
|
||||
func WriteEntryNotFound(w WireWriter, fingerprint string, since, at time.Time) error {
|
||||
module, _ := w.ModuleNode()
|
||||
return viewFinding{kind: EntryNotFoundKind, id: fingerprint,
|
||||
summary: fmt.Sprintf("%s holds a build of fingerprint %s that the catalog holds no entry for (asked since %s): what it needs of the entry stands still",
|
||||
module, fingerprint, since.UTC().Format(time.RFC3339)),
|
||||
headline: fmt.Sprintf("%s cannot find the catalog entry of a build", module), at: at.UTC()}.put(w)
|
||||
}
|
||||
|
||||
// ClearEntryNotFound clears the finding WriteEntryNotFound wrote for fingerprint.
|
||||
func ClearEntryNotFound(w WireWriter, fingerprint string) error {
|
||||
return viewFinding{kind: EntryNotFoundKind, id: fingerprint}.drop(w)
|
||||
}
|
||||
|
||||
+26
-1
@@ -28,6 +28,8 @@ type Reader struct {
|
||||
// says it in the log only.
|
||||
Findings WireWriter
|
||||
Logf func(string, ...any)
|
||||
|
||||
rows map[string]func() any // the lists Rows added to the first views (guarded by mu)
|
||||
// Changed is told of each key a followed state changes, after the picture holds it (state is `<owner>.<state>`).
|
||||
Changed func(state, key string, value json.RawMessage, deleted bool)
|
||||
// Acts records a re-read by hand before it is asked (nil records nothing
|
||||
@@ -574,13 +576,36 @@ func (r *Reader) Statuses() []Status {
|
||||
return out
|
||||
}
|
||||
|
||||
// Rows adds a list the part's `<prefix>_first_views` says beside the views' statuses, under name (design 47 G A-103: the
|
||||
// entries by fingerprint the catalog half's follower keeps). rows is called at each reading.
|
||||
func (r *Reader) Rows(name string, rows func() any) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
if r.rows == nil {
|
||||
r.rows = map[string]func() any{}
|
||||
}
|
||||
r.rows[name] = rows
|
||||
}
|
||||
|
||||
// firstViews is what `<prefix>_first_views` answers: the views' statuses and each list added by Rows.
|
||||
func (r *Reader) firstViews() map[string]any {
|
||||
out := map[string]any{"views": r.Statuses()}
|
||||
r.mu.Lock()
|
||||
rows := maps.Clone(r.rows)
|
||||
r.mu.Unlock()
|
||||
for name, f := range rows {
|
||||
out[name] = f()
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// Tools are the reader's two tools (design 25 § 12.2): `<prefix>_first_views` says where each first view stands and
|
||||
// `<prefix>_first_view_again` asks one again by hand (a hand act, counted in the status).
|
||||
func (r *Reader) Tools(prefix string) []stdio.Tool {
|
||||
return []stdio.Tool{
|
||||
{Name: prefix + "_first_views",
|
||||
Description: "Where each first view this module keeps stands (novox/hq ADR 0615): the owner and its tool, the revision the picture holds, when it was asked and answered, how many asks (by hand among them) and whether its condition is raised. (r)",
|
||||
Run: func(map[string]any) (any, error) { return map[string]any{"views": r.Statuses()}, nil }},
|
||||
Run: func(map[string]any) (any, error) { return r.firstViews(), nil }},
|
||||
{Name: prefix + "_first_view_again",
|
||||
Description: "Ask a first view again by hand, for the state named (<owner>.<state>), or every one with none: a hand act, for a picture the operator doubts. Answers where it stands after. (a)",
|
||||
Input: map[string]any{"type": "object", "properties": map[string]any{
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strings"
|
||||
@@ -558,6 +559,19 @@ func subViews(t *testing.T) {
|
||||
if err != nil || len(again.(map[string]any)["views"].([]Status)) != 1 || f.Status().ByHand != 1 || acted != 1 {
|
||||
t.Errorf("asked again by hand: %v, %v", again, err)
|
||||
}
|
||||
// a list added by Rows is said beside the statuses (design 47 G A-103), and the entry-not-found finding is written and cleared
|
||||
r.Rows("entries", func() any { return []string{"fp1"} })
|
||||
seen, _ := r.Tools("reader")[0].Run(nil)
|
||||
if got := seen.(map[string]any)["entries"]; !reflect.DeepEqual(got, []string{"fp1"}) {
|
||||
t.Errorf("first views entries = %v", got)
|
||||
}
|
||||
nf := &findingsMap{}
|
||||
if err := WriteEntryNotFound(nf, "fp1", now(), now()); err != nil || nf.m[EntryNotFoundKind+"/fp1"].Summary == "" {
|
||||
t.Errorf("entry-not-found not written: %+v, %v", nf.m, err)
|
||||
}
|
||||
if err := ClearEntryNotFound(nf, "fp1"); err != nil || len(nf.m) != 0 {
|
||||
t.Errorf("entry-not-found not cleared: %+v, %v", nf.m, err)
|
||||
}
|
||||
})
|
||||
t.Run("a process's reader follows the read tool its caller names, reads the state as a read-only bucket, each key's written time handed on, and opens no other", func(t *testing.T) {
|
||||
r := &ProcessReader{Module: "reader", Bound: time.Nanosecond, Wait: time.Millisecond, Now: now,
|
||||
|
||||
+1
-1
@@ -73,7 +73,7 @@ func requireFields[T any](what string, fields ...string) error {
|
||||
}
|
||||
|
||||
// ViewKinds are the finding kinds a views reader writes through the part's writer (design 47 G §639).
|
||||
var ViewKinds = []string{ViewWaitsKind, UnansweredKind, UnsaidKind, TooLargeKind}
|
||||
var ViewKinds = []string{ViewWaitsKind, UnansweredKind, UnsaidKind, TooLargeKind, EntryNotFoundKind}
|
||||
|
||||
var (
|
||||
sessionForm = regexp.MustCompile(`^[a-z0-9][a-z0-9._-]*@[a-z0-9][a-z0-9-]*$`)
|
||||
|
||||
Reference in New Issue
Block a user