mesh/delivery merged: source.main moved to novox/mesh-delivery@be88162cde31
mesh/check being carried to a192052d82fe on be88162cde31 | from 0490043990e2
The grants part, the enrolment seat and the watchman now act without reading the lease, so a controller placed again would write beside them with nothing saying so. The assigner refuses every proposal, seed or joining node's write that would place mesh-controller, naming why. Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
104 lines
6.5 KiB
Go
104 lines
6.5 KiB
Go
package assign
|
|
|
|
import (
|
|
"encoding/json"
|
|
"slices"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/asks"
|
|
"git.novox.be/novox/mesh-sdk/go/delivery"
|
|
)
|
|
|
|
// ADR 0567 §2, §5, §7, §8: what the seat's holder decides of a proposal: every refusal named at once, a valid one laid
|
|
// with the base revision of each key it changes, the writes at those revisions, the controller's calls that keep its
|
|
// assignments in step, the authorising ask bound to the changes, and the export read as the controller answers it.
|
|
func TestThePlacementSeatDecides(t *testing.T) {
|
|
anchor := delivery.Placement{V: delivery.Version, Node: "anchor", Decided: delivery.NodeDecided{Hub: true, Address: "10.0.0.1"},
|
|
Modules: []delivery.PlacedModule{{Module: "store", Ports: map[string]int{"5432": 5432}}, {Module: "web"}}}
|
|
laptop := delivery.Placement{V: delivery.Version, Node: "laptop", Decided: delivery.NodeDecided{Address: "10.0.0.2"}, Modules: []delivery.PlacedModule{{Module: "shell"}}}
|
|
in := func(changes ...delivery.PlacementProposalChange) ProposeIn {
|
|
return ProposeIn{ID: "p-one", By: "operator@laptop", Why: "move web", At: time.Unix(1, 0), Changes: changes,
|
|
Current: map[string]delivery.Placement{"anchor": anchor, "laptop": laptop}, Revisions: map[string]uint64{"anchor": 4, "laptop": 9},
|
|
Held: func(m string) bool { return m != "ghost" },
|
|
Recorded: func(n string) bool { return n == "anchor" || n == "laptop" }}
|
|
}
|
|
move := []delivery.PlacementProposalChange{{Op: delivery.OpTakeOff, Node: "anchor", Module: "web"}, {Op: delivery.OpPlace, Node: "laptop", Module: "web"}}
|
|
p, laid, findings := Propose(in(move...))
|
|
if len(findings) != 0 || p.State != delivery.ProposalAnswering || p.Base["anchor"] != 4 || p.Base["laptop"] != 9 ||
|
|
!slices.Equal(laid["laptop"].Assigned(), []string{"shell", "web"}) || !slices.Equal(laid["anchor"].Assigned(), []string{"store"}) {
|
|
t.Fatalf("a move: %+v, %+v, %v", p, laid, findings)
|
|
}
|
|
if _, err := delivery.EncodePlacementProposal(p); err != nil {
|
|
t.Fatalf("the proposal cannot be written: %v", err)
|
|
}
|
|
for name, c := range map[string]struct {
|
|
in ProposeIn
|
|
want string
|
|
}{
|
|
"a module not held": {in(delivery.PlacementProposalChange{Op: delivery.OpPlace, Node: "laptop", Module: "ghost"}), "the indexer holds no module ghost"},
|
|
"a node with no record": {in(delivery.PlacementProposalChange{Op: delivery.OpPlace, Node: "stranger", Module: "web"}), "stranger is a node with no record"},
|
|
"nothing changes": {in(delivery.PlacementProposalChange{Op: delivery.OpPlace, Node: "anchor", Module: "web"}), "leave every placement as it is"},
|
|
"no why": {func() ProposeIn { i := in(move...); i.Why = " "; return i }(), "says why"},
|
|
"no caller": {func() ProposeIn { i := in(move...); i.By = ""; return i }(), "names no caller"},
|
|
"a seed by hand": {in(delivery.PlacementProposalChange{Op: delivery.OpSeed, Node: "laptop", Whole: &laptop}), "gone with adopt"},
|
|
// 1126: the controller ended by its unassignment; placed again it would be a second writer beside the parts that
|
|
// act without its lease.
|
|
"the controller placed again": {in(delivery.PlacementProposalChange{Op: delivery.OpPlace, Node: "laptop", Module: "mesh-controller"}), "ended by its unassignment"},
|
|
"the controller seeded whole": {in(delivery.PlacementProposalChange{Op: delivery.OpSeed, Node: "anchor", Whole: &delivery.Placement{V: delivery.Version, Node: "anchor",
|
|
Decided: anchor.Decided, Modules: []delivery.PlacedModule{{Module: "mesh-controller"}}}}), "two writers (issue 1126)"},
|
|
"a pin of nobody placed": {in(delivery.PlacementProposalChange{Op: delivery.OpPin, Node: "laptop", Provision: "db", Provider: &delivery.Provider{Node: "laptop", Module: "store"}}), "not placed there"},
|
|
} {
|
|
if _, _, f := Propose(c.in); !slices.ContainsFunc(f, func(s string) bool { return strings.Contains(s, c.want) }) {
|
|
t.Errorf("%s: findings %v, want one naming %q", name, f, c.want)
|
|
}
|
|
}
|
|
|
|
// Every kind of change is taken: the placement is the one source, and the controller's verbs limit nothing (ADR 0567 §8).
|
|
for _, c := range []delivery.PlacementProposalChange{
|
|
{Op: delivery.OpConfig, Node: "anchor", Module: "web", Values: map[string]any{"puid": 1}},
|
|
{Op: delivery.OpPlace, Node: "laptop", Module: "web", Ports: map[string]int{"80": 8080}, Bindings: map[string]delivery.Provider{"db": {Node: "anchor", Module: "store"}}},
|
|
{Op: delivery.OpConfig, Node: "anchor", Module: "web", Layer: map[string]any{"places": []any{}}},
|
|
{Op: delivery.OpConfig, Node: "anchor", Module: "web", MeshWide: map[string]any{"accesses": []any{}}},
|
|
{Op: delivery.OpDecide, Node: "laptop", Field: "site", Value: json.RawMessage(`"attic"`)},
|
|
{Op: delivery.OpHold, Node: "anchor", Module: "store", Claim: "node-shell", Scope: "node"},
|
|
} {
|
|
if _, _, f := Propose(in(c)); len(f) != 0 {
|
|
t.Errorf("%s is refused: %v", c.Op, f)
|
|
}
|
|
}
|
|
|
|
// A key moved while the warrant wrote: what is left to write is the other nodes' changes, to be laid and asked again.
|
|
if rest, ok := Rest(p, map[string]*delivery.Placement{"anchor": laid["anchor"]}); !ok || len(rest.Changes) != 1 || rest.Changes[0].Node != "laptop" ||
|
|
len(rest.Base) != 1 || rest.State != delivery.ProposalAnswering || rest.Must != nil {
|
|
t.Errorf("the rest = %+v, %v", rest, ok)
|
|
}
|
|
if _, ok := Rest(p, laid); ok {
|
|
t.Error("a proposal written whole leaves something to write")
|
|
}
|
|
|
|
// The seat says it waits while one of its states is not bound, naming it, and clears once all are.
|
|
for _, unbound := range [][]string{{delivery.PlacementPulsesBucket}, nil} {
|
|
c := SeatWaitsCondition(unbound)
|
|
if _, err := asks.EncodeCondition(c); err != nil || (len(unbound) > 0) != (c.State == asks.StateRaised) ||
|
|
(len(unbound) > 0 && !strings.Contains(c.Explanation, "the seat waits for its states: "+delivery.PlacementPulsesBucket)) {
|
|
t.Errorf("waiting for %v: %+v, %v", unbound, c, err)
|
|
}
|
|
}
|
|
|
|
writes, err := Writes(p, laid)
|
|
if err != nil || len(writes) != 2 || writes[0].Key != "anchor.node" || writes[0].Base != 4 || writes[1].Base != 9 {
|
|
t.Fatalf("writes = %+v, %v", writes, err)
|
|
}
|
|
a, err := PlacementAsk("placement.p-one", p, "move web (proposed by operator@laptop)", 7, time.Unix(100, 0))
|
|
if err != nil || a.Ask.Check(time.Unix(100, 0)) != nil || a.Ask.Options[0].Level != asks.Approve || a.Ask.Options[0].Performs(PlacementAct(p)) != nil {
|
|
t.Fatalf("the ask = %+v, %v", a, err)
|
|
}
|
|
q := p
|
|
q.Changes = append(slices.Clone(p.Changes), delivery.PlacementProposalChange{Op: delivery.OpUnpin, Node: "laptop", Provision: "x"})
|
|
if a.Ask.Options[0].Performs(PlacementAct(q)) == nil {
|
|
t.Error("the ask's yes binds changes it was not asked about")
|
|
}
|
|
}
|