package activator import ( "context" "strings" "sync" "testing" "time" "github.com/carlosframework/platform/internal/instancectl" ) // queuingControl is root's channel during an edge restart (#292): a start // whose job systemd has queued blocks until the client gives up, and the // job outlives that. Only a stop of the unit replaces it. type queuingControl struct { *fakeControl mu sync.Mutex queued bool } func (q *queuingControl) Start(ctx context.Context, host string, role instancectl.Role, max int64) (string, error) { q.rec("start " + host + " " + string(role)) q.mu.Lock() q.queued = true q.mu.Unlock() <-ctx.Done() return "", ctx.Err() } func (q *queuingControl) Stop(ctx context.Context, host string, role instancectl.Role) error { q.mu.Lock() q.queued = false q.mu.Unlock() return q.fakeControl.Stop(ctx, host, role) } func (q *queuingControl) stillQueued() bool { q.mu.Lock() defer q.mu.Unlock() return q.queued } // A legacy start that is cancelled while root's start job is queued comes // back marked, so the wake's cleanup knows the job may still run; the // proof stops the unit, which clears the job, and only then passes. func TestACancelledLegacyStartIsMarkedAndItsStopClearsTheQueuedJob(t *testing.T) { q := &queuingControl{fakeControl: &fakeControl{}} unit := &fakeUnit{} unit.set("inactive", "", false, false) b, rt := nestedBackend(t, &fakeControl{}, unit) b.Control = q ctx, cancel := context.WithCancel(context.Background()) go func() { for !q.stillQueued() { time.Sleep(5 * time.Millisecond) } cancel() }() p, _, err := b.Start(ctx, rt) if err == nil || p != nil { t.Fatalf("Start = %v, %v; want no process and an error", p, err) } if !legacyStartMayRun(err) { t.Fatalf("a cancelled legacy start is not marked as one whose job may run: %v", err) } if !q.stillQueued() { t.Fatal("the fake lost its queued job on cancellation; it must model a job that survives") } pctx, pcancel := context.WithTimeout(context.Background(), 2*time.Second) defer pcancel() if err := b.LegacyRunGone(pctx, rt); err != nil { t.Fatalf("LegacyRunGone: %v", err) } if q.stillQueued() { t.Fatal("the proof passed with the start job still queued") } if !strings.Contains(q.joined(), "stop "+rt.Host+" run") { t.Fatalf("the run unit was not stopped through instance-control; calls:\n%s", q.joined()) } } // The proof fails, and so keeps the lease, while the unit is not stopped // or its cgroup still holds a process. func TestLegacyRunGoneRefusesWhileTheUnitRuns(t *testing.T) { t.Parallel() for _, tc := range []struct { name string active string populated bool }{ {"active", "active", true}, {"deactivating", "deactivating", true}, {"inactive with processes left", "inactive", true}, } { t.Run(tc.name, func(t *testing.T) { unit := &fakeUnit{} unit.set(tc.active, "inv", true, tc.populated) b, rt := nestedBackend(t, &fakeControl{}, unit) ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond) defer cancel() if err := b.LegacyRunGone(ctx, rt); err == nil { t.Fatal("LegacyRunGone proved a unit that still runs") } }) } }