Read whether a module is being delivered on a node in one place, so the view and the condition keeper judge a delivery alike (issue 1952 S7, ADR 0564 clause 13)
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
package delivery
|
||||
|
||||
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.
|
||||
|
||||
// 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
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package delivery
|
||||
|
||||
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 (assigner.go Want,
|
||||
// delivery.go Have; agent.go writes a refusal as phase failed at the prior fingerprint).
|
||||
func TestMotionOf(t *testing.T) {
|
||||
want := Want{V: Version, FP: "fp-new", Composed: "sha256:new"}
|
||||
at := func(fp, composed string, phase Phase, health Health, detail string) Have {
|
||||
return Have{V: Version, 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{V: Version, 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, HealthUnknown, ""), 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", PhaseChecking, HealthUnknown, ""), 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, HealthUnknown, "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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+14
-18
@@ -8,8 +8,9 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// 1153/AC1: the stuck key is the stopped key's bucket and key, split at the first dot, refused as a pulse key is.
|
||||
// 1153/AC4: the watchman's module and its three buckets, each that module's state.
|
||||
func TestWatchmanBuckets(t *testing.T) {
|
||||
func TestStuckKey(t *testing.T) {
|
||||
if WatchmanModule != "mesh-delivery-watchman" {
|
||||
t.Fatalf("WatchmanModule = %q", WatchmanModule)
|
||||
}
|
||||
@@ -18,10 +19,6 @@ func TestWatchmanBuckets(t *testing.T) {
|
||||
t.Errorf("the %s bucket is %q, not %s_%s", name, bucket, WatchmanModule, name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 1153/AC1: the stuck key is the stopped key's bucket and key, split at the first dot, refused as a pulse key is.
|
||||
func TestStuckKey(t *testing.T) {
|
||||
key, err := StuckKey(PresenceBucket, "ace")
|
||||
if err != nil || key != "mesh-delivery-watchman_presence.ace" {
|
||||
t.Fatalf("StuckKey = %q, %v", key, err)
|
||||
@@ -47,7 +44,19 @@ func TestStuckKey(t *testing.T) {
|
||||
}
|
||||
|
||||
// 1153/AC2: a stuck value round-trips with "v":1, and an unknown or missing version is refused loudly.
|
||||
// 1153/AC3: equal stuck values are the same; one moved is not.
|
||||
func TestStuckValue(t *testing.T) {
|
||||
{
|
||||
a := Stuck{V: Version, Bucket: "b", Key: "k", Waited: "w", Since: time.Unix(100, 0).UTC()}
|
||||
b := a
|
||||
if !SameStuck(a, b) {
|
||||
t.Fatal("equal values are not the same")
|
||||
}
|
||||
b.Moved = true
|
||||
if SameStuck(a, b) {
|
||||
t.Fatal("a moved value is the same as a stuck one")
|
||||
}
|
||||
}
|
||||
s := Stuck{Bucket: PresenceBucket, Key: "ace", Waited: "ace to come back online",
|
||||
Since: time.Date(2026, 10, 18, 20, 0, 0, 0, time.UTC)}
|
||||
raw, err := EncodeStuck(s)
|
||||
@@ -73,16 +82,3 @@ func TestStuckValue(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 1153/AC3: equal stuck values are the same; one moved is not.
|
||||
func TestSameStuck(t *testing.T) {
|
||||
a := Stuck{V: Version, Bucket: "b", Key: "k", Waited: "w", Since: time.Unix(100, 0).UTC()}
|
||||
b := a
|
||||
if !SameStuck(a, b) {
|
||||
t.Fatal("equal values are not the same")
|
||||
}
|
||||
b.Moved = true
|
||||
if SameStuck(a, b) {
|
||||
t.Fatal("a moved value is the same as a stuck one")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user