A check pending redelivery is read before the let-through fetches (none is handed back); a deleted module is done for the plan's key; the queue an ask came from is let go of when it ends. Tries say they run under their own id.
176 lines
6.5 KiB
Go
176 lines
6.5 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"sort"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/novox/mesh-controller/internal/inventory"
|
|
"github.com/novox/mesh-controller/internal/link"
|
|
)
|
|
|
|
// Issue 1687 (ADR 0483 clause 2): a delivery plan's key lives from the ask of its first build until every build of its last
|
|
// tier is built, or the plan fails, stops or waits on the operator.
|
|
|
|
type fakeBuildingStore struct {
|
|
mu sync.Mutex
|
|
keys map[string]link.PlanBuilding
|
|
puts map[string]int
|
|
}
|
|
|
|
func newFakeBuildingStore() *fakeBuildingStore {
|
|
return &fakeBuildingStore{keys: map[string]link.PlanBuilding{}, puts: map[string]int{}}
|
|
}
|
|
|
|
func (f *fakeBuildingStore) Put(_ context.Context, plan string, value []byte) error {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
var v link.PlanBuilding
|
|
if err := json.Unmarshal(value, &v); err != nil {
|
|
return err
|
|
}
|
|
f.keys[plan] = v
|
|
f.puts[plan]++
|
|
return nil
|
|
}
|
|
|
|
func (f *fakeBuildingStore) Delete(_ context.Context, plan string) error {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
delete(f.keys, plan)
|
|
return nil
|
|
}
|
|
|
|
func (f *fakeBuildingStore) Keys(context.Context) ([]string, error) {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
var out []string
|
|
for k := range f.keys {
|
|
out = append(out, k)
|
|
}
|
|
sort.Strings(out)
|
|
return out, nil
|
|
}
|
|
|
|
func aPlan(id, state string, tiers [][]string, states map[string]string) inventory.Plan {
|
|
p := inventory.Plan{ID: id, State: state, Tiers: tiers, Modules: map[string]*inventory.PlanModule{}}
|
|
now := time.Now().Add(-time.Minute)
|
|
for m, st := range states {
|
|
pm := &inventory.PlanModule{State: st}
|
|
if st != "" {
|
|
pm.AskedAt = &now
|
|
}
|
|
p.Modules[m] = pm
|
|
}
|
|
return p
|
|
}
|
|
|
|
func TestAPlanIsInItsBuildPhaseFromTheFirstAskToTheLastBuild(t *testing.T) {
|
|
tiers := [][]string{{"a", "b"}, {"c"}}
|
|
cases := []struct {
|
|
name string
|
|
plan inventory.Plan
|
|
want bool
|
|
}{
|
|
{"no build asked yet", aPlan("p", inventory.PlanBuilding, tiers, nil), false},
|
|
{"the first tier asked", aPlan("p", inventory.PlanBuilding, tiers, map[string]string{"a": "asked", "b": "asked"}), true},
|
|
// Between two tiers: the first built and sent (rolling), the second not asked yet.
|
|
{"between two tiers", aPlan("p", inventory.PlanRolling, tiers, map[string]string{"a": "built", "b": "built"}), true},
|
|
{"the last tier asked", aPlan("p", inventory.PlanBuilding, tiers, map[string]string{"a": "built", "b": "built", "c": "asked"}), true},
|
|
{"every build built", aPlan("p", inventory.PlanRolling, tiers, map[string]string{"a": "built", "b": "built", "c": "built"}), false},
|
|
// A module the plan deletes is done as far as its tiers go (release_plan.go), so it holds no key through the rollout.
|
|
{"a deleted module is done", aPlan("p", inventory.PlanRolling, tiers, map[string]string{"a": "built", "b": "built", "c": planDeleted}), false},
|
|
{"failed", aPlan("p", inventory.PlanFailed, tiers, map[string]string{"a": "failed", "b": "asked"}), false},
|
|
{"superseded", aPlan("p", inventory.PlanSuperseded, tiers, map[string]string{"a": "asked"}), false},
|
|
{"a batch whose window is open", aPlan("p", inventory.PlanAssembling, tiers, map[string]string{"a": "asked"}), false},
|
|
}
|
|
for _, c := range cases {
|
|
if got := buildPhaseOf(c.plan); got != c.want {
|
|
t.Errorf("%s: in the build phase = %v, want %v", c.name, got, c.want)
|
|
}
|
|
}
|
|
waiting := aPlan("p", inventory.PlanBuilding, tiers, map[string]string{"a": "asked"})
|
|
waiting.Delivery = &inventory.PlanDelivery{Awaits: "the operator's word"}
|
|
if buildPhaseOf(waiting) {
|
|
t.Error("a plan that waits on the operator is in its build phase")
|
|
}
|
|
}
|
|
|
|
// The key's life: put, refreshed, deleted at the phase's end; a plan that left the phase unseen loses its key at the next look.
|
|
func TestAPlanKeyIsPutRefreshedAndDeletedAtThePhasesEnd(t *testing.T) {
|
|
ctx := context.Background()
|
|
store := newFakeBuildingStore()
|
|
tiers := [][]string{{"a"}, {"b"}}
|
|
building := aPlan("plan-1", inventory.PlanBuilding, tiers, map[string]string{"a": "asked"})
|
|
|
|
if faults := syncBuilding(ctx, store, []inventory.Plan{building}, time.Now()); len(faults) > 0 {
|
|
t.Fatal(faults)
|
|
}
|
|
if _, ok := store.keys["plan-1"]; !ok {
|
|
t.Fatal("no key while the first build is asked")
|
|
}
|
|
first := store.keys["plan-1"]
|
|
// Refreshed: put again, the phase's start kept.
|
|
time.Sleep(2 * time.Millisecond)
|
|
syncBuilding(ctx, store, []inventory.Plan{building}, time.Now())
|
|
if store.puts["plan-1"] != 2 || !store.keys["plan-1"].Seen.After(first.Seen) || !store.keys["plan-1"].Since.Equal(first.Since) {
|
|
t.Errorf("the key was not refreshed with its start kept: %d puts, %+v then %+v", store.puts["plan-1"], first, store.keys["plan-1"])
|
|
}
|
|
// Between the tiers: still there.
|
|
between := aPlan("plan-1", inventory.PlanRolling, tiers, map[string]string{"a": "built"})
|
|
syncBuilding(ctx, store, []inventory.Plan{between}, time.Now())
|
|
if _, ok := store.keys["plan-1"]; !ok {
|
|
t.Error("the key was gone between two tiers")
|
|
}
|
|
// A key of another plan that is no longer in the phase.
|
|
store.keys["plan-0"] = link.PlanBuilding{Plan: "plan-0"}
|
|
done := aPlan("plan-1", inventory.PlanRolling, tiers, map[string]string{"a": "built", "b": "built"})
|
|
syncBuilding(ctx, store, []inventory.Plan{done}, time.Now())
|
|
if len(store.keys) != 0 {
|
|
t.Errorf("keys remain after the last build was built: %v", store.keys)
|
|
}
|
|
}
|
|
|
|
// keepBuildingKey, told of each save, puts the key while the plan builds and deletes it when the plan fails.
|
|
func TestASavedPlanPutsOrDeletesItsKey(t *testing.T) {
|
|
ctx := context.Background()
|
|
store := newFakeBuildingStore()
|
|
var s buildingStore = store
|
|
buildingKeys.Store(&s)
|
|
defer buildingKeys.Store(nil)
|
|
|
|
p := aPlan("plan-7", inventory.PlanBuilding, [][]string{{"a"}}, map[string]string{"a": "asked"})
|
|
keepBuildingKey(ctx, p)
|
|
if _, ok := store.keys["plan-7"]; !ok {
|
|
t.Fatal("the key was not put when the plan was saved building")
|
|
}
|
|
p.State = inventory.PlanFailed
|
|
keepBuildingKey(ctx, p)
|
|
if _, ok := store.keys["plan-7"]; ok {
|
|
t.Fatal("the key stayed after the plan failed")
|
|
}
|
|
}
|
|
|
|
// The plan's key is there before its first build is asked, so a check cannot take a place between the ask and the plan's save.
|
|
func TestThePlansKeyIsPutBeforeItsFirstBuildIsAsked(t *testing.T) {
|
|
store := newFakeBuildingStore()
|
|
var s buildingStore = store
|
|
buildingKeys.Store(&s)
|
|
defer buildingKeys.Store(nil)
|
|
p, byName := aTierOfThree()
|
|
keyThere := false
|
|
was := askABuild
|
|
askABuild = func(context.Context, buildSource, string, string) (string, error) {
|
|
_, keyThere = store.keys["plan-1"]
|
|
return "build-x", nil
|
|
}
|
|
t.Cleanup(func() { askABuild = was })
|
|
askModule(t.Context(), p, "a", byName)
|
|
if !keyThere {
|
|
t.Fatal("the plan's key was not there when its first build was asked")
|
|
}
|
|
}
|