| 1 | package activator |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "strings" |
| 6 | "sync" |
| 7 | "testing" |
| 8 | "time" |
| 9 | |
| 10 | "github.com/carlosframework/platform/internal/instancectl" |
| 11 | ) |
| 12 | |
| 13 | // queuingControl is root's channel during an edge restart (#292): a start |
| 14 | // whose job systemd has queued blocks until the client gives up, and the |
| 15 | // job outlives that. Only a stop of the unit replaces it. |
| 16 | type queuingControl struct { |
| 17 | *fakeControl |
| 18 | mu sync.Mutex |
| 19 | queued bool |
| 20 | } |
| 21 | |
| 22 | func (q *queuingControl) Start(ctx context.Context, host string, role instancectl.Role, max int64) (string, error) { |
| 23 | q.rec("start " + host + " " + string(role)) |
| 24 | q.mu.Lock() |
| 25 | q.queued = true |
| 26 | q.mu.Unlock() |
| 27 | <-ctx.Done() |
| 28 | return "", ctx.Err() |
| 29 | } |
| 30 | |
| 31 | func (q *queuingControl) Stop(ctx context.Context, host string, role instancectl.Role) error { |
| 32 | q.mu.Lock() |
| 33 | q.queued = false |
| 34 | q.mu.Unlock() |
| 35 | return q.fakeControl.Stop(ctx, host, role) |
| 36 | } |
| 37 | |
| 38 | func (q *queuingControl) stillQueued() bool { |
| 39 | q.mu.Lock() |
| 40 | defer q.mu.Unlock() |
| 41 | return q.queued |
| 42 | } |
| 43 | |
| 44 | // A legacy start that is cancelled while root's start job is queued comes |
| 45 | // back marked, so the wake's cleanup knows the job may still run; the |
| 46 | // proof stops the unit, which clears the job, and only then passes. |
| 47 | func TestACancelledLegacyStartIsMarkedAndItsStopClearsTheQueuedJob(t *testing.T) { |
| 48 | q := &queuingControl{fakeControl: &fakeControl{}} |
| 49 | unit := &fakeUnit{} |
| 50 | unit.set("inactive", "", false, false) |
| 51 | b, rt := nestedBackend(t, &fakeControl{}, unit) |
| 52 | b.Control = q |
| 53 | |
| 54 | ctx, cancel := context.WithCancel(context.Background()) |
| 55 | go func() { |
| 56 | for !q.stillQueued() { |
| 57 | time.Sleep(5 * time.Millisecond) |
| 58 | } |
| 59 | cancel() |
| 60 | }() |
| 61 | p, _, err := b.Start(ctx, rt) |
| 62 | if err == nil || p != nil { |
| 63 | t.Fatalf("Start = %v, %v; want no process and an error", p, err) |
| 64 | } |
| 65 | if !legacyStartMayRun(err) { |
| 66 | t.Fatalf("a cancelled legacy start is not marked as one whose job may run: %v", err) |
| 67 | } |
| 68 | if !q.stillQueued() { |
| 69 | t.Fatal("the fake lost its queued job on cancellation; it must model a job that survives") |
| 70 | } |
| 71 | |
| 72 | pctx, pcancel := context.WithTimeout(context.Background(), 2*time.Second) |
| 73 | defer pcancel() |
| 74 | if err := b.LegacyRunGone(pctx, rt); err != nil { |
| 75 | t.Fatalf("LegacyRunGone: %v", err) |
| 76 | } |
| 77 | if q.stillQueued() { |
| 78 | t.Fatal("the proof passed with the start job still queued") |
| 79 | } |
| 80 | if !strings.Contains(q.joined(), "stop "+rt.Host+" run") { |
| 81 | t.Fatalf("the run unit was not stopped through instance-control; calls:\n%s", q.joined()) |
| 82 | } |
| 83 | } |
| 84 | |
| 85 | // The proof fails, and so keeps the lease, while the unit is not stopped |
| 86 | // or its cgroup still holds a process. |
| 87 | func TestLegacyRunGoneRefusesWhileTheUnitRuns(t *testing.T) { |
| 88 | t.Parallel() |
| 89 | for _, tc := range []struct { |
| 90 | name string |
| 91 | active string |
| 92 | populated bool |
| 93 | }{ |
| 94 | {"active", "active", true}, |
| 95 | {"deactivating", "deactivating", true}, |
| 96 | {"inactive with processes left", "inactive", true}, |
| 97 | } { |
| 98 | t.Run(tc.name, func(t *testing.T) { |
| 99 | unit := &fakeUnit{} |
| 100 | unit.set(tc.active, "inv", true, tc.populated) |
| 101 | b, rt := nestedBackend(t, &fakeControl{}, unit) |
| 102 | ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond) |
| 103 | defer cancel() |
| 104 | if err := b.LegacyRunGone(ctx, rt); err == nil { |
| 105 | t.Fatal("LegacyRunGone proved a unit that still runs") |
| 106 | } |
| 107 | }) |
| 108 | } |
| 109 | } |
| 110 | |