@@ -155,6 +155,152 @@ var _ = Describe("controller", func() {
155155 Expect (err ).To (Equal (context .DeadlineExceeded ))
156156 })
157157
158+ Context ("prometheus metric reconcile_timeouts" , func () {
159+ var reconcileTimeouts dto.Metric
160+
161+ BeforeEach (func () {
162+ ctrlmetrics .ReconcileTimeouts .Reset ()
163+ reconcileTimeouts .Reset ()
164+ ctrl .Name = testControllerName
165+ })
166+
167+ It ("should increment when ReconciliationTimeout fires" , func (ctx SpecContext ) {
168+ Expect (func () error {
169+ Expect (ctrlmetrics .ReconcileTimeouts .WithLabelValues (ctrl .Name ).Write (& reconcileTimeouts )).To (Succeed ())
170+ if reconcileTimeouts .GetCounter ().GetValue () != 0.0 {
171+ return fmt .Errorf ("metric reconcile timeouts not reset" )
172+ }
173+ return nil
174+ }()).Should (Succeed ())
175+
176+ ctrl .ReconciliationTimeout = time .Duration (1 ) // One nanosecond
177+ ctrl .Do = reconcile .Func (func (ctx context.Context , _ reconcile.Request ) (reconcile.Result , error ) {
178+ <- ctx .Done ()
179+ return reconcile.Result {}, ctx .Err ()
180+ })
181+ _ , err := ctrl .Reconcile (ctx ,
182+ reconcile.Request {NamespacedName : types.NamespacedName {Namespace : "foo" , Name : "bar" }})
183+ Expect (err ).To (HaveOccurred ())
184+ Expect (err ).To (Equal (context .DeadlineExceeded ))
185+
186+ Expect (ctrlmetrics .ReconcileTimeouts .WithLabelValues (ctrl .Name ).Write (& reconcileTimeouts )).To (Succeed ())
187+ Expect (reconcileTimeouts .GetCounter ().GetValue ()).To (Equal (1.0 ))
188+ })
189+
190+ It ("should not increment when user code cancels context early" , func (ctx SpecContext ) {
191+ Expect (func () error {
192+ Expect (ctrlmetrics .ReconcileTimeouts .WithLabelValues (ctrl .Name ).Write (& reconcileTimeouts )).To (Succeed ())
193+ if reconcileTimeouts .GetCounter ().GetValue () != 0.0 {
194+ return fmt .Errorf ("metric reconcile timeouts not reset" )
195+ }
196+ return nil
197+ }()).Should (Succeed ())
198+
199+ ctrl .ReconciliationTimeout = 10 * time .Second
200+ userCancelCause := errors .New ("user cancellation" )
201+ ctrl .Do = reconcile .Func (func (ctx context.Context , _ reconcile.Request ) (reconcile.Result , error ) {
202+ // User code creates its own timeout with a different cause
203+ userCtx , cancel := context .WithTimeoutCause (ctx , time .Millisecond , userCancelCause )
204+ defer cancel ()
205+ <- userCtx .Done ()
206+ return reconcile.Result {}, context .Cause (userCtx )
207+ })
208+ _ , err := ctrl .Reconcile (ctx ,
209+ reconcile.Request {NamespacedName : types.NamespacedName {Namespace : "foo" , Name : "bar" }})
210+ Expect (err ).To (HaveOccurred ())
211+ Expect (errors .Is (err , userCancelCause )).To (BeTrue ())
212+
213+ // Metric should not be incremented because the wrapper timeout didn't fire
214+ Expect (ctrlmetrics .ReconcileTimeouts .WithLabelValues (ctrl .Name ).Write (& reconcileTimeouts )).To (Succeed ())
215+ Expect (reconcileTimeouts .GetCounter ().GetValue ()).To (Equal (0.0 ))
216+ })
217+
218+ It ("should not increment when reconciliation completes before timeout" , func (ctx SpecContext ) {
219+ Expect (func () error {
220+ Expect (ctrlmetrics .ReconcileTimeouts .WithLabelValues (ctrl .Name ).Write (& reconcileTimeouts )).To (Succeed ())
221+ if reconcileTimeouts .GetCounter ().GetValue () != 0.0 {
222+ return fmt .Errorf ("metric reconcile timeouts not reset" )
223+ }
224+ return nil
225+ }()).Should (Succeed ())
226+
227+ ctrl .ReconciliationTimeout = 10 * time .Second
228+ ctrl .Do = reconcile .Func (func (ctx context.Context , _ reconcile.Request ) (reconcile.Result , error ) {
229+ // Reconcile completes successfully before timeout
230+ return reconcile.Result {}, nil
231+ })
232+ _ , err := ctrl .Reconcile (ctx ,
233+ reconcile.Request {NamespacedName : types.NamespacedName {Namespace : "foo" , Name : "bar" }})
234+ Expect (err ).NotTo (HaveOccurred ())
235+
236+ // Metric should not be incremented because the timeout was not exceeded
237+ Expect (ctrlmetrics .ReconcileTimeouts .WithLabelValues (ctrl .Name ).Write (& reconcileTimeouts )).To (Succeed ())
238+ Expect (reconcileTimeouts .GetCounter ().GetValue ()).To (Equal (0.0 ))
239+ })
240+
241+ It ("should increment multiple times when multiple reconciles timeout" , func (ctx SpecContext ) {
242+ Expect (func () error {
243+ Expect (ctrlmetrics .ReconcileTimeouts .WithLabelValues (ctrl .Name ).Write (& reconcileTimeouts )).To (Succeed ())
244+ if reconcileTimeouts .GetCounter ().GetValue () != 0.0 {
245+ return fmt .Errorf ("metric reconcile timeouts not reset" )
246+ }
247+ return nil
248+ }()).Should (Succeed ())
249+
250+ ctrl .ReconciliationTimeout = time .Duration (1 ) // One nanosecond
251+ ctrl .Do = reconcile .Func (func (ctx context.Context , _ reconcile.Request ) (reconcile.Result , error ) {
252+ <- ctx .Done ()
253+ return reconcile.Result {}, ctx .Err ()
254+ })
255+
256+ const numTimeouts = 3
257+ // Call Reconcile multiple times, each should timeout and increment the metric
258+ for i := range numTimeouts {
259+ _ , err := ctrl .Reconcile (ctx ,
260+ reconcile.Request {NamespacedName : types.NamespacedName {Namespace : "foo" , Name : fmt .Sprintf ("bar%d" , i )}})
261+ Expect (err ).To (HaveOccurred ())
262+ Expect (err ).To (Equal (context .DeadlineExceeded ))
263+ }
264+
265+ // Metric should be incremented 3 times
266+ Expect (ctrlmetrics .ReconcileTimeouts .WithLabelValues (ctrl .Name ).Write (& reconcileTimeouts )).To (Succeed ())
267+ Expect (reconcileTimeouts .GetCounter ().GetValue ()).To (Equal (float64 (numTimeouts )))
268+ })
269+
270+ It ("should not increment when parent context is cancelled" , func (ctx SpecContext ) {
271+ Expect (func () error {
272+ Expect (ctrlmetrics .ReconcileTimeouts .WithLabelValues (ctrl .Name ).Write (& reconcileTimeouts )).To (Succeed ())
273+ if reconcileTimeouts .GetCounter ().GetValue () != 0.0 {
274+ return fmt .Errorf ("metric reconcile timeouts not reset" )
275+ }
276+ return nil
277+ }()).Should (Succeed ())
278+
279+ // Create a parent context that will be cancelled
280+ parentCtx , cancel := context .WithCancel (ctx )
281+ defer cancel ()
282+
283+ ctrl .ReconciliationTimeout = 10 * time .Second
284+ ctrl .Do = reconcile .Func (func (ctx context.Context , _ reconcile.Request ) (reconcile.Result , error ) {
285+ // Wait for parent cancellation
286+ <- ctx .Done ()
287+ return reconcile.Result {}, ctx .Err ()
288+ })
289+
290+ // Cancel parent context immediately
291+ cancel ()
292+
293+ _ , err := ctrl .Reconcile (parentCtx ,
294+ reconcile.Request {NamespacedName : types.NamespacedName {Namespace : "foo" , Name : "bar" }})
295+ Expect (err ).To (HaveOccurred ())
296+ Expect (err ).To (Equal (context .Canceled ))
297+
298+ // Metric should not be incremented because the wrapper timeout didn't fire
299+ Expect (ctrlmetrics .ReconcileTimeouts .WithLabelValues (ctrl .Name ).Write (& reconcileTimeouts )).To (Succeed ())
300+ Expect (reconcileTimeouts .GetCounter ().GetValue ()).To (Equal (0.0 ))
301+ })
302+ })
303+
158304 It ("should not configure a timeout if ReconciliationTimeout is zero" , func (ctx SpecContext ) {
159305 ctrl .Do = reconcile .Func (func (ctx context.Context , _ reconcile.Request ) (reconcile.Result , error ) {
160306 defer GinkgoRecover ()
0 commit comments