Skip to content

Commit a6ec31f

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 d6829e9 commit a6ec31f

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
@@ -74,8 +74,8 @@ type Controller struct {
7474

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

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

8080
// Log is used to log messages to users during reconciliation, or for example when a watch is started.
8181
Log logr.Logger
@@ -113,13 +113,16 @@ func (c *Controller) Watch(src source.Source, evthdler handler.EventHandler, prc
113113
}
114114
}
115115

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

122-
return nil
124+
c.Log.Info("Starting EventSource", "source", src)
125+
return src.Start(evthdler, c.Queue, prct...)
123126
}
124127

125128
// Start implements controller.Controller
@@ -143,7 +146,7 @@ func (c *Controller) Start(stop <-chan struct{}) error {
143146
// NB(directxman12): launch the sources *before* trying to wait for the
144147
// caches to sync so that they have a chance to register their intendeded
145148
// caches.
146-
for _, watch := range c.watches {
149+
for _, watch := range c.startWatches {
147150
c.Log.Info("Starting EventSource", "source", watch.src)
148151
if err := watch.src.Start(watch.handler, c.Queue, watch.predicates...); err != nil {
149152
return err
@@ -153,7 +156,7 @@ func (c *Controller) Start(stop <-chan struct{}) error {
153156
// Start the SharedIndexInformer factories to begin populating the SharedIndexInformer caches
154157
c.Log.Info("Starting Controller")
155158

156-
for _, watch := range c.watches {
159+
for _, watch := range c.startWatches {
157160
syncingSource, ok := watch.src.(source.SyncingSource)
158161
if !ok {
159162
continue
@@ -167,6 +170,12 @@ func (c *Controller) Start(stop <-chan struct{}) error {
167170
}
168171
}
169172

173+
// All the watches have been started, we can reset the local slice.
174+
//
175+
// We should never hold watches more than necessary, each watch source can hold a backing cache,
176+
// which won't be garbage collected if we hold a reference to it.
177+
c.startWatches = nil
178+
170179
if c.JitterPeriod == 0 {
171180
c.JitterPeriod = 1 * time.Second
172181
}

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)