Skip to content

Commit 78d0026

Browse files
committed
🐛 Controller.Watch() should not store watches if already started
The controller internal struct holds a list of watches (as []watchDescription) when someone calls .Watch() to then start the watches and informers once we're ready to call Start(). This behavior caused a memory leak in the case Watch was called after a controller has already been started and if the source.Kind's cache was either stopped or not available any longer. The leak was caused by the watches internal slice holding on to all references to each watch ever issued (and their respective caches). Signed-off-by: Vince Prignano <[email protected]>
1 parent 29c2e32 commit 78d0026

File tree

2 files changed

+20
-11
lines changed

2 files changed

+20
-11
lines changed

pkg/internal/controller/controller.go

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,8 @@ type Controller struct {
7171

7272
// TODO(community): Consider initializing a logger with the Controller Name as the tag
7373

74-
// watches maintains a list of sources, handlers, and predicates to start when the controller is started.
75-
watches []watchDescription
74+
// startWatches maintains a list of sources, handlers, and predicates to start when the controller is started.
75+
startWatches []watchDescription
7676

7777
// Log is used to log messages to users during reconciliation, or for example when a watch is started.
7878
Log logr.Logger
@@ -108,13 +108,16 @@ func (c *Controller) Watch(src source.Source, evthdler handler.EventHandler, prc
108108
}
109109
}
110110

111-
c.watches = append(c.watches, watchDescription{src: src, handler: evthdler, predicates: prct})
112-
if c.Started {
113-
c.Log.Info("Starting EventSource", "source", src)
114-
return src.Start(evthdler, c.Queue, prct...)
111+
// Controller hasn't started yet, store the watches locally and return.
112+
//
113+
// These watches are going to be held on the controller struct until the manager or user calls Start(...).
114+
if !c.Started {
115+
c.startWatches = append(c.startWatches, watchDescription{src: src, handler: evthdler, predicates: prct})
116+
return nil
115117
}
116118

117-
return nil
119+
c.Log.Info("Starting EventSource", "source", src)
120+
return src.Start(evthdler, c.Queue, prct...)
118121
}
119122

120123
// Start implements controller.Controller
@@ -135,7 +138,7 @@ func (c *Controller) Start(stop <-chan struct{}) error {
135138
// NB(directxman12): launch the sources *before* trying to wait for the
136139
// caches to sync so that they have a chance to register their intendeded
137140
// caches.
138-
for _, watch := range c.watches {
141+
for _, watch := range c.startWatches {
139142
c.Log.Info("Starting EventSource", "source", watch.src)
140143
if err := watch.src.Start(watch.handler, c.Queue, watch.predicates...); err != nil {
141144
return err
@@ -145,7 +148,7 @@ func (c *Controller) Start(stop <-chan struct{}) error {
145148
// Start the SharedIndexInformer factories to begin populating the SharedIndexInformer caches
146149
c.Log.Info("Starting Controller")
147150

148-
for _, watch := range c.watches {
151+
for _, watch := range c.startWatches {
149152
syncingSource, ok := watch.src.(source.SyncingSource)
150153
if !ok {
151154
continue
@@ -159,6 +162,12 @@ func (c *Controller) Start(stop <-chan struct{}) error {
159162
}
160163
}
161164

165+
// All the watches have been started, we can reset the local slice.
166+
//
167+
// We should never hold watches more than necessary, each watch source can hold a backing cache,
168+
// which won't be garbage collected if we hold a reference to it.
169+
c.startWatches = nil
170+
162171
if c.JitterPeriod == 0 {
163172
c.JitterPeriod = 1 * time.Second
164173
}

pkg/internal/controller/controller_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ var _ = Describe("controller", func() {
9191
Describe("Start", func() {
9292
It("should return an error if there is an error waiting for the informers", func(done Done) {
9393
f := false
94-
ctrl.watches = []watchDescription{{
94+
ctrl.startWatches = []watchDescription{{
9595
src: source.NewKindWithCache(&corev1.Pod{}, &informertest.FakeInformers{Synced: &f}),
9696
}}
9797
ctrl.Name = "foo"
@@ -115,7 +115,7 @@ var _ = Describe("controller", func() {
115115
Expect(err).NotTo(HaveOccurred())
116116
_, err = c.GetInformer(context.TODO(), &appsv1.ReplicaSet{})
117117
Expect(err).NotTo(HaveOccurred())
118-
ctrl.watches = []watchDescription{{
118+
ctrl.startWatches = []watchDescription{{
119119
src: source.NewKindWithCache(&appsv1.Deployment{}, &informertest.FakeInformers{}),
120120
}}
121121

0 commit comments

Comments
 (0)