Skip to content

Commit ba51d28

Browse files
committed
Change shutdown order to shutdown warmup runnables in parallel with other runnables.
1 parent dcf4b8b commit ba51d28

File tree

2 files changed

+60
-5
lines changed

2 files changed

+60
-5
lines changed

pkg/internal/controller/controller_test.go

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import (
2929
. "github.com/onsi/gomega"
3030
"github.com/prometheus/client_golang/prometheus"
3131
dto "github.com/prometheus/client_model/go"
32+
"go.uber.org/goleak"
3233
appsv1 "k8s.io/api/apps/v1"
3334
corev1 "k8s.io/api/core/v1"
3435
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -1217,7 +1218,9 @@ var _ = Describe("controller", func() {
12171218
cfg, err := testenv.Start()
12181219
Expect(err).NotTo(HaveOccurred())
12191220
m, err := manager.New(cfg, manager.Options{
1220-
LeaderElection: true,
1221+
LeaderElection: true,
1222+
LeaderElectionID: "some-leader-election-id",
1223+
LeaderElectionNamespace: "default",
12211224
})
12221225
Expect(err).NotTo(HaveOccurred())
12231226

@@ -1316,6 +1319,50 @@ var _ = Describe("controller", func() {
13161319

13171320
Eventually(didWatchStart.Load).Should(BeTrue(), "watch should be started if it is added after Warmup")
13181321
})
1322+
1323+
DescribeTable("should not leak goroutines when manager is stopped with warmup runnable",
1324+
func(leaderElection bool) {
1325+
ctx, cancel := context.WithCancel(context.Background())
1326+
defer cancel()
1327+
1328+
ctrl.CacheSyncTimeout = time.Second
1329+
1330+
By("Creating a manager")
1331+
testenv = &envtest.Environment{}
1332+
cfg, err := testenv.Start()
1333+
Expect(err).NotTo(HaveOccurred())
1334+
m, err := manager.New(cfg, manager.Options{
1335+
LeaderElection: leaderElection,
1336+
LeaderElectionID: "some-leader-election-id",
1337+
LeaderElectionNamespace: "default",
1338+
})
1339+
Expect(err).NotTo(HaveOccurred())
1340+
1341+
ctrl.startWatches = []source.TypedSource[reconcile.Request]{
1342+
source.Func(func(ctx context.Context, _ workqueue.TypedRateLimitingInterface[reconcile.Request]) error {
1343+
<-ctx.Done()
1344+
return nil
1345+
}),
1346+
}
1347+
Expect(m.Add(ctrl)).To(Succeed())
1348+
1349+
// ignore needs to go after the testenv.Start() call to ignore the apiserver
1350+
// process
1351+
currentGRs := goleak.IgnoreCurrent()
1352+
go func() {
1353+
defer GinkgoRecover()
1354+
Expect(m.Start(ctx)).To(Succeed())
1355+
}()
1356+
1357+
<-m.Elected()
1358+
By("stopping the manager via context")
1359+
cancel()
1360+
1361+
Eventually(func() error { return goleak.Find(currentGRs) }).Should(Succeed())
1362+
},
1363+
Entry("manager with leader election enabled", true),
1364+
Entry("manager without leader election enabled", false),
1365+
)
13191366
})
13201367

13211368
Describe("Warmup with warmup disabled", func() {

pkg/manager/internal.go

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -539,6 +539,18 @@ func (cm *controllerManager) engageStopProcedure(stopComplete <-chan struct{}) e
539539
}()
540540

541541
go func() {
542+
go func() {
543+
// Stop the warmup runnables in a separate goroutine to avoid blocking.
544+
// It is important to stop the warmup runnables in parallel with the other runnables
545+
// since we cannot assume ordering of whether or not one of the warmup runnables or one
546+
// of the other runnables is holding a lock.
547+
// Cancelling the wrong runnable (one that is not holding the lock) will cause the
548+
// shutdown sequence to block indefinitely as it will wait for the runnable that is
549+
// holding the lock to finish.
550+
cm.logger.Info("Stopping and waiting for warmup runnables")
551+
cm.runnables.Warmup.StopAndWait(cm.shutdownCtx)
552+
}()
553+
542554
// First stop the non-leader election runnables.
543555
cm.logger.Info("Stopping and waiting for non leader election runnables")
544556
cm.runnables.Others.StopAndWait(cm.shutdownCtx)
@@ -549,10 +561,6 @@ func (cm *controllerManager) engageStopProcedure(stopComplete <-chan struct{}) e
549561
cm.runnables.LeaderElection.startOnce.Do(func() {})
550562
cm.runnables.LeaderElection.StopAndWait(cm.shutdownCtx)
551563

552-
// Stop the warmup runnables
553-
cm.logger.Info("Stopping and waiting for warmup runnables")
554-
cm.runnables.Warmup.StopAndWait(cm.shutdownCtx)
555-
556564
// Stop the caches before the leader election runnables, this is an important
557565
// step to make sure that we don't race with the reconcilers by receiving more events
558566
// from the API servers and enqueueing them.

0 commit comments

Comments
 (0)