CAT-V3: the vault's unanswered first view of the placement is a finding the condition keeper raises
mesh-vault gains the state findings (old section); past two minutes without the assigner's first view it writes first-view-unanswered naming the owner and the tool, the answer deletes it. mesh-conditions reads mesh-vault.findings and holds a row for the kind. Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
This commit is contained in:
@@ -99,6 +99,9 @@ var Homes = map[string]Home{
|
||||
"rename-window-held": {Source: "config", Observer: configModule},
|
||||
"retirement-held": {Source: "config", Observer: configModule},
|
||||
"retiring-unanswered": {Source: "config", Observer: configModule},
|
||||
// A reader whose owner does not answer its first view past two minutes (ADR 0615 §6): written by each reader in its own
|
||||
// findings (mesh-config, mesh-vault), so the kind has no one observer.
|
||||
"first-view-unanswered": {Source: "views", Why: "every reader of the indexer's or the assigner's state writes its own"},
|
||||
// The kinds that end with the controller and are carried until its lease write (clauses 4, 9).
|
||||
"controller-deaf": {Source: "S4", Controller: true, Carried: true},
|
||||
"lease-lost": {Source: "S12", Controller: true, Carried: true},
|
||||
@@ -162,6 +165,7 @@ const (
|
||||
messenger = "messenger"
|
||||
artifactStore = "artifact-store-tools"
|
||||
configModule = "mesh-config"
|
||||
vaultModule = "mesh-vault"
|
||||
)
|
||||
|
||||
// homeOf is the home of a condition's kind, and whether the table has one.
|
||||
|
||||
@@ -40,7 +40,7 @@ const HistoryKeptFor = 90 * 24 * time.Hour
|
||||
// table of 2026-10-29 for the provisioner's and the router's; ADR 0571 clause 2 for the seat artifact-store's holder,
|
||||
// which observes S25, image-missing).
|
||||
var Observers = []string{health.WatchmanModule, health.SelfCheckModule, health.HealersModule, delivery.AssignerModule,
|
||||
"mesh-provisioner", "messenger", artifactStore, configModule}
|
||||
"mesh-provisioner", "messenger", artifactStore, configModule, vaultModule}
|
||||
|
||||
// The other states it reads, as `<module>.<name>`.
|
||||
const (
|
||||
|
||||
@@ -294,7 +294,7 @@ func TestEveryControllerKindHasItsHome(t *testing.T) {
|
||||
t.Errorf("the ended probe's kind %s is %+v", kind, h)
|
||||
}
|
||||
}
|
||||
if len(Homes) != 88 || carried != 37 { // 19 of ADR 0564, the 63 of the table, plan-failed and stalled, and mesh-config's four (design 32)
|
||||
if len(Homes) != 89 || carried != 37 { // 19 of ADR 0564, the 63 of the table, plan-failed and stalled, mesh-config's four (design 32) and the readers' unanswered first view
|
||||
t.Errorf("the table holds %d kinds, %d carried", len(Homes), carried)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
"mesh-provisioner.findings",
|
||||
"artifact-store-tools.findings",
|
||||
"mesh-config.findings",
|
||||
"mesh-vault.findings",
|
||||
"mesh-healers.attempts",
|
||||
"mesh-delivery-assigner.wants",
|
||||
"node-engine.have",
|
||||
|
||||
@@ -122,7 +122,11 @@ func watchLeavers(env *Env) bool {
|
||||
if node, ok := leaverKey(key); ok {
|
||||
env.Vault.NodeLeft(node)
|
||||
}
|
||||
})
|
||||
}, func(key string, v json.RawMessage) error {
|
||||
_, err := stdio.State(FindingsState).Put(key, v)
|
||||
return err
|
||||
},
|
||||
func(key string) error { return stdio.State(FindingsState).Delete(key) })
|
||||
beginPlacementView(placementView)
|
||||
}
|
||||
return true
|
||||
|
||||
@@ -28,11 +28,15 @@ const (
|
||||
FirstViewAgainTool = "vault_first_view_again"
|
||||
)
|
||||
|
||||
// FindingsState is the vault's state of findings, which the condition keeper reads (ADR 0564): a finding names no secret.
|
||||
const FindingsState = "findings"
|
||||
|
||||
// placementView is the picture; nil until main begins it.
|
||||
var placementView *views.Follower
|
||||
|
||||
// newPlacementView makes the follower: ask calls the tool, gone is told of a deleted key.
|
||||
func newPlacementView(ask func(key string, body any) (json.RawMessage, error), gone func(key string)) *views.Follower {
|
||||
func newPlacementView(ask func(key string, body any) (json.RawMessage, error), gone func(key string),
|
||||
put func(key string, value json.RawMessage) error, drop func(key string) error) *views.Follower {
|
||||
return &views.Follower{Owner: delivery.AssignerModule, Tool: PlacementTool, State: "placement", Logf: say,
|
||||
Ask: func(_ context.Context, after string) (views.Answer, error) {
|
||||
body := map[string]any{"values": true}
|
||||
@@ -45,9 +49,21 @@ func newPlacementView(ask func(key string, body any) (json.RawMessage, error), g
|
||||
}
|
||||
return views.DecodeAnswer(unwrapToolAnswer(raw))
|
||||
},
|
||||
// Past its bound the vault's finding names the owner and the tool, and the condition keeper raises it (the
|
||||
// answer deletes it); the vault keeps asking meanwhile.
|
||||
Raise: func(s views.Status) {
|
||||
say("the vault has no first view of the assigner's placement: %s has not answered since %s (%s); it keeps asking", s.Tool,
|
||||
s.Asked.Format(time.RFC3339), s.Error)
|
||||
if k, v, err := views.UnansweredFinding(Seat, s, time.Now()); err == nil {
|
||||
if err := put(k, v); err != nil {
|
||||
say("the finding %s is not written: %v", k, err)
|
||||
}
|
||||
}
|
||||
},
|
||||
Clear: func(s views.Status) {
|
||||
if k, _, err := views.UnansweredFinding(Seat, s, time.Now()); err == nil {
|
||||
_ = drop(k)
|
||||
}
|
||||
},
|
||||
Changed: func(key string, _ json.RawMessage, deleted bool, _ time.Time) {
|
||||
if deleted {
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-delivery/sdk/views"
|
||||
)
|
||||
|
||||
// ADR 0615 §6: the vault started while the assigner is down keeps asking, never reads its bucket, and past the bound
|
||||
// writes a finding naming the owner and the tool for the condition keeper; the answer deletes it. Before the answer a read
|
||||
// of the placement is an error.
|
||||
func TestAnUnansweredFirstViewOfThePlacementIsAFindingTheAnswerClears(t *testing.T) {
|
||||
var mu sync.Mutex
|
||||
up := false
|
||||
held := map[string]json.RawMessage{}
|
||||
ask := func(string, any) (json.RawMessage, error) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
if !up {
|
||||
return nil, errors.New("nats: no responders available for request")
|
||||
}
|
||||
raw, _ := json.Marshal(views.Answer{V: 1, Owner: "mesh-delivery-assigner", State: "placement", Revision: 1, Created: time.Now(),
|
||||
Values: map[string]json.RawMessage{"g14.node": json.RawMessage(`{}`)}})
|
||||
return raw, nil
|
||||
}
|
||||
put := func(k string, v json.RawMessage) error { mu.Lock(); held[k] = v; mu.Unlock(); return nil }
|
||||
drop := func(k string) error { mu.Lock(); delete(held, k); mu.Unlock(); return nil }
|
||||
f := newPlacementView(ask, func(string) {}, put, drop)
|
||||
f.Bound, f.Wait = 30*time.Millisecond, 10*time.Millisecond
|
||||
reader := viewPlacement(func() *views.Follower { return f })
|
||||
if _, err := reader.Keys(); err == nil {
|
||||
t.Fatal("the placement was read before its first view")
|
||||
}
|
||||
done := make(chan error, 1)
|
||||
go func() { done <- f.Start(context.Background()) }()
|
||||
count := func() int { mu.Lock(); defer mu.Unlock(); return len(held) }
|
||||
for end := time.Now().Add(5 * time.Second); count() == 0 && time.Now().Before(end); time.Sleep(10 * time.Millisecond) {
|
||||
}
|
||||
if count() != 1 {
|
||||
t.Fatalf("past its bound the vault wrote %d findings, want one", count())
|
||||
}
|
||||
mu.Lock()
|
||||
up = true
|
||||
mu.Unlock()
|
||||
if err := <-done; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if count() != 0 {
|
||||
t.Errorf("the answer left %d findings", count())
|
||||
}
|
||||
if keys, err := reader.Keys(); err != nil || len(keys) != 1 || keys[0] != "g14.node" {
|
||||
t.Errorf("keys %v, %v", keys, err)
|
||||
}
|
||||
}
|
||||
@@ -146,7 +146,7 @@ func TestNoBucketHoldsASecret(t *testing.T) {
|
||||
if err := json.Unmarshal(raw, &manifest); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if s, _ := json.Marshal(manifest["state"]); string(s) != `[{"history":2,"name":"bus-accounts"},"signers","bound","node-logins"]` {
|
||||
if s, _ := json.Marshal(manifest["state"]); string(s) != `[{"history":2,"name":"bus-accounts"},"signers","bound","node-logins","findings"]` {
|
||||
t.Errorf("the manifest declares buckets %s: a secret is in no bucket, sealed or not (ADR 0201 §13), and the "+
|
||||
"buckets are the accounts' hashes (ADR 0547 clause 9), the signing keys' public halves (ADR 0550 §2) and the "+
|
||||
"credentials' outcomes, names and generations alone (ADR 0570 clauses 3, 6) and each node's login in use (ADR 0583)", s)
|
||||
@@ -195,11 +195,9 @@ func TestNoBucketHoldsASecret(t *testing.T) {
|
||||
code = []byte(strings.Replace(string(code), "stdio.State(HaveRead).Keys(", "", 1))
|
||||
code = []byte(strings.Replace(string(code), "stdio.State(WantedRead).Watch(", "", 1))
|
||||
code = []byte(strings.Replace(string(code), "stdio.State(WantedRead).Get(", "", 1))
|
||||
// And the seat's placement, where modules stand (placed.go): its keys and one key, read.
|
||||
code = []byte(strings.Replace(string(code), "stdio.State(PlacementRead).Keys(", "", 1))
|
||||
code = []byte(strings.Replace(string(code), "stdio.State(PlacementRead).Get(", "", 1))
|
||||
// And one watch of its nodes' keys, for a node that leaves: its node-logins key goes with it (noderotation.go).
|
||||
code = []byte(strings.Replace(string(code), "stdio.State(PlacementRead).Watch(", "", 1))
|
||||
// And its findings (ADR 0615 §6): the finding of a first view unanswered, written and deleted, never a secret.
|
||||
code = []byte(strings.Replace(string(code), "stdio.State(FindingsState).Put(", "", 1))
|
||||
code = []byte(strings.Replace(string(code), "stdio.State(FindingsState).Delete(", "", 1))
|
||||
// And its outcome entries (novox/hq ADR 0570 clauses 3, 6): a value's name and generation, never a value; and
|
||||
// one read of the seat's provisions, which hold none.
|
||||
for _, call := range []string{"Get(", "Put(", "Delete(", "Keys("} {
|
||||
|
||||
@@ -44,7 +44,8 @@
|
||||
},
|
||||
"signers",
|
||||
"bound",
|
||||
"node-logins"
|
||||
"node-logins",
|
||||
"findings"
|
||||
],
|
||||
"receives": {
|
||||
"secret": "${dir:grants}/mesh.json"
|
||||
|
||||
Reference in New Issue
Block a user