Skip to content

Commit 4ea3a6d

Browse files
authored
Merge pull request #84 from authzed/cancelfix
fix: store cancel func before launching controller goroutine
2 parents 5cdd104 + 06642a5 commit 4ea3a6d

5 files changed

Lines changed: 55 additions & 6 deletions

File tree

bootstrap/crds.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ const (
2626
)
2727

2828
// CRD installs the CRDs in the filesystem into the kube cluster configured by the rest config.
29+
//
2930
// Deprecated: Use CRDs instead.
3031
func CRD(restConfig *rest.Config, crdFS fs.ReadDirFS, dir string) error {
3132
return CRDs(context.Background(), restConfig, crdFS, dir)

manager/manager.go

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -150,12 +150,16 @@ func (m *Manager) Go(controllers ...Controller) error {
150150
for _, c := range controllers {
151151
c := c
152152
m.healthzHandler.AddHealthChecker(controllerhealthz.NamedHealthChecker(c.Name(), c.HealthChecker()))
153+
// Create the cancel context and store it before launching the goroutine.
154+
// This prevents a race condition where Cancel() is called between Go()
155+
// returning and the goroutine storing the cancel function, which would
156+
// cause the controller to run indefinitely with an uncancellable context.
157+
cCtx, cancel := context.WithCancel(ctx) //nolint:gosec // cancel stored in cancelFuncs map for later use
158+
m.lock.Lock()
159+
m.cancelFuncs[c] = cancel
160+
m.lock.Unlock()
153161
errG.Go(func() error {
154-
ctx, cancel := context.WithCancel(ctx)
155-
m.lock.Lock()
156-
m.cancelFuncs[c] = cancel
157-
m.lock.Unlock()
158-
c.Start(ctx, runtime.GOMAXPROCS(0))
162+
c.Start(cCtx, runtime.GOMAXPROCS(0))
159163
return nil
160164
})
161165
if ctx.Err() != nil {

manager/manager_test.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,44 @@ func testController(t *testing.T, name string) Controller {
8585
})
8686
}
8787

88+
// TestGoThenImmediateCancelStopsController verifies that calling Cancel immediately
89+
// after Go correctly cancels the controller. There is a potential race condition
90+
// where if the cancel context is created inside the goroutine launched by Go,
91+
// a Cancel call between Go() returning and the goroutine executing will miss the
92+
// cancel function, leaving the controller running indefinitely.
93+
func TestGoThenImmediateCancelStopsController(t *testing.T) {
94+
t.Parallel()
95+
ctx := t.Context()
96+
97+
m := NewManager(&config.DebuggingConfiguration{}, ":"+getFreePort(t), nil, nil)
98+
ready := make(chan struct{})
99+
go func() { _ = m.Start(ctx, ready) }()
100+
<-ready
101+
102+
ctrl := &blockingController{BasicController: *NewBasicController("blocking"), done: make(chan struct{})}
103+
require.NoError(t, m.Go(ctrl))
104+
// Cancel immediately, before the goroutine launched by Go has had a chance to run.
105+
m.Cancel(ctrl)
106+
107+
select {
108+
case <-ctrl.done:
109+
// Controller was cancelled and shut down correctly.
110+
case <-time.After(5 * time.Second):
111+
t.Fatal("controller did not shut down after Cancel — likely race condition in Manager.Go")
112+
}
113+
}
114+
115+
// blockingController is a Controller whose Start blocks until the context is cancelled.
116+
type blockingController struct {
117+
BasicController
118+
done chan struct{}
119+
}
120+
121+
func (c *blockingController) Start(ctx context.Context, _ int) {
122+
<-ctx.Done()
123+
close(c.done)
124+
}
125+
88126
func requireCancelFnCount(t *testing.T, m *Manager, count int) {
89127
t.Helper()
90128
require.Eventually(t, func() bool {

state/state_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2276,7 +2276,7 @@ func TestContinueWithCancellation(t *testing.T) {
22762276
}
22772277

22782278
func ExampleWithErrorHandler() {
2279-
ctx, cancel := context.WithCancel(context.Background())
2279+
ctx, cancel := context.WithCancel(context.Background()) //nolint:gosec // cancel called inside pipeline step
22802280

22812281
// Set up error handler
22822282
ctx = WithErrorHandler(ctx, func(_ error) Step {

typed/registry.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ func (r *Registry) NewFilteredDynamicSharedInformerFactory(key FactoryKey, clien
9494
}
9595

9696
// ListerFor returns a typed Lister from a Registry
97+
//
9798
// Deprecated: Use MustListerForKey instead
9899
func ListerFor[K runtime.Object](r *Registry, key RegistryKey) *Lister[K] {
99100
return MustListerForKey[K](r, key)
@@ -114,6 +115,7 @@ func ListerForKey[K runtime.Object](r *Registry, key RegistryKey) (*Lister[K], e
114115
}
115116

116117
// IndexerFor returns a typed Indexer from a Registry
118+
//
117119
// Deprecated: Use MustIndexerForKey instead
118120
func IndexerFor[K runtime.Object](r *Registry, key RegistryKey) *Indexer[K] {
119121
return MustIndexerForKey[K](r, key)
@@ -154,6 +156,7 @@ func (r *Registry) Remove(key FactoryKey) {
154156
}
155157

156158
// InformerFactoryFor returns GVR-specific InformerFactory from the Registry.
159+
//
157160
// Deprecated: use MustInformerFactoryForKey instead.
158161
func (r *Registry) InformerFactoryFor(key RegistryKey) informers.GenericInformer {
159162
return r.MustInformerFactoryForKey(key)
@@ -182,6 +185,7 @@ func (r *Registry) InformerFactoryForKey(key RegistryKey) (informers.GenericInfo
182185
}
183186

184187
// ListerFor returns the GVR-specific Lister from the Registry
188+
//
185189
// Deprecated: use MustListerForKey instead.
186190
func (r *Registry) ListerFor(key RegistryKey) cache.GenericLister {
187191
return r.MustInformerFactoryForKey(key).Lister()
@@ -204,6 +208,7 @@ func (r *Registry) ListerForKey(key RegistryKey) (cache.GenericLister, error) {
204208
}
205209

206210
// InformerFor returns the GVR-specific Informer from the Registry
211+
//
207212
// Deprecated: use MustInformerForKey instead.
208213
func (r *Registry) InformerFor(key RegistryKey) cache.SharedIndexInformer {
209214
return r.MustInformerFactoryForKey(key).Informer()
@@ -226,6 +231,7 @@ func (r *Registry) InformerForKey(key RegistryKey) (cache.SharedIndexInformer, e
226231
}
227232

228233
// IndexerFor returns the GVR-specific Indexer from the Registry
234+
//
229235
// Deprecated: use MustIndexerForKey instead.
230236
func (r *Registry) IndexerFor(key RegistryKey) cache.Indexer {
231237
return r.MustInformerForKey(key).GetIndexer()

0 commit comments

Comments
 (0)