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
236 lines
9.9 KiB
Go
236 lines
9.9 KiB
Go
package assign
|
|
|
|
import (
|
|
"fmt"
|
|
"maps"
|
|
"slices"
|
|
"strings"
|
|
"time"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/asks"
|
|
"git.novox.be/novox/mesh-sdk/go/delivery"
|
|
)
|
|
|
|
// The seat mesh-placement (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
|
|
// 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
|
|
// 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 []delivery.PlacementProposalChange
|
|
Current map[string]delivery.Placement
|
|
Revisions map[string]uint64
|
|
Held func(module string) bool
|
|
Recorded func(node string) bool
|
|
// Warrant is the enrolment's warrant an add-node carries (ADR 0587 §7); only that change has one.
|
|
Warrant *delivery.AddNodeWarrant
|
|
}
|
|
|
|
// 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,
|
|
// 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) (delivery.PlacementProposal, map[string]*delivery.Placement, []string) {
|
|
p := delivery.PlacementProposal{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]uint64{}, State: delivery.ProposalAnswering}
|
|
var findings []string
|
|
if p.By == "" {
|
|
findings = append(findings, "the runtime names no caller: a proposal is made by someone")
|
|
}
|
|
if p.Why == "" {
|
|
findings = append(findings, "a proposal says why: it is shown to the operator")
|
|
}
|
|
if len(in.Changes) == 0 {
|
|
findings = append(findings, "a proposal changes something: it names no change")
|
|
}
|
|
for i, c := range in.Changes {
|
|
if c.Op == delivery.OpSeed {
|
|
findings = append(findings, fmt.Sprintf("change %d: the seed is gone with adopt, the controller's export being gone", i+1))
|
|
}
|
|
}
|
|
laid, lay := delivery.LayChanges(in.Current, in.Changes)
|
|
findings = append(findings, lay...)
|
|
findings = append(findings, ControllerRefusals(in.Changes, laid)...)
|
|
all := map[string]delivery.Placement{}
|
|
for node, pl := range in.Current {
|
|
all[node] = pl
|
|
}
|
|
out := map[string]*delivery.Placement{}
|
|
for node, pl := range laid {
|
|
if before, ok := in.Current[node]; ok && delivery.SamePlacement(before, pl) {
|
|
continue
|
|
}
|
|
pl := pl
|
|
all[node] = pl
|
|
out[node] = &pl
|
|
p.Base[node] = 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.
|
|
for _, node := range p.RemovedNodes() {
|
|
if _, ok := in.Current[node]; !ok {
|
|
continue
|
|
}
|
|
delete(all, node)
|
|
out[node] = nil
|
|
p.Base[node] = in.Revisions[node]
|
|
}
|
|
if len(findings) == 0 && len(out) == 0 {
|
|
findings = append(findings, "the changes leave every placement as it is")
|
|
}
|
|
held, recorded := in.Held, in.Recorded
|
|
if held == nil {
|
|
held = func(string) bool { return false }
|
|
}
|
|
if recorded == nil {
|
|
recorded = func(string) bool { return false }
|
|
}
|
|
// A joining node writes its record only once it holds its account, which the add-node's write gives it: this one change by
|
|
// this one caller is the exception to a node with no record (ADR 0587 §7, changing ADR 0567 §2).
|
|
joining := map[string]bool{}
|
|
for _, c := range in.Changes {
|
|
if c.Op == delivery.OpAddNode {
|
|
joining[c.Node] = true
|
|
}
|
|
}
|
|
if len(joining) > 0 {
|
|
was := recorded
|
|
recorded = func(node string) bool { return joining[node] || was(node) }
|
|
}
|
|
for _, r := range delivery.CheckPlacements(all, held, recorded) {
|
|
findings = append(findings, r)
|
|
}
|
|
return p, out, findings
|
|
}
|
|
|
|
// 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
|
|
// (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 []delivery.PlacementProposalChange, laid map[string]delivery.Placement) []string {
|
|
var out []string
|
|
nodes := map[string]bool{}
|
|
for i, c := range changes {
|
|
places := c.Module == ControllerModule && c.Op != delivery.OpTakeOff
|
|
if c.Whole != nil && slices.Contains(c.Whole.Assigned(), ControllerModule) {
|
|
places = true
|
|
}
|
|
if places {
|
|
out = append(out, fmt.Sprintf("change %d places %s on %s: %s", i+1, ControllerModule, c.Node, ControllerEnded))
|
|
nodes[c.Node] = true
|
|
}
|
|
}
|
|
for _, node := range slices.Sorted(maps.Keys(laid)) {
|
|
if !nodes[node] && slices.Contains(laid[node].Assigned(), ControllerModule) {
|
|
out = append(out, fmt.Sprintf("%s would run %s: %s", node, ControllerModule, ControllerEnded))
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// Write is one node a warrant writes (§5): the node, its node key `<node>.node` (the commit of the node's keys,
|
|
// mesh-sdk delivery.PlacementKeys.Writes), the revision of the node's placement 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 *delivery.Placement
|
|
}
|
|
|
|
// Writes is every key a proposal writes, by node order.
|
|
func Writes(p delivery.PlacementProposal, laid map[string]*delivery.Placement) ([]Write, error) {
|
|
var out []Write
|
|
for _, node := range p.Nodes() {
|
|
key, err := delivery.PlacementNodeKey(node)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out = append(out, Write{Node: node, Key: key, Base: p.Base[node], Value: laid[node]})
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// The kinds of the condition (and finding) of a refused controller call, which the assigner raised until it stopped calling
|
|
// the controller's verbs (ADR 0567 §8, issue 774).
|
|
const (
|
|
KindControllerCall = "controller-call"
|
|
KindControllerPin = "controller-pin"
|
|
)
|
|
|
|
// 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) asks.Condition {
|
|
return asks.Condition{Key: delivery.AssignerModule + "." + kind + "." + node + "." + module,
|
|
Subject: asks.Scope{Scope: "module", ID: module, Machine: node}, State: asks.StateCleared,
|
|
Resolved: "the assigner calls no verb of the controller any more: placement 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 delivery.PlacementProposal) asks.Act {
|
|
return asks.Act{"placement": "write", "proposal": p.ID, "changes": delivery.ChangesDigest(p)}
|
|
}
|
|
|
|
// PlacementAsking 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 delivery.PlacementProposal, 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
|
|
// proves who answered takes it, with the brief of the verdict by node (judge.Brief) as its explanation.
|
|
func PlacementAsk(key string, p delivery.PlacementProposal, brief string, asking int, now time.Time) (delivery.AssignerAsk, error) {
|
|
binds, err := asks.ActDigest(PlacementAct(p))
|
|
if err != nil {
|
|
return delivery.AssignerAsk{}, err
|
|
}
|
|
nodes := p.Nodes()
|
|
headline := fmt.Sprintf("Change placement on %s?", strings.Join(nodes, ", "))
|
|
if n := len([]rune(headline)); n > asks.HeadlineLength {
|
|
headline = fmt.Sprintf("Change placement on %d machines?", len(nodes))
|
|
}
|
|
return delivery.AssignerAsk{Ask: asks.Ask{
|
|
ID: delivery.AskingID(key, asking),
|
|
Headline: headline,
|
|
Explanation: brief,
|
|
Options: []asks.Option{
|
|
{ID: OptionYes, Label: "Approve", Does: "the placement of " + strings.Join(nodes, ", ") + " is written as proposed",
|
|
Level: asks.Approve, Binds: binds},
|
|
{ID: OptionNo, Label: "Decline", Does: "nothing is written, and the proposal ends", Level: asks.Acknowledge},
|
|
},
|
|
Who: asks.Operator,
|
|
Expires: now.Add(asks.ApproveLasts),
|
|
OnExpiry: "nothing is written, and the proposal ends",
|
|
About: key,
|
|
}, Asking: asking}, nil
|
|
}
|
|
|
|
// 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 delivery.PlacementProposal, written map[string]*delivery.Placement) (delivery.PlacementProposal, bool) {
|
|
rest := p
|
|
rest.Changes, rest.Base = nil, map[string]uint64{}
|
|
for _, c := range p.Changes {
|
|
if _, done := written[c.Node]; !done {
|
|
rest.Changes = append(rest.Changes, c)
|
|
}
|
|
}
|
|
for node, base := range p.Base {
|
|
if _, done := written[node]; !done {
|
|
rest.Base[node] = base
|
|
}
|
|
}
|
|
rest.Must, rest.Verdict, rest.Ask, rest.State = nil, nil, "", delivery.ProposalAnswering
|
|
return rest, len(rest.Changes) > 0
|
|
}
|