253 lines
11 KiB
Go
253 lines
11 KiB
Go
package assign
|
|
|
|
import (
|
|
"fmt"
|
|
"time"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/asks"
|
|
"git.novox.be/novox/mesh-sdk/go/delivery"
|
|
)
|
|
|
|
// A put (novox/hq ADR 0546): one version of a module put on one node by the assigner's tool assigner_put. The tool writes the put
|
|
// key; each pass takes it one step further from what the world holds (StepPut), and the rules pass over the node's want for
|
|
// the module from its asking until it ends (Module.Puts).
|
|
|
|
// ComposeWithin is how long a put waits for the composition and grant it needs (a take-in of a build just done writes them)
|
|
// before it is refused.
|
|
const ComposeWithin = 10 * time.Minute
|
|
|
|
// PutWorld is what one step of a put reads.
|
|
type PutWorld struct {
|
|
Node, Module string
|
|
Put delivery.Put
|
|
// Assigned says the module stands assigned to the node.
|
|
Assigned bool
|
|
// Bus says the module provides the mesh's bus (ADR 0539 §15); Recorded that it is of policy record (§14).
|
|
Bus, Recorded bool
|
|
// Build is the state of the builder's build at the put's fingerprint, "" when no key is written; BuildWords its error.
|
|
Build, BuildWords string
|
|
// Newest is the module's newest fingerprint at the trunk, which the builder builds by itself.
|
|
Newest string
|
|
// Composed is the composition of the module for the node at the fingerprint, as its writer holds it; nil when there is
|
|
// none. Granted says the grant at the fingerprint names the node's accounts. Both are read, never asked for (clause 4).
|
|
Composed *delivery.ComposedDigest
|
|
Granted bool
|
|
// Answer is the option the operator chose on this put's own ask (its asking, PutAsking), "" when none; AskExpired says
|
|
// that ask expired unanswered.
|
|
Answer string
|
|
AskExpired bool
|
|
// Snapshot says where the bus's streams were snapshotted for this put, once they were; Snapshotting that it was
|
|
// asked; SnapshotFailed why it was not taken.
|
|
Snapshot string
|
|
Snapshotting bool
|
|
SnapshotFailed string
|
|
// Have is what the node says it runs of the module; nil when it says nothing.
|
|
Have *delivery.Have
|
|
Now time.Time
|
|
}
|
|
|
|
// PutNext is what a step decides: the put as it stands after it, and the acts the pass does for it.
|
|
type PutNext struct {
|
|
Put delivery.Put
|
|
// Ask writes the put's ask about the move (PutAsk).
|
|
Ask bool
|
|
// Snapshot has the bus's node back the bus up now, its dump the streams' snapshot (ADR 0235).
|
|
Snapshot bool
|
|
// Warranted records, once, in the controller's hand-act log that the bus's want was written on the operator's warrant.
|
|
Warranted bool
|
|
}
|
|
|
|
// StepPut takes a put one step further from what the world holds (ADR 0546 clauses 3 to 8).
|
|
func StepPut(w PutWorld) PutNext {
|
|
p := w.Put
|
|
if delivery.PutEnded(p.Step) {
|
|
return PutNext{Put: p}
|
|
}
|
|
say := func(step, format string, args ...any) PutNext {
|
|
return PutNext{Put: p.Said(w.Now, step, fmt.Sprintf(format, args...))}
|
|
}
|
|
fp := short(p.FP)
|
|
if !w.Assigned {
|
|
return say(delivery.PutRefused, "%s is not assigned to %s: a put moves a module where it stands assigned (mesh-controller.assign first)", w.Module, w.Node)
|
|
}
|
|
switch w.Build {
|
|
case delivery.BuildDone:
|
|
case delivery.BuildFailed:
|
|
return say(delivery.PutRefused, "the builder's build of %s at %s failed: %s", w.Module, fp, w.BuildWords)
|
|
case "":
|
|
if p.FP != w.Newest {
|
|
// The builder builds the trunk's newest fingerprint and the candidates', and takes no ask for another yet (ADR 0546
|
|
// clause 3): an older fingerprint it never built, or whose key it removed, cannot be put.
|
|
return say(delivery.PutRefused, "the builder holds no build of %s at %s, and builds by itself only the trunk's newest (%s): put that, or a version it built", w.Module, fp, short(w.Newest))
|
|
}
|
|
return say(delivery.PutBuild, "waits for the builder's build of %s at %s, the trunk's newest, which it builds by itself", w.Module, fp)
|
|
default:
|
|
return say(delivery.PutBuild, "waits for the builder's build of %s at %s, which is %s", w.Module, fp, w.Build)
|
|
}
|
|
if w.Composed == nil || !w.Granted {
|
|
missing := fmt.Sprintf("no composition of %s for %s at %s is written", w.Module, w.Node, fp)
|
|
if w.Composed != nil {
|
|
missing = fmt.Sprintf("the grant of %s at %s does not name %s's accounts", w.Module, fp, w.Node)
|
|
}
|
|
since := w.Now
|
|
if p.Step == delivery.PutCompose && len(p.Steps) > 0 {
|
|
since = p.Steps[len(p.Steps)-1].At
|
|
}
|
|
if w.Now.Sub(since) > ComposeWithin {
|
|
return say(delivery.PutRefused, "%s after %s: only a composition written by the take-in of its build (or one of the five kept) is put; "+
|
|
"for another the hand path is mesh-hand-compose (ADR 0539 §10)", missing, ComposeWithin)
|
|
}
|
|
return say(delivery.PutCompose, "waits up to %s: %s yet", ComposeWithin, missing)
|
|
}
|
|
d := *w.Composed
|
|
if p.Step == delivery.PutWanted || p.Step == delivery.PutHolding {
|
|
return PutNext{Put: applied(w, p)}
|
|
}
|
|
p.Composed = d.Digest
|
|
if needsPerson(w, d) {
|
|
switch w.Answer {
|
|
case OptionNo:
|
|
return say(delivery.PutRefused, "the operator answered no to putting %s at %s on %s", w.Module, fp, w.Node)
|
|
case OptionYes:
|
|
default:
|
|
if w.AskExpired {
|
|
return say(delivery.PutRefused, "the operator's ask about putting %s at %s on %s expired unanswered: the node keeps what it runs", w.Module, fp, w.Node)
|
|
}
|
|
next := say(delivery.PutPerson, "waits for the operator's yes to putting %s at %s on %s (composition %s, judged %s)", w.Module, fp, w.Node, short(d.Digest), d.Judgement)
|
|
if p.Ask == "" {
|
|
next.Ask = true
|
|
next.Put.Ask, _ = delivery.RecordAskKey(w.Node, w.Module, d.Digest)
|
|
}
|
|
return next
|
|
}
|
|
if w.Bus {
|
|
switch {
|
|
case w.Snapshot != "":
|
|
p.Snapshot = w.Snapshot
|
|
case w.SnapshotFailed != "":
|
|
return say(delivery.PutRefused, "the bus's streams were not snapshotted, so the bus is not put: %s", w.SnapshotFailed)
|
|
case w.Snapshotting:
|
|
return say(delivery.PutSnapshot, "the operator said yes; waits for %s's backup holder to snapshot the bus's streams", w.Node)
|
|
default:
|
|
next := say(delivery.PutSnapshot, "the operator said yes; asked %s's backup holder to snapshot the bus's streams", w.Node)
|
|
next.Snapshot = true
|
|
return next
|
|
}
|
|
}
|
|
}
|
|
words := fmt.Sprintf("the want of %s on %s names %s (composition %s)", w.Module, w.Node, fp, short(d.Digest))
|
|
if p.Hold {
|
|
words += ", held until released"
|
|
}
|
|
if p.Snapshot != "" {
|
|
words += "; the streams were snapshotted: " + p.Snapshot
|
|
}
|
|
next := PutNext{Put: applied(w, p.Said(w.Now, delivery.PutWanted, words))}
|
|
next.Warranted = w.Bus && p.Ask != ""
|
|
return next
|
|
}
|
|
|
|
// applied is a wanted put once the node's have says how it went: holding or done when it runs the composition healthy.
|
|
func applied(w PutWorld, p delivery.Put) delivery.Put {
|
|
h := w.Have
|
|
if h == nil || h.FP != p.FP || h.Composed != p.Composed {
|
|
return p
|
|
}
|
|
switch {
|
|
case h.Phase == delivery.PhaseRunning && h.Health == delivery.HealthOK && p.Hold:
|
|
return p.Said(w.Now, delivery.PutHolding, fmt.Sprintf("%s runs %s at %s healthy; held there until released", w.Node, w.Module, short(p.FP)))
|
|
case h.Phase == delivery.PhaseRunning && h.Health == delivery.HealthOK:
|
|
return p.Said(w.Now, delivery.PutDone, fmt.Sprintf("%s runs %s at %s healthy; the rules write its want again", w.Node, w.Module, short(p.FP)))
|
|
case failed(*h):
|
|
return p.Said(w.Now, p.Step, fmt.Sprintf("%s runs %s at %s and its health check failed: %s", w.Node, w.Module, short(p.FP), h.Detail))
|
|
}
|
|
return p
|
|
}
|
|
|
|
// needsPerson says whether the put's move waits for the operator: any move of the bus but a revision at the fingerprint the
|
|
// node runs that changes only its user list (ADR 0539 §7, §15), and a move of a module of policy record judged to recreate a
|
|
// container, move data, or that cannot be judged (§14).
|
|
func needsPerson(w PutWorld, d delivery.ComposedDigest) bool {
|
|
runs := w.Have != nil && w.Have.FP == w.Put.FP
|
|
if runs && w.Have.Composed == d.Digest {
|
|
return false // the node runs this composition already: nothing moves
|
|
}
|
|
if w.Bus {
|
|
return !(runs && d.UserListOnly)
|
|
}
|
|
if !w.Recorded {
|
|
return false
|
|
}
|
|
return d.Judgement == delivery.JudgedRecreates || d.Judgement == delivery.JudgedMovesData || d.Judgement == delivery.JudgedCannot
|
|
}
|
|
|
|
// PutWant is the want a put writes once it is wanted: the fingerprint and composition, marked put, and held when it holds.
|
|
func PutWant(p delivery.Put) delivery.Want {
|
|
return delivery.Want{FP: p.FP, Composed: p.Composed, Put: true, Hold: p.Hold, Words: "put: " + p.Why}
|
|
}
|
|
|
|
// PutOwns says what a put does to the node's want: owns says the rules pass over it, want the want it writes (nil: what
|
|
// stands stays). A put owns the want from its asking until it ends; a held put ends only when released.
|
|
func PutOwns(p delivery.Put) (want *delivery.Want, owns bool) {
|
|
switch p.Step {
|
|
case delivery.PutWanted, delivery.PutHolding:
|
|
w := PutWant(p)
|
|
return &w, true
|
|
case delivery.PutDone, delivery.PutReleased, delivery.PutRefused:
|
|
return nil, false
|
|
}
|
|
return nil, true
|
|
}
|
|
|
|
// PutAsking is a put's own asking number: when it was asked, in milliseconds. Each put asked under the same key is another
|
|
// asking, so the ask is written again under an id of its own and an answer to an earlier put, whose warrant names that put's
|
|
// asking, never counts for a later one (ADR 0546 clause 8: one approval binds one act).
|
|
func PutAsking(p delivery.Put) int {
|
|
if n := p.At.UnixMilli(); n > 0 {
|
|
return int(n)
|
|
}
|
|
return 0
|
|
}
|
|
|
|
// PutAct is the act a bus put's yes binds and that is performed when its want is written.
|
|
func PutAct(node, module, fp, composed string) asks.Act {
|
|
return asks.Act{"put": "want", "node": node, "module": module, "fp": fp, "composed": composed}
|
|
}
|
|
|
|
// PutAsk is the ask about a put's move (ADR 0546 clauses 7 and 8), under the record ask's key of the composition so the
|
|
// teller relays it and its answer as any record ask's. For the bus its yes is of level approve and binds the act, so only a
|
|
// channel that proves who answered takes it (ADR 0259), never the view.
|
|
func PutAsk(key, node, module string, p delivery.Put, d delivery.ComposedDigest, bus bool, now time.Time) (delivery.AssignerAsk, error) {
|
|
yes := asks.Option{ID: OptionYes, Label: "Yes, put it", Does: "the node is told to apply " + module + " at " + short(p.FP), Level: asks.Acknowledge}
|
|
what := map[delivery.Judgement]string{
|
|
delivery.JudgedRecreates: "recreates a container",
|
|
delivery.JudgedMovesData: "moves the module's data",
|
|
delivery.JudgedCannot: "cannot be judged: " + d.Words,
|
|
delivery.JudgedNeither: "recreates no container and moves no data",
|
|
delivery.JudgedUnchanged: "changes nothing the node runs",
|
|
}[d.Judgement]
|
|
explanation := fmt.Sprintf("Asked to put %s at version %s on %s: %s. The move %s.", module, short(p.FP), node, p.Why, what)
|
|
expires := now.Add(7 * 24 * time.Hour)
|
|
if bus {
|
|
binds, err := asks.ActDigest(PutAct(node, module, p.FP, d.Digest))
|
|
if err != nil {
|
|
return delivery.AssignerAsk{}, err
|
|
}
|
|
yes.Level, yes.Binds = asks.Approve, binds
|
|
yes.Does = "the bus's streams are snapshotted, then " + node + " is told to apply the bus at " + short(p.FP)
|
|
explanation += " It is the mesh's bus: every module reconnects while it restarts, and its streams are snapshotted first."
|
|
expires = now.Add(asks.ApproveLasts)
|
|
}
|
|
return delivery.AssignerAsk{Ask: asks.Ask{
|
|
ID: delivery.AskingID(key, PutAsking(p)),
|
|
Headline: fmt.Sprintf("Put %s on %s?", module, node),
|
|
Explanation: explanation,
|
|
Options: []asks.Option{yes,
|
|
{ID: OptionNo, Label: "No", Does: "the node keeps what it runs, and the put is refused", Level: asks.Acknowledge}},
|
|
Who: asks.Operator,
|
|
Expires: expires,
|
|
OnExpiry: "the node keeps what it runs",
|
|
About: key,
|
|
}, Asking: PutAsking(p)}, nil
|
|
}
|