297 lines
12 KiB
Go
297 lines
12 KiB
Go
package assign
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"sort"
|
|
"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.
|
|
|
|
// Leased says whether the lease write of ADR 0547 has happened, after which the controller's assignments are read no
|
|
// more (§8). Not before L3: until then the holder keeps the controller in step and refuses what its verbs cannot take.
|
|
var Leased = false
|
|
|
|
// 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
|
|
Seed bool
|
|
ExportDigest string
|
|
}
|
|
|
|
// 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 the
|
|
// controller's verbs cannot take before the lease write (§8), 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, Seed: in.Seed, ExportDigest: in.ExportDigest, 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) != in.Seed {
|
|
findings = append(findings, fmt.Sprintf("change %d: the seed is adopt's alone, and never mixed with other changes", i+1))
|
|
continue
|
|
}
|
|
if !Leased {
|
|
if why := c.BeforeLease(); why != "" {
|
|
findings = append(findings, fmt.Sprintf("change %d: %s", i+1, why))
|
|
}
|
|
}
|
|
}
|
|
laid, lay := delivery.LayChanges(in.Current, in.Changes)
|
|
findings = append(findings, lay...)
|
|
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]
|
|
}
|
|
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 }
|
|
}
|
|
for _, r := range delivery.CheckPlacements(all, held, recorded) {
|
|
findings = append(findings, r)
|
|
}
|
|
return p, out, findings
|
|
}
|
|
|
|
// Write is one key a warrant writes (§5): the node, its key, the revision 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.PlacementKey(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
|
|
}
|
|
|
|
// ControllerCall is one call of the controller's seat verbs the holder makes after writing an approved proposal, until
|
|
// the lease write (§8): assign, unassign, pin or unpin, with its arguments.
|
|
type ControllerCall struct {
|
|
Verb string
|
|
Args map[string]any
|
|
// Node and Module name the pair the call keeps in step (a pin names no module).
|
|
Node, Module string
|
|
}
|
|
|
|
// Key names the call, as the condition of its pair says it.
|
|
func (c ControllerCall) Key() string {
|
|
return fmt.Sprintf("mesh-controller.%s %s %s", c.Verb, c.Node, c.Module)
|
|
}
|
|
|
|
// ControllerCalls is every call that brings the controller's assignments and pins to the placement after: assign for a
|
|
// module placed on a node before not, unassign for one taken off, pin for a pin set or changed, unpin for one removed,
|
|
// each naming the proposal and its warrant in its why. In node order, unassigns before assigns, so a module moved
|
|
// between nodes is first taken off.
|
|
func ControllerCalls(before map[string]delivery.Placement, after map[string]*delivery.Placement, proposal, warrant string) []ControllerCall {
|
|
why := fmt.Sprintf("placement proposal %s, approved (%s)", proposal, warrant)
|
|
nodes := make([]string, 0, len(after))
|
|
for node := range after {
|
|
nodes = append(nodes, node)
|
|
}
|
|
sort.Strings(nodes)
|
|
var off, on, pins []ControllerCall
|
|
for _, node := range nodes {
|
|
was, now := before[node], delivery.Placement{}
|
|
if a := after[node]; a != nil {
|
|
now = *a
|
|
}
|
|
had, has := map[string]bool{}, map[string]bool{}
|
|
for _, m := range was.Modules {
|
|
had[m.Module] = true
|
|
}
|
|
for _, m := range now.Modules {
|
|
has[m.Module] = true
|
|
if !had[m.Module] {
|
|
on = append(on, ControllerCall{Verb: "assign", Node: node, Module: m.Module, Args: map[string]any{"node": node, "module": m.Module}})
|
|
}
|
|
}
|
|
for _, m := range was.Modules {
|
|
if !has[m.Module] {
|
|
off = append(off, ControllerCall{Verb: "unassign", Node: node, Module: m.Module, Args: map[string]any{"node": node, "module": m.Module}})
|
|
}
|
|
}
|
|
for _, provision := range sortedKeys(now.Pins) {
|
|
to := now.Pins[provision]
|
|
if old, ok := was.Pins[provision]; !ok || old != to {
|
|
pins = append(pins, ControllerCall{Verb: "pin", Node: node, Module: provision,
|
|
Args: map[string]any{"node": node, "provision": provision, "from": to.Node, "module": to.Module}})
|
|
}
|
|
}
|
|
for _, provision := range sortedKeys(was.Pins) {
|
|
if _, ok := now.Pins[provision]; !ok {
|
|
pins = append(pins, ControllerCall{Verb: "unpin", Node: node, Module: provision, Args: map[string]any{"node": node, "provision": provision}})
|
|
}
|
|
}
|
|
}
|
|
out := append(append(off, on...), pins...)
|
|
for i := range out {
|
|
out[i].Args["why"] = why
|
|
}
|
|
return out
|
|
}
|
|
|
|
// 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 p.Seed {
|
|
headline = "Take placement over from the controller?"
|
|
}
|
|
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
|
|
}
|
|
|
|
// ExportOf reads the controller's answer to placement-export (§7): its verb answers what the command printed, the
|
|
// JSON of every node's placement, in `output` (the controller's verbAnswer; `answer` when it carries it as data). A
|
|
// refused or unreadable answer is an error naming it.
|
|
func ExportOf(raw []byte) (map[string]delivery.Placement, error) {
|
|
var a struct {
|
|
Output string `json:"output"`
|
|
OK *bool `json:"ok"`
|
|
Answer json.RawMessage `json:"answer"`
|
|
}
|
|
if err := json.Unmarshal(raw, &a); err != nil {
|
|
return nil, fmt.Errorf("the controller's placement-export answered no verb answer: %w", err)
|
|
}
|
|
if a.OK != nil && !*a.OK {
|
|
return nil, fmt.Errorf("the controller's placement-export refused: %s", strings.TrimSpace(a.Output))
|
|
}
|
|
var byNode map[string]json.RawMessage
|
|
if len(a.Answer) > 0 && string(a.Answer) != "null" {
|
|
if err := json.Unmarshal(a.Answer, &byNode); err != nil {
|
|
return nil, fmt.Errorf("the controller's placement-export is no placement by node: %w", err)
|
|
}
|
|
} else if err := json.NewDecoder(strings.NewReader(a.Output)).Decode(&byNode); err != nil {
|
|
// The command prints its JSON first; what its standard error said after it is left.
|
|
return nil, fmt.Errorf("the controller's placement-export printed no placement by node: %w", err)
|
|
}
|
|
out := map[string]delivery.Placement{}
|
|
for node, rawP := range byNode {
|
|
p, err := delivery.DecodePlacement(rawP)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("the controller's placement-export of %s cannot be read: %w", node, err)
|
|
}
|
|
if p.Node != node {
|
|
return nil, fmt.Errorf("the controller's placement-export names %s under %s", p.Node, node)
|
|
}
|
|
out[node] = p
|
|
}
|
|
if len(out) == 0 {
|
|
return nil, fmt.Errorf("the controller's placement-export names no node")
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// SeedChanges is the seed's changes: every node's placement of the export, whole, in node order.
|
|
func SeedChanges(export map[string]delivery.Placement) []delivery.PlacementProposalChange {
|
|
var out []delivery.PlacementProposalChange
|
|
for _, node := range sortedKeys(export) {
|
|
p := export[node]
|
|
out = append(out, delivery.PlacementProposalChange{Op: delivery.OpSeed, Node: node, Whole: &p})
|
|
}
|
|
return out
|
|
}
|
|
|
|
// 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
|
|
}
|