Skip to content

Commit 0f4e99e

Browse files
authored
Merge pull request #3296 from alvaroaleman/reconciliationtimeout
✨ Add a ReconciliationTimeout option
2 parents 9d3997b + f8db32f commit 0f4e99e

File tree

5 files changed

+87
-0
lines changed

5 files changed

+87
-0
lines changed

pkg/config/controller.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,4 +81,8 @@ type Controller struct {
8181

8282
// Logger is the logger controllers should use.
8383
Logger logr.Logger
84+
85+
// ReconciliationTimeout is used as the timeout passed to the context of each Reconcile call.
86+
// By default, there is no timeout.
87+
ReconciliationTimeout time.Duration
8488
}

pkg/controller/controller.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,10 @@ type TypedOptions[request comparable] struct {
106106
// leader election do not wait on leader election to start their sources.
107107
// Defaults to false.
108108
EnableWarmup *bool
109+
110+
// ReconciliationTimeout is used as the timeout passed to the context of each Reconcile call.
111+
// By default, there is no timeout.
112+
ReconciliationTimeout time.Duration
109113
}
110114

111115
// DefaultFromConfig defaults the config from a config.Controller
@@ -141,6 +145,10 @@ func (options *TypedOptions[request]) DefaultFromConfig(config config.Controller
141145
if options.EnableWarmup == nil {
142146
options.EnableWarmup = config.EnableWarmup
143147
}
148+
149+
if options.ReconciliationTimeout == 0 {
150+
options.ReconciliationTimeout = config.ReconciliationTimeout
151+
}
144152
}
145153

146154
// Controller implements an API. A Controller manages a work queue fed reconcile.Requests
@@ -271,6 +279,7 @@ func NewTypedUnmanaged[request comparable](name string, options TypedOptions[req
271279
RecoverPanic: options.RecoverPanic,
272280
LeaderElected: options.NeedLeaderElection,
273281
EnableWarmup: options.EnableWarmup,
282+
ReconciliationTimeout: options.ReconciliationTimeout,
274283
}), nil
275284
}
276285

pkg/controller/controller_test.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -540,5 +540,40 @@ var _ = Describe("controller.Controller", func() {
540540
Expect(ok).To(BeTrue())
541541
Expect(internalCtrlOverridingWarmup.EnableWarmup).To(HaveValue(BeFalse()))
542542
})
543+
544+
It("should default ReconciliationTimeout from manager if unset", func() {
545+
m, err := manager.New(cfg, manager.Options{
546+
Controller: config.Controller{ReconciliationTimeout: 30 * time.Second},
547+
})
548+
Expect(err).NotTo(HaveOccurred())
549+
550+
c, err := controller.New("mgr-reconciliation-timeout", m, controller.Options{
551+
Reconciler: rec,
552+
})
553+
Expect(err).NotTo(HaveOccurred())
554+
555+
ctrl, ok := c.(*internalcontroller.Controller[reconcile.Request])
556+
Expect(ok).To(BeTrue())
557+
558+
Expect(ctrl.ReconciliationTimeout).To(Equal(30 * time.Second))
559+
})
560+
561+
It("should not override an existing ReconciliationTimeout", func() {
562+
m, err := manager.New(cfg, manager.Options{
563+
Controller: config.Controller{ReconciliationTimeout: 30 * time.Second},
564+
})
565+
Expect(err).NotTo(HaveOccurred())
566+
567+
c, err := controller.New("ctrl-reconciliation-timeout", m, controller.Options{
568+
Reconciler: rec,
569+
ReconciliationTimeout: time.Minute,
570+
})
571+
Expect(err).NotTo(HaveOccurred())
572+
573+
ctrl, ok := c.(*internalcontroller.Controller[reconcile.Request])
574+
Expect(ok).To(BeTrue())
575+
576+
Expect(ctrl.ReconciliationTimeout).To(Equal(time.Minute))
577+
})
543578
})
544579
})

pkg/internal/controller/controller.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,10 @@ type Options[request comparable] struct {
8282
// Defaults to false, which means that the controller will wait for leader election to start
8383
// before starting sources.
8484
EnableWarmup *bool
85+
86+
// ReconciliationTimeout is used as the timeout passed to the context of each Reconcile call.
87+
// By default, there is no timeout.
88+
ReconciliationTimeout time.Duration
8589
}
8690

8791
// Controller implements controller.Controller.
@@ -162,6 +166,8 @@ type Controller[request comparable] struct {
162166
// leader election do not wait on leader election to start their sources.
163167
// Defaults to false.
164168
EnableWarmup *bool
169+
170+
ReconciliationTimeout time.Duration
165171
}
166172

167173
// New returns a new Controller configured with the given options.
@@ -177,6 +183,7 @@ func New[request comparable](options Options[request]) *Controller[request] {
177183
RecoverPanic: options.RecoverPanic,
178184
LeaderElected: options.LeaderElected,
179185
EnableWarmup: options.EnableWarmup,
186+
ReconciliationTimeout: options.ReconciliationTimeout,
180187
}
181188
}
182189

@@ -199,6 +206,13 @@ func (c *Controller[request]) Reconcile(ctx context.Context, req request) (_ rec
199206
panic(r)
200207
}
201208
}()
209+
210+
if c.ReconciliationTimeout > 0 {
211+
var cancel context.CancelFunc
212+
ctx, cancel = context.WithTimeout(ctx, c.ReconciliationTimeout)
213+
defer cancel()
214+
}
215+
202216
return c.Do.Reconcile(ctx, req)
203217
}
204218

pkg/internal/controller/controller_test.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,31 @@ var _ = Describe("controller", func() {
142142
Expect(err).To(HaveOccurred())
143143
Expect(err.Error()).To(ContainSubstring("[recovered]"))
144144
})
145+
146+
It("should time out if ReconciliationTimeout is set", func(ctx SpecContext) {
147+
ctrl.ReconciliationTimeout = time.Duration(1) // One nanosecond
148+
ctrl.Do = reconcile.Func(func(ctx context.Context, _ reconcile.Request) (reconcile.Result, error) {
149+
<-ctx.Done()
150+
return reconcile.Result{}, ctx.Err()
151+
})
152+
_, err := ctrl.Reconcile(ctx,
153+
reconcile.Request{NamespacedName: types.NamespacedName{Namespace: "foo", Name: "bar"}})
154+
Expect(err).To(HaveOccurred())
155+
Expect(err).To(Equal(context.DeadlineExceeded))
156+
})
157+
158+
It("should not configure a timeout if ReconciliationTimeout is zero", func(ctx SpecContext) {
159+
ctrl.Do = reconcile.Func(func(ctx context.Context, _ reconcile.Request) (reconcile.Result, error) {
160+
defer GinkgoRecover()
161+
162+
_, ok := ctx.Deadline()
163+
Expect(ok).To(BeFalse())
164+
return reconcile.Result{}, nil
165+
})
166+
_, err := ctrl.Reconcile(ctx,
167+
reconcile.Request{NamespacedName: types.NamespacedName{Namespace: "foo", Name: "bar"}})
168+
Expect(err).NotTo(HaveOccurred())
169+
})
145170
})
146171

147172
Describe("Start", func() {

0 commit comments

Comments
 (0)