The assigner's seat says assignments throughout: the type, the events and the defs; the legacy key form placement.<node> leaves the client (issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
This commit is contained in:
+30
-33
@@ -1,6 +1,6 @@
|
||||
// Package meshmoduleassignerclient is the logic every module that reads where modules are placed shares over the seat
|
||||
// mesh-module-assigner (novox/hq ADR 0567, ADR 0634 §4, ADR 0625 §4): the keys of its state `placement` assembled into a
|
||||
// node's placement, who stands on which node since when, who holds a seat, and what a placement says to the composition.
|
||||
// mesh-module-assigner (novox/hq ADR 0567, ADR 0634 §4, ADR 0625 §4): the keys of its state `assignments` assembled into a
|
||||
// node's assignments, who stands on which node since when, who holds a seat, and what a assignments says to the composition.
|
||||
// The layouts are the seat's contract's (package meshmoduleassigner); the check of a proposal is the assigner's own.
|
||||
package meshmoduleassignerclient
|
||||
|
||||
@@ -26,10 +26,7 @@ const Version = keys.Version
|
||||
// NodeWord is the last word of a node key, `<node>.node`; no module is named it.
|
||||
const NodeWord = "node"
|
||||
|
||||
const (
|
||||
legacyWord = "placement"
|
||||
digestLen = 16
|
||||
)
|
||||
const digestLen = 16
|
||||
|
||||
// ErrUnknownVersion is what a decode returns for a value whose version is missing or not Version.
|
||||
var ErrUnknownVersion = errors.New("meshmoduleassignerclient: unknown value version")
|
||||
@@ -62,9 +59,9 @@ func checkVersion(data []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// CheckPlacement refuses a placement that names a node or module that is not a name, a module twice, a port that is not
|
||||
// CheckAssignments refuses a assignments that names a node or module that is not a name, a module twice, a port that is not
|
||||
// a number, a binding or pin naming no provider, and a holding by a module not placed on the node.
|
||||
func CheckPlacement(p seat.Placement) error {
|
||||
func CheckAssignments(p seat.Assignments) error {
|
||||
if err := checkName("node", p.Node); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -74,7 +71,7 @@ func CheckPlacement(p seat.Placement) error {
|
||||
return err
|
||||
}
|
||||
if seen[m.Module] {
|
||||
return fmt.Errorf("meshmoduleassignerclient: the placement of %s names %s twice", p.Node, m.Module)
|
||||
return fmt.Errorf("meshmoduleassignerclient: the assignments of %s names %s twice", p.Node, m.Module)
|
||||
}
|
||||
seen[m.Module] = true
|
||||
for port, at := range m.Ports {
|
||||
@@ -93,51 +90,51 @@ func CheckPlacement(p seat.Placement) error {
|
||||
}
|
||||
for provision, to := range p.Pins {
|
||||
if provision == "" || to.Node == "" || to.Module == "" {
|
||||
return fmt.Errorf("meshmoduleassignerclient: the placement of %s has a pin with no provision, node or module", p.Node)
|
||||
return fmt.Errorf("meshmoduleassignerclient: the assignments of %s has a pin with no provision, node or module", p.Node)
|
||||
}
|
||||
}
|
||||
for _, h := range p.Holds {
|
||||
if h.Claim == "" || !seen[h.Module] {
|
||||
return fmt.Errorf("meshmoduleassignerclient: the placement of %s has %s hold %q, which is not a module placed there or names no seat", p.Node, h.Module, h.Claim)
|
||||
return fmt.Errorf("meshmoduleassignerclient: the assignments of %s has %s hold %q, which is not a module placed there or names no seat", p.Node, h.Module, h.Claim)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// EncodePlacement is p as JSON at Version; it refuses what DecodePlacement refuses.
|
||||
func EncodePlacement(p seat.Placement) ([]byte, error) {
|
||||
if err := CheckPlacement(p); err != nil {
|
||||
// EncodeAssignments is p as JSON at Version; it refuses what DecodeAssignments refuses.
|
||||
func EncodeAssignments(p seat.Assignments) ([]byte, error) {
|
||||
if err := CheckAssignments(p); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p.V = Version
|
||||
return json.Marshal(p)
|
||||
}
|
||||
|
||||
// DecodePlacement reads a placement file or key value (the form before the node and module keys).
|
||||
func DecodePlacement(data []byte) (seat.Placement, error) {
|
||||
// DecodeAssignments reads a assignments file or key value (the form before the node and module keys).
|
||||
func DecodeAssignments(data []byte) (seat.Assignments, error) {
|
||||
if err := checkVersion(data); err != nil {
|
||||
return seat.Placement{}, err
|
||||
return seat.Assignments{}, err
|
||||
}
|
||||
var p seat.Placement
|
||||
var p seat.Assignments
|
||||
if err := json.Unmarshal(data, &p); err != nil {
|
||||
return seat.Placement{}, fmt.Errorf("meshmoduleassignerclient: value does not fit its shape: %w", err)
|
||||
return seat.Assignments{}, fmt.Errorf("meshmoduleassignerclient: value does not fit its shape: %w", err)
|
||||
}
|
||||
if err := CheckPlacement(p); err != nil {
|
||||
return seat.Placement{}, err
|
||||
if err := CheckAssignments(p); err != nil {
|
||||
return seat.Assignments{}, err
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// SamePlacement says whether two placements are the same value, so the holder writes only when they differ.
|
||||
func SamePlacement(a, b seat.Placement) bool {
|
||||
// SameAssignments says whether two assignments are the same value, so the holder writes only when they differ.
|
||||
func SameAssignments(a, b seat.Assignments) bool {
|
||||
a.Revision, b.Revision = "", ""
|
||||
ja, errA := EncodePlacement(a)
|
||||
jb, errB := EncodePlacement(b)
|
||||
ja, errA := EncodeAssignments(a)
|
||||
jb, errB := EncodeAssignments(b)
|
||||
return errA == nil && errB == nil && string(ja) == string(jb)
|
||||
}
|
||||
|
||||
// Assigned is the modules placed on the node, in the file's order.
|
||||
func Assigned(p seat.Placement) []string {
|
||||
func Assigned(p seat.Assignments) []string {
|
||||
out := make([]string, 0, len(p.Modules))
|
||||
for _, m := range p.Modules {
|
||||
out = append(out, m.Module)
|
||||
@@ -151,7 +148,7 @@ func Chosen(p seat.Provider) catalogue.Chosen {
|
||||
}
|
||||
|
||||
// ChosenPins is the node's pins as the composition reads them; nil when it has none.
|
||||
func ChosenPins(p seat.Placement) map[string]catalogue.Chosen {
|
||||
func ChosenPins(p seat.Assignments) map[string]catalogue.Chosen {
|
||||
if len(p.Pins) == 0 {
|
||||
return nil
|
||||
}
|
||||
@@ -163,7 +160,7 @@ func ChosenPins(p seat.Placement) map[string]catalogue.Chosen {
|
||||
}
|
||||
|
||||
// Bound is, by consumer module and provision, where each binding sends it; nil when none.
|
||||
func Bound(p seat.Placement) map[string]map[string]catalogue.Chosen {
|
||||
func Bound(p seat.Assignments) map[string]map[string]catalogue.Chosen {
|
||||
var out map[string]map[string]catalogue.Chosen
|
||||
for _, m := range p.Modules {
|
||||
if len(m.Bindings) == 0 {
|
||||
@@ -182,7 +179,7 @@ func Bound(p seat.Placement) map[string]map[string]catalogue.Chosen {
|
||||
|
||||
// HostPorts is, by module and the port it declares, the host port it is published on; nil when none. The check has
|
||||
// refused a port that is not a number, so none is dropped here.
|
||||
func HostPorts(p seat.Placement) map[string]map[int]int {
|
||||
func HostPorts(p seat.Assignments) map[string]map[int]int {
|
||||
var out map[string]map[int]int
|
||||
for _, m := range p.Modules {
|
||||
if len(m.Ports) == 0 {
|
||||
@@ -202,7 +199,7 @@ func HostPorts(p seat.Placement) map[string]map[int]int {
|
||||
|
||||
// Layers is one module's settings layers on the node, as the composition reads them: the whole mesh's layer when it has
|
||||
// one, then the node's, which carries the module's config values as mesh-config resolved them for this node (values,
|
||||
// nil when none) beside its placement; a module with no layer and no value has none. A module in the settings form
|
||||
// nil when none) beside its assignments; a module with no layer and no value has none. A module in the settings form
|
||||
// carries its values in the file (MeshWideValues, Values), which then stand for mesh-config's.
|
||||
func Layers(m seat.PlacedModule, node string, values map[string]any) []catalogue.Layer {
|
||||
var out []catalogue.Layer
|
||||
@@ -229,7 +226,7 @@ func Layers(m seat.PlacedModule, node string, values map[string]any) []catalogue
|
||||
}
|
||||
|
||||
// Held is the node's holdings as the composition reads them.
|
||||
func Held(p seat.Placement) []catalogue.Held {
|
||||
func Held(p seat.Assignments) []catalogue.Held {
|
||||
out := make([]catalogue.Held, 0, len(p.Holds))
|
||||
for _, h := range p.Holds {
|
||||
out = append(out, catalogue.Held{Claim: h.Claim, Scope: h.Scope, Node: p.Node, Module: h.Module, Site: h.Site, Kind: h.Kind})
|
||||
@@ -238,10 +235,10 @@ func Held(p seat.Placement) []catalogue.Held {
|
||||
}
|
||||
|
||||
// InsideAddresses is every node's address on the private network, sorted and without repeats.
|
||||
func InsideAddresses(placements map[string]seat.Placement) []string {
|
||||
func InsideAddresses(assignments map[string]seat.Assignments) []string {
|
||||
seen := map[string]bool{}
|
||||
out := []string{}
|
||||
for _, p := range placements {
|
||||
for _, p := range assignments {
|
||||
if a := strings.TrimSpace(p.Decided.Address); a != "" && !seen[a] {
|
||||
seen[a] = true
|
||||
out = append(out, a)
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
)
|
||||
|
||||
func TestPlacementClient(t *testing.T) {
|
||||
func TestAssignmentsClient(t *testing.T) {
|
||||
t.Run("proposal id", func(t *testing.T) {
|
||||
for id, ok := range map[string]bool{"p-20261028t183000-1a2b3c": true, "abc": true, "ab": false, "A-bc": false, "-abc": false, "a.bc": false, "": false} {
|
||||
if (CheckProposalID(id) == nil) != ok {
|
||||
@@ -20,17 +20,16 @@ func TestPlacementClient(t *testing.T) {
|
||||
}
|
||||
})
|
||||
t.Run("key and file", func(t *testing.T) {
|
||||
// Research 072 row 37: one node key and one key per module, a legacy key read beside them, any other refused.
|
||||
// Research 072 row 37: one node key and one key per module, any other refused.
|
||||
for key, want := range map[string]struct {
|
||||
node, module string
|
||||
kind KeyKind
|
||||
}{"g14.node": {"g14", "", NodeKind}, "g14.app": {"g14", "app", ModuleKind},
|
||||
"placement.g14": {"g14", "", LegacyKind}} {
|
||||
}{"g14.node": {"g14", "", NodeKind}, "g14.app": {"g14", "app", ModuleKind}} {
|
||||
if n, m, k, err := ParseKey(key); err != nil || n != want.node || m != want.module || k != want.kind {
|
||||
t.Errorf("ParseKey(%q) = %q, %q, %d, %v", key, n, m, k, err)
|
||||
}
|
||||
}
|
||||
for _, bad := range []string{"want.g14.x", "placement.", "placement.a.b", "g14", "g14.a.b", ".node"} {
|
||||
for _, bad := range []string{"want.g14.x", "g14", "g14.a.b", ".node"} {
|
||||
if _, _, _, err := ParseKey(bad); err == nil {
|
||||
t.Errorf("ParseKey(%q) took it", bad)
|
||||
}
|
||||
@@ -44,19 +43,15 @@ func TestPlacementClient(t *testing.T) {
|
||||
if k, err := NodeKeys("g14"); err != nil || k != "g14.>" {
|
||||
t.Errorf("NodeKeys = %q, %v", k, err)
|
||||
}
|
||||
if k, err := LegacyKey("g14"); err != nil || k != "placement.g14" {
|
||||
t.Errorf("LegacyKey = %q, %v", k, err)
|
||||
}
|
||||
for name, err := range map[string]error{"a dotted node": func() error { _, err := NodeKey("a.b"); return err }(),
|
||||
"a node named placement": func() error { _, err := NodeKey("placement"); return err }(),
|
||||
"a module named node": func() error { _, err := ModuleKey("g14", "node"); return err }()} {
|
||||
"a module named node": func() error { _, err := ModuleKey("g14", "node"); return err }()} {
|
||||
if err == nil {
|
||||
t.Errorf("%s was taken", name)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
g14 := seat.Placement{Node: "g14", Revision: "abc",
|
||||
g14 := seat.Assignments{Node: "g14", Revision: "abc",
|
||||
Decided: seat.NodeDecided{Endpoint: "g14.example:51820", Site: "home", Address: "10.77.0.2", Account: "op", Taken: []string{"zsh"}},
|
||||
Modules: []seat.PlacedModule{
|
||||
{Module: "zsh"},
|
||||
@@ -65,24 +60,24 @@ func TestPlacementClient(t *testing.T) {
|
||||
Pins: map[string]seat.Provider{"shell": {Node: "g14", Module: "zsh"}},
|
||||
Holds: []seat.Holding{{Claim: "node-shell", Scope: "node", Module: "zsh"}},
|
||||
}
|
||||
novox := seat.Placement{Node: "novox", Decided: seat.NodeDecided{Hub: true, Address: "10.77.0.1"}, Modules: []seat.PlacedModule{{Module: "postgres", Ports: map[string]int64{"5432": 5432}}},
|
||||
novox := seat.Assignments{Node: "novox", Decided: seat.NodeDecided{Hub: true, Address: "10.77.0.1"}, Modules: []seat.PlacedModule{{Module: "postgres", Ports: map[string]int64{"5432": 5432}}},
|
||||
Holds: []seat.Holding{{Claim: "mesh-example", Scope: "mesh", Module: "postgres"}}}
|
||||
|
||||
t.Run("value", func(t *testing.T) {
|
||||
raw, err := EncodePlacement(g14)
|
||||
raw, err := EncodeAssignments(g14)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
back, err := DecodePlacement(raw)
|
||||
if err != nil || back.V != Version || !SamePlacement(back, g14) {
|
||||
back, err := DecodeAssignments(raw)
|
||||
if err != nil || back.V != Version || !SameAssignments(back, g14) {
|
||||
t.Fatalf("round trip: %+v, %v", back, err)
|
||||
}
|
||||
moved := g14
|
||||
moved.Revision = "def"
|
||||
if !SamePlacement(g14, moved) {
|
||||
if !SameAssignments(g14, moved) {
|
||||
t.Error("a new revision alone counts as a change")
|
||||
}
|
||||
if _, err := DecodePlacement([]byte(`{"v":9,"node":"g14","modules":[]}`)); !errors.Is(err, ErrUnknownVersion) {
|
||||
if _, err := DecodeAssignments([]byte(`{"v":9,"node":"g14","modules":[]}`)); !errors.Is(err, ErrUnknownVersion) {
|
||||
t.Errorf("version 9: %v", err)
|
||||
}
|
||||
for name, bad := range map[string]string{
|
||||
@@ -95,7 +90,7 @@ func TestPlacementClient(t *testing.T) {
|
||||
"node not a name": `{"v":1,"node":"g.14","modules":[]}`,
|
||||
"value not shaped": `{"v":1,"node":"g14","modules":{}}`,
|
||||
} {
|
||||
if _, err := DecodePlacement([]byte(bad)); err == nil {
|
||||
if _, err := DecodeAssignments([]byte(bad)); err == nil {
|
||||
t.Errorf("%s: taken", name)
|
||||
}
|
||||
}
|
||||
@@ -134,37 +129,26 @@ func TestPlacementClient(t *testing.T) {
|
||||
if got := Layers(seat.PlacedModule{Module: "a"}, "g14", nil); got != nil {
|
||||
t.Errorf("no layer and no value is no layer: %#v", got)
|
||||
}
|
||||
empty := seat.Placement{Node: "x"}
|
||||
empty := seat.Assignments{Node: "x"}
|
||||
if ChosenPins(empty) != nil || Bound(empty) != nil || HostPorts(empty) != nil || len(Held(empty)) != 0 {
|
||||
t.Error("an empty placement reads as something")
|
||||
t.Error("an empty assignments reads as something")
|
||||
}
|
||||
})
|
||||
|
||||
// Research 072 row 37: a node's placement as its node key and module keys, written so a reader never takes a part of
|
||||
// a write, read beside the form before, and converted from it once.
|
||||
// Research 072 row 37: a node's assignments as its node key and module keys, written so a reader never takes a part of
|
||||
// a write,
|
||||
t.Run("one key per module and node", func(t *testing.T) {
|
||||
values, err := PlacementValues(g14)
|
||||
if err != nil || len(values) != 4 || values["g14.node"] == nil || values["g14.zsh"] == nil || values["g14.app"] == nil ||
|
||||
values["placement.g14"] == nil {
|
||||
t.Fatalf("PlacementValues = %v, %v", values, err)
|
||||
values, err := AssignmentsValues(g14)
|
||||
if err != nil || len(values) != 3 || values["g14.node"] == nil || values["g14.zsh"] == nil || values["g14.app"] == nil {
|
||||
t.Fatalf("AssignmentsValues = %v, %v", values, err)
|
||||
}
|
||||
got, err := PlacementsOf(values)
|
||||
if err != nil || !SamePlacement(got["g14"], g14) || len(got) != 1 {
|
||||
t.Fatalf("PlacementsOf = %+v, %v", got, err)
|
||||
got, err := AssignmentsOf(values)
|
||||
if err != nil || !SameAssignments(got["g14"], g14) || len(got) != 1 {
|
||||
t.Fatalf("AssignmentsOf = %+v, %v", got, err)
|
||||
}
|
||||
if got := InsideAddresses(map[string]seat.Placement{"g14": g14, "novox": novox, "x": {Node: "x"}, "y": {Node: "y", Decided: seat.NodeDecided{Address: " 10.77.0.1"}}}); strings.Join(got, ",") != "10.77.0.1,10.77.0.2" {
|
||||
if got := InsideAddresses(map[string]seat.Assignments{"g14": g14, "novox": novox, "x": {Node: "x"}, "y": {Node: "y", Decided: seat.NodeDecided{Address: " 10.77.0.1"}}}); strings.Join(got, ",") != "10.77.0.1,10.77.0.2" {
|
||||
t.Errorf("InsideAddresses = %v", got)
|
||||
}
|
||||
legacy, _ := EncodePlacement(novox)
|
||||
stale, _ := EncodePlacement(seat.Placement{Node: "g14"})
|
||||
both := map[string][]byte{"placement.novox": legacy, "placement.g14": stale}
|
||||
for k, v := range values {
|
||||
both[k] = v
|
||||
}
|
||||
if got, err := PlacementsOf(both); err != nil || !SamePlacement(got["g14"], g14) || !SamePlacement(got["novox"], novox) {
|
||||
t.Errorf("a node key wins over a legacy key, and a legacy key alone places its node: %+v, %v", got, err)
|
||||
}
|
||||
|
||||
keys := NewKeys()
|
||||
rev := uint64(0)
|
||||
apply := func(writes []KeyWrite, upTo int) {
|
||||
@@ -179,27 +163,19 @@ func TestPlacementClient(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
// The form before converts once: its node and module keys, the legacy key left for the readers not yet delivered.
|
||||
// A node written whole onto no keys: its module keys, then its node key.
|
||||
rev++
|
||||
legacyG14, _ := EncodePlacement(g14)
|
||||
if _, err := keys.Put("placement.g14", legacyG14, rev, false); err != nil || !keys.Legacy("g14") {
|
||||
t.Fatalf("the legacy key: %v", err)
|
||||
first, err := keys.Writes("g14", &g14)
|
||||
if err != nil || len(first) != 3 || !first[2].Commit {
|
||||
t.Fatalf("Writes = %+v, %v", first, err)
|
||||
}
|
||||
migrations, err := keys.Migrations()
|
||||
if err != nil || len(migrations["g14"]) != 3 || !migrations["g14"][2].Commit {
|
||||
// The legacy key holds the placement already: no mirror write.
|
||||
t.Fatalf("Migrations = %+v, %v", migrations, err)
|
||||
}
|
||||
apply(migrations["g14"], -1)
|
||||
if m, _ := keys.Migrations(); len(m) != 0 || keys.Legacy("g14") {
|
||||
t.Fatalf("a converted node converts again: %+v", m)
|
||||
}
|
||||
if p, r, ok := keys.Of("g14"); !ok || !SamePlacement(p, g14) || r != rev {
|
||||
t.Fatalf("Of after the conversion = %+v, %d, %v", p, r, ok)
|
||||
apply(first, -1)
|
||||
if p, r, ok := keys.Of("g14"); !ok || !SameAssignments(p, g14) || r != rev {
|
||||
t.Fatalf("Of after the write = %+v, %d, %v", p, r, ok)
|
||||
}
|
||||
|
||||
// A write under way is never read half: app's new port is not read before the node key names it.
|
||||
after := clonePlacement(g14)
|
||||
after := cloneAssignments(g14)
|
||||
after.Modules = []seat.PlacedModule{{Module: "app", Ports: map[string]int64{"8080": 18081}}, {Module: "new"}}
|
||||
after.Holds, after.Pins = nil, nil
|
||||
writes, err := keys.Writes("g14", &after)
|
||||
@@ -210,32 +186,28 @@ func TestPlacementClient(t *testing.T) {
|
||||
for _, w := range writes {
|
||||
kinds = append(kinds, fmt.Sprintf("%s:%v:%d", w.Key, w.Value != nil, w.Base))
|
||||
}
|
||||
if strings.Join(kinds, " ") != "g14.app:true:3 g14.new:true:0 g14.node:true:4 placement.g14:true:1 g14.zsh:false:2" ||
|
||||
!writes[3].Mirror {
|
||||
if strings.Join(kinds, " ") != "g14.app:true:3 g14.new:true:0 g14.node:true:4 g14.zsh:false:2" {
|
||||
t.Fatalf("Writes = %v", kinds)
|
||||
}
|
||||
apply(writes, 2)
|
||||
if p, _, _ := keys.Of("g14"); !SamePlacement(p, g14) {
|
||||
if p, _, _ := keys.Of("g14"); !SameAssignments(p, g14) {
|
||||
t.Errorf("the module keys written before the node key are read: %+v", p)
|
||||
}
|
||||
apply(writes[2:], -1)
|
||||
if p, _, _ := keys.Of("g14"); !SamePlacement(p, after) {
|
||||
if p, _, _ := keys.Of("g14"); !SameAssignments(p, after) {
|
||||
t.Errorf("the write done is not read whole: %+v", p)
|
||||
}
|
||||
if legacy, _ := PlacementsOf(map[string][]byte{"placement.g14": writes[3].Value}); !SamePlacement(legacy["g14"], after) {
|
||||
t.Errorf("the mirror is not the placement written: %+v", legacy)
|
||||
}
|
||||
// A module key that stays is not written again, nor a mirror that holds it.
|
||||
// A module key that stays is not written again.
|
||||
if again, _ := keys.Writes("g14", &after); len(again) != 1 || !again[0].Commit {
|
||||
t.Errorf("an unchanged placement writes %+v", again)
|
||||
t.Errorf("an unchanged assignments writes %+v", again)
|
||||
}
|
||||
// Keys that do not agree keep what the node had, and say so.
|
||||
stray, _ := EncodeModule(seat.AssignmentsByNodeModuleValue{Node: "g14", Placed: seat.PlacedModule{Module: "app"}})
|
||||
if _, err := keys.Put("g14.app", stray, 99, false); err == nil {
|
||||
t.Error("a module key the node key does not name the value of was taken silently")
|
||||
}
|
||||
if p, _, _ := keys.Of("g14"); !SamePlacement(p, after) {
|
||||
t.Errorf("a stray module key moved the placement: %+v", p)
|
||||
if p, _, _ := keys.Of("g14"); !SameAssignments(p, after) {
|
||||
t.Errorf("a stray module key moved the assignments: %+v", p)
|
||||
}
|
||||
// The holder's next write puts the module key right again.
|
||||
repair, _ := keys.Writes("g14", &after)
|
||||
@@ -243,16 +215,16 @@ func TestPlacementClient(t *testing.T) {
|
||||
t.Fatalf("the repair writes %+v", repair)
|
||||
}
|
||||
apply(repair, -1)
|
||||
// No placement: the legacy key first, then the node key, then the module keys; none is read after.
|
||||
// No assignments: the node key first, then the module keys; none is read after.
|
||||
gone, _ := keys.Writes("g14", nil)
|
||||
kinds = nil
|
||||
for _, w := range gone {
|
||||
kinds = append(kinds, w.Key)
|
||||
}
|
||||
if strings.Join(kinds, " ") != "placement.g14 g14.node g14.app g14.new" {
|
||||
if strings.Join(kinds, " ") != "g14.node g14.app g14.new" {
|
||||
t.Errorf("Writes(nil) = %v", kinds)
|
||||
}
|
||||
apply(gone, 2)
|
||||
apply(gone, 1)
|
||||
if _, _, ok := keys.Of("g14"); ok {
|
||||
t.Error("a node key deleted still places its node")
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ func TestDigestIsTheFirstWritersAndHoldersAndStandingRead(t *testing.T) {
|
||||
|
||||
keys := map[string][]byte{
|
||||
"mesh-module-assigner": []byte(`{"v":1,"seat":"mesh-module-assigner","scope":"mesh","holders":[{"module":"mesh-delivery-assigner","node":"novox"}]}`),
|
||||
"node-example.ace": []byte(`{"v":1,"seat":"node-example","scope":"node","node":"ace","holders":[{"module":"node-example","node":"ace"}]}`),
|
||||
"node-example.ace": []byte(`{"v":1,"seat":"node-example","scope":"node","node":"ace","holders":[{"module":"node-example","node":"ace"}]}`),
|
||||
"mesh-vault": []byte(`{"v":2,"seat":"mesh-vault","scope":"mesh","holders":[]}`),
|
||||
}
|
||||
for _, c := range []struct {
|
||||
|
||||
@@ -11,29 +11,26 @@ import (
|
||||
seat "git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
|
||||
)
|
||||
|
||||
// **One key per module and node** (research 072 row 37; issue 774). The seat's state `placement` keeps a node's placement
|
||||
// **One key per module and node** (research 072 row 37; issue 774). The seat's state `assignments` keeps a node's assignments
|
||||
// under the node's own keys, so a node watches `<node>.>` alone and a change of one module writes that module's key:
|
||||
// `<node>.node`, the node key (what a person decided of the node, its pins and holdings, and the modules placed on it,
|
||||
// each with the digest of its module key's value), and `<node>.<module>`, a module key. The node key is the commit of a
|
||||
// write; a reader takes for each module the node key names the value of its module key whose digest the node key names,
|
||||
// the current one once the node key is written, the previous one while the write is under way, so a reader never
|
||||
// composes a part of a write. The form before, one value `placement.<node>` per node, is still read and mirrored until
|
||||
// a later change deletes it.
|
||||
// composes a part of a write.
|
||||
|
||||
// KeyKind is the kind of a key of the state `placement`.
|
||||
// KeyKind is the kind of a key of the state `assignments`.
|
||||
type KeyKind int
|
||||
|
||||
// The kinds of key: a node key, a module key, and the form before (one value per node).
|
||||
// The kinds of key: a node key and a module key.
|
||||
const (
|
||||
NodeKind KeyKind = iota + 1
|
||||
ModuleKind
|
||||
LegacyKind
|
||||
)
|
||||
|
||||
// NodeKey is a node's node key, `<node>.node`. A node named `placement` is refused: its keys would read as the
|
||||
// legacy form's.
|
||||
// NodeKey is a node's node key, `<node>.node`.
|
||||
func NodeKey(node string) (string, error) {
|
||||
if err := checkPlacementNode(node); err != nil {
|
||||
if err := checkAssignmentsNode(node); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return node + "." + NodeWord, nil
|
||||
@@ -41,41 +38,30 @@ func NodeKey(node string) (string, error) {
|
||||
|
||||
// ModuleKey is a module's key on a node, `<node>.<module>`; a module named `node` is refused.
|
||||
func ModuleKey(node, module string) (string, error) {
|
||||
if err := checkPlacementNode(node); err != nil {
|
||||
if err := checkAssignmentsNode(node); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := checkPlacementModule(module); err != nil {
|
||||
if err := checkAssignmentsModule(module); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return node + "." + module, nil
|
||||
}
|
||||
|
||||
// NodeKeys is the pattern of every key of one node, `<node>.>`: what a node watches of its own placement.
|
||||
// NodeKeys is the pattern of every key of one node, `<node>.>`: what a node watches of its own assignments.
|
||||
func NodeKeys(node string) (string, error) {
|
||||
if err := checkPlacementNode(node); err != nil {
|
||||
if err := checkAssignmentsNode(node); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return node + ".>", nil
|
||||
}
|
||||
|
||||
// LegacyKey is a node's key in the form before, `placement.<node>`, read while this change is delivered.
|
||||
func LegacyKey(node string) (string, error) {
|
||||
if err := checkName("node", node); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return legacyWord + "." + node, nil
|
||||
}
|
||||
|
||||
// ParseKey reads a key of the state `placement` in either form: its node, its module (empty for a node key and
|
||||
// a legacy key) and its kind. Any other key is refused, naming it.
|
||||
// ParseKey reads a key of the state `assignments` its node, its module (empty for a node key) and its kind. Any other key is refused, naming it.
|
||||
func ParseKey(key string) (node, module string, kind KeyKind, err error) {
|
||||
first, rest, ok := strings.Cut(key, ".")
|
||||
if !ok || strings.Contains(rest, ".") || checkName("node", first) != nil || checkName("module", rest) != nil {
|
||||
return "", "", 0, fmt.Errorf("meshmoduleassignerclient: %q is not a placement key, <node>.node or <node>.<module>", key)
|
||||
return "", "", 0, fmt.Errorf("meshmoduleassignerclient: %q is not a assignments key, <node>.node or <node>.<module>", key)
|
||||
}
|
||||
switch {
|
||||
case first == legacyWord:
|
||||
return rest, "", LegacyKind, nil
|
||||
case rest == NodeWord:
|
||||
return first, "", NodeKind, nil
|
||||
default:
|
||||
@@ -83,17 +69,14 @@ func ParseKey(key string) (node, module string, kind KeyKind, err error) {
|
||||
}
|
||||
}
|
||||
|
||||
func checkPlacementNode(node string) error {
|
||||
func checkAssignmentsNode(node string) error {
|
||||
if err := checkName("node", node); err != nil {
|
||||
return err
|
||||
}
|
||||
if node == legacyWord {
|
||||
return fmt.Errorf("meshmoduleassignerclient: no node is named %q: its placement keys would read as the form before", legacyWord)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkPlacementModule(module string) error {
|
||||
func checkAssignmentsModule(module string) error {
|
||||
if err := checkName("module", module); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -105,7 +88,7 @@ func checkPlacementModule(module string) error {
|
||||
|
||||
// EncodeNode is n as JSON at Version.
|
||||
func EncodeNode(n seat.AssignmentsByNodeNodeValue) ([]byte, error) {
|
||||
if err := checkPlacementNode(n.Node); err != nil {
|
||||
if err := checkAssignmentsNode(n.Node); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
n.V = Version
|
||||
@@ -122,12 +105,12 @@ func DecodeNode(data []byte) (seat.AssignmentsByNodeNodeValue, error) {
|
||||
if err != nil {
|
||||
return n, fmt.Errorf("meshmoduleassignerclient: value does not fit its shape: %w", err)
|
||||
}
|
||||
if err := checkPlacementNode(n.Node); err != nil {
|
||||
if err := checkAssignmentsNode(n.Node); err != nil {
|
||||
return seat.AssignmentsByNodeNodeValue{}, err
|
||||
}
|
||||
seen := map[string]bool{}
|
||||
for _, r := range n.Modules {
|
||||
if err := checkPlacementModule(r.Module); err != nil {
|
||||
if err := checkAssignmentsModule(r.Module); err != nil {
|
||||
return seat.AssignmentsByNodeNodeValue{}, err
|
||||
}
|
||||
if seen[r.Module] || r.Digest == "" {
|
||||
@@ -140,10 +123,10 @@ func DecodeNode(data []byte) (seat.AssignmentsByNodeNodeValue, error) {
|
||||
|
||||
// EncodeModule is m as JSON at Version.
|
||||
func EncodeModule(m seat.AssignmentsByNodeModuleValue) ([]byte, error) {
|
||||
if err := checkPlacementNode(m.Node); err != nil {
|
||||
if err := checkAssignmentsNode(m.Node); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := checkPlacementModule(m.Placed.Module); err != nil {
|
||||
if err := checkAssignmentsModule(m.Placed.Module); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if m.Previous != nil && m.Previous.Module != m.Placed.Module {
|
||||
@@ -162,20 +145,20 @@ func DecodeModule(data []byte) (seat.AssignmentsByNodeModuleValue, error) {
|
||||
if err != nil {
|
||||
return m, fmt.Errorf("meshmoduleassignerclient: value does not fit its shape: %w", err)
|
||||
}
|
||||
if err := checkPlacementNode(m.Node); err != nil {
|
||||
if err := checkAssignmentsNode(m.Node); err != nil {
|
||||
return seat.AssignmentsByNodeModuleValue{}, err
|
||||
}
|
||||
if err := checkPlacementModule(m.Placed.Module); err != nil {
|
||||
if err := checkAssignmentsModule(m.Placed.Module); err != nil {
|
||||
return seat.AssignmentsByNodeModuleValue{}, err
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// Keys is the state `placement` as a reader holds it, key by key in either form, and every node's placement
|
||||
// Keys is the state `assignments` as a reader holds it, key by key in either form, and every node's assignments
|
||||
// assembled from it: the one reader of the state's keys for every part (issue 774), so no part parses them itself. A
|
||||
// write of the holder's under way is never assembled half: a module key's value is taken only as far as the node key
|
||||
// names its digest (see the package's comment above NodeWord), and a node whose keys do not agree keeps the
|
||||
// placement last assembled for it, Put saying why.
|
||||
// assignments last assembled for it, Put saying why.
|
||||
//
|
||||
// Safe for use from a watch and from a reader at once.
|
||||
type Keys struct {
|
||||
@@ -189,18 +172,16 @@ type keysOf struct {
|
||||
nodeRev uint64
|
||||
modules map[string]seat.AssignmentsByNodeModuleValue
|
||||
moduleRevs map[string]uint64
|
||||
legacy *seat.Placement
|
||||
legacyRev uint64
|
||||
last *seat.Placement // the placement last assembled whole, nil when none
|
||||
last *seat.Assignments // the assignments last assembled whole, nil when none
|
||||
}
|
||||
|
||||
// NewKeys holds no key yet.
|
||||
func NewKeys() *Keys { return &Keys{nodes: map[string]*keysOf{}} }
|
||||
|
||||
// PlacementsOf is every node's placement assembled from the state's keys as a reader holds them (key to value; a
|
||||
// AssignmentsOf is every node's assignments assembled from the state's keys as a reader holds them (key to value; a
|
||||
// deleted key absent), and an error naming every key that did not read and every node whose keys do not agree (that
|
||||
// node is then assembled from its module keys' values as they are).
|
||||
func PlacementsOf(keys map[string][]byte) (map[string]seat.Placement, error) {
|
||||
func AssignmentsOf(keys map[string][]byte) (map[string]seat.Assignments, error) {
|
||||
k := NewKeys()
|
||||
var errs []error
|
||||
names := make([]string, 0, len(keys))
|
||||
@@ -278,19 +259,6 @@ func (k *Keys) put(key string, value []byte, revision uint64, deleted bool) (nod
|
||||
// Its revision is kept even when its value does not read, so the holder can write over it.
|
||||
of.moduleRevs[module] = revision
|
||||
}
|
||||
case LegacyKind:
|
||||
of.legacy, of.legacyRev = nil, 0
|
||||
if !deleted {
|
||||
p, derr := DecodePlacement(value)
|
||||
switch {
|
||||
case derr != nil:
|
||||
readErr = fmt.Errorf("the placement of %s does not read, so it places nothing there: %w", node, derr)
|
||||
case p.Node != node:
|
||||
readErr = fmt.Errorf("the placement under %s names the node %q, so it places nothing there", key, p.Node)
|
||||
default:
|
||||
of.legacy, of.legacyRev = &p, revision
|
||||
}
|
||||
}
|
||||
}
|
||||
p, whole, err := of.assemble(node)
|
||||
switch {
|
||||
@@ -302,18 +270,14 @@ func (k *Keys) put(key string, value []byte, revision uint64, deleted bool) (nod
|
||||
return node, readErr, err
|
||||
}
|
||||
|
||||
// assemble is the node's placement from its keys: nil when none places it, whole false (and an error) when the node key
|
||||
// assemble is the node's assignments from its keys: nil when none places it, whole false (and an error) when the node key
|
||||
// names a module digest no module key's value has.
|
||||
func (of *keysOf) assemble(node string) (*seat.Placement, bool, error) {
|
||||
func (of *keysOf) assemble(node string) (*seat.Assignments, bool, error) {
|
||||
if of.node == nil {
|
||||
if of.legacy == nil {
|
||||
return nil, true, nil
|
||||
}
|
||||
p := clonePlacement(*of.legacy)
|
||||
return &p, true, nil
|
||||
return nil, true, nil
|
||||
}
|
||||
n := of.node
|
||||
p := seat.Placement{V: Version, Node: node, Decided: n.Decided, Pins: n.Pins, Holds: n.Holds, Modules: make([]seat.PlacedModule, 0, len(n.Modules))}
|
||||
p := seat.Assignments{V: Version, Node: node, Decided: n.Decided, Pins: n.Pins, Holds: n.Holds, Modules: make([]seat.PlacedModule, 0, len(n.Modules))}
|
||||
var astray []string
|
||||
for _, r := range n.Modules {
|
||||
m, ok := of.modules[r.Module]
|
||||
@@ -330,43 +294,39 @@ func (of *keysOf) assemble(node string) (*seat.Placement, bool, error) {
|
||||
}
|
||||
}
|
||||
if len(astray) > 0 {
|
||||
return &p, false, fmt.Errorf("the node key of %s names %s at a value no module key holds: %s keeps the placement it had",
|
||||
return &p, false, fmt.Errorf("the node key of %s names %s at a value no module key holds: %s keeps the assignments it had",
|
||||
node, strings.Join(astray, ", "), node)
|
||||
}
|
||||
return &p, true, nil
|
||||
}
|
||||
|
||||
// Of is node's placement and the revision of the key it was read from (its node key, or its legacy key), and whether
|
||||
// Of is node's assignments and the revision of the key it was read from (its node key), and whether
|
||||
// any key places it.
|
||||
func (k *Keys) Of(node string) (seat.Placement, uint64, bool) {
|
||||
func (k *Keys) Of(node string) (seat.Assignments, uint64, bool) {
|
||||
k.mu.Lock()
|
||||
defer k.mu.Unlock()
|
||||
of := k.nodes[node]
|
||||
if of == nil || of.last == nil {
|
||||
return seat.Placement{}, 0, false
|
||||
return seat.Assignments{}, 0, false
|
||||
}
|
||||
rev := of.nodeRev
|
||||
if of.node == nil {
|
||||
rev = of.legacyRev
|
||||
}
|
||||
return clonePlacement(*of.last), rev, true
|
||||
return cloneAssignments(*of.last), of.nodeRev, true
|
||||
}
|
||||
|
||||
// All is every node's placement, by node, and the revision of each one's key (Of).
|
||||
func (k *Keys) All() map[string]seat.Placement {
|
||||
// All is every node's assignments, by node, and the revision of each one's key (Of).
|
||||
func (k *Keys) All() map[string]seat.Assignments {
|
||||
out, _ := k.AllRevisions()
|
||||
return out
|
||||
}
|
||||
|
||||
// AllRevisions is All with the revision of each node's key.
|
||||
func (k *Keys) AllRevisions() (map[string]seat.Placement, map[string]uint64) {
|
||||
func (k *Keys) AllRevisions() (map[string]seat.Assignments, map[string]uint64) {
|
||||
k.mu.Lock()
|
||||
nodes := make([]string, 0, len(k.nodes))
|
||||
for n := range k.nodes {
|
||||
nodes = append(nodes, n)
|
||||
}
|
||||
k.mu.Unlock()
|
||||
out, revs := map[string]seat.Placement{}, map[string]uint64{}
|
||||
out, revs := map[string]seat.Assignments{}, map[string]uint64{}
|
||||
for _, n := range nodes {
|
||||
if p, rev, ok := k.Of(n); ok {
|
||||
out[n], revs[n] = p, rev
|
||||
@@ -375,28 +335,22 @@ func (k *Keys) AllRevisions() (map[string]seat.Placement, map[string]uint64) {
|
||||
return out, revs
|
||||
}
|
||||
|
||||
// KeyWrite is one write of a key of the state `placement`, by compare-and-set: Value nil deletes the key at
|
||||
// KeyWrite is one write of a key of the state `assignments`, by compare-and-set: Value nil deletes the key at
|
||||
// Base, Base 0 creates it, any other Base updates it at that revision.
|
||||
type KeyWrite struct {
|
||||
Node string
|
||||
Key string
|
||||
Value []byte
|
||||
Base uint64
|
||||
// Commit says this write is the node key's: once it is written, the node's placement is the new one.
|
||||
// Commit says this write is the node key's: once it is written, the node's assignments is the new one.
|
||||
Commit bool
|
||||
// Mirror says this write is the legacy key's, `placement.<node>`, kept in step after the commit for the readers not
|
||||
// yet delivered: a reader of both forms never reads it once the node key is there, so a mirror write that fails
|
||||
// harms no such reader, and is made again at the node's next write.
|
||||
Mirror bool
|
||||
}
|
||||
|
||||
// Writes are the writes that take node from the placement its keys hold to after (nil: no placement), in the order
|
||||
// Writes are the writes that take node from the assignments its keys hold to after (nil: no assignments), in the order
|
||||
// they are made: the module keys that change, each carrying the value it replaces; then the node key, the commit; then
|
||||
// the legacy key `placement.<node>` with the whole placement, a mirror for the readers not yet delivered, unless it
|
||||
// holds it already; then the deletes of the module keys after no longer names. For no placement the legacy key goes
|
||||
// first, then the node key, then the module keys, so a crash between them never leaves the legacy value to be read
|
||||
// again. A module key whose value stays is not written.
|
||||
func (k *Keys) Writes(node string, after *seat.Placement) ([]KeyWrite, error) {
|
||||
// the deletes of the module keys after no longer names. For no assignments the node key goes first, then the module
|
||||
// keys. A module key whose value stays is not written.
|
||||
func (k *Keys) Writes(node string, after *seat.Assignments) ([]KeyWrite, error) {
|
||||
nodeKey, err := NodeKey(node)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -418,10 +372,6 @@ func (k *Keys) Writes(node string, after *seat.Placement) ([]KeyWrite, error) {
|
||||
}
|
||||
}
|
||||
if after == nil {
|
||||
if of.legacy != nil {
|
||||
key, _ := LegacyKey(node)
|
||||
out = append(out, KeyWrite{Node: node, Key: key, Base: of.legacyRev})
|
||||
}
|
||||
if of.node != nil {
|
||||
out = append(out, KeyWrite{Node: node, Key: nodeKey, Base: of.nodeRev, Commit: true})
|
||||
}
|
||||
@@ -429,9 +379,9 @@ func (k *Keys) Writes(node string, after *seat.Placement) ([]KeyWrite, error) {
|
||||
return out, nil
|
||||
}
|
||||
if after.Node != node {
|
||||
return nil, fmt.Errorf("meshmoduleassignerclient: the placement written under %s names the node %q", node, after.Node)
|
||||
return nil, fmt.Errorf("meshmoduleassignerclient: the assignments written under %s names the node %q", node, after.Node)
|
||||
}
|
||||
if err := CheckPlacement(*after); err != nil {
|
||||
if err := CheckAssignments(*after); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
committed := map[string]seat.PlacedModule{}
|
||||
@@ -443,7 +393,7 @@ func (k *Keys) Writes(node string, after *seat.Placement) ([]KeyWrite, error) {
|
||||
n := seat.AssignmentsByNodeNodeValue{Node: node, Decided: after.Decided, Pins: after.Pins, Holds: after.Holds, Modules: make([]seat.AssignmentsByNodeNodeValueModulesItem, 0, len(after.Modules))}
|
||||
keep := map[string]bool{}
|
||||
for _, m := range after.Modules {
|
||||
if err := checkPlacementModule(m.Module); err != nil {
|
||||
if err := checkAssignmentsModule(m.Module); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
digest := Digest(m)
|
||||
@@ -470,58 +420,12 @@ func (k *Keys) Writes(node string, after *seat.Placement) ([]KeyWrite, error) {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, KeyWrite{Node: node, Key: nodeKey, Value: raw, Base: of.nodeRev, Commit: true})
|
||||
// The mirror of the form before, while readers of it alone may run (the delivery of this change); a later change
|
||||
// stops writing it and deletes it.
|
||||
if of.legacy == nil || !SamePlacement(*of.legacy, *after) {
|
||||
legacy, err := EncodePlacement(*after)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
key, _ := LegacyKey(node)
|
||||
out = append(out, KeyWrite{Node: node, Key: key, Value: legacy, Base: of.legacyRev, Mirror: true})
|
||||
}
|
||||
deleteModules(keep)
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// Migrations are, by node, the writes that convert every node held only in the form before (its legacy key, no node
|
||||
// key) into its node and module keys, its legacy key kept as the mirror for the readers not yet delivered (Writes); empty
|
||||
// once every node is converted, so the holder's conversion at each start is idempotent.
|
||||
func (k *Keys) Migrations() (map[string][]KeyWrite, error) {
|
||||
k.mu.Lock()
|
||||
var legacy []string
|
||||
for node, of := range k.nodes {
|
||||
if of.node == nil && of.legacy != nil && checkPlacementNode(node) == nil {
|
||||
legacy = append(legacy, node)
|
||||
}
|
||||
}
|
||||
k.mu.Unlock()
|
||||
sort.Strings(legacy)
|
||||
out := map[string][]KeyWrite{}
|
||||
var errs []error
|
||||
for _, node := range legacy {
|
||||
p, _, _ := k.Of(node)
|
||||
w, err := k.Writes(node, &p)
|
||||
if err != nil {
|
||||
errs = append(errs, fmt.Errorf("%s: %w", node, err))
|
||||
continue
|
||||
}
|
||||
out[node] = w
|
||||
}
|
||||
return out, errors.Join(errs...)
|
||||
}
|
||||
|
||||
// Legacy says node is held only in the form before.
|
||||
func (k *Keys) Legacy(node string) bool {
|
||||
k.mu.Lock()
|
||||
defer k.mu.Unlock()
|
||||
of := k.nodes[node]
|
||||
return of != nil && of.node == nil && of.legacy != nil
|
||||
}
|
||||
|
||||
// PlacementValues is every key and value of p written whole onto no keys before: its module keys, its node key and the
|
||||
// legacy mirror, as a writer that holds no key of the node (a test's bus, a fresh mesh) writes them.
|
||||
func PlacementValues(p seat.Placement) (map[string][]byte, error) {
|
||||
// AssignmentsValues is every key and value of p written whole onto no keys before: its module keys and its node key, as a writer that holds no key of the node (a test's bus, a fresh mesh) writes them.
|
||||
func AssignmentsValues(p seat.Assignments) (map[string][]byte, error) {
|
||||
writes, err := NewKeys().Writes(p.Node, &p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -533,10 +437,10 @@ func PlacementValues(p seat.Placement) (map[string][]byte, error) {
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// clonePlacement is p with its own maps and slices, so a reader's copy is not a held one.
|
||||
func clonePlacement(p seat.Placement) seat.Placement {
|
||||
// cloneAssignments is p with its own maps and slices, so a reader's copy is not a held one.
|
||||
func cloneAssignments(p seat.Assignments) seat.Assignments {
|
||||
raw, _ := json.Marshal(p)
|
||||
var out seat.Placement
|
||||
var out seat.Assignments
|
||||
_ = json.Unmarshal(raw, &out)
|
||||
if out.Modules == nil {
|
||||
out.Modules = []seat.PlacedModule{}
|
||||
|
||||
@@ -15,14 +15,14 @@ const (
|
||||
OpUnhold = "unhold"
|
||||
// OpDecide sets a decided field of the node (NodeDecided by its JSON name), its address among them.
|
||||
OpDecide = "decide"
|
||||
// OpConfig sets a module's config placement or values on the node.
|
||||
// OpConfig sets a module's config assignments or values on the node.
|
||||
OpConfig = "config"
|
||||
// OpSeed is the node's whole placement: the take-over (adopt, §7), never given by a caller.
|
||||
// OpSeed is the node's whole assignments: the take-over (adopt, §7), never given by a caller.
|
||||
OpSeed = "seed"
|
||||
// OpAddNode adds a joining node with its decided fields and the public halves of its keys, on the warrant of the
|
||||
// enrolment that issued its token (novox/hq ADR 0587 §7): the enroller's alone, the proposal's one change.
|
||||
OpAddNode = "add-node"
|
||||
// OpRemoveNode takes a joining node off placement again when its enrolment ends unconfirmed, revoked or expired: the
|
||||
// OpRemoveNode takes a joining node off assignments again when its enrolment ends unconfirmed, revoked or expired: the
|
||||
// enroller's alone, on the same warrant, the proposal's one change, of a node with no module and no holding.
|
||||
OpRemoveNode = "remove-node"
|
||||
)
|
||||
|
||||
@@ -10,15 +10,14 @@ import (
|
||||
seat "git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
|
||||
)
|
||||
|
||||
// Placed is where the placement (the seat mesh-placement's state `placement`, held by the assigner; ADR 0567, ADR 0549 §3) places each module, and since when:
|
||||
// Placed is where the assignments (the seat mesh-assignments's state `assignments`, held by the assigner; ADR 0567, ADR 0549 §3) places each module, and since when:
|
||||
// the one reader of that state for every part that asks "does module stand on node" (issue 774, design 47's third
|
||||
// milestone), in place of each part reading the controller's assignments (`mesh-controller_assignments`, which goes
|
||||
// with the controller's lease, ADR 0549 §6).
|
||||
//
|
||||
// Its input is the state's revisions in the order the bus wrote them, as a watch with the history (jetstream's
|
||||
// IncludeHistory) hands them: Put takes each, in either form of the state's keys (keys.go): a node's node key
|
||||
// names its modules (a module key says nothing of which stand), and its legacy key `placement.<node>` does until a node
|
||||
// key of it is heard. A module stands since the revision of its node's key that first named it
|
||||
// IncludeHistory) hands them: Put takes each, a node's node key
|
||||
// names its modules (a module key says nothing of which stand). A module stands since the revision of its node's key that first named it
|
||||
// in an unbroken run. A module in the first revision heard of a key stands since the zero time: that revision is the
|
||||
// key's first (the seed of ADR 0549 §4 placed what already ran there) or the oldest the bus still keeps
|
||||
// (the state's history), and either way the module may have run there before it. A key's value that does not read places
|
||||
@@ -29,17 +28,14 @@ type Placed struct {
|
||||
mu sync.Mutex
|
||||
since map[string]map[string]time.Time // node -> module -> since when it stands
|
||||
heard map[string]bool // the nodes whose key was heard at all
|
||||
// newForm are the nodes whose node key `<node>.node` was heard: their legacy key `placement.<node>` is no longer
|
||||
// read (keys.go).
|
||||
newForm map[string]bool
|
||||
}
|
||||
|
||||
// NewPlaced is a Placed that holds no placement yet.
|
||||
// NewPlaced is a Placed that holds no assignments yet.
|
||||
func NewPlaced() *Placed {
|
||||
return &Placed{since: map[string]map[string]time.Time{}, heard: map[string]bool{}, newForm: map[string]bool{}}
|
||||
return &Placed{since: map[string]map[string]time.Time{}, heard: map[string]bool{}}
|
||||
}
|
||||
|
||||
// Put takes one revision of a key of the state `placement` (its value, whether it is a delete, and when the bus wrote
|
||||
// Put takes one revision of a key of the state `assignments` (its value, whether it is a delete, and when the bus wrote
|
||||
// it). It answers the node the key is of and the modules whose standing on it changed (placed or removed), in name
|
||||
// order, and an error for a key or a value that does not read (the node then places nothing).
|
||||
func (p *Placed) Put(key string, value []byte, deleted bool, at time.Time) (node string, changed []string, err error) {
|
||||
@@ -50,37 +46,22 @@ func (p *Placed) Put(key string, value []byte, deleted bool, at time.Time) (node
|
||||
if kind == ModuleKind {
|
||||
return node, nil, nil // which modules stand is the node key's to say
|
||||
}
|
||||
p.mu.Lock()
|
||||
if kind == LegacyKind && p.newForm[node] {
|
||||
p.mu.Unlock()
|
||||
return node, nil, nil // the node is read from its node key once one was heard
|
||||
}
|
||||
if kind == NodeKind {
|
||||
p.newForm[node] = true
|
||||
}
|
||||
p.mu.Unlock()
|
||||
now := map[string]bool{}
|
||||
if !deleted {
|
||||
var modules []string
|
||||
var derr error
|
||||
var named string
|
||||
if kind == NodeKind {
|
||||
var n seat.AssignmentsByNodeNodeValue
|
||||
n, derr = DecodeNode(value)
|
||||
named = n.Node
|
||||
for _, r := range n.Modules {
|
||||
modules = append(modules, r.Module)
|
||||
}
|
||||
} else {
|
||||
var pl seat.Placement
|
||||
pl, derr = DecodePlacement(value)
|
||||
named, modules = pl.Node, Assigned(pl)
|
||||
var n seat.AssignmentsByNodeNodeValue
|
||||
n, derr = DecodeNode(value)
|
||||
named = n.Node
|
||||
for _, r := range n.Modules {
|
||||
modules = append(modules, r.Module)
|
||||
}
|
||||
switch {
|
||||
case derr != nil:
|
||||
err = fmt.Errorf("the placement of %s does not read, so it places nothing there: %w", node, derr)
|
||||
err = fmt.Errorf("the assignments of %s does not read, so it places nothing there: %w", node, derr)
|
||||
case named != node:
|
||||
err = fmt.Errorf("the placement under %s names the node %q, so it places nothing there", key, named)
|
||||
err = fmt.Errorf("the assignments under %s names the node %q, so it places nothing there", key, named)
|
||||
default:
|
||||
for _, m := range modules {
|
||||
now[m] = true
|
||||
@@ -117,7 +98,7 @@ func (p *Placed) Put(key string, value []byte, deleted bool, at time.Time) (node
|
||||
return node, changed, err
|
||||
}
|
||||
|
||||
// Seeded says some node's placement is held: before the assigner's adopt writes the first placement nothing is placed,
|
||||
// Seeded says some node's assignments is held: before the assigner's adopt writes the first assignments nothing is placed,
|
||||
// and a reader keeps what it did before (ADR 0549 §5).
|
||||
func (p *Placed) Seeded() bool {
|
||||
p.mu.Lock()
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
var proposalIDOK = regexp.MustCompile(`^[a-z0-9][a-z0-9-]{2,62}$`)
|
||||
|
||||
// CheckProposalID refuses an id that is no proposal's: lower-case letters, digits and dashes, 3 to 63 of them, so it is one
|
||||
// key token. The holder writes proposals under it, and the keys of the asks about a proposal (`placement.<id>`) carry it.
|
||||
// key token. The holder writes proposals under it, and the keys of the asks about a proposal (`assignments.<id>`) carry it.
|
||||
func CheckProposalID(id string) error {
|
||||
if !proposalIDOK.MatchString(id) {
|
||||
return fmt.Errorf("mesh-module-assigner: %q is no proposal id (lower-case letters, digits and dashes, one key token)", id)
|
||||
|
||||
@@ -2,8 +2,8 @@ package meshmoduleassignerclient
|
||||
|
||||
import "sort"
|
||||
|
||||
// MachineNames is every machine the seat places modules on, sorted, from its state `placement` as a reader holds its
|
||||
// keys: each with a node key `<node>.node` or, in the form before, `placement.<node>`. Module keys and keys of the
|
||||
// MachineNames is every machine the seat places modules on, sorted, from its state `assignments` as a reader holds its
|
||||
// keys: each with a node key `<node>.node`. Module keys and keys of the
|
||||
// state's other kinds are passed over.
|
||||
func MachineNames(keys map[string][]byte) []string {
|
||||
seen := map[string]bool{}
|
||||
|
||||
@@ -6,8 +6,8 @@ import (
|
||||
)
|
||||
|
||||
func TestMachineNamesAreTheNodesAKeyNames(t *testing.T) {
|
||||
keys := map[string][]byte{"g14.node": nil, "g14.app": nil, "ace.node": nil, "placement.novox": nil, "want.x.y": nil, "bad": nil}
|
||||
if got, want := MachineNames(keys), []string{"ace", "g14", "novox"}; !reflect.DeepEqual(got, want) {
|
||||
keys := map[string][]byte{"g14.node": nil, "g14.app": nil, "ace.node": nil, "want.x.y": nil, "bad": nil}
|
||||
if got, want := MachineNames(keys), []string{"ace", "g14"}; !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("MachineNames = %v, want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Code generated by seatgen from the seat definitions; DO NOT EDIT.
|
||||
|
||||
// Package meshmoduleassigner is the contract of the seat "mesh-module-assigner", defined whole (seats/mesh-module-assigner/seat.json): The seat that places modules on nodes (ADR 0567): the state of placement and the proposals that change it. Renamed from mesh-placement (ADR 0634 §9).
|
||||
// Package meshmoduleassigner is the contract of the seat "mesh-module-assigner", defined whole (seats/mesh-module-assigner/seat.json): The seat that places modules on nodes (ADR 0567): the state of assignments and the proposals that change it.
|
||||
package meshmoduleassigner
|
||||
|
||||
import (
|
||||
@@ -23,10 +23,10 @@ const (
|
||||
)
|
||||
|
||||
const (
|
||||
EventPlacementProposed = "placement-proposed"
|
||||
EventPlacementWithdrawn = "placement-withdrawn"
|
||||
EventPlacementJudged = "placement-judged"
|
||||
EventPlacementChanged = "placement-changed"
|
||||
EventAssignmentsProposed = "assignments-proposed"
|
||||
EventAssignmentsWithdrawn = "assignments-withdrawn"
|
||||
EventAssignmentsJudged = "assignments-judged"
|
||||
EventAssignmentsChanged = "assignments-changed"
|
||||
)
|
||||
|
||||
// Verbs is every verb the seat serves, in its definition's order: a new copy each call.
|
||||
@@ -36,7 +36,7 @@ func Verbs() []string {
|
||||
|
||||
// Events is every event published on the seat's subject: a new copy each call.
|
||||
func Events() []string {
|
||||
return []string{EventPlacementProposed, EventPlacementWithdrawn, EventPlacementJudged, EventPlacementChanged}
|
||||
return []string{EventAssignmentsProposed, EventAssignmentsWithdrawn, EventAssignmentsJudged, EventAssignmentsChanged}
|
||||
}
|
||||
|
||||
// Family is where the seat's events are kept: the shared events stream, filtered to the seat's subjects.
|
||||
@@ -45,17 +45,17 @@ const (
|
||||
FamilyMaxAgeDays = 7
|
||||
)
|
||||
|
||||
// SubjectPlacementProposed is where placement-proposed is published.
|
||||
const SubjectPlacementProposed = "mesh.seat.mesh-module-assigner.event.placement-proposed"
|
||||
// SubjectAssignmentsProposed is where assignments-proposed is published.
|
||||
const SubjectAssignmentsProposed = "mesh.seat.mesh-module-assigner.event.assignments-proposed"
|
||||
|
||||
// SubjectPlacementWithdrawn is where placement-withdrawn is published.
|
||||
const SubjectPlacementWithdrawn = "mesh.seat.mesh-module-assigner.event.placement-withdrawn"
|
||||
// SubjectAssignmentsWithdrawn is where assignments-withdrawn is published.
|
||||
const SubjectAssignmentsWithdrawn = "mesh.seat.mesh-module-assigner.event.assignments-withdrawn"
|
||||
|
||||
// SubjectPlacementJudged is where placement-judged is published.
|
||||
const SubjectPlacementJudged = "mesh.seat.mesh-module-assigner.event.placement-judged"
|
||||
// SubjectAssignmentsJudged is where assignments-judged is published.
|
||||
const SubjectAssignmentsJudged = "mesh.seat.mesh-module-assigner.event.assignments-judged"
|
||||
|
||||
// SubjectPlacementChanged is where placement-changed is published.
|
||||
const SubjectPlacementChanged = "mesh.seat.mesh-module-assigner.event.placement-changed"
|
||||
// SubjectAssignmentsChanged is where assignments-changed is published.
|
||||
const SubjectAssignmentsChanged = "mesh.seat.mesh-module-assigner.event.assignments-changed"
|
||||
|
||||
const (
|
||||
StateAssignments = "assignments"
|
||||
@@ -343,6 +343,36 @@ func DecodeSeatHoldersBySeatNodeValue(raw []byte) (SeatHoldersBySeatNodeValue, e
|
||||
return v, nil
|
||||
}
|
||||
|
||||
// Assignments is one node's assignments assembled.
|
||||
type Assignments struct {
|
||||
Decided NodeDecided `json:"decided"`
|
||||
Holds []Holding `json:"holds,omitempty"`
|
||||
Modules []PlacedModule `json:"modules"`
|
||||
Node string `json:"node"`
|
||||
// by provision, the provider chosen for the node
|
||||
Pins map[string]Provider `json:"pins,omitempty"`
|
||||
Revision string `json:"revision,omitempty"`
|
||||
V int64 `json:"v,omitempty"`
|
||||
}
|
||||
|
||||
// AssignmentsEventWarrant is who approved it, through which channel, when.
|
||||
type AssignmentsEventWarrant struct {
|
||||
At time.Time `json:"at"`
|
||||
Warrant string `json:"warrant"`
|
||||
Words string `json:"words"`
|
||||
}
|
||||
|
||||
// AssignmentsEvent is what each assignments event carries.
|
||||
type AssignmentsEvent struct {
|
||||
// for assignments-withdrawn, why it ended
|
||||
Ended string `json:"ended,omitempty"`
|
||||
// for assignments-changed, by node the revisions before and after and the modules added, removed or changed
|
||||
Nodes []map[string]any `json:"nodes,omitempty"`
|
||||
Proposal Proposal `json:"proposal"`
|
||||
// who approved it, through which channel, when
|
||||
Warrant *AssignmentsEventWarrant `json:"warrant,omitempty"`
|
||||
}
|
||||
|
||||
// FoundTunnel is a tunnel found on the machine when it was adopted: everything but its private key.
|
||||
type FoundTunnel struct {
|
||||
Address string `json:"address,omitempty"`
|
||||
@@ -396,9 +426,9 @@ type PlacedModule struct {
|
||||
Bindings map[string]Provider `json:"bindings,omitempty"`
|
||||
HasLayer bool `json:"has-layer,omitempty"`
|
||||
HasMeshWide bool `json:"has-mesh-wide,omitempty"`
|
||||
// this node's settings layer of the module's placement
|
||||
// this node's settings layer of the module's assignments
|
||||
Layer map[string]any `json:"layer,omitempty"`
|
||||
// the whole mesh's settings layer of the module's placement
|
||||
// the whole mesh's settings layer of the module's assignments
|
||||
MeshWide map[string]any `json:"mesh-wide,omitempty"`
|
||||
// config values a person set in the whole mesh's layer, for a module in the settings form
|
||||
MeshWideValues map[string]any `json:"mesh-wide-values,omitempty"`
|
||||
@@ -409,36 +439,6 @@ type PlacedModule struct {
|
||||
Values map[string]any `json:"values,omitempty"`
|
||||
}
|
||||
|
||||
// Placement is one node's placement assembled.
|
||||
type Placement struct {
|
||||
Decided NodeDecided `json:"decided"`
|
||||
Holds []Holding `json:"holds,omitempty"`
|
||||
Modules []PlacedModule `json:"modules"`
|
||||
Node string `json:"node"`
|
||||
// by provision, the provider chosen for the node
|
||||
Pins map[string]Provider `json:"pins,omitempty"`
|
||||
Revision string `json:"revision,omitempty"`
|
||||
V int64 `json:"v,omitempty"`
|
||||
}
|
||||
|
||||
// PlacementEventWarrant is who approved it, through which channel, when.
|
||||
type PlacementEventWarrant struct {
|
||||
At time.Time `json:"at"`
|
||||
Warrant string `json:"warrant"`
|
||||
Words string `json:"words"`
|
||||
}
|
||||
|
||||
// PlacementEvent is what each placement event carries.
|
||||
type PlacementEvent struct {
|
||||
// for placement-withdrawn, why it ended
|
||||
Ended string `json:"ended,omitempty"`
|
||||
// for placement-changed, by node the revisions before and after and the modules added, removed or changed
|
||||
Nodes []map[string]any `json:"nodes,omitempty"`
|
||||
Proposal Proposal `json:"proposal"`
|
||||
// who approved it, through which channel, when
|
||||
Warrant *PlacementEventWarrant `json:"warrant,omitempty"`
|
||||
}
|
||||
|
||||
// Proposal is an open proposal.
|
||||
type Proposal struct {
|
||||
Again *time.Time `json:"again,omitempty"`
|
||||
@@ -496,8 +496,8 @@ type ProposalChange struct {
|
||||
// a decided field's value
|
||||
Value any `json:"value,omitempty"`
|
||||
Values map[string]any `json:"values,omitempty"`
|
||||
// the node's whole placement, for a seed
|
||||
Whole *Placement `json:"whole,omitempty"`
|
||||
// the node's whole assignments, for a seed
|
||||
Whole *Assignments `json:"whole,omitempty"`
|
||||
}
|
||||
|
||||
// Provider is the module on a node that answers a requirement or holds a seat.
|
||||
@@ -616,17 +616,17 @@ type AgainInput struct {
|
||||
// AgainAnswer is what again answers.
|
||||
type AgainAnswer = map[string]any
|
||||
|
||||
// PlacementProposedEvent is the body of the event placement-proposed.
|
||||
type PlacementProposedEvent = PlacementEvent
|
||||
// AssignmentsProposedEvent is the body of the event assignments-proposed.
|
||||
type AssignmentsProposedEvent = AssignmentsEvent
|
||||
|
||||
// PlacementWithdrawnEvent is the body of the event placement-withdrawn.
|
||||
type PlacementWithdrawnEvent = PlacementEvent
|
||||
// AssignmentsWithdrawnEvent is the body of the event assignments-withdrawn.
|
||||
type AssignmentsWithdrawnEvent = AssignmentsEvent
|
||||
|
||||
// PlacementJudgedEvent is the body of the event placement-judged.
|
||||
type PlacementJudgedEvent = PlacementEvent
|
||||
// AssignmentsJudgedEvent is the body of the event assignments-judged.
|
||||
type AssignmentsJudgedEvent = AssignmentsEvent
|
||||
|
||||
// PlacementChangedEvent is the body of the event placement-changed.
|
||||
type PlacementChangedEvent = PlacementEvent
|
||||
// AssignmentsChangedEvent is the body of the event assignments-changed.
|
||||
type AssignmentsChangedEvent = AssignmentsEvent
|
||||
|
||||
// AssignmentsByNodeNodeValueModulesItem is a type of the contract.
|
||||
type AssignmentsByNodeNodeValueModulesItem struct {
|
||||
@@ -660,9 +660,9 @@ type ProposalsByProposalValue = Proposal
|
||||
|
||||
// CandidatesByProposalNodeValue is one value of the state candidates under the key <proposal>.<node>.
|
||||
type CandidatesByProposalNodeValue struct {
|
||||
// the node's placement with the proposal laid over it; absent when the proposal takes its key away
|
||||
Placement *Placement `json:"placement,omitempty"`
|
||||
Proposal string `json:"proposal"`
|
||||
// the node's assignments with the proposal laid over it; absent when the proposal takes its key away
|
||||
Assignments *Assignments `json:"assignments,omitempty"`
|
||||
Proposal string `json:"proposal"`
|
||||
// the proposal changes this node's composition through another's key
|
||||
Reached bool `json:"reached,omitempty"`
|
||||
V int64 `json:"v,omitempty"`
|
||||
|
||||
@@ -17,8 +17,12 @@ func TestFixtures(t *testing.T) {
|
||||
decode func([]byte) (any, error)
|
||||
subjectIs *regexp.Regexp
|
||||
}{
|
||||
{name: "a proposal says placement-proposed", subject: "mesh.seat.mesh-module-assigner.event.placement-proposed", given: "{\"proposal\":{\"at\":\"2026-11-05T12:00:00Z\",\"base\":{\"ace\":3},\"by\":\"operator\",\"changes\":[{\"module\":\"plex\",\"node\":\"ace\",\"op\":\"place\",\"ports\":{\"32400\":32400}}],\"id\":\"p-20261105t120000-aabbccdd\",\"state\":\"answering\",\"why\":\"move plex\"}}", wire: "{\"proposal\":{\"at\":\"2026-11-05T12:00:00Z\",\"base\":{\"ace\":3},\"by\":\"operator\",\"changes\":[{\"module\":\"plex\",\"node\":\"ace\",\"op\":\"place\",\"ports\":{\"32400\":32400}}],\"id\":\"p-20261105t120000-aabbccdd\",\"state\":\"answering\",\"why\":\"move plex\"}}",
|
||||
decode: func(b []byte) (any, error) { var v PlacementProposedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.mesh-module-assigner\\.event\\.placement-proposed$")},
|
||||
{name: "a proposal says assignments-proposed", subject: "mesh.seat.mesh-module-assigner.event.assignments-proposed", given: "{\"proposal\":{\"at\":\"2026-11-05T12:00:00Z\",\"base\":{\"ace\":3},\"by\":\"operator\",\"changes\":[{\"module\":\"plex\",\"node\":\"ace\",\"op\":\"place\",\"ports\":{\"32400\":32400}}],\"id\":\"p-20261105t120000-aabbccdd\",\"state\":\"answering\",\"why\":\"move plex\"}}", wire: "{\"proposal\":{\"at\":\"2026-11-05T12:00:00Z\",\"base\":{\"ace\":3},\"by\":\"operator\",\"changes\":[{\"module\":\"plex\",\"node\":\"ace\",\"op\":\"place\",\"ports\":{\"32400\":32400}}],\"id\":\"p-20261105t120000-aabbccdd\",\"state\":\"answering\",\"why\":\"move plex\"}}",
|
||||
decode: func(b []byte) (any, error) {
|
||||
var v AssignmentsProposedEvent
|
||||
err := json.Unmarshal(b, &v)
|
||||
return v, err
|
||||
}, subjectIs: regexp.MustCompile("^mesh\\.seat\\.mesh-module-assigner\\.event\\.assignments-proposed$")},
|
||||
{name: "a node's key", subject: "", given: "{\"decided\":{\"address\":\"10.0.0.2\"},\"modules\":[{\"digest\":\"0123456789abcdef\",\"module\":\"plex\"}],\"node\":\"ace\",\"v\":1}", wire: "{\"decided\":{\"address\":\"10.0.0.2\"},\"modules\":[{\"digest\":\"0123456789abcdef\",\"module\":\"plex\"}],\"node\":\"ace\",\"v\":1}",
|
||||
decode: func(b []byte) (any, error) {
|
||||
var v AssignmentsByNodeNodeValue
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
[
|
||||
{
|
||||
"name": "a proposal says placement-proposed",
|
||||
"of": "event:placement-proposed",
|
||||
"subject": "mesh.seat.mesh-module-assigner.event.placement-proposed",
|
||||
"name": "a proposal says assignments-proposed",
|
||||
"of": "event:assignments-proposed",
|
||||
"subject": "mesh.seat.mesh-module-assigner.event.assignments-proposed",
|
||||
"given": {
|
||||
"proposal": {
|
||||
"id": "p-20261105t120000-aabbccdd",
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
"grain": "whole",
|
||||
"domain": "Core",
|
||||
"decision": "novox/hq ADR 0567, ADR 0634, ADR 0635",
|
||||
"description": "The seat that places modules on nodes (ADR 0567): the state of placement and the proposals that change it. Renamed from mesh-placement (ADR 0634 §9).",
|
||||
"description": "The seat that places modules on nodes (ADR 0567): the state of assignments and the proposals that change it.",
|
||||
"verbs": [
|
||||
{
|
||||
"name": "assignments",
|
||||
"description": "Read the assignments: one node's key, the nodes one module is placed on, or every key, each with its revision, and how full the state is against its cap. With history, one node's or one module's changes in order: a revision the state keeps whole, an older one as the proposal's changes from its placement-changed event.",
|
||||
"description": "Read the assignments: one node's key, the nodes one module is placed on, or every key, each with its revision, and how full the state is against its cap. With history, one node's or one module's changes in order: a revision the state keeps whole, an older one as the proposal's changes from its assignments-changed event.",
|
||||
"optional": true,
|
||||
"input": {
|
||||
"properties": {
|
||||
@@ -42,12 +42,12 @@
|
||||
},
|
||||
"answer": {
|
||||
"type": "object",
|
||||
"description": "placement as read: nodes, keys with their revisions, history, seat holders, the state's fill"
|
||||
"description": "assignments as read: nodes, keys with their revisions, history, seat holders, the state's fill"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "proposals",
|
||||
"description": "Every open proposal of placement: its changes, the revision of each key it was laid over, its candidates, every node's answer, the differences computed and answered, its verdict and its ask. Narrowed to one by id.",
|
||||
"description": "Every open proposal of assignments: its changes, the revision of each key it was laid over, its candidates, every node's answer, the differences computed and answered, its verdict and its ask. Narrowed to one by id.",
|
||||
"optional": true,
|
||||
"input": {
|
||||
"properties": {
|
||||
@@ -65,7 +65,7 @@
|
||||
},
|
||||
{
|
||||
"name": "propose",
|
||||
"description": "Propose a change of placement, with why: laid over the current keys and refused at once, naming every finding, when the result cannot be placed (two modules on one host port, a provider not placed, a seat held once held twice, two nodes with one address, two hubs, a module the indexer does not hold, a node with no record). Otherwise the nodes it touches compose it in shadow and answer, and on a passing verdict the operator is asked to approve it. Who proposes is the caller, as the runtime proves it. An add-node from mesh-enrolment, whose warrant names its node and address, is written with no ask of its own: its Approve is the enrolment's (ADR 0587 §7).",
|
||||
"description": "Propose a change of assignments, with why: laid over the current keys and refused at once, naming every finding, when the result cannot be placed (two modules on one host port, a provider not placed, a seat held once held twice, two nodes with one address, two hubs, a module the indexer does not hold, a node with no record). Otherwise the nodes it touches compose it in shadow and answer, and on a passing verdict the operator is asked to approve it. Who proposes is the caller, as the runtime proves it. An add-node from mesh-enrolment, whose warrant names its node and address, is written with no ask of its own: its Approve is the enrolment's (ADR 0587 §7).",
|
||||
"optional": true,
|
||||
"input": {
|
||||
"additionalProperties": false,
|
||||
@@ -110,7 +110,7 @@
|
||||
},
|
||||
{
|
||||
"name": "withdraw",
|
||||
"description": "Withdraw an open proposal: its key and its candidates are deleted and placement-withdrawn carries it whole. With why.",
|
||||
"description": "Withdraw an open proposal: its key and its candidates are deleted and assignments-withdrawn carries it whole. With why.",
|
||||
"optional": true,
|
||||
"input": {
|
||||
"properties": {
|
||||
@@ -158,27 +158,27 @@
|
||||
],
|
||||
"events": [
|
||||
{
|
||||
"name": "placement-proposed",
|
||||
"name": "assignments-proposed",
|
||||
"body": {
|
||||
"$ref": "#/defs/PlacementEvent"
|
||||
"$ref": "#/defs/AssignmentsEvent"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "placement-withdrawn",
|
||||
"name": "assignments-withdrawn",
|
||||
"body": {
|
||||
"$ref": "#/defs/PlacementEvent"
|
||||
"$ref": "#/defs/AssignmentsEvent"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "placement-judged",
|
||||
"name": "assignments-judged",
|
||||
"body": {
|
||||
"$ref": "#/defs/PlacementEvent"
|
||||
"$ref": "#/defs/AssignmentsEvent"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "placement-changed",
|
||||
"name": "assignments-changed",
|
||||
"body": {
|
||||
"$ref": "#/defs/PlacementEvent"
|
||||
"$ref": "#/defs/AssignmentsEvent"
|
||||
}
|
||||
}
|
||||
],
|
||||
@@ -313,9 +313,9 @@
|
||||
"type": "boolean",
|
||||
"description": "the proposal changes this node's composition through another's key"
|
||||
},
|
||||
"placement": {
|
||||
"$ref": "#/defs/Placement",
|
||||
"description": "the node's placement with the proposal laid over it; absent when the proposal takes its key away"
|
||||
"assignments": {
|
||||
"$ref": "#/defs/Assignments",
|
||||
"description": "the node's assignments with the proposal laid over it; absent when the proposal takes its key away"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -380,7 +380,7 @@
|
||||
"description": "a node seat on one node"
|
||||
}
|
||||
],
|
||||
"description": "who holds which seat on which node, composed from every node's placement"
|
||||
"description": "who holds which seat on which node, composed from every node's assignments"
|
||||
}
|
||||
],
|
||||
"defs": {
|
||||
@@ -423,14 +423,14 @@
|
||||
},
|
||||
"mesh-wide": {
|
||||
"type": "object",
|
||||
"description": "the whole mesh's settings layer of the module's placement"
|
||||
"description": "the whole mesh's settings layer of the module's assignments"
|
||||
},
|
||||
"has-mesh-wide": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"layer": {
|
||||
"type": "object",
|
||||
"description": "this node's settings layer of the module's placement"
|
||||
"description": "this node's settings layer of the module's assignments"
|
||||
},
|
||||
"has-layer": {
|
||||
"type": "boolean"
|
||||
@@ -580,9 +580,9 @@
|
||||
"public-key"
|
||||
]
|
||||
},
|
||||
"Placement": {
|
||||
"Assignments": {
|
||||
"type": "object",
|
||||
"description": "one node's placement assembled",
|
||||
"description": "one node's assignments assembled",
|
||||
"properties": {
|
||||
"v": {
|
||||
"type": "integer"
|
||||
@@ -685,8 +685,8 @@
|
||||
"type": "object"
|
||||
},
|
||||
"whole": {
|
||||
"$ref": "#/defs/Placement",
|
||||
"description": "the node's whole placement, for a seed"
|
||||
"$ref": "#/defs/Assignments",
|
||||
"description": "the node's whole assignments, for a seed"
|
||||
},
|
||||
"decided": {
|
||||
"$ref": "#/defs/NodeDecided"
|
||||
@@ -903,9 +903,9 @@
|
||||
"state"
|
||||
]
|
||||
},
|
||||
"PlacementEvent": {
|
||||
"AssignmentsEvent": {
|
||||
"type": "object",
|
||||
"description": "what each placement event carries",
|
||||
"description": "what each assignments event carries",
|
||||
"properties": {
|
||||
"proposal": {
|
||||
"$ref": "#/defs/Proposal"
|
||||
@@ -915,7 +915,7 @@
|
||||
"items": {
|
||||
"type": "object"
|
||||
},
|
||||
"description": "for placement-changed, by node the revisions before and after and the modules added, removed or changed"
|
||||
"description": "for assignments-changed, by node the revisions before and after and the modules added, removed or changed"
|
||||
},
|
||||
"warrant": {
|
||||
"type": "object",
|
||||
@@ -940,7 +940,7 @@
|
||||
},
|
||||
"ended": {
|
||||
"type": "string",
|
||||
"description": "for placement-withdrawn, why it ended"
|
||||
"description": "for assignments-withdrawn, why it ended"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Code generated by seatgen from the seat definitions; DO NOT EDIT.
|
||||
|
||||
// The contract of the seat "mesh-module-assigner", defined whole (seats/mesh-module-assigner/seat.json): The seat that places modules on nodes (ADR 0567): the state of placement and the proposals that change it. Renamed from mesh-placement (ADR 0634 §9).
|
||||
// The contract of the seat "mesh-module-assigner", defined whole (seats/mesh-module-assigner/seat.json): The seat that places modules on nodes (ADR 0567): the state of assignments and the proposals that change it.
|
||||
|
||||
export const seat = "mesh-module-assigner";
|
||||
|
||||
@@ -13,21 +13,21 @@ export const verbs = {
|
||||
} as const;
|
||||
|
||||
export const events = {
|
||||
placementProposed: "placement-proposed",
|
||||
placementWithdrawn: "placement-withdrawn",
|
||||
placementJudged: "placement-judged",
|
||||
placementChanged: "placement-changed",
|
||||
assignmentsProposed: "assignments-proposed",
|
||||
assignmentsWithdrawn: "assignments-withdrawn",
|
||||
assignmentsJudged: "assignments-judged",
|
||||
assignmentsChanged: "assignments-changed",
|
||||
} as const;
|
||||
|
||||
export const family = { name: "mesh-module-assigner.events", maxAgeDays: 7 } as const;
|
||||
|
||||
export const placementProposedSubject = "mesh.seat.mesh-module-assigner.event.placement-proposed";
|
||||
export const assignmentsProposedSubject = "mesh.seat.mesh-module-assigner.event.assignments-proposed";
|
||||
|
||||
export const placementWithdrawnSubject = "mesh.seat.mesh-module-assigner.event.placement-withdrawn";
|
||||
export const assignmentsWithdrawnSubject = "mesh.seat.mesh-module-assigner.event.assignments-withdrawn";
|
||||
|
||||
export const placementJudgedSubject = "mesh.seat.mesh-module-assigner.event.placement-judged";
|
||||
export const assignmentsJudgedSubject = "mesh.seat.mesh-module-assigner.event.assignments-judged";
|
||||
|
||||
export const placementChangedSubject = "mesh.seat.mesh-module-assigner.event.placement-changed";
|
||||
export const assignmentsChangedSubject = "mesh.seat.mesh-module-assigner.event.assignments-changed";
|
||||
|
||||
export const states = {
|
||||
assignments: "assignments",
|
||||
@@ -178,6 +178,36 @@ export function parseSeatHoldersBySeatNodeKey(_key: string): { seat: string; nod
|
||||
return { seat, node };
|
||||
}
|
||||
|
||||
/** one node's assignments assembled */
|
||||
export interface Assignments {
|
||||
"decided": NodeDecided;
|
||||
"holds"?: Holding[];
|
||||
"modules": PlacedModule[];
|
||||
"node": string;
|
||||
/** by provision, the provider chosen for the node */
|
||||
"pins"?: Record<string, Provider>;
|
||||
"revision"?: string;
|
||||
"v"?: number;
|
||||
}
|
||||
|
||||
/** who approved it, through which channel, when */
|
||||
export interface AssignmentsEventWarrant {
|
||||
"at": string;
|
||||
"warrant": string;
|
||||
"words": string;
|
||||
}
|
||||
|
||||
/** what each assignments event carries */
|
||||
export interface AssignmentsEvent {
|
||||
/** for assignments-withdrawn, why it ended */
|
||||
"ended"?: string;
|
||||
/** for assignments-changed, by node the revisions before and after and the modules added, removed or changed */
|
||||
"nodes"?: Record<string, unknown>[];
|
||||
"proposal": Proposal;
|
||||
/** who approved it, through which channel, when */
|
||||
"warrant"?: AssignmentsEventWarrant;
|
||||
}
|
||||
|
||||
/** a tunnel found on the machine when it was adopted: everything but its private key */
|
||||
export interface FoundTunnel {
|
||||
"address"?: string;
|
||||
@@ -231,9 +261,9 @@ export interface PlacedModule {
|
||||
"bindings"?: Record<string, Provider>;
|
||||
"has-layer"?: boolean;
|
||||
"has-mesh-wide"?: boolean;
|
||||
/** this node's settings layer of the module's placement */
|
||||
/** this node's settings layer of the module's assignments */
|
||||
"layer"?: Record<string, unknown>;
|
||||
/** the whole mesh's settings layer of the module's placement */
|
||||
/** the whole mesh's settings layer of the module's assignments */
|
||||
"mesh-wide"?: Record<string, unknown>;
|
||||
/** config values a person set in the whole mesh's layer, for a module in the settings form */
|
||||
"mesh-wide-values"?: Record<string, unknown>;
|
||||
@@ -244,36 +274,6 @@ export interface PlacedModule {
|
||||
"values"?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
/** one node's placement assembled */
|
||||
export interface Placement {
|
||||
"decided": NodeDecided;
|
||||
"holds"?: Holding[];
|
||||
"modules": PlacedModule[];
|
||||
"node": string;
|
||||
/** by provision, the provider chosen for the node */
|
||||
"pins"?: Record<string, Provider>;
|
||||
"revision"?: string;
|
||||
"v"?: number;
|
||||
}
|
||||
|
||||
/** who approved it, through which channel, when */
|
||||
export interface PlacementEventWarrant {
|
||||
"at": string;
|
||||
"warrant": string;
|
||||
"words": string;
|
||||
}
|
||||
|
||||
/** what each placement event carries */
|
||||
export interface PlacementEvent {
|
||||
/** for placement-withdrawn, why it ended */
|
||||
"ended"?: string;
|
||||
/** for placement-changed, by node the revisions before and after and the modules added, removed or changed */
|
||||
"nodes"?: Record<string, unknown>[];
|
||||
"proposal": Proposal;
|
||||
/** who approved it, through which channel, when */
|
||||
"warrant"?: PlacementEventWarrant;
|
||||
}
|
||||
|
||||
/** an open proposal */
|
||||
export interface Proposal {
|
||||
"again"?: string;
|
||||
@@ -331,8 +331,8 @@ export interface ProposalChange {
|
||||
/** a decided field's value */
|
||||
"value"?: unknown;
|
||||
"values"?: Record<string, unknown>;
|
||||
/** the node's whole placement, for a seed */
|
||||
"whole"?: Placement;
|
||||
/** the node's whole assignments, for a seed */
|
||||
"whole"?: Assignments;
|
||||
}
|
||||
|
||||
/** the module on a node that answers a requirement or holds a seat */
|
||||
@@ -438,17 +438,17 @@ export interface AgainInput {
|
||||
/** is what again answers. */
|
||||
export type AgainAnswer = Record<string, unknown>;
|
||||
|
||||
/** is the body of the event placement-proposed. */
|
||||
export type PlacementProposedEvent = PlacementEvent;
|
||||
/** is the body of the event assignments-proposed. */
|
||||
export type AssignmentsProposedEvent = AssignmentsEvent;
|
||||
|
||||
/** is the body of the event placement-withdrawn. */
|
||||
export type PlacementWithdrawnEvent = PlacementEvent;
|
||||
/** is the body of the event assignments-withdrawn. */
|
||||
export type AssignmentsWithdrawnEvent = AssignmentsEvent;
|
||||
|
||||
/** is the body of the event placement-judged. */
|
||||
export type PlacementJudgedEvent = PlacementEvent;
|
||||
/** is the body of the event assignments-judged. */
|
||||
export type AssignmentsJudgedEvent = AssignmentsEvent;
|
||||
|
||||
/** is the body of the event placement-changed. */
|
||||
export type PlacementChangedEvent = PlacementEvent;
|
||||
/** is the body of the event assignments-changed. */
|
||||
export type AssignmentsChangedEvent = AssignmentsEvent;
|
||||
|
||||
export interface AssignmentsByNodeNodeValueModulesItem {
|
||||
/** the digest of the module key's value it stands for */
|
||||
@@ -481,8 +481,8 @@ export type ProposalsByProposalValue = Proposal;
|
||||
|
||||
/** is one value of the state candidates under the key <proposal>.<node>. */
|
||||
export interface CandidatesByProposalNodeValue {
|
||||
/** the node's placement with the proposal laid over it; absent when the proposal takes its key away */
|
||||
"placement"?: Placement;
|
||||
/** the node's assignments with the proposal laid over it; absent when the proposal takes its key away */
|
||||
"assignments"?: Assignments;
|
||||
"proposal": string;
|
||||
/** the proposal changes this node's composition through another's key */
|
||||
"reached"?: boolean;
|
||||
|
||||
Reference in New Issue
Block a user