One rule for the state a membership issues and a manifest keeps, in both forms (issue 1966, recovery) #177
@@ -4,6 +4,8 @@ import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -205,3 +207,21 @@ func setAsideAlso(unknown string, paths []string) string {
|
||||
}
|
||||
return unknown + " (read without " + strings.Join(paths, ", ") + ")"
|
||||
}
|
||||
|
||||
// KeptStates are the local names of the state a manifest keeps, each a key-value bucket of its own: the old `state`
|
||||
// section's and every key-value entry it asks for in `requires` (ADR 0595 §1, §11), sorted, once each. It is the one
|
||||
// rule every reader of what a module keeps calls (novox/hq issue 1966): a process's `bus.state` and a seat
|
||||
// contribution's state name one of these, in either form.
|
||||
func KeptStates(m Manifest) []string {
|
||||
var out []string
|
||||
for _, s := range m.State {
|
||||
out = append(out, s.Name)
|
||||
}
|
||||
for _, e := range m.BusState {
|
||||
if e.Storage == StorageKeyValue {
|
||||
out = append(out, e.Name)
|
||||
}
|
||||
}
|
||||
sort.Strings(out)
|
||||
return slices.Compact(out)
|
||||
}
|
||||
|
||||
@@ -78,6 +78,9 @@ func TestARequirementsLongForm(t *testing.T) {
|
||||
// 1966/AC3: a ledger type with each of the four effects round-trips through JSON unchanged.
|
||||
{"a ledger with its type", `[{"name":"records","storage":"ledger","template":"ledger","options":{"max-mib":1024},
|
||||
"types":[` + string(typeJSON) + `]}]`, "", []BusStateEntry{ledger}, nil, ""},
|
||||
// Issue 1966: a key-value entry of `requires` is kept state beside the old section's, by its local name.
|
||||
{"a key-value entry beside an old state", `[{"name":"holds","storage":"key-value","template":"state"}], "state": ["seen"]`,
|
||||
"", []BusStateEntry{{Name: "holds", Storage: StorageKeyValue, Template: TemplateState}}, nil, ""},
|
||||
// A field given twice at any depth is refused, named by its path: a decoder would keep the last without a word.
|
||||
{"a field given twice", `[], "requires": ["postgres-database"]`, "", nil, nil, "the field requires is given twice"},
|
||||
{"a field given twice deeper", `[{"name":"records","storage":"ledger","name":"other","template":"ledger"}]`,
|
||||
@@ -100,6 +103,14 @@ func TestARequirementsLongForm(t *testing.T) {
|
||||
if !reflect.DeepEqual(got.BusState, c.busState) {
|
||||
t.Errorf("%s: bus state read as %+v, want %+v", c.name, got.BusState, c.busState)
|
||||
}
|
||||
// Issue 1966: a ledger is no kept state; KeptStates names the old section's and the key-value entries' alone.
|
||||
var keeps []string
|
||||
if c.name == "a key-value entry beside an old state" {
|
||||
keeps = []string{"holds", "seen"}
|
||||
}
|
||||
if kept := KeptStates(got); !reflect.DeepEqual(kept, keeps) {
|
||||
t.Errorf("%s: kept states %v, want %v", c.name, kept, keeps)
|
||||
}
|
||||
for _, path := range c.aside {
|
||||
if !strings.Contains(got.SetAside(), path) {
|
||||
t.Errorf("%s: %s is not named among what was set aside: %q", c.name, path, got.SetAside())
|
||||
|
||||
@@ -6,6 +6,8 @@ package membership
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"slices"
|
||||
"sort"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
)
|
||||
@@ -171,3 +173,38 @@ type WaitIssued struct {
|
||||
Event string `json:"event"`
|
||||
Subject string `json:"subject"`
|
||||
}
|
||||
|
||||
// StateIssued is the state this membership issues its module's code by a local name, and whether it does: what every
|
||||
// runtime that serves a module's state calls reads, so no reader keeps a second copy of the rule (novox/hq issue 1966).
|
||||
// An entry of the old `state` list wins (ADR 0201); otherwise a binding of a key-value entry the module asks for in
|
||||
// `requires` is issued by its local name, as its own and written (ADR 0595 §1, §11: a moved module keeps its local
|
||||
// names), on the bucket the bus module named.
|
||||
func (m Membership) StateIssued(name string) (StateIssued, bool) {
|
||||
for _, s := range m.State {
|
||||
if s.Name == name {
|
||||
return s, true
|
||||
}
|
||||
}
|
||||
for _, b := range m.Bindings {
|
||||
if b.Local == name && b.Storage == catalogue.StorageKeyValue && b.Bucket != "" {
|
||||
return StateIssued{Name: b.Local, Bucket: b.Bucket, Writes: true}, true
|
||||
}
|
||||
}
|
||||
return StateIssued{}, false
|
||||
}
|
||||
|
||||
// StateNames are the local names StateIssued answers, the old list's and the key-value bindings', sorted, once each:
|
||||
// what a refusal names as issued.
|
||||
func (m Membership) StateNames() []string {
|
||||
var out []string
|
||||
for _, s := range m.State {
|
||||
out = append(out, s.Name)
|
||||
}
|
||||
for _, b := range m.Bindings {
|
||||
if b.Storage == catalogue.StorageKeyValue && b.Bucket != "" {
|
||||
out = append(out, b.Local)
|
||||
}
|
||||
}
|
||||
sort.Strings(out)
|
||||
return slices.Compact(out)
|
||||
}
|
||||
|
||||
@@ -59,6 +59,31 @@ func TestAMembershipRoundTripsAsIssued(t *testing.T) {
|
||||
if string(before) != `{"node":"ace","module":"forge","serves":[{"subject":"s"}],"emits":"e","tools":"t","state":[{"name":"repos","bucket":"forge_repos","writes":true}]}` {
|
||||
t.Fatalf("a membership without bindings encodes otherwise than before: %s", before)
|
||||
}
|
||||
// Issue 1966 (F1): what a membership issues by a local name is one rule. An old `state` entry wins; a key-value
|
||||
// binding of `requires` is issued by its local name, own and written; any other binding, and a name of neither, is not.
|
||||
issuing := Membership{State: []StateIssued{{Name: "repos", Bucket: "forge_repos"}},
|
||||
Bindings: []BusBinding{{Local: "holds", Storage: catalogue.StorageKeyValue, Template: catalogue.TemplateState, Bucket: "forge_holds"},
|
||||
{Local: "repos", Storage: catalogue.StorageKeyValue, Bucket: "forge_repos2"},
|
||||
{Local: "blobs", Storage: catalogue.StorageObjectStore, ObjectStore: "forge_blobs"},
|
||||
{Local: "events", Storage: catalogue.StorageStream, Stream: "forge_events"}}}
|
||||
for _, c := range []struct {
|
||||
name string
|
||||
want StateIssued
|
||||
ok bool
|
||||
}{
|
||||
{"holds", StateIssued{Name: "holds", Bucket: "forge_holds", Writes: true}, true},
|
||||
{"repos", StateIssued{Name: "repos", Bucket: "forge_repos"}, true},
|
||||
{"blobs", StateIssued{}, false},
|
||||
{"events", StateIssued{}, false},
|
||||
{"other", StateIssued{}, false},
|
||||
} {
|
||||
if got, ok := issuing.StateIssued(c.name); ok != c.ok || got != c.want {
|
||||
t.Errorf("StateIssued(%q) = %+v, %v; want %+v, %v", c.name, got, ok, c.want, c.ok)
|
||||
}
|
||||
}
|
||||
if got := issuing.StateNames(); !reflect.DeepEqual(got, []string{"holds", "repos"}) {
|
||||
t.Errorf("StateNames = %v", got)
|
||||
}
|
||||
var wire map[string]json.RawMessage
|
||||
_ = json.Unmarshal(raw, &wire)
|
||||
for _, k := range []string{"receives", "mesh", "seat-traffic"} {
|
||||
|
||||
Reference in New Issue
Block a user