mirror of
https://github.com/Control-D-Inc/ctrld.git
synced 2026-10-12 01:58:46 +02:00
fix(mobile): port synchronized controller teardown
This commit is contained in:
1 parent
4fb8e506e2
commit
14d8e20cd5
2 files changed
+353
-22
No files matched your search
@@ -0,0 +1,275 @@
|
||||
package ctrld_library
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"testing/synctest"
|
||||
"time"
|
||||
|
||||
"github.com/Control-D-Inc/ctrld/cmd/cli"
|
||||
)
|
||||
|
||||
// Tests replace the first host-touching calls, not the Controller methods.
|
||||
// They must not run in parallel because these seams are package globals.
|
||||
func stubMobile(t *testing.T, run func(*cli.AppConfig, *cli.AppCallback, chan struct{}), check func(int64, chan struct{}) int) {
|
||||
t.Helper()
|
||||
oldRun, oldCheck := runMobile, checkDeactivationPin
|
||||
runMobile, checkDeactivationPin = run, check
|
||||
t.Cleanup(func() { runMobile, checkDeactivationPin = oldRun, oldCheck })
|
||||
}
|
||||
|
||||
func startController(c *Controller) <-chan struct{} {
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
c.Start("uid", "provision", "hostname", "home", "doh", 2, "log")
|
||||
close(done)
|
||||
}()
|
||||
return done
|
||||
}
|
||||
|
||||
func TestControllerConcurrentLifecycle(t *testing.T) {
|
||||
synctest.Test(t, func(t *testing.T) {
|
||||
c := NewController(nil)
|
||||
var runs, active atomic.Int32
|
||||
var pinCalls int // Deliberately non-atomic: PIN checks must be serialized.
|
||||
stubMobile(t, func(config *cli.AppConfig, _ *cli.AppCallback, stop chan struct{}) {
|
||||
if active.Add(1) != 1 {
|
||||
t.Error("overlapping mobile runs")
|
||||
}
|
||||
defer active.Add(-1)
|
||||
runs.Add(1)
|
||||
want := cli.AppConfig{CdUID: "uid", ProvisionID: "provision", CustomHostname: "hostname", HomeDir: "home", UpstreamProto: "doh", Verbose: 2, LogPath: "log"}
|
||||
if *config != want {
|
||||
t.Errorf("config = %+v, want %+v", *config, want)
|
||||
}
|
||||
<-stop
|
||||
}, func(_ int64, _ chan struct{}) int {
|
||||
pinCalls++
|
||||
return 0
|
||||
})
|
||||
|
||||
if c.IsRunning() || c.Stop(true, 0) != 0 {
|
||||
t.Fatal("new controller must be stopped")
|
||||
}
|
||||
for cycle := 0; cycle < 25; cycle++ {
|
||||
var starters sync.WaitGroup
|
||||
for i := 0; i < 16; i++ {
|
||||
starters.Add(1)
|
||||
go func() { defer starters.Done(); <-startController(c) }()
|
||||
}
|
||||
synctest.Wait()
|
||||
if !c.IsRunning() || runs.Load() != int32(cycle+1) {
|
||||
t.Fatal("concurrent Starts did not claim exactly one run")
|
||||
}
|
||||
var stoppers sync.WaitGroup
|
||||
for i := 0; i < 16; i++ {
|
||||
stoppers.Add(1)
|
||||
go func() {
|
||||
defer stoppers.Done()
|
||||
c.IsRunning()
|
||||
if code := c.Stop(false, 1234); code != 0 {
|
||||
t.Errorf("Stop = %d", code)
|
||||
}
|
||||
c.IsRunning()
|
||||
}()
|
||||
}
|
||||
stoppers.Wait()
|
||||
starters.Wait()
|
||||
if c.IsRunning() || active.Load() != 0 {
|
||||
t.Fatal("Stop returned before run completion")
|
||||
}
|
||||
}
|
||||
if pinCalls != 25*16 {
|
||||
t.Fatalf("PIN checks = %d, want %d", pinCalls, 25*16)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestControllerStopWaitsAndTimeoutKeepsGuard(t *testing.T) {
|
||||
synctest.Test(t, func(t *testing.T) {
|
||||
c := NewController(nil)
|
||||
release := make(chan struct{})
|
||||
var runs int
|
||||
stubMobile(t, func(_ *cli.AppConfig, _ *cli.AppCallback, stop chan struct{}) {
|
||||
runs++
|
||||
<-stop
|
||||
<-release
|
||||
}, func(int64, chan struct{}) int { t.Error("restart checked PIN"); return 1 })
|
||||
started := startController(c)
|
||||
synctest.Wait()
|
||||
stopped := make(chan struct{})
|
||||
before := time.Now()
|
||||
go func() {
|
||||
var stoppers sync.WaitGroup
|
||||
for i := 0; i < 8; i++ {
|
||||
stoppers.Add(1)
|
||||
go func() { defer stoppers.Done(); c.Stop(true, 0) }()
|
||||
}
|
||||
stoppers.Wait()
|
||||
close(stopped)
|
||||
}()
|
||||
synctest.Wait()
|
||||
select {
|
||||
case <-stopped:
|
||||
t.Fatal("Stop did not wait for teardown")
|
||||
default:
|
||||
}
|
||||
c.Start("", "", "", "", "", 0, "")
|
||||
if !c.IsRunning() || runs != 1 {
|
||||
t.Fatal("guard released during teardown")
|
||||
}
|
||||
<-stopped // synctest advances virtual time, not wall time.
|
||||
if elapsed := time.Since(before); elapsed != stopTimeout {
|
||||
t.Fatalf("Stop timeout = %v, want %v", elapsed, stopTimeout)
|
||||
}
|
||||
c.Start("", "", "", "", "", 0, "")
|
||||
if !c.IsRunning() || runs != 1 {
|
||||
t.Fatal("timeout released a live run's guard")
|
||||
}
|
||||
close(release)
|
||||
<-started
|
||||
if c.IsRunning() {
|
||||
t.Fatal("completed teardown retained guard")
|
||||
}
|
||||
started = startController(c)
|
||||
synctest.Wait()
|
||||
c.Stop(true, 0)
|
||||
<-started
|
||||
if runs != 2 || c.IsRunning() {
|
||||
t.Fatal("could not restart after timed-out teardown completed")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestControllerPinBehavior(t *testing.T) {
|
||||
synctest.Test(t, func(t *testing.T) {
|
||||
c := NewController(nil)
|
||||
var runStop chan struct{}
|
||||
var pins []int64
|
||||
stubMobile(t, func(_ *cli.AppConfig, _ *cli.AppCallback, stop chan struct{}) {
|
||||
runStop = stop
|
||||
<-stop
|
||||
}, func(pin int64, stop chan struct{}) int {
|
||||
pins = append(pins, pin)
|
||||
if stop != runStop {
|
||||
t.Error("PIN check received wrong run")
|
||||
}
|
||||
if pin == 1234 {
|
||||
return 0
|
||||
}
|
||||
return 42
|
||||
})
|
||||
if code := c.Stop(false, 9); code != 42 {
|
||||
t.Fatalf("idle Stop PIN result = %d, want 42", code)
|
||||
}
|
||||
started := startController(c)
|
||||
synctest.Wait()
|
||||
if code := c.Stop(false, 8); code != 42 || !c.IsRunning() {
|
||||
t.Fatalf("invalid PIN Stop = %d, running = %v", code, c.IsRunning())
|
||||
}
|
||||
select {
|
||||
case <-runStop:
|
||||
t.Fatal("invalid PIN stopped run")
|
||||
default:
|
||||
}
|
||||
if code := c.Stop(false, 1234); code != 0 {
|
||||
t.Fatalf("valid PIN Stop = %d", code)
|
||||
}
|
||||
<-started
|
||||
started = startController(c)
|
||||
synctest.Wait()
|
||||
c.Stop(true, 9999)
|
||||
<-started
|
||||
if len(pins) != 3 || pins[0] != 9 || pins[1] != 8 || pins[2] != 1234 {
|
||||
t.Fatalf("PIN calls = %v", pins)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestControllerSlowPinCannotStopReplacement(t *testing.T) {
|
||||
synctest.Test(t, func(t *testing.T) {
|
||||
c := NewController(nil)
|
||||
pinStarted, releasePin := make(chan struct{}), make(chan struct{})
|
||||
var currentStop chan struct{}
|
||||
stubMobile(t, func(_ *cli.AppConfig, _ *cli.AppCallback, stop chan struct{}) {
|
||||
currentStop = stop
|
||||
<-stop
|
||||
}, func(_ int64, stop chan struct{}) int {
|
||||
if stop != currentStop {
|
||||
t.Error("wrong PIN session")
|
||||
}
|
||||
close(pinStarted)
|
||||
<-releasePin
|
||||
return 0
|
||||
})
|
||||
first := startController(c)
|
||||
synctest.Wait()
|
||||
oldStop := make(chan struct{})
|
||||
go func() { c.Stop(false, 1234); close(oldStop) }()
|
||||
<-pinStarted
|
||||
c.Stop(true, 0)
|
||||
<-first
|
||||
second := startController(c)
|
||||
synctest.Wait()
|
||||
close(releasePin)
|
||||
<-oldStop
|
||||
if !c.IsRunning() {
|
||||
t.Fatal("old Stop cleared replacement guard")
|
||||
}
|
||||
select {
|
||||
case <-currentStop:
|
||||
t.Fatal("old Stop cancelled replacement")
|
||||
default:
|
||||
}
|
||||
c.Stop(true, 0)
|
||||
<-second
|
||||
})
|
||||
}
|
||||
|
||||
func TestControllerMixedConcurrentCalls(t *testing.T) {
|
||||
c := NewController(nil)
|
||||
var active atomic.Int32
|
||||
stubMobile(t, func(config *cli.AppConfig, _ *cli.AppCallback, _ chan struct{}) {
|
||||
if active.Add(1) != 1 {
|
||||
t.Error("overlapping mobile runs")
|
||||
}
|
||||
defer active.Add(-1)
|
||||
runtime.Gosched()
|
||||
if config.CdUID != "uid" {
|
||||
t.Error("active config changed")
|
||||
}
|
||||
}, func(int64, chan struct{}) int { return 0 })
|
||||
var workers sync.WaitGroup
|
||||
for i := 0; i < 12; i++ {
|
||||
workers.Add(1)
|
||||
go func() {
|
||||
defer workers.Done()
|
||||
for j := 0; j < 100; j++ {
|
||||
c.Start("uid", "", "", "", "", 0, "")
|
||||
c.IsRunning()
|
||||
c.Stop(j%2 == 0, 1234)
|
||||
}
|
||||
}()
|
||||
}
|
||||
workers.Wait()
|
||||
if c.IsRunning() || active.Load() != 0 {
|
||||
t.Fatal("concurrent calls left a run active")
|
||||
}
|
||||
}
|
||||
|
||||
func TestControllerRunReturnReleasesGuard(t *testing.T) {
|
||||
c := NewController(nil)
|
||||
var runs int
|
||||
stubMobile(t, func(*cli.AppConfig, *cli.AppCallback, chan struct{}) { runs++ }, func(int64, chan struct{}) int { return 0 })
|
||||
for i := 0; i < 2; i++ {
|
||||
c.Start("", "", "", "", "", 0, "")
|
||||
if c.IsRunning() {
|
||||
t.Fatal("returned run retained guard")
|
||||
}
|
||||
}
|
||||
if runs != 2 {
|
||||
t.Fatalf("runs = %d, want 2", runs)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user