The assigner's state and verb placement are assignments, with its events, tools, views and every caller; the assigner copies the retired state at its start (issue 1966)

Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
This commit is contained in:
2026-11-05 22:13:20 +01:00
parent 369ebd092d
commit 0eafb2fe95
164 changed files with 1699 additions and 1498 deletions
@@ -2,14 +2,14 @@ package assign
import (
"fmt"
"mesh-delivery-assigner/internal/placement"
"mesh-delivery-assigner/internal/assignment"
"regexp"
"strings"
"time"
mwc "git.novox.be/novox/mesh-contracts/placement/go/clients/meshwantsclient"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
mwc "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshwantsclient"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-assigner/asks"
occ "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient"
@@ -69,7 +69,7 @@ func RestoreAsk(key, node, module, fp, words string, now time.Time) asks.Ask {
// words, under the assigner's own name (ADR 0437 clause 9), keyed by the held fingerprint. needsYou is true for a module holding
// data, whose restore ask waits for the operator.
func HeldCondition(module, fp, node, words string, raised, needsYou bool) oc.Condition {
key := placement.Module + ".held." + module + "." + strings.TrimPrefix(fp, meshcatalogclient.SourcePrefix2)
key := assignment.Module + ".held." + module + "." + strings.TrimPrefix(fp, meshcatalogclient.SourcePrefix2)
subject := oc.ConditionSubject{Scope: "module", ID: module, Machine: node}
if !raised {
return oc.Condition{Key: key, Subject: subject, State: occ.StateCleared}
@@ -92,7 +92,7 @@ func HeldCondition(module, fp, node, words string, raised, needsYou bool) oc.Con
// the node keeps its want. It names the node, the module, fp and the have's words, keyed by node and module, and is cleared
// (raised false) once the node no longer fails alone.
func NodeFailedCondition(node, module, fp string, f NodeFailure, raised bool) oc.Condition {
key := placement.Module + ".failed-alone." + node + "." + module
key := assignment.Module + ".failed-alone." + node + "." + module
subject := oc.ConditionSubject{Scope: "module", ID: module, Machine: node}
if !raised {
return oc.Condition{Key: key, Subject: subject, State: occ.StateCleared,
@@ -115,7 +115,7 @@ func NodeFailedCondition(node, module, fp string, f NodeFailure, raised bool) oc
// naming the module, the node and the rule holding its first want back with the rule's words, under the assigner's own name keyed by
// node and module, and cleared (raised false) when the node has a want for it again.
func NeitherCondition(node, module string, h Holding, raised bool) oc.Condition {
key := placement.Module + ".neither." + node + "." + module
key := assignment.Module + ".neither." + node + "." + module
subject := oc.ConditionSubject{Scope: "module", ID: module, Machine: node}
if !raised {
return oc.Condition{Key: key, Subject: subject, State: occ.StateCleared,
@@ -237,7 +237,7 @@ func BackupAsk(key, node, module, fp, digest, words string, asking int, now time
// BackupCondition is the condition raised when a backup before an apply fails and the module is held on the node (ADR 0539 §16),
// naming the node, the module and the backup's words; cleared under the same key.
func BackupCondition(node, module, fp, words string, raised bool) oc.Condition {
key := placement.Module + ".backup." + node + "." + module + "." + strings.TrimPrefix(fp, meshcatalogclient.SourcePrefix2)
key := assignment.Module + ".backup." + node + "." + module + "." + strings.TrimPrefix(fp, meshcatalogclient.SourcePrefix2)
subject := oc.ConditionSubject{Scope: "module", ID: module, Machine: node}
if !raised {
return oc.Condition{Key: key, Subject: subject, State: occ.StateCleared}
@@ -2,16 +2,16 @@ package assign
import (
"fmt"
"mesh-delivery-assigner/internal/placement"
"mesh-delivery-assigner/internal/assignment"
"mesh-delivery-assigner/internal/puts"
"slices"
"strings"
"testing"
"time"
mwc "git.novox.be/novox/mesh-contracts/placement/go/clients/meshwantsclient"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
mwc "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshwantsclient"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-assigner/asks"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-view/viewstate"
@@ -148,7 +148,7 @@ func saidConditionsAreFindings(t *testing.T) {
{HeldCondition("mail", fNew, "ace", "refused", true, true), LiftedCondition("mail", fNew), "module.mail." + strings.TrimPrefix(fNew, meshcatalogclient.SourcePrefix2) + ".held"},
{NodeFailedCondition("ace", "mail", fNew, NodeFailure{Words: "x", Others: []string{"g14"}}, true), NodeFailedCondition("ace", "mail", fNew, NodeFailure{}, false), "module.ace.mail.failed-alone"},
{NeitherCondition("ace", "mail", Holding{Rule: 3, Words: "w"}, true), NeitherCondition("ace", "mail", Holding{}, false), "module.ace.mail.neither"},
{SeatWaitsCondition([]string{"placement"}), SeatWaitsCondition(nil), "module.mesh-delivery-assigner.seat-states"},
{SeatWaitsCondition([]string{"assignments"}), SeatWaitsCondition(nil), "module.mesh-delivery-assigner.seat-states"},
{BackupCondition("ace", "mail", fNew, "disk full", true), BackupCondition("ace", "mail", fNew, "", false), "module.ace.mail." + strings.TrimPrefix(fNew, meshcatalogclient.SourcePrefix2) + ".backup"},
} {
puts, deletes, said := map[string]mcseat.FindingsByKeyValue{}, []string{}, 0
@@ -162,7 +162,7 @@ func saidConditionsAreFindings(t *testing.T) {
t.Fatalf("%s: %v", c.key, err)
}
got, ok := puts[c.key]
if !ok || got.Severity != c.raised.Severity || got.Headline != c.raised.Headline || got.Observer != placement.Module ||
if !ok || got.Severity != c.raised.Severity || got.Headline != c.raised.Headline || got.Observer != assignment.Module ||
got.Subject.Machine != c.raised.Subject.Machine || said != 0 {
t.Fatalf("%s: finding %+v (written %v), said %d", c.key, got, puts, said)
}
@@ -1,7 +1,7 @@
package assign
import (
"mesh-delivery-assigner/internal/placement"
"mesh-delivery-assigner/internal/assignment"
"strings"
occ "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient"
@@ -11,8 +11,8 @@ import (
// SeatWaitsCondition is the assigner's word on the seat mesh-module-assigner while a state of the seat is not bound (ADR 0567
// §1): raised naming each, cleared when none is left.
func SeatWaitsCondition(unbound []string) oc.Condition {
key := placement.Module + ".seat-states"
subject := oc.ConditionSubject{Scope: "module", ID: placement.Module}
key := assignment.Module + ".seat-states"
subject := oc.ConditionSubject{Scope: "module", ID: assignment.Module}
if len(unbound) == 0 {
return oc.Condition{Key: key, Subject: subject, State: occ.StateCleared, Resolved: "the seat mesh-module-assigner holds all its states."}
}
@@ -3,7 +3,7 @@ package assign
import (
"errors"
"fmt"
"mesh-delivery-assigner/internal/placement"
"mesh-delivery-assigner/internal/assignment"
"strings"
"time"
@@ -21,13 +21,13 @@ import (
// assigner no longer says it straight (never both).
// FindingsBucket is the assigner's findings.
var FindingsBucket = mcc.FindingsBucket(placement.Module)
var FindingsBucket = mcc.FindingsBucket(assignment.Module)
// FindingOf is a said condition as a finding: its key `<scope>.<id>.<kind>`, from the condition's own key
// `mesh-delivery-assigner.<kind>.<id…>` (the subject's id when the key names none), its words unchanged. raised is false for
// a clear, whose key is to be deleted; value is nil then.
func FindingOf(c oc.Condition, at time.Time) (key string, value []byte, raised bool, err error) {
rest, ok := strings.CutPrefix(c.Key, placement.Module+".")
rest, ok := strings.CutPrefix(c.Key, assignment.Module+".")
if !ok || rest == "" {
return "", nil, false, fmt.Errorf("the condition %q is not the assigner's", c.Key)
}
@@ -56,7 +56,7 @@ func FindingOf(c oc.Condition, at time.Time) (key string, value []byte, raised b
}
f := mcc.Observation{Scope: scope, ID: id, Kind: kind, Machine: c.Subject.Machine, Severity: string(c.Severity), Summary: summary,
Headline: c.Headline, Explanation: c.Explanation, Resolved: c.Resolved, Needs: c.Needs, Actions: actions,
Source: "assigner." + kind}.Finding(placement.Module, "", at)
Source: "assigner." + kind}.Finding(assignment.Module, "", at)
value, err = mcc.EncodeFinding(f)
return key, value, true, err
}
@@ -3,8 +3,8 @@ package assign
import (
"encoding/json"
"fmt"
"mesh-delivery-assigner/internal/assignment"
"mesh-delivery-assigner/internal/buckets"
"mesh-delivery-assigner/internal/placement"
"sort"
"strings"
"time"
@@ -20,7 +20,7 @@ import (
// catalogue.Upgrade: a person's choice over the module's word, its irreplaceable data and the default, the bus as any module (ADR 0601 §1).
// PoliciesBucket is the assigner's state `policies`.
var PoliciesBucket = placement.Module + "_" + buckets.PoliciesState
var PoliciesBucket = assignment.Module + "_" + buckets.PoliciesState
// DecodePolicies reads the policies state's values by key (the module); a value that cannot be read is left out, named.
func DecodePolicies(values map[string][]byte) (map[string]catalogue.PersonPolicy, []string) {
@@ -76,7 +76,7 @@ func ProvisionFailedFinding(key string, p meshprovisionerclient.Provision, at ti
// encode is the observation as the assigner's finding at at, in the seat's layout.
func encode(o mcc.Observation, at time.Time) ([]byte, error) {
return mcc.EncodeFinding(o.Finding(placement.Module, "", at))
return mcc.EncodeFinding(o.Finding(assignment.Module, "", at))
}
func also(provider, consumer string) []string {
@@ -109,7 +109,7 @@ func ProvisionMissingFinding(key string, bound time.Duration, at time.Time) ([]b
Resolved: provision + " for " + module + " on " + node + " has its key", Source: "assigner." + KindProvisionMissing}, at)
}
// KindUnplaced is, from the lease write, a node that runs but no placement names (ADR 0571 §6).
// KindUnplaced is, from the lease write, a node that runs but no assignments names (ADR 0571 §6).
const KindUnplaced = "unplaced"
// UnplacedKey is its finding's key, `machine.<node>.unplaced`.
@@ -117,12 +117,12 @@ func UnplacedKey(node string) string {
return mcc.Key(mcc.ScopeMachine, node, KindUnplaced)
}
// UnplacedFinding is the finding of a node no placement names since the lease write.
// UnplacedFinding is the finding of a node no assignments names since the lease write.
func UnplacedFinding(node string, at time.Time) ([]byte, error) {
summary := fmt.Sprintf("%s runs, but no placement names it: since the controller handed its lease away nothing composes it, "+
summary := fmt.Sprintf("%s runs, but no assignments names it: since the controller handed its lease away nothing composes it, "+
"so the assigner writes it no want", node)
return encode(mcc.Observation{Scope: mcc.ScopeMachine, ID: node, Kind: KindUnplaced, Machine: node, Summary: summary,
Headline: clipHeadline(node + " has no placement"), Needs: "place it with mesh-module-assigner.propose.",
Headline: clipHeadline(node + " has no assignments"), Needs: "place it with mesh-module-assigner.propose.",
Explanation: occ.NeedsYou + "place it with mesh-module-assigner.propose. " + summary + ".",
Resolved: node + " is placed", Source: "assigner." + KindUnplaced}, at)
}
@@ -4,8 +4,8 @@ import (
"fmt"
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
"git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
mwc "git.novox.be/novox/mesh-contracts/placement/go/clients/meshwantsclient"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
mwc "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshwantsclient"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-assigner/asks"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
"mesh-delivery-assigner/internal/puts"
@@ -75,7 +75,7 @@ func StepPut(w PutWorld) PutNext {
}
fp := short(p.FP)
if !w.Assigned {
return say(puts.Refused, "%s is not assigned to %s: a put moves a module where it stands assigned (propose its placement first)", w.Module, w.Node)
return say(puts.Refused, "%s is not assigned to %s: a put moves a module where it stands assigned (propose its assignments first)", w.Module, w.Node)
}
switch w.Build {
case builds.BuildDone:
@@ -10,27 +10,27 @@ import (
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-assigner/asks"
mmac "git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient"
mma "git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
occ "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient"
oc "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel"
mmac "git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient"
mma "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
"mesh-delivery-assigner/internal/placement"
plc "mesh-delivery-assigner/internal/placement"
"mesh-delivery-assigner/internal/assignment"
plc "mesh-delivery-assigner/internal/assignment"
)
// The seat mesh-module-assigner (novox/hq ADR 0567), held by the assigner beside its wants: placement is its state
// `placement`, changed only by a proposal (§2) the nodes it touches answer (§3), judged (§4) and written on the
// The seat mesh-module-assigner (novox/hq ADR 0567), held by the assigner beside its wants: assignments is its state
// `assignments`, changed only by a proposal (§2) the nodes it touches answer (§3), judged (§4) and written on the
// operator's Approve at the revision it was laid over (§5). What here decides; the tools and the passes do.
// ProposeIn is what a proposal is laid over: its id, who and why, its changes; every node's current placement and the
// ProposeIn is what a proposal is laid over: its id, who and why, its changes; every node's current assignments and the
// revision of its key; which modules the indexer holds and which nodes have a record; and, for the seed, the export's
// digest.
type ProposeIn struct {
ID, By, Why string
At time.Time
Changes []mma.ProposalChange
Current map[string]mma.Placement
Current map[string]mma.Assignments
Revisions map[string]uint64
Held func(module string) bool
Recorded func(node string) bool
@@ -39,11 +39,11 @@ type ProposeIn struct {
}
// Propose lays a proposal over the current keys (§2): the proposal (state answering, each changed node's base revision),
// the laid placement of every node whose key it changes, and every finding, which refuses it whole: a change that does not fit its node, and what
// CheckPlacements refuses of the result laid over every placement (two modules on one host port, a provider not placed,
// the laid assignments of every node whose key it changes, and every finding, which refuses it whole: a change that does not fit its node, and what
// CheckAssignments refuses of the result laid over every assignments (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). A key whose value the proposal leaves as it is changes nothing and is not laid.
func Propose(in ProposeIn) (mma.Proposal, map[string]*mma.Placement, []string) {
func Propose(in ProposeIn) (mma.Proposal, map[string]*mma.Assignments, []string) {
p := mma.Proposal{ID: in.ID, By: strings.TrimSpace(in.By), Why: strings.TrimSpace(in.Why), At: in.At.UTC(),
Changes: in.Changes, Warrant: in.Warrant, Base: map[string]int64{}, State: mmac.ProposalAnswering}
var findings []string
@@ -64,13 +64,13 @@ func Propose(in ProposeIn) (mma.Proposal, map[string]*mma.Placement, []string) {
laid, lay := plc.LayChanges(in.Current, in.Changes)
findings = append(findings, lay...)
findings = append(findings, ControllerRefusals(in.Changes, laid)...)
all := map[string]mma.Placement{}
all := map[string]mma.Assignments{}
for node, pl := range in.Current {
all[node] = pl
}
out := map[string]*mma.Placement{}
out := map[string]*mma.Assignments{}
for node, pl := range laid {
if before, ok := in.Current[node]; ok && mmac.SamePlacement(before, pl) {
if before, ok := in.Current[node]; ok && mmac.SameAssignments(before, pl) {
continue
}
pl := pl
@@ -79,7 +79,7 @@ func Propose(in ProposeIn) (mma.Proposal, map[string]*mma.Placement, []string) {
p.Base[node] = int64(in.Revisions[node])
}
// A remove-node deletes the key of a joining node that never confirmed (the operator's decision of 2026-11-02 01:05): its
// candidate is no placement, and the checks across nodes read the mesh without it. LayChanges said why one cannot go.
// candidate is no assignments, and the checks across nodes read the mesh without it. LayChanges said why one cannot go.
for _, node := range plc.RemovedNodes(p) {
if _, ok := in.Current[node]; !ok {
continue
@@ -89,7 +89,7 @@ func Propose(in ProposeIn) (mma.Proposal, map[string]*mma.Placement, []string) {
p.Base[node] = int64(in.Revisions[node])
}
if len(findings) == 0 && len(out) == 0 {
findings = append(findings, "the changes leave every placement as it is")
findings = append(findings, "the changes leave every assignments as it is")
}
held, recorded := in.Held, in.Recorded
if held == nil {
@@ -110,7 +110,7 @@ func Propose(in ProposeIn) (mma.Proposal, map[string]*mma.Placement, []string) {
was := recorded
recorded = func(node string) bool { return joining[node] || was(node) }
}
for _, r := range plc.CheckPlacements(all, held, recorded) {
for _, r := range plc.CheckAssignments(all, held, recorded) {
findings = append(findings, r)
}
return p, out, findings
@@ -119,14 +119,14 @@ func Propose(in ProposeIn) (mma.Proposal, map[string]*mma.Placement, []string) {
// ControllerModule is the controller's module, which nothing places again (issue 1126).
const ControllerModule = "mesh-controller"
// ControllerEnded is why a placement of the controller is refused: the parts that acted only once its lease was handed
// ControllerEnded is why assignments of the controller is refused: the parts that acted only once its lease was handed
// (consumers, the user list, enrolment, dead letters) now act without reading the lease, so a controller placed again
// would write beside them, silently.
const ControllerEnded = "the controller ended by its unassignment on 2026-11-02; placing it again would make two writers (issue 1126)"
// ControllerRefusals names every change, and every node as laid, that places the controller: by a place, by a seed's
// whole placement, or in what the changes lay. None when nothing does.
func ControllerRefusals(changes []mma.ProposalChange, laid map[string]mma.Placement) []string {
// whole assignments, or in what the changes lay. None when nothing does.
func ControllerRefusals(changes []mma.ProposalChange, laid map[string]mma.Assignments) []string {
var out []string
nodes := map[string]bool{}
for i, c := range changes {
@@ -148,16 +148,16 @@ func ControllerRefusals(changes []mma.ProposalChange, laid map[string]mma.Placem
}
// Write is one node a warrant writes (§5): the node, its node key `<node>.node` (the commit of the node's keys,
// mesh-sdk mmac.Keys.Writes), the revision of the node's placement it is written at (0: it has none, so
// mesh-sdk mmac.Keys.Writes), the revision of the node's assignments it is written at (0: it has none, so
// it is created) and its value (nil: it is deleted).
type Write struct {
Node, Key string
Base uint64
Value *mma.Placement
Value *mma.Assignments
}
// Writes is every key a proposal writes, by node order.
func Writes(p mma.Proposal, laid map[string]*mma.Placement) ([]Write, error) {
func Writes(p mma.Proposal, laid map[string]*mma.Assignments) ([]Write, error) {
var out []Write
for _, node := range plc.Nodes(p) {
key, err := mmac.NodeKey(node)
@@ -179,40 +179,40 @@ const (
// CallClearedCondition clears the condition of a refused controller call a previous process raised, for its pair (node and
// module, or node and provision for a pin).
func CallClearedCondition(kind, node, module string) oc.Condition {
return oc.Condition{Key: placement.Module + "." + kind + "." + node + "." + module,
return oc.Condition{Key: assignment.Module + "." + kind + "." + node + "." + module,
Subject: oc.ConditionSubject{Scope: "module", ID: module, Machine: node}, State: occ.StateCleared,
Resolved: "the assigner calls no verb of the controller any more: placement is the one source."}
Resolved: "the assigner calls no verb of the controller any more: assignments is the one source."}
}
// PlacementAct is the act an approved proposal's yes binds: writing exactly its changes at its base revisions.
func PlacementAct(p mma.Proposal) occ.Act {
return occ.Act{"placement": "write", "proposal": p.ID, "changes": plc.ChangesDigest(p)}
// AssignmentsAct is the act an approved proposal's yes binds: writing exactly its changes at its base revisions.
func AssignmentsAct(p mma.Proposal) occ.Act {
return occ.Act{"assignments": "write", "proposal": p.ID, "changes": plc.ChangesDigest(p)}
}
// PlacementAsking is a proposal's asking number: each judging that raises its ask again (again, a key moved) is
// AssignmentsAsking is a proposal's asking number: each judging that raises its ask again (again, a key moved) is
// another asking, under an id of its own.
func PlacementAsking(p mma.Proposal, at time.Time) int {
func AssignmentsAsking(p mma.Proposal, at time.Time) int {
return int(at.Unix())
}
// PlacementAsk is the authorising ask about a proposal (§5): an Approve that binds its act, so only a channel that
// AssignmentsAsk is the authorising ask about a proposal (§5): an Approve that binds its act, so only a channel that
// proves who answered takes it, with the brief of the verdict by node (judge.Brief) as its explanation.
func PlacementAsk(key string, p mma.Proposal, brief string, asking int, now time.Time) (asks.Ask, error) {
binds, err := occ.ActDigest(PlacementAct(p))
func AssignmentsAsk(key string, p mma.Proposal, brief string, asking int, now time.Time) (asks.Ask, error) {
binds, err := occ.ActDigest(AssignmentsAct(p))
if err != nil {
return asks.Ask{}, err
}
nodes := plc.Nodes(p)
headline := fmt.Sprintf("Change placement on %s?", strings.Join(nodes, ", "))
headline := fmt.Sprintf("Change assignments on %s?", strings.Join(nodes, ", "))
if n := len([]rune(headline)); n > occ.HeadlineLength {
headline = fmt.Sprintf("Change placement on %d machines?", len(nodes))
headline = fmt.Sprintf("Change assignments on %d machines?", len(nodes))
}
return assignerAsk(asking, oc.Ask{
ID: meshdeliveryclient.AskingID(key, asking),
Headline: headline,
Explanation: brief,
Options: []oc.Option{
{ID: OptionYes, Label: "Approve", Does: "the placement of " + strings.Join(nodes, ", ") + " is written as proposed",
{ID: OptionYes, Label: "Approve", Does: "the assignments of " + strings.Join(nodes, ", ") + " is written as proposed",
Level: occ.Approve, Binds: binds},
{ID: OptionNo, Label: "Decline", Does: "nothing is written, and the proposal ends", Level: occ.Acknowledge},
},
@@ -225,7 +225,7 @@ func PlacementAsk(key string, p mma.Proposal, brief string, asking int, now time
// Rest is what a proposal leaves to write once the keys of written are: its changes of the other nodes, to be laid again
// over the keys as the write left them (§5: a key moved while it was written). False when nothing is left.
func Rest(p mma.Proposal, written map[string]*mma.Placement) (mma.Proposal, bool) {
func Rest(p mma.Proposal, written map[string]*mma.Assignments) (mma.Proposal, bool) {
rest := p
rest.Changes, rest.Base = nil, map[string]int64{}
for _, c := range p.Changes {
@@ -8,23 +8,23 @@ import (
"testing"
"time"
mmac "git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient"
mma "git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
occ "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient"
mmac "git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient"
mma "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
plc "mesh-delivery-assigner/internal/placement"
plc "mesh-delivery-assigner/internal/assignment"
)
// 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 := mma.Placement{V: keys.Version, Node: "anchor", Decided: mma.NodeDecided{Hub: true, Address: "10.0.0.1"},
func TestTheAssignmentsSeatDecides(t *testing.T) {
anchor := mma.Assignments{V: keys.Version, Node: "anchor", Decided: mma.NodeDecided{Hub: true, Address: "10.0.0.1"},
Modules: []mma.PlacedModule{{Module: "store", Ports: map[string]int64{"5432": 5432}}, {Module: "web"}}}
laptop := mma.Placement{V: keys.Version, Node: "laptop", Decided: mma.NodeDecided{Address: "10.0.0.2"}, Modules: []mma.PlacedModule{{Module: "shell"}}}
laptop := mma.Assignments{V: keys.Version, Node: "laptop", Decided: mma.NodeDecided{Address: "10.0.0.2"}, Modules: []mma.PlacedModule{{Module: "shell"}}}
in := func(changes ...mma.ProposalChange) ProposeIn {
return ProposeIn{ID: "p-one", By: "operator@laptop", Why: "move web", At: time.Unix(1, 0), Changes: changes,
Current: map[string]mma.Placement{"anchor": anchor, "laptop": laptop}, Revisions: map[string]uint64{"anchor": 4, "laptop": 9},
Current: map[string]mma.Assignments{"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" }}
}
@@ -43,14 +43,14 @@ func TestThePlacementSeatDecides(t *testing.T) {
}{
"a module not held": {in(mma.ProposalChange{Op: mmac.OpPlace, Node: "laptop", Module: "ghost"}), "the indexer holds no module ghost"},
"a node with no record": {in(mma.ProposalChange{Op: mmac.OpPlace, Node: "stranger", Module: "web"}), "stranger is a node with no record"},
"nothing changes": {in(mma.ProposalChange{Op: mmac.OpPlace, Node: "anchor", Module: "web"}), "leave every placement as it is"},
"nothing changes": {in(mma.ProposalChange{Op: mmac.OpPlace, Node: "anchor", Module: "web"}), "leave every assignments 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(mma.ProposalChange{Op: mmac.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(mma.ProposalChange{Op: mmac.OpPlace, Node: "laptop", Module: "mesh-controller"}), "ended by its unassignment"},
"the controller seeded whole": {in(mma.ProposalChange{Op: mmac.OpSeed, Node: "anchor", Whole: &mma.Placement{V: keys.Version, Node: "anchor",
"the controller seeded whole": {in(mma.ProposalChange{Op: mmac.OpSeed, Node: "anchor", Whole: &mma.Assignments{V: keys.Version, Node: "anchor",
Decided: anchor.Decided, Modules: []mma.PlacedModule{{Module: "mesh-controller"}}}}), "two writers (issue 1126)"},
"a pin of nobody placed": {in(mma.ProposalChange{Op: mmac.OpPin, Node: "laptop", Provision: "db", Provider: &mma.Provider{Node: "laptop", Module: "store"}}), "not placed there"},
} {
@@ -59,7 +59,7 @@ func TestThePlacementSeatDecides(t *testing.T) {
}
}
// Every kind of change is taken: the placement is the one source, and the controller's verbs limit nothing (ADR 0567 §8).
// Every kind of change is taken: the assignments is the one source, and the controller's verbs limit nothing (ADR 0567 §8).
for _, c := range []mma.ProposalChange{
{Op: mmac.OpConfig, Node: "anchor", Module: "web", Values: map[string]any{"puid": 1}},
{Op: mmac.OpPlace, Node: "laptop", Module: "web", Ports: map[string]int64{"80": 8080}, Bindings: map[string]mma.Provider{"db": {Node: "anchor", Module: "store"}}},
@@ -74,7 +74,7 @@ func TestThePlacementSeatDecides(t *testing.T) {
}
// 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]*mma.Placement{"anchor": laid["anchor"]}); !ok || len(rest.Changes) != 1 || rest.Changes[0].Node != "laptop" ||
if rest, ok := Rest(p, map[string]*mma.Assignments{"anchor": laid["anchor"]}); !ok || len(rest.Changes) != 1 || rest.Changes[0].Node != "laptop" ||
len(rest.Base) != 1 || rest.State != mmac.ProposalAnswering || rest.Must != nil {
t.Errorf("the rest = %+v, %v", rest, ok)
}
@@ -95,13 +95,13 @@ func TestThePlacementSeatDecides(t *testing.T) {
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 || occ.Check(mustAsk(t, a), time.Unix(100, 0)) != nil || mustAsk(t, a).Options[0].Level != occ.Approve || occ.Performs(mustAsk(t, a).Options[0], PlacementAct(p)) != nil {
a, err := AssignmentsAsk("assignments.p-one", p, "move web (proposed by operator@laptop)", 7, time.Unix(100, 0))
if err != nil || occ.Check(mustAsk(t, a), time.Unix(100, 0)) != nil || mustAsk(t, a).Options[0].Level != occ.Approve || occ.Performs(mustAsk(t, a).Options[0], AssignmentsAct(p)) != nil {
t.Fatalf("the ask = %+v, %v", a, err)
}
q := p
q.Changes = append(slices.Clone(p.Changes), mma.ProposalChange{Op: mmac.OpUnpin, Node: "laptop", Provision: "x"})
if occ.Performs(mustAsk(t, a).Options[0], PlacementAct(q)) == nil {
if occ.Performs(mustAsk(t, a).Options[0], AssignmentsAct(q)) == nil {
t.Error("the ask's yes binds changes it was not asked about")
}
}
@@ -4,8 +4,8 @@ import (
"fmt"
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
"git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
mwc "git.novox.be/novox/mesh-contracts/placement/go/clients/meshwantsclient"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
mwc "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshwantsclient"
"git.novox.be/novox/mesh-delivery/modules/mesh-grants/grantsstate"
"maps"
"mesh-delivery-assigner/internal/puts"
@@ -90,9 +90,9 @@ type Module struct {
// needs, ADR 0618 §1): a node's want is held only while that node's engine reads none of them (ADR 0624 §2). Nil: the
// build names none, and holds nothing.
FormNeeds []string
// Self says whether node composes the module itself: the catalogue's placement names the node (ADR 0547 §2). It is the one
// Self says whether node composes the module itself: the catalogue's assignments names the node (ADR 0547 §2). It is the one
// source of a want's Self: every want the rules write or keep is derived from it (selfDerived), so a want written Self while
// the node was placed does not outlive the placement (issue 1126). Nil: no node does.
// the node was placed does not outlive the assignments (issue 1126). Nil: no node does.
Self func(node string) bool
}
@@ -587,11 +587,11 @@ func Wants(m Module, n Nodes, wants map[string]mwc.Want, lastHealthy map[string]
return out
}
// selfDerived is next with its Self derived from the placement now (Module.Self), whichever rule made it, and whether it is
// selfDerived is next with its Self derived from the assignments now (Module.Self), whichever rule made it, and whether it is
// written. A placed node's want names the build alone (ADR 0547 §2). An unplaced node's want that a rule kept from the want
// written while the node was placed (held at F, held back by rule 3 or 5, a data module asked about) names the controller's
// composition of the same fingerprint, asked as any move of a record module is (ADR 0539 §14): on 2026-10-27, after the
// placement seed's revert, g14's systemd-resolved (held) and forticlient (rule 5), and shanks's forticlient (rule 3), kept
// assignments seed's revert, g14's systemd-resolved (held) and forticlient (rule 5), and shanks's forticlient (rule 3), kept
// their Self wants, which their node agents, no longer placed, could not compose (issue 1126). With no such composition, or
// while its ask waits, the want written stays and the node's holding says why.
func (m Module) selfDerived(node string, before mwc.Want, had bool, next mwc.Want, n Nodes, out *Outcome) (mwc.Want, bool) {
@@ -3,8 +3,8 @@ package assign
import (
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
"git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
mwc "git.novox.be/novox/mesh-contracts/placement/go/clients/meshwantsclient"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
mwc "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshwantsclient"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-assigner/asks"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
"git.novox.be/novox/mesh-delivery/modules/mesh-grants/grantsstate"
@@ -348,7 +348,7 @@ func TestEachRuleHoldsTheWantBack(t *testing.T) {
}
}
// 1126/AC-SW1, AC-SW2, AC-SW3: a want written Self while g14 was placed, kept by a rule after the placement is gone, names
// 1126/AC-SW1, AC-SW2, AC-SW3: a want written Self while g14 was placed, kept by a rule after the assignments is gone, names
// the controller's composition of the fingerprint the rule keeps (on 2026-10-27 g14's systemd-resolved, held, and forticlient,
// rule 5, kept their Self wants, which g14 could no longer compose); with no such composition, or while a record ask waits, it
// stays and is said; a placed node's want is Self with no composition.
@@ -1,4 +1,4 @@
package placement
package assignment
import (
"encoding/json"
@@ -9,16 +9,16 @@ import (
"testing"
"time"
client "git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient"
seat "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
client "git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient"
seat "git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
"git.novox.be/novox/mesh-sdk/go/keys"
)
// ADR 0567 §2, §4: what a change says, the holder's check of every placement, a proposal's changes laid over the keys,
// ADR 0567 §2, §4: what a change says, the holder's check of every assignments, a proposal's changes laid over the keys,
// its digests (held to the values the earlier holder's code made, in the field order it wrote), its refusals, and who holds
// which seat.
func TestPlacement(t *testing.T) {
g14 := seat.Placement{Node: "g14", Revision: "abc",
func TestAssignments(t *testing.T) {
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"},
Tunnel: &seat.FoundTunnel{Interface: "wg0", Config: "c", Port: 51820, Mtu: 1420, PublicKey: "K", Peers: []seat.FoundTunnelPeer{{PublicKey: "P", Address: "10.0.0.3"}}},
PeerNames: map[string]string{"P": "pi"}},
@@ -29,13 +29,13 @@ func TestPlacement(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-store", Scope: "mesh", Module: "postgres"}}}
current := map[string]seat.Placement{"g14": g14, "novox": novox}
current := map[string]seat.Assignments{"g14": g14, "novox": novox}
const id = "p-20261028t183000-1a2b3c"
// the enroller is whichever module holds the seat mesh-node-enroller: no module is named for it
const enroller = "enroll-lab"
enrollers := EnrollersOf(map[string]seat.Placement{"ace": {Node: "ace",
enrollers := EnrollersOf(map[string]seat.Assignments{"ace": {Node: "ace",
Holds: []seat.Holding{{Claim: "mesh-node-enroller", Scope: "mesh", Module: enroller}}}})
if !slices.Equal(enrollers, []string{enroller}) {
t.Fatalf("the holders of the enroller seat = %v", enrollers)
@@ -54,7 +54,7 @@ func TestPlacement(t *testing.T) {
if got := ChangeOf(g14, pinned); !reflect.DeepEqual(got.Changed, []string{"app", "zsh"}) {
t.Errorf("a pin change changes every module on the node: %+v", got)
}
if got := ChangeOf(seat.Placement{}, novox); !reflect.DeepEqual(got.Added, []string{"postgres"}) || got.Node != "novox" {
if got := ChangeOf(seat.Assignments{}, novox); !reflect.DeepEqual(got.Added, []string{"postgres"}) || got.Node != "novox" {
t.Errorf("a new node: %+v", got)
}
c := []Change{{Node: "novox"}, {Node: "g14", Before: 1, After: 2}}
@@ -64,18 +64,18 @@ func TestPlacement(t *testing.T) {
}
})
t.Run("the check of every placement", func(t *testing.T) {
t.Run("the check of every assignments", func(t *testing.T) {
known := func(m string) bool { return m != "ghost" }
enrolled := func(n string) bool { return n != "stranger" }
if r := CheckPlacements(current, known, enrolled); len(r) != 0 {
t.Fatalf("a sound placement is refused: %v", r)
if r := CheckAssignments(current, known, enrolled); len(r) != 0 {
t.Fatalf("a sound assignments is refused: %v", r)
}
// A replicated mesh seat (ADR 0223) is held on two nodes; a seat that is not replicated is refused there.
for name, refused := range map[string]bool{"mesh-dns-resolver": false, "mesh-broker": true} {
a := seat.Placement{Node: "ace", Modules: []seat.PlacedModule{{Module: "dns"}}, Holds: []seat.Holding{{Claim: name, Scope: "mesh", Module: "dns"}}}
a := seat.Assignments{Node: "ace", Modules: []seat.PlacedModule{{Module: "dns"}}, Holds: []seat.Holding{{Claim: name, Scope: "mesh", Module: "dns"}}}
b := a
b.Node = "novox"
r := CheckPlacements(map[string]seat.Placement{"ace": a, "novox": b}, known, enrolled)
r := CheckAssignments(map[string]seat.Assignments{"ace": a, "novox": b}, known, enrolled)
if (len(r) != 0) != refused {
t.Errorf("%s on two nodes: refusals %v, want refused %v", name, r, refused)
}
@@ -89,7 +89,7 @@ func TestPlacement(t *testing.T) {
twice := novox
twice.Node = "stranger"
twice.Decided.Address = "10.77.0.2"
r := CheckPlacements(map[string]seat.Placement{"g14": clash, "novox": novox, "stranger": twice}, known, enrolled)
r := CheckAssignments(map[string]seat.Assignments{"g14": clash, "novox": novox, "stranger": twice}, known, enrolled)
for _, want := range []string{"holds no module ghost", "ghost and app are both published on host port 18080",
"bound to ace/postgres", "pin of shell names g14/bash", "stranger is a node with no record",
"mesh-store, held once for the mesh, is held on novox and stranger",
@@ -113,7 +113,7 @@ func TestPlacement(t *testing.T) {
t.Fatalf("laid %+v, %v", laid, findings)
}
if !reflect.DeepEqual(client.Assigned(current["g14"]), []string{"zsh", "app"}) {
t.Error("laying changed the current placement")
t.Error("laying changed the current assignments")
}
if fresh, f := LayChanges(current, []seat.ProposalChange{{Op: client.OpPlace, Node: "pi", Module: "zsh"}}); len(f) != 0 || fresh["pi"].Node != "pi" {
t.Errorf("a node with no key: %+v, %v", fresh, f)
@@ -125,7 +125,7 @@ func TestPlacement(t *testing.T) {
"not placed": {seat.ProposalChange{Op: client.OpTakeOff, Node: "g14", Module: "nope"}, "not placed"},
"no such field": {seat.ProposalChange{Op: client.OpDecide, Node: "g14", Field: "colour", Value: "x"}, "no decided field"},
"wrong type": {seat.ProposalChange{Op: client.OpDecide, Node: "g14", Field: "hub", Value: "x"}, "does not take"},
"unknown op": {seat.ProposalChange{Op: "move", Node: "g14"}, "no change of placement"},
"unknown op": {seat.ProposalChange{Op: "move", Node: "g14"}, "no change of assignments"},
"pin of nobody": {seat.ProposalChange{Op: client.OpPin, Node: "g14", Provision: "x"}, "provider"},
"not held": {seat.ProposalChange{Op: client.OpUnhold, Node: "g14", Module: "app", Claim: "x"}, "holds no"},
"bad port": {seat.ProposalChange{Op: client.OpPlace, Node: "g14", Module: "x", Ports: map[string]int64{"a": 1}}, "port"},
@@ -190,8 +190,8 @@ func TestPlacement(t *testing.T) {
t.Errorf("the digest of %s is %s, and the earlier holder made %s", name, c.got, c.want)
}
}
if ExportDigest(current) != ExportDigest(map[string]seat.Placement{"novox": novox, "g14": g14}) || ExportDigest(current) == ExportDigest(nil) {
t.Error("the export digest depends on order or not on the placements")
if ExportDigest(current) != ExportDigest(map[string]seat.Assignments{"novox": novox, "g14": g14}) || ExportDigest(current) == ExportDigest(nil) {
t.Error("the export digest depends on order or not on the assignments")
}
// 774/S42/AC1 (ADR 0587 §7): an add-node carries its enrolment's warrant naming its node and address, as its one
// change, from the enroller alone; a warrant naming another node or address is refused.
@@ -267,7 +267,7 @@ func TestPlacement(t *testing.T) {
AddNodeRefusal(remove, enroller, enrollers) != "" || !slices.Equal(RemovedNodes(remove), []string{"lab"}) {
t.Errorf("a remove-node: %v, %q", err, AddNodeRefusal(remove, "mesh-delivery-assigner", enrollers))
}
joined := map[string]seat.Placement{"lab": {V: keys.Version, Node: "lab", Decided: seat.NodeDecided{Address: "10.0.0.7"}, Modules: []seat.PlacedModule{}}}
joined := map[string]seat.Assignments{"lab": {V: keys.Version, Node: "lab", Decided: seat.NodeDecided{Address: "10.0.0.7"}, Modules: []seat.PlacedModule{}}}
if laid, f := LayChanges(joined, remove.Changes); len(f) != 0 || len(laid) != 0 {
t.Errorf("a joining node removed: %v, %v", laid, f)
}
@@ -301,9 +301,9 @@ func TestPlacement(t *testing.T) {
}
})
// ADR 0571 clause 4: who holds which seat, from placement; a node seat by its node.
// ADR 0571 clause 4: who holds which seat, from assignments; a node seat by its node.
t.Run("who holds which seat", func(t *testing.T) {
held := SeatHoldersOf(map[string]seat.Placement{
held := SeatHoldersOf(map[string]seat.Assignments{
"ace": {Node: "ace", Holds: []seat.Holding{{Claim: "mesh-dns-resolver", Scope: "mesh", Module: "dnsmasq"}, {Claim: "node-files", Scope: "node", Module: "mesh-files"}}},
"novox": {Node: "novox", Holds: []seat.Holding{{Claim: "mesh-dns-resolver", Scope: "mesh", Module: "dnsmasq"}, {Claim: "mesh-broker", Scope: "mesh", Module: "nats"},
{Claim: "photo-gallery", Scope: "mesh", Module: "photos"}}},
@@ -1,17 +1,17 @@
package placement
package assignment
import (
"encoding/json"
"fmt"
"strings"
client "git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient"
seat "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
client "git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient"
seat "git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
"git.novox.be/novox/mesh-sdk/go/keys"
)
// Placement candidates (novox/hq ADR 0567 §3): for each node a proposal must hear from, the holder writes one key
// `<proposal>.<node>` in its state `candidates`: the node's placement with the proposal laid over it for a node whose key
// Assignments candidates (novox/hq ADR 0567 §3): for each node a proposal must hear from, the holder writes one key
// `<proposal>.<node>` in its state `candidates`: the node's assignments with the proposal laid over it for a node whose key
// it changes, or the node's current key for one whose composition it changes only through another node (Reached). The
// layouts are the seat's contract's (CandidatesByProposalNodeValue); what refuses a candidate is here.
@@ -31,7 +31,7 @@ func CandidateKey(proposal, node string) (string, error) {
func ParseCandidateKey(key string) (proposal, node string, err error) {
proposal, node, ok := strings.Cut(key, ".")
if !ok || keys.CheckName("node", node) != nil || client.CheckProposalID(proposal) != nil {
return "", "", fmt.Errorf("placement: %q is not a placement candidate key, <proposal>.<node>", key)
return "", "", fmt.Errorf("assignments: %q is not an assignments candidate key, <proposal>.<node>", key)
}
return proposal, node, nil
}
@@ -40,8 +40,8 @@ func checkCandidate(c seat.CandidatesByProposalNodeValue) error {
if err := client.CheckProposalID(c.Proposal); err != nil {
return err
}
if c.Placement != nil {
return client.CheckPlacement(*c.Placement)
if c.Assignments != nil {
return client.CheckAssignments(*c.Assignments)
}
return nil
}
@@ -52,15 +52,15 @@ func EncodeCandidate(c seat.CandidatesByProposalNodeValue) ([]byte, error) {
return nil, err
}
c.V = keys.Version
if c.Placement != nil {
p := *c.Placement
if c.Assignments != nil {
p := *c.Assignments
p.V = keys.Version
c.Placement = &p
c.Assignments = &p
}
return json.Marshal(c)
}
// DecodeCandidate reads a candidate; it refuses an unknown version, a proposal id that is not one, and a placement the
// DecodeCandidate reads a candidate; it refuses an unknown version, a proposal id that is not one, and assignments the
// client refuses.
func DecodeCandidate(data []byte) (seat.CandidatesByProposalNodeValue, error) {
if err := keys.CheckVersion(data); err != nil {
@@ -68,7 +68,7 @@ func DecodeCandidate(data []byte) (seat.CandidatesByProposalNodeValue, error) {
}
c, err := seat.DecodeCandidatesByProposalNodeValue(data)
if err != nil {
return seat.CandidatesByProposalNodeValue{}, fmt.Errorf("placement: a candidate does not fit its shape: %w", err)
return seat.CandidatesByProposalNodeValue{}, fmt.Errorf("assignments: a candidate does not fit its shape: %w", err)
}
if err := checkCandidate(c); err != nil {
return seat.CandidatesByProposalNodeValue{}, err
@@ -1,9 +1,9 @@
// Package placement is the assigner's own check of a placement and of a proposal that changes it (novox/hq ADR 0567 §2,
// Package assignments is the assigner's own check of assignments and of a proposal that changes it (novox/hq ADR 0567 §2,
// ADR 0634 §1 on where a seat's code goes): what the holder of the seat mesh-module-assigner decides before it writes
// anything. The layouts it works on are the seat's contract's (package meshmoduleassigner); what every reader of the
// placement shares (the keys assembled into a node's placement, the node key's digest, who holds a seat) is the seat's
// assignments shares (the keys assembled into a node's assignments, the node key's digest, who holds a seat) is the seat's
// client's (package meshmoduleassignerclient). Nothing here is read by another module.
package placement
package assignment
import (
"encoding/json"
@@ -11,8 +11,8 @@ import (
"sort"
"strings"
client "git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient"
seat "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
client "git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient"
seat "git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/keys"
)
@@ -25,19 +25,19 @@ var KeptApart = []struct{ A, B, Record string }{
{A: "mesh-watcher", B: "mesh-self-check", Record: "ADR 0227 §6, ADR 0564 clause 6"},
}
// CheckPlacements is the holder's check of a proposal laid over every placement (ADR 0567 §2), over the placements by
// CheckAssignments is the holder's check of a proposal laid over every assignments (ADR 0567 §2), over the assignments by
// node, the modules that are known and the nodes with a record. It returns every refusal, in node order, never only the
// first: a placement naming a node not enrolled or a module that is not known (held by the indexer, or provided by the
// first: assignments naming a node not enrolled or a module that is not known (held by the indexer, or provided by the
// mesh itself: the caller's `known` says both); two modules of one node on the same host port; a pin, binding or holding
// naming a provider that is not placed where it says; a seat of scope mesh held twice, unless the seat is replicated
// (catalogue.Seat's Replicated, ADR 0223), which is held once per node; two nodes with one address on the private
// network; more than one hub; two modules KeptApart on one node. A host port a manifest declares without a number and a
// requirement left unbound are the composition's to find (its ForeseenPorts and Unbound).
func CheckPlacements(placements map[string]seat.Placement, known func(module string) bool, enrolled func(node string) bool) []string {
func CheckAssignments(assignments map[string]seat.Assignments, known func(module string) bool, enrolled func(node string) bool) []string {
var refusals []string
nodes := sortedKeys(placements)
nodes := sortedKeys(assignments)
placed := func(p seat.Provider) bool {
other, ok := placements[p.Node]
other, ok := assignments[p.Node]
if !ok {
return false
}
@@ -52,9 +52,9 @@ func CheckPlacements(placements map[string]seat.Placement, known func(module str
addresses := map[string]string{}
var hubs []string
for _, node := range nodes {
p := placements[node]
p := assignments[node]
if p.Node != node {
refusals = append(refusals, fmt.Sprintf("the placement of %s names the node %q", node, p.Node))
refusals = append(refusals, fmt.Sprintf("the assignments of %s names the node %q", node, p.Node))
}
if a := strings.TrimSpace(p.Decided.Address); a != "" {
if other, twice := addresses[a]; twice {
@@ -122,8 +122,8 @@ func CheckPlacements(placements map[string]seat.Placement, known func(module str
return refusals
}
// Change is what `placement-changed` says of one node (ADR 0567 §6): the node, the revision of its key before (0: it had
// none) and after (0: the key was deleted), and its modules added, removed or changed against the placement before.
// Change is what `assignments-changed` says of one node (ADR 0567 §6): the node, the revision of its key before (0: it had
// none) and after (0: the key was deleted), and its modules added, removed or changed against the assignments before.
type Change struct {
Node string `json:"node"`
Before uint64 `json:"before"`
@@ -141,10 +141,10 @@ func (c Change) Body() map[string]any {
return out
}
// ChangeOf is what changed from before to after on one node; before is the zero Placement for a node new to placement.
// ChangeOf is what changed from before to after on one node; before is the zero Assignments for a node new to assignments.
// A change of the node's pins or holdings counts as a change of every module placed on it both times, since the
// composition of each may read them.
func ChangeOf(before, after seat.Placement) Change {
func ChangeOf(before, after seat.Assignments) Change {
c := Change{Node: after.Node}
if c.Node == "" {
c.Node = before.Node
@@ -176,7 +176,7 @@ func ChangeOf(before, after seat.Placement) Change {
return c
}
// SortChanges orders a placement-changed event's nodes by node.
// SortChanges orders assignments-changed event's nodes by node.
func SortChanges(c []Change) {
sort.Slice(c, func(i, j int) bool { return c[i].Node < c[j].Node })
}
@@ -187,14 +187,14 @@ func jsonEqual(a, b any) bool {
return string(ja) == string(jb)
}
// SeatHoldersOf is the state composed from every node's placement (ADR 0567 §1): each holding by the seat it claims,
// SeatHoldersOf is the state composed from every node's assignments (ADR 0567 §1): each holding by the seat it claims,
// keyed as the client's SeatHoldersKey says, with the module holding it and its node. A claim naming a seat's former name
// is keyed by the seat's name (ADR 0122); one naming no seat is kept, marked Outside. Deterministic, so a writer writes
// only a key that differs.
func SeatHoldersOf(placements map[string]seat.Placement) map[string]seat.SeatHolders {
func SeatHoldersOf(assignments map[string]seat.Assignments) map[string]seat.SeatHolders {
out := map[string]seat.SeatHolders{}
for _, n := range sortedKeys(placements) {
for _, h := range placements[n].Holds {
for _, n := range sortedKeys(assignments) {
for _, h := range assignments[n].Holds {
node := ""
if h.Scope == "node" {
node = n
@@ -1,4 +1,4 @@
package placement
package assignment
import (
"crypto/sha256"
@@ -6,7 +6,7 @@ import (
"encoding/json"
"fmt"
seat "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
seat "git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
"git.novox.be/novox/mesh-sdk/go/keys"
)
@@ -72,7 +72,7 @@ type tunnelPeerForm struct {
Address string `json:"address,omitempty"`
}
type placementForm struct {
type assignmentsForm struct {
V int `json:"v"`
Node string `json:"node"`
Revision string `json:"revision,omitempty"`
@@ -107,7 +107,7 @@ type changeForm struct {
Layer map[string]any `json:"layer,omitempty"`
MeshValues map[string]any `json:"mesh-wide-values,omitempty"`
Values map[string]any `json:"values,omitempty"`
Whole *placementForm `json:"whole,omitempty"`
Whole *assignmentsForm `json:"whole,omitempty"`
Decided *decidedForm `json:"decided,omitempty"`
Keys *joiningForm `json:"keys,omitempty"`
}
@@ -142,8 +142,8 @@ func decidedOf(d seat.NodeDecided) decidedForm {
Adopted: d.Adopted, Taken: d.Taken, Tunnel: tunnelOf(d.Tunnel), PeerNames: d.PeerNames}
}
func placementOf(p seat.Placement) placementForm {
out := placementForm{V: int(p.V), Node: p.Node, Revision: p.Revision, Decided: decidedOf(p.Decided),
func assignmentsOf(p seat.Assignments) assignmentsForm {
out := assignmentsForm{V: int(p.V), Node: p.Node, Revision: p.Revision, Decided: decidedOf(p.Decided),
Pins: providersOf(p.Pins)}
if p.Modules != nil {
out.Modules = []placedForm{}
@@ -170,7 +170,7 @@ func changeOf(c seat.ProposalChange) changeForm {
out.Value, _ = json.Marshal(c.Value)
}
if c.Whole != nil {
w := placementOf(*c.Whole)
w := assignmentsOf(*c.Whole)
out.Whole = &w
}
if c.Decided != nil {
@@ -183,14 +183,14 @@ func changeOf(c seat.ProposalChange) changeForm {
return out
}
// ExportDigest is the digest of an export's answer, every node's placement, as the seed proposes it: the same
// placements give the same digest, whatever order they came in.
func ExportDigest(placements map[string]seat.Placement) string {
// ExportDigest is the digest of an export's answer, every node's assignments, as the seed proposes it: the same
// assignments give the same digest, whatever order they came in.
func ExportDigest(assignments map[string]seat.Assignments) string {
h := sha256.New()
for _, node := range sortedKeys(placements) {
p := placements[node]
p.V = keys.Version // the export was made with the placement encoded at the keys' version
raw, _ := json.Marshal(placementOf(p))
for _, node := range sortedKeys(assignments) {
p := assignments[node]
p.V = keys.Version // the export was made with the assignments encoded at the keys' version
raw, _ := json.Marshal(assignmentsOf(p))
fmt.Fprintf(h, "%s\n%s\n", node, raw)
}
return "sha256:" + hex.EncodeToString(h.Sum(nil))
@@ -1,4 +1,4 @@
package placement
package assignment
import (
"os"
@@ -1,4 +1,4 @@
package placement
package assignment
import (
"crypto/rand"
@@ -9,13 +9,13 @@ import (
"strings"
"time"
client "git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient"
seat "git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
enroller "git.novox.be/novox/mesh-contracts/identity-and-access/go/seats/meshnodeenroller"
client "git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient"
seat "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
"git.novox.be/novox/mesh-sdk/go/keys"
)
// A placement changes only by a proposal (novox/hq ADR 0567 §2): `propose` takes why and a list of changes; the holder
// Assignments changes only by a proposal (novox/hq ADR 0567 §2): `propose` takes why and a list of changes; the holder
// lays them over the current keys, names the revision of each key it laid them over, and either refuses at once with every
// finding or writes the proposal under its id in its state `proposals` and a candidate for each node it must hear from.
@@ -50,7 +50,7 @@ func CheckChange(c seat.ProposalChange) error {
}
needs := func(what string, ok bool) error {
if !ok {
return fmt.Errorf("placement: a %s change of %s names no %s", c.Op, c.Node, what)
return fmt.Errorf("assignments: a %s change of %s names no %s", c.Op, c.Node, what)
}
return nil
}
@@ -67,50 +67,50 @@ func CheckChange(c seat.ProposalChange) error {
return needs("field or value", c.Field != "" && c.Value != nil)
case client.OpSeed:
if c.Whole == nil || c.Whole.Node != c.Node {
return fmt.Errorf("placement: the seed of %s names no placement of that node", c.Node)
return fmt.Errorf("assignments: the seed of %s names no assignments of that node", c.Node)
}
return client.CheckPlacement(*c.Whole)
return client.CheckAssignments(*c.Whole)
case client.OpAddNode:
if c.Decided == nil || c.Decided.Address == "" || c.Keys == nil || c.Keys.Overlay == "" {
return fmt.Errorf("placement: the add-node of %s names no address or no tunnel key", c.Node)
return fmt.Errorf("assignments: the add-node of %s names no address or no tunnel key", c.Node)
}
// The identity keys come at redeem, whole or not at all: an identity with no sealing key, or the reverse.
if (c.Keys.Identity == "") != (c.Keys.Sealing == "") || (c.Keys.Identity == "" && c.Keys.Serving != "") {
return fmt.Errorf("placement: the add-node of %s names part of its identity keys; they come whole at redeem", c.Node)
return fmt.Errorf("assignments: the add-node of %s names part of its identity keys; they come whole at redeem", c.Node)
}
if c.Decided.Hub {
return fmt.Errorf("placement: the add-node of %s makes it the hub, which a joining node never is", c.Node)
return fmt.Errorf("assignments: the add-node of %s makes it the hub, which a joining node never is", c.Node)
}
if c.Module != "" || len(c.Ports)+len(c.Bindings) > 0 || c.Whole != nil {
return fmt.Errorf("placement: the add-node of %s places a module; a module is placed by a proposal of its own", c.Node)
return fmt.Errorf("assignments: the add-node of %s places a module; a module is placed by a proposal of its own", c.Node)
}
return nil
case client.OpRemoveNode:
if c.Module != "" || c.Decided != nil || c.Keys != nil || c.Whole != nil {
return fmt.Errorf("placement: the remove-node of %s names more than its node", c.Node)
return fmt.Errorf("assignments: the remove-node of %s names more than its node", c.Node)
}
return nil
}
return fmt.Errorf("placement: %q is no change of placement: place, take-off, pin, unpin, hold, unhold, decide, config, add-node or remove-node", c.Op)
return fmt.Errorf("assignments: %q is no change of assignments: place, take-off, pin, unpin, hold, unhold, decide, config, add-node or remove-node", c.Op)
}
// LayChanges is the placements of the nodes the changes name with the changes laid over current, in the order given;
// current is every node's placement now (absent: the node has no key). It returns, by node, the laid placement, and
// LayChanges is the assignments of the nodes the changes name with the changes laid over current, in the order given;
// current is every node's assignments now (absent: the node has no key). It returns, by node, the laid assignments, and
// every finding: a change that does not fit its node (a module taken off that is not placed, configured or holding
// there; a field NodeDecided does not have) or a placement the client refuses. What CheckPlacements refuses across nodes
// there; a field NodeDecided does not have) or assignments the client refuses. What CheckAssignments refuses across nodes
// is the caller's to ask of the result laid over current.
func LayChanges(current map[string]seat.Placement, changes []seat.ProposalChange) (map[string]seat.Placement, []string) {
laid := map[string]seat.Placement{}
func LayChanges(current map[string]seat.Assignments, changes []seat.ProposalChange) (map[string]seat.Assignments, []string) {
laid := map[string]seat.Assignments{}
var findings []string
of := func(node string) seat.Placement {
of := func(node string) seat.Assignments {
if p, ok := laid[node]; ok {
return p
}
p, ok := current[node]
if !ok {
return seat.Placement{V: keys.Version, Node: node, Modules: []seat.PlacedModule{}}
return seat.Assignments{V: keys.Version, Node: node, Modules: []seat.PlacedModule{}}
}
return clonePlacement(p)
return cloneAssignments(p)
}
for i, c := range changes {
if err := CheckChange(c); err != nil {
@@ -129,14 +129,14 @@ func LayChanges(current map[string]seat.Placement, changes []seat.ProposalChange
}
switch c.Op {
case client.OpSeed:
p = clonePlacement(*c.Whole)
p = cloneAssignments(*c.Whole)
case client.OpAddNode:
// A joining node has no key yet (ADR 0587 §7): one that has is no joining node.
if _, has := current[c.Node]; has {
findings = append(findings, fmt.Sprintf("change %d: %s is placed already, so it does not join", i+1, c.Node))
continue
}
p = seat.Placement{V: keys.Version, Node: c.Node, Decided: *c.Decided, Modules: []seat.PlacedModule{}}
p = seat.Assignments{V: keys.Version, Node: c.Node, Decided: *c.Decided, Modules: []seat.PlacedModule{}}
case client.OpRemoveNode:
// Only a joining node never confirmed goes this way: one with no module and no holding (a node that runs
// anything is taken off module by module, by a proposal the operator approves).
@@ -227,7 +227,7 @@ func LayChanges(current map[string]seat.Placement, changes []seat.ProposalChange
laid[c.Node] = p
}
for _, node := range sortedKeys(laid) {
if err := client.CheckPlacement(laid[node]); err != nil {
if err := client.CheckAssignments(laid[node]); err != nil {
findings = append(findings, err.Error())
}
}
@@ -275,9 +275,9 @@ var decidedFields = func() map[string]bool {
return out
}()
func clonePlacement(p seat.Placement) seat.Placement {
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{}
@@ -301,33 +301,33 @@ func CheckProposal(p seat.Proposal) error {
return err
}
if strings.TrimSpace(p.By) == "" || strings.TrimSpace(p.Why) == "" {
return fmt.Errorf("placement: the proposal %s names no proposer or no why", p.ID)
return fmt.Errorf("assignments: the proposal %s names no proposer or no why", p.ID)
}
if len(p.Base) == 0 || (len(p.Changes) == 0) != p.HandBack || (p.HandBack && p.Seed) {
return fmt.Errorf("placement: the proposal %s changes nothing, or is a hand-back with changes", p.ID)
return fmt.Errorf("assignments: the proposal %s changes nothing, or is a hand-back with changes", p.ID)
}
for i, c := range p.Changes {
if err := CheckChange(c); err != nil {
return fmt.Errorf("placement: the proposal %s, change %d: %w", p.ID, i+1, err)
return fmt.Errorf("assignments: the proposal %s, change %d: %w", p.ID, i+1, err)
}
if (c.Op == client.OpSeed) != p.Seed {
return fmt.Errorf("placement: the proposal %s mixes the seed with other changes", p.ID)
return fmt.Errorf("assignments: the proposal %s mixes the seed with other changes", p.ID)
}
if _, ok := p.Base[c.Node]; !ok {
return fmt.Errorf("placement: the proposal %s changes %s and names no base revision of its key", p.ID, c.Node)
return fmt.Errorf("assignments: the proposal %s changes %s and names no base revision of its key", p.ID, c.Node)
}
}
switch p.State {
case client.ProposalAnswering, client.ProposalFailed:
case client.ProposalAsked:
if p.Ask == "" || p.Verdict == nil || p.Verdict.State != VerdictPass {
return fmt.Errorf("placement: the proposal %s is asked with no ask or no passing verdict", p.ID)
return fmt.Errorf("assignments: the proposal %s is asked with no ask or no passing verdict", p.ID)
}
default:
return fmt.Errorf("placement: the proposal %s is in no state %q", p.ID, p.State)
return fmt.Errorf("assignments: the proposal %s is in no state %q", p.ID, p.State)
}
if p.Seed && p.ExportDigest == "" {
return fmt.Errorf("placement: the seed %s names no export digest", p.ID)
return fmt.Errorf("assignments: the seed %s names no export digest", p.ID)
}
return checkAddNode(p)
}
@@ -344,25 +344,25 @@ func checkAddNode(p seat.Proposal) error {
}
if adds == 0 {
if p.Warrant != nil {
return fmt.Errorf("placement: the proposal %s carries an enrolment's warrant and adds no node", p.ID)
return fmt.Errorf("assignments: the proposal %s carries an enrolment's warrant and adds no node", p.ID)
}
return nil
}
if adds != 1 || len(p.Changes) != 1 || p.HandBack || p.Seed {
return fmt.Errorf("placement: the proposal %s adds a node beside other changes; an add-node is a proposal's one change", p.ID)
return fmt.Errorf("assignments: the proposal %s adds a node beside other changes; an add-node is a proposal's one change", p.ID)
}
c, w := p.Changes[0], p.Warrant
if w == nil || w.Ask == "" || w.Warrant == "" {
return fmt.Errorf("placement: the add-node of %s carries no warrant of its enrolment", c.Node)
return fmt.Errorf("assignments: the add-node of %s carries no warrant of its enrolment", c.Node)
}
if c.Op == client.OpRemoveNode {
if w.Node != c.Node {
return fmt.Errorf("placement: the remove-node of %s carries the warrant of %s", c.Node, w.Node)
return fmt.Errorf("assignments: the remove-node of %s carries the warrant of %s", c.Node, w.Node)
}
return nil
}
if w.Node != c.Node || w.Address != c.Decided.Address {
return fmt.Errorf("placement: the add-node of %s at %s carries the warrant of %s at %s", c.Node, c.Decided.Address, w.Node, w.Address)
return fmt.Errorf("assignments: the add-node of %s at %s carries the warrant of %s at %s", c.Node, c.Decided.Address, w.Node, w.Address)
}
return nil
}
@@ -383,10 +383,10 @@ func AddNodeRefusal(p seat.Proposal, caller string, enrollers []string) string {
return ""
}
// EnrollersOf is the modules that hold the seat mesh-node-enroller on the placements given, sorted: the state seat-holders
// EnrollersOf is the modules that hold the seat mesh-node-enroller on the assignments given, sorted: the state seat-holders
// composed from them (SeatHoldersOf), read by the client's HoldersOf.
func EnrollersOf(placements map[string]seat.Placement) []string {
holders, _ := client.HoldersOf(rawHolders(SeatHoldersOf(placements)), enroller.Seat, "")
func EnrollersOf(assignments map[string]seat.Assignments) []string {
holders, _ := client.HoldersOf(rawHolders(SeatHoldersOf(assignments)), enroller.Seat, "")
var out []string
for _, h := range holders {
if !slices.Contains(out, h.Module) {
@@ -422,7 +422,7 @@ func DecodeProposal(data []byte) (seat.Proposal, error) {
}
p, err := seat.DecodeProposalsByProposalValue(data)
if err != nil {
return seat.Proposal{}, fmt.Errorf("placement: a proposal does not fit its shape: %w", err)
return seat.Proposal{}, fmt.Errorf("assignments: a proposal does not fit its shape: %w", err)
}
if err := CheckProposal(p); err != nil {
return seat.Proposal{}, err
@@ -1,6 +1,6 @@
package placement
package assignment
import seat "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
import seat "git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
// Module is the module that holds the seat mesh-module-assigner: the states of the seat are named after it, as every
// module state is (a bucket is `<module>_<state>`), and its manifest declares them.
@@ -8,7 +8,7 @@ const Module = "mesh-delivery-assigner"
// The buckets of the seat's states, which only the holder writes (ADR 0567 §1).
const (
PlacementBucket = Module + "_" + seat.StatePlacement
AssignmentsBucket = Module + "_" + seat.StateAssignments
ProposalsBucket = Module + "_" + seat.StateProposals
CandidatesBucket = Module + "_" + seat.StateCandidates
PulsesBucket = Module + "_" + seat.StatePulses
@@ -16,8 +16,8 @@ const (
)
const (
// PlacementHistory is the revisions the placement bucket keeps per key: the bus's most (§6).
PlacementHistory = 64
// AssignmentsHistory is the revisions the assignments bucket keeps per key: the bus's most (§6).
AssignmentsHistory = 64
// EventDays is how long the seat's events are kept: they ride the shared events stream on the seat's own subject
// (the seat's family, 7 days).
EventDays = 7
@@ -7,13 +7,13 @@ package buckets
import (
"git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
watchdogs "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshwatchdogs"
wants "git.novox.be/novox/mesh-contracts/placement/go/seats/meshwants"
catalog "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
delivery "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery"
wants "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshwants"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
"git.novox.be/novox/mesh-delivery/modules/mesh-grants/grantsstate"
plc "mesh-delivery-assigner/internal/placement"
plc "mesh-delivery-assigner/internal/assignment"
)
// The states the assigner keeps: the wants seat's, and its own asks, puts and policies.
@@ -1,5 +1,5 @@
// Package judge is how the seat mesh-module-assigner's holder judges a proposal (novox/hq ADR 0567 §3, §4, as ADR 0593 §8
// changes them): what it changes on each node by its placement (Intended), every node answering it, and the verdict of
// changes them): what it changes on each node by its assignments (Intended), every node answering it, and the verdict of
// their answers (JudgeAnswers). The assigner's own (ADR 0593 §1), moved back from mesh-sdk's compose/judge: the holder
// composes nothing, since every composition is its node's engine's, and a node answers through the candidates path
// with what it composed. It holds no bus client and no timer: the holder gives it what it read.
@@ -17,24 +17,24 @@ import (
"time"
nec "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
mma "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
mma "git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
plc "mesh-delivery-assigner/internal/placement"
plc "mesh-delivery-assigner/internal/assignment"
)
// AnswerBound is how long the holder waits for a node's answer after its candidate was written: the time to live of the
// node's pulse key (placement.PulseAnswerKey), whose expiry names it as giving no answer, never passed. The same answer
// node's pulse key (assignment.PulseAnswerKey), whose expiry names it as giving no answer, never passed. The same answer
// bound a check gives a node (ADR 0593 §8, §7: 60 seconds, nec.AnswerBound). A variable so a test on the bus
// can shorten it.
var AnswerBound = nec.AnswerBound
// Intended is, by node, the modules whose placed entry the proposal changes there, and the nodes that answer it: every
// node (ADR 0593 §8, changing ADR 0567 §4's list of the nodes that must answer), sorted. current is the placement as it
// stands, laid the proposal's placements by node (nil removes the node's key) and nodes every node with a record. A
// node (ADR 0593 §8, changing ADR 0567 §4's list of the nodes that must answer), sorted. current is the assignments as it
// stands, laid the proposal's assignments by node (nil removes the node's key) and nodes every node with a record. A
// node's set holds the modules the proposal adds, removes or whose placed entry differs on that node; a node with no
// key now gets an empty set (§7: the seed is answered as the same). What a node's compositions change beyond its own
// placement it says itself in its answer, marked in the brief as not computed.
func Intended(current map[string]mma.Placement, laid map[string]*mma.Placement, nodes []string) (intended map[string]map[string]bool, must []string) {
// assignments it says itself in its answer, marked in the brief as not computed.
func Intended(current map[string]mma.Assignments, laid map[string]*mma.Assignments, nodes []string) (intended map[string]map[string]bool, must []string) {
intended = map[string]map[string]bool{}
all := map[string]bool{}
for _, n := range nodes {
@@ -50,7 +50,7 @@ func Intended(current map[string]mma.Placement, laid map[string]*mma.Placement,
before, placed := current[node]
after, changesKey := laid[node]
if !changesKey || !placed {
continue // untouched, or new to placement: every answer must be the same
continue // untouched, or new to assignments: every answer must be the same
}
var afterModules []mma.PlacedModule
if after != nil {
@@ -8,20 +8,20 @@ import (
"testing"
"time"
mma "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
mma "git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
plc "mesh-delivery-assigner/internal/placement"
plc "mesh-delivery-assigner/internal/assignment"
)
// ADR 0567 §3, §4 (rows moved from the assigner's check of a pull request's placement, ADR 0559 §5): what a proposal
// ADR 0567 §3, §4 (rows moved from the assigner's check of a pull request's assignments, ADR 0559 §5): what a proposal
// changes by node, and the verdict of the answers: differences shown and failing nothing, a refusal and a silent node
// failing, a later answer judging again, the seed failing on any difference.
func TestJudge(t *testing.T) {
current := map[string]mma.Placement{"anchor": {Node: "anchor", Modules: []mma.PlacedModule{{Module: "mail"}}}, "server": {Node: "server"}}
laid := map[string]*mma.Placement{
current := map[string]mma.Assignments{"anchor": {Node: "anchor", Modules: []mma.PlacedModule{{Module: "mail"}}}, "server": {Node: "server"}}
laid := map[string]*mma.Assignments{
"anchor": {Node: "anchor", Modules: []mma.PlacedModule{{Module: "mail", Ports: map[string]int64{"25": 2525}}, {Module: "web"}}},
"laptop": {Node: "laptop", Modules: []mma.PlacedModule{{Module: "web"}}}}
// ADR 0593 §8: every node answers; what the placement changes is read from the placement alone.
// ADR 0593 §8: every node answers; what the assignments changes is read from the assignments alone.
got, must := Intended(current, laid, []string{"anchor", "laptop", "server"})
if !got["anchor"]["mail"] || !got["anchor"]["web"] || len(got["laptop"]) != 0 || len(got["server"]) != 0 ||
!slices.Equal(must, []string{"anchor", "laptop", "server"}) {