diff --git a/go/motion/motion.go b/go/motion/motion.go new file mode 100644 index 0000000..22f22b8 --- /dev/null +++ b/go/motion/motion.go @@ -0,0 +1,107 @@ +// Package motion derives whether a module is being delivered on a node, from its want and its have (novox/hq ADR 0564 clause +// 13 and Limits 12, ADR 0540 §7; issue 1952 S7). The delivery view (Delivery) says it `moving`, `settled` or `stuck`, and +// the condition keeper (Health and repair) holds a module's own findings while it is delivered; two domains read the one +// derivation, so it is mesh-sdk's (ADR 0634 §1), over plain values: the caller fills Want and Have from the assigner's +// want and the node-engine's have as their contracts lay them out, so this package names no seat and imports no contract. +package motion + +import "time" + +// **Whether a module is being delivered on a node, read from its want and its have** (novox/hq ADR 0564 clause 13 and +// Limits 12, ADR 0540 §7; issue 1952 S7): one derivation, used by the delivery view, which says a module `moving`, +// `settled` or `stuck` (the messenger's hold hears those), and by the condition keeper, which holds a module's own +// findings while it is delivered there and raises what still stands when the delivery ends. Before, it lived in the view +// alone (mesh-delivery-view internal/view motionOf), and a second reader would have judged a delivery its own way. + +// Want is what the derivation reads of a module's want on a node: its fingerprint and composition, and whether it is removed. +type Want struct { + FP string + Composed string + Removed bool +} + +// Have is what it reads of the module's have on that node: the fingerprint and composition it runs, its phase and health +// (the node-engine's words) and the have's own detail. +type Have struct { + FP string + Composed string + Phase string + Health string + Detail string +} + +// The phases and healths of a have the derivation tells apart, as the node-engine writes them. +const ( + PhaseApplying = "applying" + PhaseRunning = "running" + PhaseFailed = "failed" + PhaseHeld = "held" + HealthOK = "ok" + HealthFailed = "failed" +) + +// The motion of a module on a node, as the view says it. +const ( + MotionMoving = "moving" + MotionSettled = "settled" + MotionStuck = "stuck" +) + +// The outcomes of a motion that settled or stuck. +const ( + // OutcomeRunning is a settled module: the have reached the want, running and healthy. + OutcomeRunning = "running" + // OutcomeFailed is a have at the wanted fingerprint whose apply or health check failed. + OutcomeFailed = "failed" + // OutcomeStopped is a module held, or a want the watchman says stopped moving past its bound. + OutcomeStopped = "stopped" +) + +// DefaultDeliveryBound is how long a module's delivery holds its own findings when its manifest names no bound (ADR 0564 +// clause 13 (d)): past it, what is held is raised, saying the delivery did not end. +const DefaultDeliveryBound = 15 * time.Minute + +// Motion is where one module's want stands on one node. +type Motion struct { + // State is MotionMoving, MotionSettled or MotionStuck; empty when there is no want to move to (none, removed, or + // naming no fingerprint). + State string + // Outcome and Words are a settled or stuck motion's: running; failed with the have's words; stopped with the have's + // words (held) or the watchman's (stopped moving). + Outcome string + Words string + // Delivering says the module is being delivered there (clause 13 (a)): its want names a fingerprint other than its + // have's, or its have's phase is applying. + Delivering bool + // Failed says the delivery ended failed (Limits 12): the node refused or failed its apply (the have's phase failed, + // at whichever fingerprint it kept), or the have at the wanted fingerprint is unhealthy. What a hold kept is raised + // at once. Apart from State, which says failed only at the wanted fingerprint, as the view always has. + Failed bool +} + +// Ended says the delivery ended, failed or settled healthy at the want: what a hold kept is judged now. +func (m Motion) Ended() bool { return m.Failed || (m.State == MotionSettled && !m.Delivering) } + +// MotionOf is the motion of a module on a node from its want (wanted: one was read), its have (has: one was read) and +// the watchman's words when it says the want stopped moving (stuck: it says so and the want has not moved since). +func MotionOf(w Want, wanted bool, h Have, has bool, stuckWords string, stuck bool) Motion { + if !wanted || w.Removed || w.FP == "" { + return Motion{} + } + m := Motion{Delivering: !has || h.FP != w.FP || h.Phase == PhaseApplying, + Failed: has && (h.Phase == PhaseFailed || (h.FP == w.FP && h.Health == HealthFailed))} + applied := has && h.FP == w.FP && (h.Composed == "" || h.Composed == w.Composed) + switch { + case applied && h.Phase == PhaseRunning && h.Health == HealthOK: + m.State, m.Outcome = MotionSettled, OutcomeRunning + case has && h.Phase == PhaseHeld: + m.State, m.Outcome, m.Words = MotionStuck, OutcomeStopped, h.Detail + case has && h.FP == w.FP && (h.Health == HealthFailed || h.Phase == PhaseFailed): + m.State, m.Outcome, m.Words = MotionStuck, OutcomeFailed, h.Detail + case stuck: + m.State, m.Outcome, m.Words = MotionStuck, OutcomeStopped, stuckWords + default: + m.State = MotionMoving + } + return m +} diff --git a/go/motion/motion_test.go b/go/motion/motion_test.go new file mode 100644 index 0000000..42b4d1d --- /dev/null +++ b/go/motion/motion_test.go @@ -0,0 +1,57 @@ +package motion + +import "testing" + +// 1952 S7 (ADR 0564 clause 13 (a), (c) and Limits 12; ADR 0540 §7): one derivation of whether a module is being +// delivered on a node, read by the view (moving, settled, stuck, as its motionOf said them) and by the condition keeper +// (delivering, failed, ended). Wants and haves as the assigner and the node-engine write them (a refusal is written as phase failed at the prior fingerprint). +func TestMotionOf(t *testing.T) { + want := Want{FP: "fp-new", Composed: "sha256:new"} + at := func(fp, composed string, phase, health string, detail string) Have { + return Have{FP: fp, Composed: composed, Phase: phase, Health: health, Detail: detail} + } + for _, c := range []struct { + name string + w Want + wanted bool + h Have + has bool + stuck string + state string + outcome string + delivering bool + failed bool + ended bool + }{ + {name: "no want", h: at("fp-old", "", PhaseRunning, HealthOK, ""), has: true}, + {name: "a removed want", w: Want{Removed: true}, wanted: true}, + {name: "a want no node has yet", w: want, wanted: true, state: MotionMoving, delivering: true}, + {name: "the old fingerprint still runs", w: want, wanted: true, h: at("fp-old", "sha256:old", PhaseRunning, HealthOK, ""), has: true, + state: MotionMoving, delivering: true}, + {name: "applying the wanted fingerprint", w: want, wanted: true, h: at("fp-new", "sha256:new", PhaseApplying, "unknown", ""), has: true, + state: MotionMoving, delivering: true}, + {name: "checking at the want: no longer delivering, not yet settled", w: want, wanted: true, + h: at("fp-new", "sha256:new", "checking", "unknown", ""), has: true, state: MotionMoving}, + {name: "running healthy at the want", w: want, wanted: true, h: at("fp-new", "sha256:new", PhaseRunning, HealthOK, ""), has: true, + state: MotionSettled, outcome: OutcomeRunning, ended: true}, + {name: "running healthy at another composition", w: want, wanted: true, h: at("fp-new", "sha256:other", PhaseRunning, HealthOK, ""), has: true, + state: MotionMoving}, + {name: "unhealthy at the want", w: want, wanted: true, h: at("fp-new", "sha256:new", PhaseRunning, HealthFailed, "exit 1"), has: true, + state: MotionStuck, outcome: OutcomeFailed, failed: true, ended: true}, + {name: "its apply failed at the want", w: want, wanted: true, h: at("fp-new", "sha256:new", PhaseFailed, "unknown", "no image"), has: true, + state: MotionStuck, outcome: OutcomeFailed, failed: true, ended: true}, + {name: "refused, the prior fingerprint kept", w: want, wanted: true, + h: at("fp-old", "sha256:old", PhaseFailed, HealthOK, "not composed: no grant"), has: true, + state: MotionMoving, delivering: true, failed: true, ended: true}, + {name: "held on the node", w: want, wanted: true, h: at("fp-old", "sha256:old", PhaseHeld, HealthOK, "held by a person"), has: true, + state: MotionStuck, outcome: OutcomeStopped, delivering: true}, + {name: "the watchman says it stopped moving", w: want, wanted: true, h: at("fp-old", "sha256:old", PhaseRunning, HealthOK, ""), has: true, + stuck: "ace to apply fp-new", state: MotionStuck, outcome: OutcomeStopped, delivering: true}, + } { + m := MotionOf(c.w, c.wanted, c.h, c.has, c.stuck, c.stuck != "") + if m.State != c.state || m.Outcome != c.outcome || m.Delivering != c.delivering || m.Failed != c.failed || m.Ended() != c.ended { + t.Errorf("%s: %+v (ended %v), want state %q outcome %q delivering %v failed %v ended %v", c.name, m, m.Ended(), + c.state, c.outcome, c.delivering, c.failed, c.ended) + } + } +} diff --git a/go/packages_list_test.go b/go/packages_list_test.go index 66df9f7..c160b28 100644 --- a/go/packages_list_test.go +++ b/go/packages_list_test.go @@ -24,7 +24,7 @@ import ( // request: does every module share it, or is it a shape more than one part reads? var meshSDKPackages = []string{ ".", "announce", "announcetest", "buildstep", "buscalls", "buscompose", "busstate", "busretry", "calladdress", "catalogue", "channel", - "composer", "declaration", "delivery", "forgekey", "handacts", "health", "internal/beside", "keys", "kvlimit", "membership", + "composer", "declaration", "delivery", "forgekey", "handacts", "health", "internal/beside", "keys", "kvlimit", "membership", "motion", "objectlink", "outward", "overlay", "parts", "pernode", "process", "provider", "provider/harness", "provisions", "sdkversion", "seal", "seatdef", "seatlist", "seatlist/seatlisttest", "testbus", "views", "views/viewstest", }