seatlist: Start returns at once and reads the seat on a goroutine; Ready says when the first read ended, Status says it is reading (ADR 0636 §1; issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
This commit is contained in:
@@ -674,6 +674,30 @@
|
||||
],
|
||||
"description": "what this machine's engine reads of a manifest (ADR 0618 §3)"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "seat-list",
|
||||
"description": "The seat list this engine holds and what it waits for (novox/hq ADR 0636 §7): each seat's source (trunk or bootstrap) and digest, the seats held from the bootstrap alone, the definitions refused, the last error and whether the seat mesh-catalog has answered. Read-only.",
|
||||
"callers": "the operator and the healers",
|
||||
"input": {
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
},
|
||||
"answer": {
|
||||
"$ref": "#/defs/SeatList"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "seat-list-again",
|
||||
"description": "Make this engine read the seat list from the seat mesh-catalog now (novox/hq ADR 0636 §7), the recovery of a list unread; it answers what it holds afterwards, and the error when the read failed. A read that changed nothing still ends the engine's wait.",
|
||||
"callers": "the operator and the healers",
|
||||
"input": {
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
},
|
||||
"answer": {
|
||||
"$ref": "#/defs/SeatList"
|
||||
}
|
||||
}
|
||||
],
|
||||
"events": [
|
||||
@@ -1069,6 +1093,109 @@
|
||||
}
|
||||
],
|
||||
"description": "each node's health as its engine reports it"
|
||||
},
|
||||
{
|
||||
"name": "findings",
|
||||
"template": "state",
|
||||
"writer": "holder",
|
||||
"key-forms": [
|
||||
{
|
||||
"key": "<key>",
|
||||
"value": {
|
||||
"type": "object",
|
||||
"description": "one thing an observer sees wrong now",
|
||||
"properties": {
|
||||
"v": {
|
||||
"type": "integer",
|
||||
"description": "layout version, 1"
|
||||
},
|
||||
"key": {
|
||||
"type": "string",
|
||||
"description": "scope.id.token"
|
||||
},
|
||||
"kind": {
|
||||
"type": "string"
|
||||
},
|
||||
"subject": {
|
||||
"$ref": "#/defs/FindingSubject"
|
||||
},
|
||||
"severity": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"urgent",
|
||||
"warning"
|
||||
]
|
||||
},
|
||||
"summary": {
|
||||
"type": "string",
|
||||
"description": "one line"
|
||||
},
|
||||
"headline": {
|
||||
"type": "string"
|
||||
},
|
||||
"explanation": {
|
||||
"type": "string"
|
||||
},
|
||||
"resolved": {
|
||||
"type": "string"
|
||||
},
|
||||
"needs": {
|
||||
"type": "string"
|
||||
},
|
||||
"actions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/defs/FindingAction"
|
||||
}
|
||||
},
|
||||
"evidence": {
|
||||
"type": "array",
|
||||
"description": "newest first, at most 10",
|
||||
"items": {
|
||||
"$ref": "#/defs/FindingEvidence"
|
||||
}
|
||||
},
|
||||
"source": {
|
||||
"type": "string",
|
||||
"description": "the row, probe or event that saw it"
|
||||
},
|
||||
"resolver": {
|
||||
"type": "string",
|
||||
"description": "self, operator, agent or healer:<name>"
|
||||
},
|
||||
"ref": {
|
||||
"type": "string",
|
||||
"description": "the key's last part when it is not the kind"
|
||||
},
|
||||
"observer": {
|
||||
"type": "string",
|
||||
"description": "the module that wrote it"
|
||||
},
|
||||
"run": {
|
||||
"type": "string",
|
||||
"description": "what of its own saw it last"
|
||||
},
|
||||
"at": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"v",
|
||||
"key",
|
||||
"kind",
|
||||
"subject",
|
||||
"severity",
|
||||
"summary",
|
||||
"source",
|
||||
"observer",
|
||||
"at"
|
||||
]
|
||||
},
|
||||
"description": "one finding of the observer that wrote it"
|
||||
}
|
||||
],
|
||||
"description": "the findings this node's engine raises about itself, one key per finding, in the layout of the seat mesh-conditions (its findings state): a key written raises the condition of the same key, a key deleted clears it (novox/hq ADR 0564 §2, ADR 0636 §1); today `machine.<node>.seat-list-unread`"
|
||||
}
|
||||
],
|
||||
"defs": {
|
||||
@@ -1412,6 +1539,165 @@
|
||||
"said"
|
||||
],
|
||||
"description": "the answer of a read-only judgement"
|
||||
},
|
||||
"SeatListHeld": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"seat": {
|
||||
"type": "string"
|
||||
},
|
||||
"source": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"trunk",
|
||||
"bootstrap"
|
||||
]
|
||||
},
|
||||
"digest": {
|
||||
"type": "string",
|
||||
"description": "the digest of the definition held; empty for a bootstrapped one"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"seat",
|
||||
"source"
|
||||
]
|
||||
},
|
||||
"SeatList": {
|
||||
"type": "object",
|
||||
"description": "what an engine holds of the seat list (novox/hq ADR 0636 §7)",
|
||||
"properties": {
|
||||
"status": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"heard": {
|
||||
"type": "boolean",
|
||||
"description": "the seat mesh-catalog has answered"
|
||||
},
|
||||
"held": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/defs/SeatListHeld"
|
||||
}
|
||||
},
|
||||
"waiting": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "the seats held from the bootstrap alone"
|
||||
},
|
||||
"refused": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "a trunk definition that does not parse, by seat: the one held stands"
|
||||
},
|
||||
"last-error": {
|
||||
"type": "string"
|
||||
},
|
||||
"revision": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"heard",
|
||||
"held"
|
||||
]
|
||||
},
|
||||
"health": {
|
||||
"type": "string",
|
||||
"description": "the one line the engine says of it; empty when nothing waits"
|
||||
},
|
||||
"error": {
|
||||
"type": "string",
|
||||
"description": "why the read failed, for seat-list-again"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"status",
|
||||
"health"
|
||||
]
|
||||
},
|
||||
"FindingSubject": {
|
||||
"type": "object",
|
||||
"description": "what the condition is about",
|
||||
"properties": {
|
||||
"scope": {
|
||||
"type": "string",
|
||||
"description": "machine, plan, call, build, merge, provider, seat, bus, core, probe, mesh, delivery or module"
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "its id within the scope"
|
||||
},
|
||||
"machine": {
|
||||
"type": "string",
|
||||
"description": "the machine it concerns, when there is one"
|
||||
},
|
||||
"also": {
|
||||
"type": "array",
|
||||
"description": "the other machines it concerns",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"scope",
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"FindingAction": {
|
||||
"type": "object",
|
||||
"description": "one answer the operator may give from a notification",
|
||||
"properties": {
|
||||
"label": {
|
||||
"type": "string"
|
||||
},
|
||||
"verb": {
|
||||
"type": "string",
|
||||
"description": "the seat verb the operator channel calls"
|
||||
},
|
||||
"machine": {
|
||||
"type": "string"
|
||||
},
|
||||
"arguments": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"level": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"acknowledge",
|
||||
"approve"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"label",
|
||||
"verb"
|
||||
]
|
||||
},
|
||||
"FindingEvidence": {
|
||||
"type": "object",
|
||||
"description": "one observation, as it was said",
|
||||
"properties": {
|
||||
"at": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"said": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"at",
|
||||
"said"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+30
-8
@@ -112,6 +112,7 @@ type Reader struct {
|
||||
refused map[string]string // seats whose trunk definition cannot be parsed, with why: the one that stands stays
|
||||
// what the last ask of the seat came to (Status)
|
||||
spoken bool
|
||||
ready chan struct{} // closed once the first read of the seat has come to an end, answered or not
|
||||
revision uint64
|
||||
lastErr string
|
||||
}
|
||||
@@ -128,7 +129,9 @@ type Held struct {
|
||||
// are the seats whose trunk definition did not parse (the one held stands), LastError is why the last ask failed, and
|
||||
// Revision is the seat's state revision of the last listing.
|
||||
type Status struct {
|
||||
Heard bool `json:"heard"`
|
||||
Heard bool `json:"heard"`
|
||||
// Reading says the first read of the seat has not come to an end yet.
|
||||
Reading bool `json:"reading,omitempty"`
|
||||
Held []Held `json:"held"`
|
||||
Waiting []string `json:"waiting,omitempty"`
|
||||
Refused map[string]string `json:"refused,omitempty"`
|
||||
@@ -140,7 +143,7 @@ type Status struct {
|
||||
func (r *Reader) Status() Status {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
st := Status{Heard: r.spoken, LastError: r.lastErr, Revision: r.revision, Held: []Held{}}
|
||||
st := Status{Heard: r.spoken, Reading: !r.isReady(), LastError: r.lastErr, Revision: r.revision, Held: []Held{}}
|
||||
for seat := range r.defs {
|
||||
h := Held{Seat: seat, Source: SourceTrunk, Digest: r.digests[seat]}
|
||||
if r.boot[seat] {
|
||||
@@ -163,6 +166,8 @@ func (r *Reader) Status() Status {
|
||||
// Health is Status as the one line a module's health says: empty when the seat has been heard and nothing waits.
|
||||
func (st Status) Health() string {
|
||||
switch {
|
||||
case st.Reading:
|
||||
return fmt.Sprintf("the seat list is held from the bootstrap (%d seats) and nothing new is acted on: the first read of the seat %s has not ended", len(st.Held), Seat)
|
||||
case !st.Heard:
|
||||
return fmt.Sprintf("the seat list is held from the bootstrap (%d seats) and nothing new is acted on: the seat %s has not answered%s",
|
||||
len(st.Held), Seat, withError(st.LastError))
|
||||
@@ -179,15 +184,16 @@ func withError(e string) string {
|
||||
return " (" + e + ")"
|
||||
}
|
||||
|
||||
// Start returns a reader holding the bootstrap, asks the seat for every definition and follows its two events. A first
|
||||
// ask the seat did not answer is no failure: the reader holds the bootstrap, acts on nothing new (MayAct) and reads again
|
||||
// on the seat's next event or on Reload. Start returns the subscription's error alone, which the host may ask again by
|
||||
// calling Start again on a new reader.
|
||||
// Start returns at once a reader holding the bootstrap, following the seat's two events, and reads the seat for every
|
||||
// definition on a goroutine of its own, so a slow seat holds nobody's start (ADR 0636 §1). Until that read has come to an
|
||||
// end the reader acts on nothing new (MayAct) and Status says it is reading; Ready is closed then. A first read the seat
|
||||
// did not answer is no failure: the reader holds the bootstrap and reads again on the seat's next event or on Reload.
|
||||
// Start returns the subscription's error alone, which the host may ask again by calling Start again on a new reader.
|
||||
func Start(cfg Config) (*Reader, error) {
|
||||
if cfg.Bus == nil {
|
||||
return nil, errors.New("seatlist: no bus to read the seat list through")
|
||||
}
|
||||
r := &Reader{cfg: cfg, defs: map[string]seatdef.Definition{}, digests: map[string]string{}, boot: map[string]bool{}, refused: map[string]string{}}
|
||||
r := &Reader{cfg: cfg, defs: map[string]seatdef.Definition{}, digests: map[string]string{}, boot: map[string]bool{}, refused: map[string]string{}, ready: make(chan struct{})}
|
||||
for _, d := range cfg.Bootstrap {
|
||||
r.defs[d.Seat] = d
|
||||
r.boot[d.Seat] = true
|
||||
@@ -197,10 +203,26 @@ func Start(cfg Config) (*Reader, error) {
|
||||
return r, fmt.Errorf("seatlist: following %s: %w", ev, err)
|
||||
}
|
||||
}
|
||||
_ = r.Reload()
|
||||
go func() {
|
||||
_ = r.Reload()
|
||||
close(r.ready)
|
||||
}()
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// Ready is closed once the first read of the seat, which Start began, has come to an end, whether the seat answered or
|
||||
// not: a test or a host that needs the reader's first state waits on it.
|
||||
func (r *Reader) Ready() <-chan struct{} { return r.ready }
|
||||
|
||||
func (r *Reader) isReady() bool {
|
||||
select {
|
||||
case <-r.ready:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Seats are the held seats in reading order, as catalogue takes them.
|
||||
func (r *Reader) Seats() []catalogue.Seat {
|
||||
r.mu.Lock()
|
||||
|
||||
@@ -54,6 +54,7 @@ func TestAReaderStartsFromTheBootstrapAndReadsTheSeatOnceItAnswers(t *testing.T)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
<-r.Ready()
|
||||
if st := r.Status(); st.Heard || st.LastError == "" || !r.MayAct("node-engine") || r.MayAct("node-files") {
|
||||
t.Fatalf("with the seat silent: %+v, may act on node-files: %v", st, r.MayAct("node-files"))
|
||||
}
|
||||
|
||||
@@ -16,11 +16,15 @@ import (
|
||||
type fakeOwner struct {
|
||||
table map[string]string // seat -> definition JSON, trunk
|
||||
down bool
|
||||
asked []string // "key seat"
|
||||
gate chan struct{} // when set, an ask waits for it to be closed
|
||||
asked []string // "key seat"
|
||||
handlers map[string]func(string, json.RawMessage) error
|
||||
}
|
||||
|
||||
func (f *fakeOwner) Ask(key string, body any) (json.RawMessage, error) {
|
||||
if f.gate != nil {
|
||||
<-f.gate
|
||||
}
|
||||
args, _ := body.(map[string]any)
|
||||
seat, _ := args["seat"].(string)
|
||||
f.asked = append(f.asked, key+" "+seat)
|
||||
@@ -82,6 +86,7 @@ func TestTheSeatListIsReadFromTheSeatAndFollowsItsEvents(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
<-r.Ready()
|
||||
if owner.handlers[Pattern(ChangedEvent)] == nil || owner.handlers[Pattern(RemovedEvent)] == nil || Pattern(ChangedEvent) != "mesh-catalog.seat-definition-changed" {
|
||||
t.Fatalf("subscribed to %v", owner.handlers)
|
||||
}
|
||||
@@ -157,6 +162,7 @@ func TestTheBootstrapIsHeldAndNothingNewIsActedOnUntilTheSeatAnswers(t *testing.
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
<-r.Ready()
|
||||
if got := len(r.Definitions()); got != len(boot) {
|
||||
t.Fatalf("the reader holds %d definitions, the bootstrap has %d", got, len(boot))
|
||||
}
|
||||
@@ -199,6 +205,7 @@ func TestTheTwoToolsReadTheHeldListAndReadItAgainForTheirCallers(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
<-r.Ready()
|
||||
callers := []string{stdio.AnyPerson, "mesh-healers"}
|
||||
tools := Tools(r, "seat_list", "seat_list_again", callers)
|
||||
out, err := tools[0].Run(map[string]any{})
|
||||
@@ -226,6 +233,7 @@ func TestAReaderStartedWhileTheSeatWasSilentReadsTheWholeListAtItsFirstEvent(t *
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
<-r.Ready()
|
||||
if r.Status().Heard || r.MayAct("node-b") {
|
||||
t.Fatal("the silent seat was heard")
|
||||
}
|
||||
@@ -245,6 +253,7 @@ func TestAReaderStartedWhileTheSeatWasSilentIsHeardAtItsChangedEvent(t *testing.
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
<-r.Ready()
|
||||
if r.Status().Heard || r.MayAct("node-files") {
|
||||
t.Fatal("the silent seat must leave the reader unheard")
|
||||
}
|
||||
@@ -257,3 +266,22 @@ func TestAReaderStartedWhileTheSeatWasSilentIsHeardAtItsChangedEvent(t *testing.
|
||||
t.Fatalf("after the changed event: %+v, node-b held %v: want heard and the whole list read", st, r.Has("node-b"))
|
||||
}
|
||||
}
|
||||
|
||||
// ADR 0636 §1: Start returns at once whatever the seat does; until the first read has ended the reader acts on nothing new
|
||||
// and says it is reading.
|
||||
func TestStartReturnsAtOnceAndTheReaderSaysItIsReadingUntilTheSeatAnswers(t *testing.T) {
|
||||
owner := &fakeOwner{gate: make(chan struct{}), table: map[string]string{"node-b": def("node-b", "node", "put")}}
|
||||
r, err := Start(Config{Bus: owner, Bootstrap: Bootstrapped()})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
st := r.Status()
|
||||
if !st.Reading || st.Heard || !strings.Contains(st.Health(), "first read") || r.MayAct("node-b") || !r.MayAct("node-engine") {
|
||||
t.Fatalf("while the seat is slow: %+v", st)
|
||||
}
|
||||
close(owner.gate)
|
||||
<-r.Ready()
|
||||
if st := r.Status(); st.Reading || !st.Heard || !r.MayAct("node-b") {
|
||||
t.Fatalf("after the read: %+v", st)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user