VW5: the view says its answers on its views family
The teller follows the view's answers by view_read_answers and views.answers instead of reading the view's state; the view emits from its marks state view-marks, as the other owners do. Builds: design 47 G §431; lead answers A-175, A-178 Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
This commit is contained in:
@@ -449,6 +449,7 @@ func main() {
|
||||
// view_first_views and view_first_view_again: where the view's process stands with its first views of the indexer's and
|
||||
// the assigner's states (ADR 0615 §6), and a re-read by hand.
|
||||
tools := append(Tools(r, u), gate.Tools()...)
|
||||
tools = append(tools, AnswersReadTool(func(name string) views.Source { return views.KeptSource{State: stdio.State(name)} }, time.Now))
|
||||
tools = append(tools, views.StatusTools(viewstate.ViewModule, "view_first_views", "view_first_view_again", stdio.State(views.StatusState), meshhandactsclient.Recorder{Ask: stdio.Ask}, time.Now)...)
|
||||
if err := stdio.Serve("", tools); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "[mesh-delivery-view-tools] %v\n", err)
|
||||
@@ -456,6 +457,14 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
// AnswersReadTool is view_read_answers, the view's ordinary read tool of its own state answers (mesh-sdk views.ReadTool): the
|
||||
// teller's first view of the answers the view says on views.answers (design 47 G A-175, A-178).
|
||||
func AnswersReadTool(state func(string) views.Source, now func() time.Time) stdio.Tool {
|
||||
return views.ReadTool(viewstate.ViewModule, viewstate.ViewAnswersReadTool, viewstate.ViewAnswersState,
|
||||
"the operator's answers given in the view to the assigner's asks, one key <ask key>.view each, which the teller fills the ask from.",
|
||||
state(viewstate.ViewAnswersState), state(views.MarksState), now)
|
||||
}
|
||||
|
||||
// pendingOf asks the indexer's tool indexer_pending (declared in invokes): what it holds back and why (ADR 0633 §5).
|
||||
func pendingOf() (takein.PendingAnswer, error) {
|
||||
raw, err := stdio.Ask(view.IndexerModule+".indexer_pending", map[string]any{})
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshcatalog"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-sdk/graph"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-view/viewstate"
|
||||
"git.novox.be/novox/mesh-sdk/go/views"
|
||||
)
|
||||
|
||||
@@ -193,3 +194,25 @@ var catalogAnswer = meshcatalog.RevisionAnswer{
|
||||
{Module: "b", Repository: "novox/r", Commit: "c2", Path: "modules/b", Manifest: map[string]any{"module": "b"}, None: "builder's fingerprint not yet known"},
|
||||
},
|
||||
}
|
||||
|
||||
// view_read_answers answers the view's own answers as every owner's read tool answers its state (design 47 G A-178): one
|
||||
// key's value, and every key when none is named.
|
||||
func TestTheViewReadsItsAnswersAsAnOwner(t *testing.T) {
|
||||
answers := source{"hold.plex.ab.view": `{"asking":"a1","option":"release"}`}
|
||||
tool := AnswersReadTool(func(name string) views.Source {
|
||||
if name == viewstate.ViewAnswersState {
|
||||
return answers
|
||||
}
|
||||
return source{}
|
||||
}, time.Now)
|
||||
if tool.Name != "view_read_answers" {
|
||||
t.Fatalf("the tool is %s", tool.Name)
|
||||
}
|
||||
for args, want := range map[string]map[string]any{"release": {"key": "hold.plex.ab.view"}, "hold.plex.ab.view": {}} {
|
||||
out, err := tool.Run(want)
|
||||
raw, _ := json.Marshal(out)
|
||||
if err != nil || !strings.Contains(string(raw), args) {
|
||||
t.Errorf("%v: %s, %v", args, raw, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ import (
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-sdk/buildstep"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-view/viewstate"
|
||||
"git.novox.be/novox/mesh-sdk/go/process"
|
||||
"git.novox.be/novox/mesh-sdk/go/views"
|
||||
"git.novox.be/novox/mesh-sdk/go/views/viewstest"
|
||||
"github.com/nats-io/nats-server/v2/server"
|
||||
"github.com/nats-io/nats.go"
|
||||
@@ -516,7 +517,15 @@ func TestProcessWholeWritesTheAnswerToAnAskNeedingNoProof(t *testing.T) { // 129
|
||||
}
|
||||
askBucket := bucket(view.AssignerAsksBucket, view.AssignerAsksHistory)
|
||||
viewBucket := bucket(viewstate.ViewAnswersBucket, viewstate.ViewAnswersHistory)
|
||||
bucket(viewstate.ViewModule+"_"+views.MarksState, 1)
|
||||
teller := bucket(view.AnswersBucket, view.AnswersHistory)
|
||||
// The view says its answers on its views family (design 47 G A-178), which the teller follows.
|
||||
said := make(chan *nats.Msg, 8)
|
||||
sub, err := nc.ChanSubscribe("mesh.mod."+viewstate.ViewModule+".event."+views.EventPrefix+viewstate.ViewAnswersState, said)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { sub.Unsubscribe() })
|
||||
|
||||
dir, err := os.MkdirTemp(os.Getenv("TMPDIR"), "i")
|
||||
if err != nil {
|
||||
@@ -575,6 +584,14 @@ func TestProcessWholeWritesTheAnswerToAnAskNeedingNoProof(t *testing.T) { // 129
|
||||
t.Fatalf("answer = %+v, %v", a, err)
|
||||
}
|
||||
wait(func(s view.Snapshot) bool { return len(s.Asks) == 1 && s.Asks[0].Chosen == "release" }, "the ask never showed the view's choice")
|
||||
select {
|
||||
case msg := <-said:
|
||||
if !strings.Contains(string(msg.Data), key+".view") {
|
||||
t.Errorf("the view said %s, not its answer under %s.view", msg.Data, key)
|
||||
}
|
||||
case <-time.After(20 * time.Second):
|
||||
t.Error("the view never said its answer on views.answers")
|
||||
}
|
||||
|
||||
// The merge's Approve is no ask of the view's (design 47 G §431): refused by its key, never written.
|
||||
mkey, err := meshdeliveryclient.MergeAskKey("feat/x", []meshdeliveryclient.Head{{Repository: "novox/mesh-delivery", Number: 12, Head: "0123456789abcdef0123456789abcdef01234567"}})
|
||||
@@ -935,4 +952,3 @@ func TestProcessWholeSaysModulesInMotion(t *testing.T) {
|
||||
t.Errorf("events said by type = %v, want moving 2, settled 1, stuck 1", count)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -130,6 +130,8 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
|
||||
}
|
||||
defer writer.Close()
|
||||
reader.Findings = func() views.WireWriter { return writer }
|
||||
sayErrs := make(chan error, 1)
|
||||
sayAnswers(ctx, js, conn, cred, writer, sayErrs, logger.Printf)
|
||||
for _, o := range ownersFollowed {
|
||||
if _, err := reader.Follow(o.owner, o.state, o.tool); err != nil {
|
||||
return err
|
||||
@@ -151,8 +153,10 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
|
||||
}
|
||||
reader.Start(ctx, js, cred, statusBucket)
|
||||
catalogHeld(ctx, catalog, model)
|
||||
var said error
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
case said = <-sayErrs:
|
||||
case err := <-webUp:
|
||||
if !errors.Is(err, http.ErrServerClosed) {
|
||||
return fmt.Errorf("the web view stopped: %w", err)
|
||||
@@ -161,7 +165,23 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
|
||||
shutdown, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
srv.Shutdown(shutdown)
|
||||
return nil
|
||||
return said
|
||||
}
|
||||
|
||||
// sayAnswers says each change of the view's own state answers on its views family (`views.answers`), from the revision its
|
||||
// marks state view-marks says it said up to, as every owner says its states (design 47 G A-175, A-178): the teller follows it
|
||||
// by view_read_answers and that family, never by reading the state. A watch that ends is sent on errs, and the program ends
|
||||
// with it, loudly.
|
||||
func sayAnswers(ctx context.Context, js jetstream.JetStream, nc process.Subscriber, cred process.Account, writer views.WireWriter, errs chan<- error, logf func(string, ...any)) {
|
||||
marks := viewstate.ViewModule + "_" + views.MarksState
|
||||
process.AwaitBucket(ctx, js, nc, marks, "the view's answers are not said on its views family, so the teller waits on its first view", logf).Ready(func(mk jetstream.KeyValue) {
|
||||
em := views.Emitter{Owner: viewstate.ViewModule, Subject: cred.EventSubject, Publish: views.JetStreamPublish(js), Marks: mk, Findings: writer, Logf: logf}
|
||||
process.AwaitBucket(ctx, js, nc, viewstate.ViewAnswersBucket, "the view's answers are not said on its views family", logf).Ready(func(kv jetstream.KeyValue) {
|
||||
if err := em.Start(ctx, viewstate.ViewAnswersState, kv, errs); err != nil {
|
||||
logf("%v: the teller asks view_read_answers at each change until the view starts again", err)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// kvWriter is the view's one write: a key of its own bucket answers (manifest state entry answers), which the teller reads.
|
||||
|
||||
@@ -146,8 +146,8 @@ func TestManifestIsTakenAndReadsOnly(t *testing.T) { // 1134/AC7
|
||||
t.Errorf("consumes = %v, want %v", m.Consumes, want)
|
||||
}
|
||||
// The one event the view says: a source revision is delivered (1906/D9, ADR 0539 §17), derived by it from the wants and the haves.
|
||||
if !reflect.DeepEqual(m.Emits, []string{"delivered", "reached", "moving", "settled", "stuck"}) {
|
||||
t.Errorf("emits = %v, want exactly delivered, reached (ADR 0596 §2), moving, settled and stuck (1906/D9, D10)", m.Emits)
|
||||
if !reflect.DeepEqual(m.Emits, []string{"delivered", "reached", "moving", "settled", "stuck", "views.answers"}) {
|
||||
t.Errorf("emits = %v, want exactly delivered, reached (ADR 0596 §2), moving, settled and stuck (1906/D9, D10), and views.answers (A-178)", m.Emits)
|
||||
}
|
||||
// The view uses no build seat: it reads their workers by the broker's named grant and publishes no ask (ADR 0525 §2).
|
||||
if len(m.Uses) != 0 {
|
||||
@@ -156,7 +156,7 @@ func TestManifestIsTakenAndReadsOnly(t *testing.T) { // 1134/AC7
|
||||
// The states the view writes: the operator's answers to the asks that need no proof (ADR 0528 clause 6, 1297/B9), which the
|
||||
// teller reads, and how far each repository reached (ADR 0556 clause 2, issue 1280), which the gate reads.
|
||||
// Its first views of the owners' states and the finding of one unanswered past its bound (ADR 0615 §6).
|
||||
if want := []state{{Name: "answers", History: 5}, {Name: "reach", History: 1}, {Name: "first-views", History: 1}}; !reflect.DeepEqual(m.State, want) {
|
||||
if want := []state{{Name: "answers", History: 5}, {Name: "reach", History: 1}, {Name: "first-views", History: 1}, {Name: "view-marks", History: 1}}; !reflect.DeepEqual(m.State, want) {
|
||||
t.Errorf("state = %+v, want %+v", m.State, want)
|
||||
}
|
||||
if _, ok := m.OwnSecrets["broker"]; !ok || len(m.OwnSecrets) != 1 {
|
||||
@@ -167,7 +167,7 @@ func TestManifestIsTakenAndReadsOnly(t *testing.T) { // 1134/AC7
|
||||
}
|
||||
// The two read tools of the indexer's modules (issue 1562), carried by a bundle the runtime launches; the indexer
|
||||
// itself serves no tool (ADR 0437 §4). Carrying them gives the tool runner read access to every bucket the view reads.
|
||||
if want := []string{"view_status", "indexer_module", "indexer_modules", "indexer_candidate", "indexer_candidates", "indexer_predefined", "indexer_config_candidates", "indexer_packages", "builder_build", "builder_builds", "builder_history", "assigner_wants", "assigner_want", "assigner_differences", "node_haves", "node_trusts", "build_usage", "view_reach", "indexer_module_history", "indexer_heard", "view_tiers", "view_first_views", "view_first_view_again", "node_manifest_reads", "view_seat_list", "view_seat_list_again"}; !reflect.DeepEqual(m.Tools, want) {
|
||||
if want := []string{"view_status", "indexer_module", "indexer_modules", "indexer_candidate", "indexer_candidates", "indexer_predefined", "indexer_config_candidates", "indexer_packages", "builder_build", "builder_builds", "builder_history", "assigner_wants", "assigner_want", "assigner_differences", "node_haves", "node_trusts", "build_usage", "view_reach", "indexer_module_history", "indexer_heard", "view_tiers", "view_first_views", "view_first_view_again", "node_manifest_reads", "view_seat_list", "view_seat_list_again", "view_read_answers"}; !reflect.DeepEqual(m.Tools, want) {
|
||||
t.Errorf("tools = %v, want %v", m.Tools, want)
|
||||
}
|
||||
// D-11: the holders and buckets the view names are its manifest's (no contract names a holder, novox/hq ADR 0634 §1): a
|
||||
|
||||
@@ -29,14 +29,16 @@
|
||||
"view_first_view_again",
|
||||
"node_manifest_reads",
|
||||
"view_seat_list",
|
||||
"view_seat_list_again"
|
||||
"view_seat_list_again",
|
||||
"view_read_answers"
|
||||
],
|
||||
"emits": [
|
||||
"delivered",
|
||||
"reached",
|
||||
"moving",
|
||||
"settled",
|
||||
"stuck"
|
||||
"stuck",
|
||||
"views.answers"
|
||||
],
|
||||
"state": [
|
||||
{
|
||||
@@ -50,6 +52,10 @@
|
||||
{
|
||||
"name": "first-views",
|
||||
"history": 1
|
||||
},
|
||||
{
|
||||
"name": "view-marks",
|
||||
"history": 1
|
||||
}
|
||||
],
|
||||
"consumes": [
|
||||
|
||||
@@ -136,8 +136,12 @@ func SameReach(a, b Reach) bool {
|
||||
// ViewAnswersBucket is the delivery view's state `answers`: one key per ask it answered, and ViewAnswersHistory the revisions it
|
||||
// keeps per key.
|
||||
const (
|
||||
ViewAnswersBucket = ViewModule + "_answers"
|
||||
ViewAnswersState = "answers"
|
||||
ViewAnswersBucket = ViewModule + "_" + ViewAnswersState
|
||||
ViewAnswersHistory = 5
|
||||
// ViewAnswersReadTool is the view's read tool of its state answers (mesh-sdk views.ReadTool), whose changes it says on its
|
||||
// views family as views.answers; its marks are its state view-marks (views.MarksState) (design 47 G A-178).
|
||||
ViewAnswersReadTool = "view_read_answers"
|
||||
)
|
||||
|
||||
// ViewAnswer is the value of the view's answer to an ask of the assigner: the asking it answers, the option chosen and the words
|
||||
|
||||
Reference in New Issue
Block a user