161 lines
7.3 KiB
Go
161 lines
7.3 KiB
Go
package supervisor_test
|
|
|
|
import (
|
|
"git.cin.su/m/Assistant/runtime/supervisor"
|
|
"path/filepath"
|
|
"testing"
|
|
)
|
|
|
|
func TestLifeboatRollsBackCandidateToLastKnownGood(t *testing.T) {
|
|
l := supervisor.Lifeboat{}
|
|
l.Start(supervisor.Release{ID: "good", Version: "1"})
|
|
l.Record(supervisor.Observation{ReleaseID: "good", Ready: true})
|
|
l.Start(supervisor.Release{ID: "candidate", Version: "2"})
|
|
l.Record(supervisor.Observation{ReleaseID: "candidate", Ready: false})
|
|
if !l.Rollback() || l.Current.ID != "good" || l.Current.State != supervisor.ReleaseRolledBack {
|
|
t.Fatalf("%#v", l)
|
|
}
|
|
}
|
|
|
|
func TestPersistentLifeboatRestoresLastKnownGoodAfterCandidateFailure(t *testing.T) {
|
|
store, err := supervisor.NewDurableStore(filepath.Join(t.TempDir(), "control", "lifeboat.json"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
first := supervisor.NewPersistentLifeboat(store)
|
|
if ok, err := first.Start(supervisor.Release{ID: "good", Version: "1"}); err != nil || !ok {
|
|
t.Fatalf("start good: %v %v", ok, err)
|
|
}
|
|
if ok, err := first.Record(supervisor.Observation{ReleaseID: "good", Ready: true, ObservedAtUnixMillis: 1}); err != nil || !ok {
|
|
t.Fatalf("record good: %v %v", ok, err)
|
|
}
|
|
if ok, err := first.Start(supervisor.Release{ID: "candidate", Version: "2"}); err != nil || !ok {
|
|
t.Fatalf("start candidate: %v %v", ok, err)
|
|
}
|
|
if ok, err := first.Record(supervisor.Observation{ReleaseID: "candidate", Ready: false, Reason: "candidate crashed", ObservedAtUnixMillis: 2}); err != nil || !ok {
|
|
t.Fatalf("record failure: %v %v", ok, err)
|
|
}
|
|
second := supervisor.NewPersistentLifeboat(store)
|
|
if ok, err := second.Rollback(); err != nil || !ok {
|
|
t.Fatalf("rollback after restart: %v %v", ok, err)
|
|
}
|
|
state, err := second.Snapshot()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if state.Current.ID != "good" || state.Current.State != supervisor.ReleaseRolledBack || state.LastKnownGood.ID != "good" {
|
|
t.Fatalf("state=%+v", state)
|
|
}
|
|
}
|
|
|
|
func TestPersistentSupervisorRestoresTrafficModeAfterRestart(t *testing.T) {
|
|
store, err := supervisor.NewDurableStore(filepath.Join(t.TempDir(), "control", "release-state.json"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
first := supervisor.NewPersistentLifeboat(store)
|
|
if ok, err := first.Start(supervisor.Release{ID: "candidate", Version: "2"}); err != nil || !ok {
|
|
t.Fatalf("start: %v %v", ok, err)
|
|
}
|
|
if ok, err := first.Record(supervisor.Observation{ReleaseID: "candidate", Ready: true, ObservedAtUnixMillis: 1}); err != nil || !ok {
|
|
t.Fatalf("observe: %v %v", ok, err)
|
|
}
|
|
if ok, err := first.SetTraffic(supervisor.TrafficCanaryHeader, "http://stable:8084", "http://candidate:8084"); err != nil || !ok {
|
|
t.Fatalf("canary: %v %v", ok, err)
|
|
}
|
|
second := supervisor.NewPersistentLifeboat(store)
|
|
state, err := second.Snapshot()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if state.TrafficMode != supervisor.TrafficCanaryHeader || state.CandidateUpstream != "http://candidate:8084" || state.LastKnownGood.ID != "candidate" {
|
|
t.Fatalf("state=%+v", state)
|
|
}
|
|
if ok, err := second.RollbackTraffic(); err != nil || !ok {
|
|
t.Fatalf("rollback traffic: %v %v", ok, err)
|
|
}
|
|
state, err = second.Snapshot()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if state.TrafficMode != supervisor.TrafficStable || state.Current.State != supervisor.ReleaseRolledBack {
|
|
t.Fatalf("rollback state=%+v", state)
|
|
}
|
|
}
|
|
|
|
func testReleaseSet(id, revision, web, core, cognitive, research string) supervisor.ReleaseSet {
|
|
return supervisor.ReleaseSet{ID: id, Revision: revision, Components: map[string]supervisor.ComponentRelease{
|
|
supervisor.ComponentCreatorWeb: {Name: supervisor.ComponentCreatorWeb, Revision: revision, Upstream: web, HealthURL: web + "/healthz", ReadyURL: web + "/readyz"},
|
|
supervisor.ComponentCoreRuntime: {Name: supervisor.ComponentCoreRuntime, Revision: revision, Upstream: core, HealthURL: core + "/healthz", ReadyURL: core + "/readyz"},
|
|
supervisor.ComponentCognitiveWorker: {Name: supervisor.ComponentCognitiveWorker, Revision: revision, Upstream: cognitive, HealthURL: cognitive + "/healthz", ReadyURL: cognitive + "/readyz"},
|
|
supervisor.ComponentResearchRuntime: {Name: supervisor.ComponentResearchRuntime, Revision: revision, Upstream: research, HealthURL: research + "/healthz", ReadyURL: research + "/readyz"},
|
|
}}
|
|
}
|
|
|
|
func TestReleaseSetRejectsMixedRevisionsAndMissingComponents(t *testing.T) {
|
|
set := testReleaseSet("candidate", "r1", "http://web", "http://core", "http://cognitive", "http://research")
|
|
if !set.Validate() {
|
|
t.Fatal("complete release set should validate")
|
|
}
|
|
set.Components[supervisor.ComponentCoreRuntime] = supervisor.ComponentRelease{Name: supervisor.ComponentCoreRuntime, Revision: "r0", Upstream: "http://core", HealthURL: "http://core/healthz", ReadyURL: "http://core/readyz"}
|
|
if set.Validate() {
|
|
t.Fatal("mixed revision release set must be rejected")
|
|
}
|
|
}
|
|
|
|
func TestPersistentReleaseSetRollbackPreservesWholeLastKnownGood(t *testing.T) {
|
|
store, err := supervisor.NewDurableStore(filepath.Join(t.TempDir(), "control", "release-set.json"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
first := supervisor.NewPersistentLifeboat(store)
|
|
good := testReleaseSet("stable-r1", "r1", "http://web", "http://core", "http://cognitive", "http://research")
|
|
candidate := testReleaseSet("candidate-r2", "r2", "http://web-c", "http://core-c", "http://cognitive-c", "http://research-c")
|
|
if ok, err := first.EstablishBaselineSet(good); err != nil || !ok {
|
|
t.Fatalf("baseline: %v %v", ok, err)
|
|
}
|
|
if ok, err := first.StartSet(candidate); err != nil || !ok {
|
|
t.Fatalf("start: %v %v", ok, err)
|
|
}
|
|
observation := supervisor.ReleaseSetObservation{ReleaseID: candidate.ID, Ready: true, Components: map[string]supervisor.ComponentObservation{}}
|
|
for name := range candidate.Components {
|
|
observation.Components[name] = supervisor.ComponentObservation{Revision: candidate.Revision, Health: true, Ready: true}
|
|
}
|
|
if ok, err := first.RecordSet(observation); err != nil || !ok {
|
|
t.Fatalf("observe: %v %v", ok, err)
|
|
}
|
|
if ok, err := first.SetTrafficForSets(supervisor.TrafficCanaryHeader); err != nil || !ok {
|
|
t.Fatalf("canary: %v %v", ok, err)
|
|
}
|
|
second := supervisor.NewPersistentLifeboat(store)
|
|
if ok, err := second.RollbackSet(); err != nil || !ok {
|
|
t.Fatalf("rollback: %v %v", ok, err)
|
|
}
|
|
state, err := second.Snapshot()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if state.StableReleaseSet == nil || state.StableReleaseSet.Revision != "r1" || state.TrafficMode != supervisor.TrafficStable {
|
|
t.Fatalf("state=%+v", state)
|
|
}
|
|
}
|
|
|
|
func TestReleaseSetPromotionRetainsPreviousStableForRollback(t *testing.T) {
|
|
good := testReleaseSet("stable-r1", "r1", "http://web", "http://core", "http://cognitive", "http://research")
|
|
candidate := testReleaseSet("candidate-r2", "r2", "http://web-c", "http://core-c", "http://cognitive-c", "http://research-c")
|
|
l := supervisor.Lifeboat{}
|
|
if !l.EstablishBaselineSet(good) || !l.StartSet(candidate) {
|
|
t.Fatal("release set setup failed")
|
|
}
|
|
observation := supervisor.ReleaseSetObservation{ReleaseID: candidate.ID, Ready: true, Components: map[string]supervisor.ComponentObservation{}}
|
|
for name := range candidate.Components {
|
|
observation.Components[name] = supervisor.ComponentObservation{Revision: candidate.Revision, Health: true, Ready: true}
|
|
}
|
|
if !l.RecordSet(observation) || !l.PromoteSet() {
|
|
t.Fatal("promotion failed")
|
|
}
|
|
if l.RollbackTargetReleaseSet == nil || l.RollbackTargetReleaseSet.Revision != "r1" {
|
|
t.Fatalf("rollback target=%+v", l.RollbackTargetReleaseSet)
|
|
}
|
|
}
|