| 1 | package rollout |
| 2 | |
| 3 | import ( |
| 4 | "testing" |
| 5 | "time" |
| 6 | |
| 7 | "github.com/carlosframework/platform/internal/activator" |
| 8 | ) |
| 9 | |
| 10 | func noBlocks(string, string, bool) bool { return false } |
| 11 | |
| 12 | // awakeQueue is an app queue whose hosts all serve v1, their good build. |
| 13 | func awakeQueue(blocks func(build, host string, served bool) bool, hosts ...string) *appQueue { |
| 14 | q := newAppQueue("bab", "blog", blocks) |
| 15 | for _, h := range hosts { |
| 16 | q.observe(h, "v1", "v1", t0) |
| 17 | } |
| 18 | return q |
| 19 | } |
| 20 | |
| 21 | func TestAReleaseOntoTheServingBuildIsDropped(t *testing.T) { |
| 22 | q := awakeQueue(noBlocks, "a") |
| 23 | q.wantBuild("a", activator.CauseRelease, "v1", t0) |
| 24 | if d, ok := q.pending(q.hosts["a"]); ok { |
| 25 | t.Fatalf("pending = %+v, want nothing: v1 is already serving", d.ID) |
| 26 | } |
| 27 | } |
| 28 | |
| 29 | func TestRestartAndConfigOntoTheServingBuildAreRealWork(t *testing.T) { |
| 30 | q := awakeQueue(noBlocks, "a", "b") |
| 31 | q.wantBuild("a", activator.CauseRelease, "v1", t0) |
| 32 | q.wantRestart("a", "r1", t0) |
| 33 | d, ok := q.pending(q.hosts["a"]) |
| 34 | if !ok || d.ID != (Identity{Host: "a", Cause: activator.CauseRestart, Build: "v1", RestartID: "r1"}) { |
| 35 | t.Fatalf("restart pending = %+v, %v", d.ID, ok) |
| 36 | } |
| 37 | if d.Class != classRecover { |
| 38 | t.Fatal("a restart is recovery: no failure record, no halt") |
| 39 | } |
| 40 | q.wantConfig("b", "c1", t0) |
| 41 | d, ok = q.pending(q.hosts["b"]) |
| 42 | if !ok || d.ID != (Identity{Host: "b", Cause: activator.CauseConfig, Build: "v1", ConfigRev: "c1"}) { |
| 43 | t.Fatalf("config pending = %+v, %v", d.ID, ok) |
| 44 | } |
| 45 | } |
| 46 | |
| 47 | func TestDedupeComparesTheWholeIdentity(t *testing.T) { |
| 48 | q := awakeQueue(noBlocks, "a") |
| 49 | if !q.wantBuild("a", activator.CauseRelease, "v2", t0) { |
| 50 | t.Fatal("the first desire of v2 must register") |
| 51 | } |
| 52 | rev := q.newest("a") |
| 53 | if q.wantBuild("a", activator.CauseRelease, "v2", t0) { |
| 54 | t.Fatal("the same desire again is the same attempt") |
| 55 | } |
| 56 | if q.newest("a") != rev { |
| 57 | t.Fatal("a duplicate moved the newest revision") |
| 58 | } |
| 59 | if !q.wantBuild("a", activator.CauseRetry, "v2", t0) { |
| 60 | t.Fatal("a retry onto the same build is a different identity") |
| 61 | } |
| 62 | } |
| 63 | |
| 64 | func TestANewerDesiredBuildSupersedesPerInstance(t *testing.T) { |
| 65 | q := awakeQueue(noBlocks, "a", "b") |
| 66 | q.wantBuild("a", activator.CauseRelease, "v2", t0) |
| 67 | q.wantBuild("b", activator.CauseRelease, "v2", t0) |
| 68 | ra := q.newest("a") |
| 69 | q.wantBuild("a", activator.CauseRelease, "v3", t0.Add(20*time.Second)) |
| 70 | d, _ := q.pending(q.hosts["a"]) |
| 71 | if d.ID.Build != "v3" || d.SchedRev != q.newest("a") || q.newest("a") == ra { |
| 72 | t.Fatalf("a pending %+v rev %d newest %d (was %d)", d.ID, d.SchedRev, q.newest("a"), ra) |
| 73 | } |
| 74 | if d, _ := q.pending(q.hosts["b"]); d.ID.Build != "v2" { |
| 75 | t.Fatal("supersession is per instance: b keeps v2 until its own desire moves") |
| 76 | } |
| 77 | } |
| 78 | |
| 79 | func TestRestartAndConfigCoalesceAndDoNotSupersede(t *testing.T) { |
| 80 | q := awakeQueue(noBlocks, "a") |
| 81 | q.wantBuild("a", activator.CauseRelease, "v2", t0) |
| 82 | q.wantRestart("a", "r1", t0) |
| 83 | q.wantRestart("a", "r2", t0) |
| 84 | q.wantConfig("a", "c1", t0) |
| 85 | q.wantConfig("a", "c2", t0) |
| 86 | d, _ := q.pending(q.hosts["a"]) |
| 87 | want := Identity{Host: "a", Cause: activator.CauseRelease, Build: "v2", ConfigRev: "c2", RestartID: "r2"} |
| 88 | if d.ID != want { |
| 89 | t.Fatalf("pending = %+v, want %+v: one attempt covers the newest restart and config", d.ID, want) |
| 90 | } |
| 91 | rev := q.newest("a") |
| 92 | q.wantRestart("a", "r3", t0) |
| 93 | q.wantConfig("a", "c3", t0) |
| 94 | if q.newest("a") != rev { |
| 95 | t.Fatal("a restart or config request must not abandon an attempt past spawn") |
| 96 | } |
| 97 | } |
| 98 | |
| 99 | func TestABlockedBuildFallsThroughToRecovery(t *testing.T) { |
| 100 | held := func(build, _ string, served bool) bool { return build == "v2" && !served } |
| 101 | q := awakeQueue(held, "a") |
| 102 | q.wantBuild("a", activator.CauseRelease, "v2", t0) |
| 103 | if _, ok := q.pending(q.hosts["a"]); ok { |
| 104 | t.Fatal("a held build must not be attempted") |
| 105 | } |
| 106 | q.wantRestart("a", "r1", t0) |
| 107 | d, ok := q.pending(q.hosts["a"]) |
| 108 | if !ok || d.ID.Cause != activator.CauseRestart || d.ID.Build != "v1" { |
| 109 | t.Fatalf("pending = %+v: the restart onto the serving build still goes ahead", d.ID) |
| 110 | } |
| 111 | } |
| 112 | |
| 113 | func TestAMoveBackToTheGoodBuildIsRecovery(t *testing.T) { |
| 114 | q := newAppQueue("bab", "blog", func(_, _ string, served bool) bool { return !served }) |
| 115 | q.observe("a", "v2", "v1", t0) // serving v2, which has not settled; good v1 |
| 116 | q.wantBuild("a", activator.CauseRelease, "v1", t0) |
| 117 | d, ok := q.pending(q.hosts["a"]) |
| 118 | if !ok || d.Class != classRecover { |
| 119 | t.Fatalf("pending = %+v %v: moving back onto a build it has served is recovery", d, ok) |
| 120 | } |
| 121 | } |
| 122 | |
| 123 | func TestNothingIsSwappedOnASleeper(t *testing.T) { |
| 124 | q := newAppQueue("bab", "blog", noBlocks) |
| 125 | q.observe("a", "", "v1", t0) |
| 126 | q.wantBuild("a", activator.CauseRelease, "v2", t0) |
| 127 | if _, ok := q.pending(q.hosts["a"]); ok { |
| 128 | t.Fatal("a sleeping instance boots its installed build on its next wake; there is nothing to swap") |
| 129 | } |
| 130 | } |
| 131 | |