| 1 | package activator |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "errors" |
| 6 | "sync" |
| 7 | "testing" |
| 8 | "time" |
| 9 | ) |
| 10 | |
| 11 | // Final review wave 2, N2: a planned first wake that ran its build and was |
| 12 | // ended with no verdict tells the scheduler the build ran (GenCancelled, |
| 13 | // Executed, Gen.Build naming it), so a hold build's run holds the instance |
| 14 | // off its good build. Two ways it happens: an intentional stop cancels the |
| 15 | // observation of a healthy first wake, or the wake's lifecycle ends it after |
| 16 | // the exec (here Shutdown during the health check). The second, which |
| 17 | // reported nothing before, is reported for a hold first wake only, under the |
| 18 | // host's outcome fence, so no new wake plans before the scheduler has it. |
| 19 | // Since Task 5 every planned wake with a token ends with a terminal event: |
| 20 | // one that ran nothing is cancelled, not Executed, and an overlap wake ended |
| 21 | // the same way is cancelled Executed. Neither records anything, so neither |
| 22 | // is delivered under the fence. |
| 23 | func TestAFirstWakeEndedAfterItsExecReportsThatItRan(t *testing.T) { |
| 24 | const host = "one.example.com" |
| 25 | endInHealth := func(t *testing.T, mode SwapMode, refuse bool) (*Service, *genLog, *planGenBackend, chan bool) { |
| 26 | dir := t.TempDir() |
| 27 | good, next := stagedID(t, dir, "v1"), stagedID(t, dir, "v2") |
| 28 | plan := WakePlan{Boot: next, Mode: mode, FirstWake: true, Token: 41, ConfigRev: "sha256:c1"} |
| 29 | if mode == ModeOverlap { |
| 30 | plan.Fallback = &good |
| 31 | } |
| 32 | if refuse { |
| 33 | plan.Check = func(string) error { return errors.New("held") } |
| 34 | } |
| 35 | a, b, evs := planActivator(t, plan) |
| 36 | fenced := make(chan bool, 4) |
| 37 | a.OnGen = func(ev GenEvent) { |
| 38 | if ev.Kind == GenCancelled { |
| 39 | a.mu.Lock() |
| 40 | _, held := a.outcomeFences[host] |
| 41 | a.mu.Unlock() |
| 42 | fenced <- held |
| 43 | } |
| 44 | evs.add(ev) |
| 45 | } |
| 46 | entered := make(chan struct{}) |
| 47 | var once sync.Once |
| 48 | a.genHealth = func(ctx context.Context, _ Gen) error { |
| 49 | once.Do(func() { close(entered) }) |
| 50 | <-ctx.Done() |
| 51 | return ctx.Err() |
| 52 | } |
| 53 | go a.beginRequest(context.Background(), testRoute(host)) //nolint:errcheck |
| 54 | if !refuse { |
| 55 | <-entered |
| 56 | } else { |
| 57 | time.Sleep(100 * time.Millisecond) |
| 58 | } |
| 59 | a.Shutdown(context.Background()) |
| 60 | return a, evs, b, fenced |
| 61 | } |
| 62 | |
| 63 | t.Run("hold, ended in its health check", func(t *testing.T) { |
| 64 | _, evs, _, fenced := endInHealth(t, ModeHold, false) |
| 65 | ev := evs.wait(t, GenCancelled) |
| 66 | if !ev.Executed || ev.Gen.Build != "v2" || !ev.FirstWake || ev.Mode != ModeHold || ev.Token != 41 { |
| 67 | t.Fatalf("event %+v: want GenCancelled, Executed, naming v2, with the plan's token", ev) |
| 68 | } |
| 69 | if !<-fenced { |
| 70 | t.Fatal("the host's outcome fence was not held while the event was delivered") |
| 71 | } |
| 72 | }) |
| 73 | t.Run("overlap, ended in its health check", func(t *testing.T) { |
| 74 | _, evs, _, fenced := endInHealth(t, ModeOverlap, false) |
| 75 | ev := evs.wait(t, GenCancelled) |
| 76 | if !ev.Executed || ev.Gen.Build != "v2" || ev.Mode != ModeOverlap || ev.Token != 41 { |
| 77 | t.Fatalf("event %+v: want GenCancelled, Executed, naming v2, in overlap mode, with the plan's token", ev) |
| 78 | } |
| 79 | if <-fenced { |
| 80 | t.Fatal("an overlap wake's cancellation records nothing, yet it held the outcome fence") |
| 81 | } |
| 82 | time.Sleep(100 * time.Millisecond) |
| 83 | if k := evs.kinds(); len(k) != 1 { |
| 84 | t.Fatalf("events %v: want the one cancellation", k) |
| 85 | } |
| 86 | }) |
| 87 | t.Run("hold, refused before its exec", func(t *testing.T) { |
| 88 | _, evs, b, fenced := endInHealth(t, ModeHold, true) |
| 89 | ev := evs.wait(t, GenCancelled) |
| 90 | if ev.Executed || ev.Token != 41 || len(b.started()) != 0 { |
| 91 | t.Fatalf("event %+v, started %+v: a wake that ran nothing is cancelled, not Executed", ev, b.started()) |
| 92 | } |
| 93 | if <-fenced { |
| 94 | t.Fatal("a cancellation with nothing run held the outcome fence") |
| 95 | } |
| 96 | time.Sleep(100 * time.Millisecond) |
| 97 | if k := evs.kinds(); len(k) != 1 { |
| 98 | t.Fatalf("events %v: want the one cancellation", k) |
| 99 | } |
| 100 | }) |
| 101 | t.Run("hold, its observation cancelled", func(t *testing.T) { |
| 102 | shrinkFirstWake(t, 55*time.Second, 25*time.Second, 2*time.Second) |
| 103 | next := stagedID(t, t.TempDir(), "v2") |
| 104 | a, _, evs := planActivator(t, WakePlan{Boot: next, Mode: ModeHold, FirstWake: true, Token: 42}) |
| 105 | mustServe(t, a) |
| 106 | evs.wait(t, GenHealthy) |
| 107 | a.BounceFor(host, CauseConfig) |
| 108 | ev := evs.wait(t, GenCancelled) |
| 109 | if !ev.Executed || ev.Gen.Build != "v2" || ev.Mode != ModeHold || ev.Token != 42 { |
| 110 | t.Fatalf("event %+v: want the cancelled observation reported as executed, naming v2", ev) |
| 111 | } |
| 112 | }) |
| 113 | } |
| 114 | |