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 (propose its placement 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 }