@@ -155,6 +155,128 @@ 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+ Expect (ctrlmetrics .ReconcileTimeouts .WithLabelValues (ctrl .Name ).Write (& reconcileTimeouts )).To (Succeed ())
166+ Expect (reconcileTimeouts .GetCounter ().GetValue ()).To (Equal (0.0 ))
167+ })
168+
169+ It ("should increment when ReconciliationTimeout context timeout hits DeadlineExceeded" , func (ctx SpecContext ) {
170+ ctrl .ReconciliationTimeout = time .Duration (1 )
171+ ctrl .Do = reconcile .Func (func (ctx context.Context , _ reconcile.Request ) (reconcile.Result , error ) {
172+ <- ctx .Done ()
173+ return reconcile.Result {}, ctx .Err ()
174+ })
175+ _ , err := ctrl .Reconcile (ctx ,
176+ reconcile.Request {NamespacedName : types.NamespacedName {Namespace : "foo" , Name : "bar" }})
177+ Expect (err ).To (HaveOccurred ())
178+ Expect (err ).To (Equal (context .DeadlineExceeded ))
179+
180+ Expect (ctrlmetrics .ReconcileTimeouts .WithLabelValues (ctrl .Name ).Write (& reconcileTimeouts )).To (Succeed ())
181+ Expect (reconcileTimeouts .GetCounter ().GetValue ()).To (Equal (1.0 ))
182+ })
183+
184+ It ("should not increment when user code cancels context earlier than the ReconciliationTimeout" , func (ctx SpecContext ) {
185+ ctrl .ReconciliationTimeout = 10 * time .Second
186+ userCancelCause := errors .New ("user cancellation" )
187+ ctrl .Do = reconcile .Func (func (ctx context.Context , _ reconcile.Request ) (reconcile.Result , error ) {
188+ // User code creates its own timeout with a different cause
189+ userCtx , cancel := context .WithTimeoutCause (ctx , time .Millisecond , userCancelCause )
190+ defer cancel ()
191+ <- userCtx .Done ()
192+ return reconcile.Result {}, context .Cause (userCtx )
193+ })
194+ _ , err := ctrl .Reconcile (ctx ,
195+ reconcile.Request {NamespacedName : types.NamespacedName {Namespace : "foo" , Name : "bar" }})
196+ Expect (err ).To (HaveOccurred ())
197+ Expect (errors .Is (err , userCancelCause )).To (BeTrue ())
198+
199+ // Metric should not be incremented because the context timeout didn't hit DeadlineExceeded
200+ Expect (ctrlmetrics .ReconcileTimeouts .WithLabelValues (ctrl .Name ).Write (& reconcileTimeouts )).To (Succeed ())
201+ Expect (reconcileTimeouts .GetCounter ().GetValue ()).To (Equal (0.0 ))
202+ })
203+
204+ It ("should not increment when reconciliation completes before timeout" , func (ctx SpecContext ) {
205+ ctrl .ReconciliationTimeout = 10 * time .Second
206+ ctrl .Do = reconcile .Func (func (ctx context.Context , _ reconcile.Request ) (reconcile.Result , error ) {
207+ // Reconcile completes successfully before timeout
208+ return reconcile.Result {}, nil
209+ })
210+ _ , err := ctrl .Reconcile (ctx ,
211+ reconcile.Request {NamespacedName : types.NamespacedName {Namespace : "foo" , Name : "bar" }})
212+ Expect (err ).NotTo (HaveOccurred ())
213+
214+ // Metric should not be incremented because the timeout was not exceeded
215+ Expect (ctrlmetrics .ReconcileTimeouts .WithLabelValues (ctrl .Name ).Write (& reconcileTimeouts )).To (Succeed ())
216+ Expect (reconcileTimeouts .GetCounter ().GetValue ()).To (Equal (0.0 ))
217+ })
218+
219+ It ("should increment multiple times when multiple reconciles timeout" , func (ctx SpecContext ) {
220+ ctrl .ReconciliationTimeout = time .Duration (1 )
221+ ctrl .Do = reconcile .Func (func (ctx context.Context , _ reconcile.Request ) (reconcile.Result , error ) {
222+ <- ctx .Done ()
223+ return reconcile.Result {}, ctx .Err ()
224+ })
225+
226+ const numTimeouts = 3
227+ // Call Reconcile multiple times, each should timeout and increment the metric
228+ for i := range numTimeouts {
229+ _ , err := ctrl .Reconcile (ctx ,
230+ reconcile.Request {NamespacedName : types.NamespacedName {Namespace : "foo" , Name : fmt .Sprintf ("bar%d" , i )}})
231+ Expect (err ).To (HaveOccurred ())
232+ Expect (err ).To (Equal (context .DeadlineExceeded ))
233+ }
234+
235+ // Metric should be incremented 3 times
236+ Expect (ctrlmetrics .ReconcileTimeouts .WithLabelValues (ctrl .Name ).Write (& reconcileTimeouts )).To (Succeed ())
237+ Expect (reconcileTimeouts .GetCounter ().GetValue ()).To (Equal (float64 (numTimeouts )))
238+ })
239+
240+ It ("should not increment when parent context is cancelled" , func (ctx SpecContext ) {
241+ parentCtx , cancel := context .WithCancel (ctx )
242+ defer cancel ()
243+
244+ ctrl .ReconciliationTimeout = 10 * time .Second
245+ ctrl .Do = reconcile .Func (func (ctx context.Context , _ reconcile.Request ) (reconcile.Result , error ) {
246+ // Wait for parent cancellation
247+ <- ctx .Done ()
248+ return reconcile.Result {}, ctx .Err ()
249+ })
250+
251+ // Cancel parent context immediately
252+ cancel ()
253+
254+ _ , err := ctrl .Reconcile (parentCtx ,
255+ reconcile.Request {NamespacedName : types.NamespacedName {Namespace : "foo" , Name : "bar" }})
256+ Expect (err ).To (HaveOccurred ())
257+ Expect (err ).To (Equal (context .Canceled ))
258+
259+ // Metric should not be incremented because the wrapper timeout didn't fire
260+ Expect (ctrlmetrics .ReconcileTimeouts .WithLabelValues (ctrl .Name ).Write (& reconcileTimeouts )).To (Succeed ())
261+ Expect (reconcileTimeouts .GetCounter ().GetValue ()).To (Equal (0.0 ))
262+ })
263+
264+ It ("should not increment when ReconciliationTimeout is zero" , func (ctx SpecContext ) {
265+ // Ensure ReconciliationTimeout is zero (not set)
266+ ctrl .ReconciliationTimeout = 0
267+ ctrl .Do = reconcile .Func (func (ctx context.Context , _ reconcile.Request ) (reconcile.Result , error ) {
268+ return reconcile.Result {}, nil
269+ })
270+ _ , err := ctrl .Reconcile (ctx ,
271+ reconcile.Request {NamespacedName : types.NamespacedName {Namespace : "foo" , Name : "bar" }})
272+ Expect (err ).NotTo (HaveOccurred ())
273+
274+ // Metric should not be incremented because ReconciliationTimeout is not set
275+ Expect (ctrlmetrics .ReconcileTimeouts .WithLabelValues (ctrl .Name ).Write (& reconcileTimeouts )).To (Succeed ())
276+ Expect (reconcileTimeouts .GetCounter ().GetValue ()).To (Equal (0.0 ))
277+ })
278+ })
279+
158280 It ("should not configure a timeout if ReconciliationTimeout is zero" , func (ctx SpecContext ) {
159281 ctrl .Do = reconcile .Func (func (ctx context.Context , _ reconcile.Request ) (reconcile.Result , error ) {
160282 defer GinkgoRecover ()
0 commit comments