@@ -390,7 +390,159 @@ void leaderElectionMissingLeaseNameThrowsWhenOtherPropertiesPresent() {
390390 .withMessageContaining ("lease-name" );
391391 }
392392
393- /** Returns true when the two types are the same or one is the boxed/unboxed form of the other. */
393+ // -- retry ------------------------------------------------------------------
394+
395+ /** A minimal reconciler used to obtain a base ControllerConfiguration in retry tests. */
396+ @ io .javaoperatorsdk .operator .api .reconciler .ControllerConfiguration
397+ private static class DummyReconciler
398+ implements io .javaoperatorsdk .operator .api .reconciler .Reconciler <
399+ io .fabric8 .kubernetes .api .model .ConfigMap > {
400+ @ Override
401+ public io .javaoperatorsdk .operator .api .reconciler .UpdateControl <
402+ io .fabric8 .kubernetes .api .model .ConfigMap >
403+ reconcile (
404+ io .fabric8 .kubernetes .api .model .ConfigMap r ,
405+ io .javaoperatorsdk .operator .api .reconciler .Context <
406+ io .fabric8 .kubernetes .api .model .ConfigMap >
407+ ctx ) {
408+ return io .javaoperatorsdk .operator .api .reconciler .UpdateControl .noUpdate ();
409+ }
410+ }
411+
412+ private static io .javaoperatorsdk .operator .api .config .ControllerConfiguration <
413+ io .fabric8 .kubernetes .api .model .ConfigMap >
414+ baseControllerConfig () {
415+ return new BaseConfigurationService ().getConfigurationFor (new DummyReconciler ());
416+ }
417+
418+ private static io .javaoperatorsdk .operator .processing .retry .GenericRetry applyAndGetRetry (
419+ java .util .function .Consumer <
420+ io .javaoperatorsdk .operator .api .config .ControllerConfigurationOverrider <
421+ io .fabric8 .kubernetes .api .model .ConfigMap >>
422+ consumer ) {
423+ var overrider =
424+ io .javaoperatorsdk .operator .api .config .ControllerConfigurationOverrider .override (
425+ baseControllerConfig ());
426+ consumer .accept (overrider );
427+ return (io .javaoperatorsdk .operator .processing .retry .GenericRetry ) overrider .build ().getRetry ();
428+ }
429+
430+ @ Test
431+ void retryIsNotConfiguredWhenNoRetryPropertiesPresent () {
432+ var loader = new ConfigLoader (mapProvider (Map .of ()));
433+ var consumer = loader .<io .fabric8 .kubernetes .api .model .ConfigMap >applyControllerConfigs ("ctrl" );
434+ var overrider =
435+ io .javaoperatorsdk .operator .api .config .ControllerConfigurationOverrider .override (
436+ baseControllerConfig ());
437+ consumer .accept (overrider );
438+ // no retry property set → retry stays at the controller's default (null or unchanged)
439+ var result = overrider .build ();
440+ // The consumer must not throw and the config is buildable
441+ assertThat (result ).isNotNull ();
442+ }
443+
444+ @ Test
445+ void retryQueriesExpectedKeys () {
446+ var queriedKeys = new ArrayList <String >();
447+ ConfigProvider recordingProvider =
448+ new ConfigProvider () {
449+ @ Override
450+ public <T > Optional <T > getValue (String key , Class <T > type ) {
451+ queriedKeys .add (key );
452+ return Optional .empty ();
453+ }
454+ };
455+ new ConfigLoader (recordingProvider ).applyControllerConfigs ("ctrl" );
456+ assertThat (queriedKeys )
457+ .contains (
458+ "josdk.controller.ctrl.retry.max-attempts" ,
459+ "josdk.controller.ctrl.retry.initial-interval" ,
460+ "josdk.controller.ctrl.retry.interval-multiplier" ,
461+ "josdk.controller.ctrl.retry.max-interval" );
462+ }
463+
464+ @ Test
465+ void retryMaxAttemptsIsApplied () {
466+ var loader =
467+ new ConfigLoader (mapProvider (Map .of ("josdk.controller.ctrl.retry.max-attempts" , 10 )));
468+ var retry = applyAndGetRetry (loader .applyControllerConfigs ("ctrl" ));
469+ assertThat (retry .getMaxAttempts ()).isEqualTo (10 );
470+ // other fields stay at their defaults
471+ assertThat (retry .getInitialInterval ())
472+ .isEqualTo (
473+ io .javaoperatorsdk .operator .processing .retry .GenericRetry
474+ .defaultLimitedExponentialRetry ()
475+ .getInitialInterval ());
476+ }
477+
478+ @ Test
479+ void retryInitialIntervalIsApplied () {
480+ var loader =
481+ new ConfigLoader (mapProvider (Map .of ("josdk.controller.ctrl.retry.initial-interval" , 500L )));
482+ var retry = applyAndGetRetry (loader .applyControllerConfigs ("ctrl" ));
483+ assertThat (retry .getInitialInterval ()).isEqualTo (500L );
484+ }
485+
486+ @ Test
487+ void retryIntervalMultiplierIsApplied () {
488+ var loader =
489+ new ConfigLoader (
490+ mapProvider (Map .of ("josdk.controller.ctrl.retry.interval-multiplier" , 2.0 )));
491+ var retry = applyAndGetRetry (loader .applyControllerConfigs ("ctrl" ));
492+ assertThat (retry .getIntervalMultiplier ()).isEqualTo (2.0 );
493+ }
494+
495+ @ Test
496+ void retryMaxIntervalIsApplied () {
497+ var loader =
498+ new ConfigLoader (mapProvider (Map .of ("josdk.controller.ctrl.retry.max-interval" , 30000L )));
499+ var retry = applyAndGetRetry (loader .applyControllerConfigs ("ctrl" ));
500+ assertThat (retry .getMaxInterval ()).isEqualTo (30000L );
501+ }
502+
503+ @ Test
504+ void retryAllPropertiesApplied () {
505+ var values = new HashMap <String , Object >();
506+ values .put ("josdk.controller.ctrl.retry.max-attempts" , 7 );
507+ values .put ("josdk.controller.ctrl.retry.initial-interval" , 1000L );
508+ values .put ("josdk.controller.ctrl.retry.interval-multiplier" , 3.0 );
509+ values .put ("josdk.controller.ctrl.retry.max-interval" , 60000L );
510+ var loader = new ConfigLoader (mapProvider (values ));
511+ var retry = applyAndGetRetry (loader .applyControllerConfigs ("ctrl" ));
512+ assertThat (retry .getMaxAttempts ()).isEqualTo (7 );
513+ assertThat (retry .getInitialInterval ()).isEqualTo (1000L );
514+ assertThat (retry .getIntervalMultiplier ()).isEqualTo (3.0 );
515+ assertThat (retry .getMaxInterval ()).isEqualTo (60000L );
516+ }
517+
518+ @ Test
519+ void retryStartsFromDefaultLimitedExponentialRetryDefaults () {
520+ // Only max-attempts is overridden — other fields must still be the defaults.
521+ var defaults =
522+ io .javaoperatorsdk .operator .processing .retry .GenericRetry .defaultLimitedExponentialRetry ();
523+ var loader =
524+ new ConfigLoader (mapProvider (Map .of ("josdk.controller.ctrl.retry.max-attempts" , 3 )));
525+ var retry = applyAndGetRetry (loader .applyControllerConfigs ("ctrl" ));
526+ assertThat (retry .getMaxAttempts ()).isEqualTo (3 );
527+ assertThat (retry .getInitialInterval ()).isEqualTo (defaults .getInitialInterval ());
528+ assertThat (retry .getIntervalMultiplier ()).isEqualTo (defaults .getIntervalMultiplier ());
529+ assertThat (retry .getMaxInterval ()).isEqualTo (defaults .getMaxInterval ());
530+ }
531+
532+ @ Test
533+ void retryIsIsolatedPerControllerName () {
534+ var values = new HashMap <String , Object >();
535+ values .put ("josdk.controller.alpha.retry.max-attempts" , 4 );
536+ values .put ("josdk.controller.beta.retry.max-attempts" , 9 );
537+ var loader = new ConfigLoader (mapProvider (values ));
538+
539+ var alphaRetry = applyAndGetRetry (loader .applyControllerConfigs ("alpha" ));
540+ var betaRetry = applyAndGetRetry (loader .applyControllerConfigs ("beta" ));
541+
542+ assertThat (alphaRetry .getMaxAttempts ()).isEqualTo (4 );
543+ assertThat (betaRetry .getMaxAttempts ()).isEqualTo (9 );
544+ }
545+
394546 private static boolean isTypeCompatible (Class <?> methodParam , Class <?> bindingType ) {
395547 if (methodParam == bindingType ) return true ;
396548 if (methodParam == boolean .class && bindingType == Boolean .class ) return true ;
0 commit comments