Skip to content

Commit 7658e31

Browse files
authored
feature: controller reconciliation max delay (#871)
1 parent 59ce66f commit 7658e31

30 files changed

+288
-34
lines changed

docs/documentation/features.md

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -160,9 +160,7 @@ larger than the `.observedGeneration` field on status. In order to have this fea
160160
.
161161

162162
If these conditions are fulfilled and generation awareness not turned off, the observed generation is automatically set
163-
by the framework after the `reconcile` method is called. There is just one exception, when the reconciler returns
164-
with `UpdateControl.updateResource`, in this case the status is not updated, just the custom resource - however, this
165-
update will lead to a new reconciliation. Note that the observed generation is updated also
163+
by the framework after the `reconcile` method is called. Note that the observed generation is updated also
166164
when `UpdateControl.noUpdate()` is returned from the reconciler. See this feature working in
167165
the [WebPage example](https://github.com/java-operator-sdk/java-operator-sdk/blob/b91221bb54af19761a617bf18eef381e8ceb3b4c/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageStatus.java#L5)
168166
.
@@ -224,6 +222,30 @@ public class DeploymentReconciler
224222
}
225223
```
226224

225+
## Max Interval Between Reconciliations
226+
227+
In case informers are all in place and reconciler is implemented correctly, there is no need for additional triggers.
228+
However, it's a [common practice](https://github.com/java-operator-sdk/java-operator-sdk/issues/848#issuecomment-1016419966)
229+
to have a failsafe periodic trigger in place,
230+
just to make sure the resources are reconciled after certain time. This functionality is in place by default, there
231+
is quite high interval (currently 10 hours) while the reconciliation is triggered. See how to override this using
232+
the standard annotation:
233+
234+
```java
235+
@ControllerConfiguration(finalizerName = NO_FINALIZER,
236+
reconciliationMaxInterval = @ReconciliationMaxInterval(
237+
interval = 50,
238+
timeUnit = TimeUnit.MILLISECONDS))
239+
```
240+
241+
The event is not propagated in a fixed rate, rather it's scheduled after each reconciliation. So the
242+
next reconciliation will after at most within the specified interval after last reconciliation.
243+
244+
This feature can be turned off by setting `reconciliationMaxInterval` to [`Constants.NO_RECONCILIATION_MAX_INTERVAL`](https://github.com/java-operator-sdk/java-operator-sdk/blob/442e7d8718e992a36880e42bd0a5c01affaec9df/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Constants.java#L8-L8)
245+
or any non-positive number.
246+
247+
The automatic retries are not affected by this feature, in case of an error no schedule is set by this feature.
248+
227249
## Automatic Retries on Error
228250

229251
When an exception is thrown from a controller, the framework will schedule an automatic retry of the reconciliation. The

operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfiguration.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package io.javaoperatorsdk.operator.api.config;
22

33
import java.lang.reflect.ParameterizedType;
4+
import java.time.Duration;
45
import java.util.Collections;
6+
import java.util.Optional;
57
import java.util.Set;
68

79
import io.fabric8.kubernetes.api.model.HasMetadata;
@@ -114,4 +116,8 @@ default boolean useFinalizer() {
114116
default ResourceEventFilter<R> getEventFilter() {
115117
return ResourceEventFilters.passthrough();
116118
}
119+
120+
default Optional<Duration> reconciliationMaxInterval() {
121+
return Optional.of(Duration.ofHours(10L));
122+
}
117123
}

operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationOverrider.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package io.javaoperatorsdk.operator.api.config;
22

3+
import java.time.Duration;
34
import java.util.HashSet;
45
import java.util.List;
56
import java.util.Set;
@@ -16,6 +17,7 @@ public class ControllerConfigurationOverrider<R extends HasMetadata> {
1617
private String labelSelector;
1718
private ResourceEventFilter<R> customResourcePredicate;
1819
private final ControllerConfiguration<R> original;
20+
private Duration reconciliationMaxInterval;
1921

2022
private ControllerConfigurationOverrider(ControllerConfiguration<R> original) {
2123
finalizer = original.getFinalizer();
@@ -24,7 +26,9 @@ private ControllerConfigurationOverrider(ControllerConfiguration<R> original) {
2426
retry = original.getRetryConfiguration();
2527
labelSelector = original.getLabelSelector();
2628
customResourcePredicate = original.getEventFilter();
29+
reconciliationMaxInterval = original.reconciliationMaxInterval().orElse(null);
2730
this.original = original;
31+
2832
}
2933

3034
public ControllerConfigurationOverrider<R> withFinalizer(String finalizer) {
@@ -74,6 +78,12 @@ public ControllerConfigurationOverrider<R> withCustomResourcePredicate(
7478
return this;
7579
}
7680

81+
public ControllerConfigurationOverrider<R> withReconciliationMaxInterval(
82+
Duration reconciliationMaxInterval) {
83+
this.reconciliationMaxInterval = reconciliationMaxInterval;
84+
return this;
85+
}
86+
7787
public ControllerConfiguration<R> build() {
7888
return new DefaultControllerConfiguration<>(
7989
original.getAssociatedReconcilerClassName(),
@@ -86,6 +96,7 @@ public ControllerConfiguration<R> build() {
8696
labelSelector,
8797
customResourcePredicate,
8898
original.getResourceClass(),
99+
reconciliationMaxInterval,
89100
original.getConfigurationService());
90101
}
91102

operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DefaultControllerConfiguration.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package io.javaoperatorsdk.operator.api.config;
22

3+
import java.time.Duration;
34
import java.util.Collections;
5+
import java.util.Optional;
46
import java.util.Set;
57

68
import io.fabric8.kubernetes.api.model.HasMetadata;
@@ -20,6 +22,7 @@ public class DefaultControllerConfiguration<R extends HasMetadata>
2022
private final String labelSelector;
2123
private final ResourceEventFilter<R> resourceEventFilter;
2224
private final Class<R> resourceClass;
25+
private final Duration reconciliationMaxInterval;
2326
private ConfigurationService service;
2427

2528
public DefaultControllerConfiguration(
@@ -33,6 +36,7 @@ public DefaultControllerConfiguration(
3336
String labelSelector,
3437
ResourceEventFilter<R> resourceEventFilter,
3538
Class<R> resourceClass,
39+
Duration reconciliationMaxInterval,
3640
ConfigurationService service) {
3741
this.associatedControllerClassName = associatedControllerClassName;
3842
this.name = name;
@@ -41,6 +45,7 @@ public DefaultControllerConfiguration(
4145
this.generationAware = generationAware;
4246
this.namespaces =
4347
namespaces != null ? Collections.unmodifiableSet(namespaces) : Collections.emptySet();
48+
this.reconciliationMaxInterval = reconciliationMaxInterval;
4449
this.watchAllNamespaces = this.namespaces.isEmpty();
4550
this.retryConfiguration =
4651
retryConfiguration == null
@@ -122,4 +127,9 @@ public Class<R> getResourceClass() {
122127
public ResourceEventFilter<R> getEventFilter() {
123128
return resourceEventFilter;
124129
}
130+
131+
@Override
132+
public Optional<Duration> reconciliationMaxInterval() {
133+
return Optional.ofNullable(reconciliationMaxInterval);
134+
}
125135
}

operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Constants.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ public final class Constants {
55
public static final String EMPTY_STRING = "";
66
public static final String WATCH_CURRENT_NAMESPACE = "JOSDK_WATCH_CURRENT";
77
public static final String NO_FINALIZER = "JOSDK_NO_FINALIZER";
8+
public static final long NO_RECONCILIATION_MAX_INTERVAL = -1L;
89

910
private Constants() {}
1011
}

operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ControllerConfiguration.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import java.lang.annotation.Retention;
55
import java.lang.annotation.RetentionPolicy;
66
import java.lang.annotation.Target;
7+
import java.util.concurrent.TimeUnit;
78

89
import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilter;
910

@@ -56,4 +57,8 @@
5657
*/
5758
@SuppressWarnings("rawtypes")
5859
Class<? extends ResourceEventFilter>[] eventFilters() default {};
60+
61+
ReconciliationMaxInterval reconciliationMaxInterval() default @ReconciliationMaxInterval(
62+
interval = 10, timeUnit = TimeUnit.HOURS);
63+
5964
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package io.javaoperatorsdk.operator.api.reconciler;
2+
3+
import java.lang.annotation.ElementType;
4+
import java.lang.annotation.Retention;
5+
import java.lang.annotation.RetentionPolicy;
6+
import java.lang.annotation.Target;
7+
import java.util.concurrent.TimeUnit;
8+
9+
@Retention(RetentionPolicy.RUNTIME)
10+
@Target({ElementType.TYPE})
11+
public @interface ReconciliationMaxInterval {
12+
13+
/**
14+
* A max delay between two reconciliations. Having this value larger than zero, will ensure that a
15+
* reconciliation is scheduled with a target interval after the last reconciliation. Note that
16+
* this not applies for retries, in case of an exception reconciliation is not scheduled. This is
17+
* not a fixed rate, in other words a new reconciliation is scheduled after each reconciliation.
18+
* <p/>
19+
* If an interval is specified by {@link UpdateControl} or {@link DeleteControl}, those take
20+
* precedence.
21+
* <p/>
22+
* This is a fail-safe feature, in the sense that if informers are in place and the reconciler
23+
* implementation is correct, this feature can be turned off.
24+
* <p/>
25+
* Use NO_RECONCILIATION_MAX_INTERVAL in {@link Constants} to turn off this feature.
26+
*
27+
* @return max delay between reconciliations
28+
**/
29+
long interval();
30+
31+
/**
32+
* @return time unit for max delay between reconciliations
33+
*/
34+
TimeUnit timeUnit() default TimeUnit.HOURS;
35+
36+
}

operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcher.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -245,9 +245,12 @@ private PostExecutionControl<R> createPostExecutionControl(R updatedCustomResour
245245
private void updatePostExecutionControlWithReschedule(
246246
PostExecutionControl<R> postExecutionControl,
247247
BaseControl<?> baseControl) {
248-
baseControl.getScheduleDelay().ifPresent(postExecutionControl::withReSchedule);
248+
baseControl.getScheduleDelay().ifPresentOrElse(postExecutionControl::withReSchedule,
249+
() -> controller.getConfiguration().reconciliationMaxInterval()
250+
.ifPresent(m -> postExecutionControl.withReSchedule(m.toMillis())));
249251
}
250252

253+
251254
private PostExecutionControl<R> handleCleanup(R resource, Context context) {
252255
log.debug(
253256
"Executing delete for resource: {} with version: {}",

operator-framework-core/src/test/java/io/javaoperatorsdk/operator/ControllerManagerTest.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,8 @@ private static class TestControllerConfiguration<R extends HasMetadata>
6060

6161
public TestControllerConfiguration(Reconciler<R> controller, Class<R> crClass) {
6262
super(null, getControllerName(controller),
63-
CustomResource.getCRDName(crClass), null, false, null, null, null, null, crClass, null);
63+
CustomResource.getCRDName(crClass), null, false, null, null, null, null, crClass,
64+
null, null);
6465
this.controller = controller;
6566
}
6667

operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcherTest.java

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package io.javaoperatorsdk.operator.processing.event;
22

3+
import java.time.Duration;
34
import java.util.ArrayList;
45
import java.util.Optional;
56
import java.util.concurrent.TimeUnit;
@@ -47,13 +48,15 @@ class ReconciliationDispatcherTest {
4748

4849
private static final String DEFAULT_FINALIZER = "javaoperatorsdk.io/finalizer";
4950
public static final String ERROR_MESSAGE = "ErrorMessage";
51+
public static final long RECONCILIATION_MAX_INTERVAL = 10L;
5052
private TestCustomResource testCustomResource;
5153
private ReconciliationDispatcher<TestCustomResource> reconciliationDispatcher;
5254
private final Reconciler<TestCustomResource> reconciler = mock(Reconciler.class,
5355
withSettings().extraInterfaces(ErrorStatusHandler.class));
5456
private final ConfigurationService configService = mock(ConfigurationService.class);
5557
private final CustomResourceFacade<TestCustomResource> customResourceFacade =
5658
mock(ReconciliationDispatcher.CustomResourceFacade.class);
59+
private ControllerConfiguration configuration = mock(ControllerConfiguration.class);
5760

5861
@BeforeEach
5962
void setup() {
@@ -63,17 +66,23 @@ void setup() {
6366
}
6467

6568
private <R extends HasMetadata> ReconciliationDispatcher<R> init(R customResource,
66-
Reconciler<R> reconciler, ControllerConfiguration<R> configuration,
69+
Reconciler<R> reconciler, ControllerConfiguration configuration,
6770
CustomResourceFacade<R> customResourceFacade, boolean useFinalizer) {
71+
6872
configuration = configuration == null ? mock(ControllerConfiguration.class) : configuration;
73+
ReconciliationDispatcherTest.this.configuration = configuration;
6974
final var finalizer = useFinalizer ? DEFAULT_FINALIZER : Constants.NO_FINALIZER;
7075
when(configuration.getFinalizer()).thenReturn(finalizer);
7176
when(configuration.useFinalizer()).thenCallRealMethod();
7277
when(configuration.getName()).thenReturn("EventDispatcherTestController");
7378
when(configuration.getResourceClass()).thenReturn((Class<R>) customResource.getClass());
7479
when(configuration.getRetryConfiguration()).thenReturn(RetryConfiguration.DEFAULT);
80+
when(configuration.reconciliationMaxInterval())
81+
.thenReturn(Optional.of(Duration.ofHours(RECONCILIATION_MAX_INTERVAL)));
82+
7583
when(configuration.getConfigurationService()).thenReturn(configService);
7684

85+
7786
/*
7887
* We need this for mock reconcilers to properly generate the expected UpdateControl: without
7988
* this, calls such as `when(reconciler.reconcile(eq(testCustomResource),
@@ -429,6 +438,35 @@ void callErrorStatusHandlerEvenOnFirstError() {
429438
any(), any());
430439
}
431440

441+
@Test
442+
void schedulesReconciliationIfMaxDelayIsSet() {
443+
testCustomResource.addFinalizer(DEFAULT_FINALIZER);
444+
445+
when(reconciler.reconcile(eq(testCustomResource), any()))
446+
.thenReturn(UpdateControl.noUpdate());
447+
448+
PostExecutionControl control =
449+
reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource));
450+
451+
assertThat(control.getReScheduleDelay()).isPresent()
452+
.hasValue(TimeUnit.HOURS.toMillis(RECONCILIATION_MAX_INTERVAL));
453+
}
454+
455+
@Test
456+
void canSkipSchedulingMaxDelayIf() {
457+
testCustomResource.addFinalizer(DEFAULT_FINALIZER);
458+
459+
when(reconciler.reconcile(eq(testCustomResource), any()))
460+
.thenReturn(UpdateControl.noUpdate());
461+
when(configuration.reconciliationMaxInterval())
462+
.thenReturn(Optional.empty());
463+
464+
PostExecutionControl control =
465+
reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource));
466+
467+
assertThat(control.getReScheduleDelay()).isNotPresent();
468+
}
469+
432470
private ObservedGenCustomResource createObservedGenCustomResource() {
433471
ObservedGenCustomResource observedGenCustomResource = new ObservedGenCustomResource();
434472
observedGenCustomResource.setMetadata(new ObjectMeta());

0 commit comments

Comments
 (0)