Skip to content

Commit f54bdd0

Browse files
metacosmcsviri
andauthored
feat: decouple configuration of rate limiting and retry from controller (#1317)
Co-authored-by: csviri <csviri@gmail.com>
1 parent 9c39d9d commit f54bdd0

File tree

21 files changed

+370
-92
lines changed

21 files changed

+370
-92
lines changed

docs/documentation/features.md

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -346,12 +346,18 @@ Users can override it by implementing their own
346346
[`RateLimiter`](https://github.com/java-operator-sdk/java-operator-sdk/blob/ce4d996ee073ebef5715737995fc3d33f4751275/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/rate/RateLimiter.java)
347347
.
348348

349-
To configure the default rate limiter use `@ControllerConfiguration` annotation. The following
350-
configuration limits
351-
each resource to reconcile at most twice within a 3 second interval:
349+
To configure the default rate limiter use the `@LimitingRateOverPeriod` annotation on your
350+
`Reconciler` class. The following configuration limits each resource to reconcile at most twice
351+
within a 3 second interval:
352352

353-
`@ControllerConfiguration(rateLimit = @RateLimit(limitForPeriod = 2,refreshPeriod = 3,refreshPeriodTimeUnit = TimeUnit.SECONDS))`
354-
.
353+
```java
354+
355+
@LimitingRateOverPeriod(maxReconciliations = 2, within = 3, unit = TimeUnit.SECONDS)
356+
@ControllerConfiguration
357+
public class MyReconciler implements Reconciler<MyCR> {
358+
359+
}
360+
```
355361

356362
Thus, if a given resource was reconciled twice in one second, no further reconciliation for this
357363
resource will happen before two seconds have elapsed. Note that, since rate is limited on a
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package io.javaoperatorsdk.operator.api.config;
2+
3+
import java.lang.annotation.Annotation;
4+
5+
public interface AnnotationConfigurable<C extends Annotation> {
6+
void initFrom(C configuration);
7+
}

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

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

3+
import java.lang.annotation.Annotation;
4+
import java.lang.reflect.Constructor;
35
import java.lang.reflect.InvocationTargetException;
46
import java.time.Duration;
57
import java.util.Arrays;
@@ -27,14 +29,14 @@
2729
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource;
2830
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResourceConfig;
2931
import io.javaoperatorsdk.operator.processing.dependent.workflow.Condition;
30-
import io.javaoperatorsdk.operator.processing.event.rate.PeriodRateLimiter;
3132
import io.javaoperatorsdk.operator.processing.event.rate.RateLimiter;
3233
import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilter;
3334
import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilters;
3435
import io.javaoperatorsdk.operator.processing.event.source.filter.VoidGenericFilter;
3536
import io.javaoperatorsdk.operator.processing.event.source.filter.VoidOnAddFilter;
3637
import io.javaoperatorsdk.operator.processing.event.source.filter.VoidOnDeleteFilter;
3738
import io.javaoperatorsdk.operator.processing.event.source.filter.VoidOnUpdateFilter;
39+
import io.javaoperatorsdk.operator.processing.retry.Retry;
3840

3941
import static io.javaoperatorsdk.operator.api.reconciler.Constants.DEFAULT_NAMESPACES_SET;
4042

@@ -154,12 +156,39 @@ public Optional<Duration> reconciliationMaxInterval() {
154156

155157
@Override
156158
public RateLimiter getRateLimiter() {
157-
if (annotation.rateLimit() != null) {
158-
return new PeriodRateLimiter(Duration.of(annotation.rateLimit().refreshPeriod(),
159-
annotation.rateLimit().refreshPeriodTimeUnit().toChronoUnit()),
160-
annotation.rateLimit().limitForPeriod());
161-
} else {
162-
return io.javaoperatorsdk.operator.api.config.ControllerConfiguration.super.getRateLimiter();
159+
final Class<? extends RateLimiter> rateLimiterClass = annotation.rateLimiter();
160+
return instantiateAndConfigureIfNeeded(rateLimiterClass, RateLimiter.class);
161+
}
162+
163+
@Override
164+
public Retry getRetry() {
165+
final Class<? extends Retry> retryClass = annotation.retry();
166+
return instantiateAndConfigureIfNeeded(retryClass, Retry.class);
167+
}
168+
169+
@SuppressWarnings("unchecked")
170+
private <T> T instantiateAndConfigureIfNeeded(Class<? extends T> targetClass,
171+
Class<T> expectedType) {
172+
try {
173+
final Constructor<? extends T> constructor = targetClass.getDeclaredConstructor();
174+
constructor.setAccessible(true);
175+
final var instance = constructor.newInstance();
176+
if (instance instanceof AnnotationConfigurable) {
177+
AnnotationConfigurable configurable = (AnnotationConfigurable) instance;
178+
final Class<? extends Annotation> configurationClass =
179+
(Class<? extends Annotation>) Utils.getFirstTypeArgumentFromSuperClassOrInterface(
180+
targetClass, AnnotationConfigurable.class);
181+
final var configAnnotation = reconciler.getClass().getAnnotation(configurationClass);
182+
if (configAnnotation != null) {
183+
configurable.initFrom(configAnnotation);
184+
}
185+
}
186+
return instance;
187+
} catch (InstantiationException | IllegalAccessException | InvocationTargetException
188+
| NoSuchMethodException e) {
189+
throw new OperatorException("Couldn't instantiate " + expectedType.getSimpleName() + " '"
190+
+ targetClass.getName() + "' for '" + getName()
191+
+ "' reconciler. You need to provide an accessible no-arg constructor.", e);
163192
}
164193
}
165194

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

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,17 @@
88
import io.fabric8.kubernetes.api.model.HasMetadata;
99
import io.javaoperatorsdk.operator.ReconcilerUtils;
1010
import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceSpec;
11-
import io.javaoperatorsdk.operator.processing.event.rate.PeriodRateLimiter;
11+
import io.javaoperatorsdk.operator.processing.event.rate.LinearRateLimiter;
1212
import io.javaoperatorsdk.operator.processing.event.rate.RateLimiter;
1313
import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilter;
1414
import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilters;
1515
import io.javaoperatorsdk.operator.processing.retry.GenericRetry;
16+
import io.javaoperatorsdk.operator.processing.retry.GradualRetry;
1617
import io.javaoperatorsdk.operator.processing.retry.Retry;
1718

1819
public interface ControllerConfiguration<R extends HasMetadata> extends ResourceConfiguration<R> {
1920

20-
RateLimiter DEFAULT_RATE_LIMITER = new PeriodRateLimiter();
21+
RateLimiter DEFAULT_RATE_LIMITER = LinearRateLimiter.deactivatedRateLimiter();
2122

2223
default String getName() {
2324
return ReconcilerUtils.getDefaultReconcilerName(getAssociatedReconcilerClassName());
@@ -34,15 +35,20 @@ default boolean isGenerationAware() {
3435
String getAssociatedReconcilerClassName();
3536

3637
default Retry getRetry() {
37-
return GenericRetry.fromConfiguration(getRetryConfiguration()); // NOSONAR
38+
final var configuration = getRetryConfiguration();
39+
return !RetryConfiguration.DEFAULT.equals(configuration)
40+
? GenericRetry.fromConfiguration(configuration)
41+
: GenericRetry.DEFAULT; // NOSONAR
3842
}
3943

4044
/**
41-
* Use getRetry instead.
45+
* Use {@link #getRetry()} instead.
4246
*
4347
* @return configuration for retry.
48+
* @deprecated provide your own {@link Retry} implementation or use the {@link GradualRetry}
49+
* annotation instead
4450
*/
45-
@Deprecated
51+
@Deprecated(forRemoval = true)
4652
default RetryConfiguration getRetryConfiguration() {
4753
return RetryConfiguration.DEFAULT;
4854
}

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

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -106,14 +106,17 @@ public ControllerConfigurationOverrider<R> watchingAllNamespaces() {
106106
return this;
107107
}
108108

109-
public ControllerConfigurationOverrider<R> withRetry(Retry retry) {
110-
this.retry = retry;
109+
/**
110+
* @deprecated Use {@link #withRetry(Retry)} instead
111+
*/
112+
@Deprecated(forRemoval = true)
113+
public ControllerConfigurationOverrider<R> withRetry(RetryConfiguration retry) {
114+
this.retry = GenericRetry.fromConfiguration(retry);
111115
return this;
112116
}
113117

114-
@Deprecated
115-
public ControllerConfigurationOverrider<R> withRetry(RetryConfiguration retry) {
116-
this.retry = GenericRetry.fromConfiguration(retry);
118+
public ControllerConfigurationOverrider<R> withRetry(Retry retry) {
119+
this.retry = retry;
117120
return this;
118121
}
119122

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
import io.fabric8.kubernetes.api.model.HasMetadata;
1212
import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceSpec;
13+
import io.javaoperatorsdk.operator.processing.event.rate.LinearRateLimiter;
1314
import io.javaoperatorsdk.operator.processing.event.rate.RateLimiter;
1415
import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilter;
1516
import io.javaoperatorsdk.operator.processing.retry.Retry;
@@ -60,7 +61,8 @@ public DefaultControllerConfiguration(
6061
? ControllerConfiguration.super.getRetry()
6162
: retry;
6263
this.resourceEventFilter = resourceEventFilter;
63-
this.rateLimiter = rateLimiter;
64+
this.rateLimiter =
65+
rateLimiter != null ? rateLimiter : LinearRateLimiter.deactivatedRateLimiter();
6466
this.dependents = dependents != null ? dependents : Collections.emptyList();
6567
}
6668

Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
package io.javaoperatorsdk.operator.api.config;
22

33
public class DefaultRetryConfiguration implements RetryConfiguration {
4+
45
}

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

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

3+
import io.javaoperatorsdk.operator.processing.retry.GradualRetry;
4+
5+
/**
6+
* @deprecated specify your own {@link io.javaoperatorsdk.operator.processing.retry.Retry}
7+
* implementation or use {@link GradualRetry} annotation instead
8+
*/
9+
@Deprecated(forRemoval = true)
310
public interface RetryConfiguration {
411

512
RetryConfiguration DEFAULT = new DefaultRetryConfiguration();

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

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,14 @@
99

1010
import io.fabric8.kubernetes.api.model.HasMetadata;
1111
import io.javaoperatorsdk.operator.api.reconciler.dependent.Dependent;
12+
import io.javaoperatorsdk.operator.processing.event.rate.LinearRateLimiter;
13+
import io.javaoperatorsdk.operator.processing.event.rate.RateLimiter;
1214
import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilter;
1315
import io.javaoperatorsdk.operator.processing.event.source.filter.VoidGenericFilter;
1416
import io.javaoperatorsdk.operator.processing.event.source.filter.VoidOnAddFilter;
1517
import io.javaoperatorsdk.operator.processing.event.source.filter.VoidOnUpdateFilter;
18+
import io.javaoperatorsdk.operator.processing.retry.GenericRetry;
19+
import io.javaoperatorsdk.operator.processing.retry.Retry;
1620

1721
@Retention(RetentionPolicy.RUNTIME)
1822
@Target({ElementType.TYPE})
@@ -92,13 +96,27 @@ ReconciliationMaxInterval reconciliationMaxInterval() default @ReconciliationMax
9296
interval = 10);
9397

9498

95-
RateLimit rateLimit() default @RateLimit;
96-
9799
/**
98100
* Optional list of {@link Dependent} configurations which associate a resource type to a
99101
* {@link io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource} implementation
100102
*
101103
* @return the list of {@link Dependent} configurations
102104
*/
103105
Dependent[] dependents() default {};
106+
107+
/**
108+
* Optional {@link Retry} implementation for the associated controller to use.
109+
*
110+
* @return the class providing the {@link Retry} implementation to use, needs to provide an
111+
* accessible no-arg constructor.
112+
*/
113+
Class<? extends Retry> retry() default GenericRetry.class;
114+
115+
/**
116+
* Optional {@link RateLimiter} implementation for the associated controller to use.
117+
*
118+
* @return the class providing the {@link RateLimiter} implementation to use, needs to provide an
119+
* accessible no-arg constructor.
120+
*/
121+
Class<? extends RateLimiter> rateLimiter() default LinearRateLimiter.class;
104122
}

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

Lines changed: 0 additions & 23 deletions
This file was deleted.
Lines changed: 32 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,38 +6,37 @@
66
import java.util.Map;
77
import java.util.Optional;
88

9+
import io.javaoperatorsdk.operator.api.config.AnnotationConfigurable;
910
import io.javaoperatorsdk.operator.processing.event.ResourceID;
1011

1112
/**
12-
* A Simple rate limiter that limits the number of permission for a time interval.
13+
* A simple rate limiter that limits the number of permission for a time interval.
1314
*/
14-
public class PeriodRateLimiter implements RateLimiter {
15+
public class LinearRateLimiter
16+
implements RateLimiter, AnnotationConfigurable<RateLimited> {
1517

16-
public static final int DEFAULT_REFRESH_PERIOD_SECONDS = 10;
17-
public static final int DEFAULT_LIMIT_FOR_PERIOD = 3;
18-
public static final Duration DEFAULT_REFRESH_PERIOD =
19-
Duration.ofSeconds(DEFAULT_REFRESH_PERIOD_SECONDS);
20-
21-
/** To turn off rate limiting set limit fod period to a non-positive number */
18+
/** To turn off rate limiting set limit for period to a non-positive number */
2219
public static final int NO_LIMIT_PERIOD = -1;
2320

2421
private Duration refreshPeriod;
25-
private int limitForPeriod;
22+
private int limitForPeriod = NO_LIMIT_PERIOD;
2623

27-
private Map<ResourceID, RateState> limitData = new HashMap<>();
24+
private final Map<ResourceID, RateState> limitData = new HashMap<>();
2825

29-
public PeriodRateLimiter() {
30-
this(DEFAULT_REFRESH_PERIOD, DEFAULT_LIMIT_FOR_PERIOD);
26+
public static LinearRateLimiter deactivatedRateLimiter() {
27+
return new LinearRateLimiter();
3128
}
3229

33-
public PeriodRateLimiter(Duration refreshPeriod, int limitForPeriod) {
30+
LinearRateLimiter() {}
31+
32+
public LinearRateLimiter(Duration refreshPeriod, int limitForPeriod) {
3433
this.refreshPeriod = refreshPeriod;
3534
this.limitForPeriod = limitForPeriod;
3635
}
3736

3837
@Override
3938
public Optional<Duration> acquirePermission(ResourceID resourceID) {
40-
if (limitForPeriod <= 0) {
39+
if (!isActivated()) {
4140
return Optional.empty();
4241
}
4342
var actualState = limitData.computeIfAbsent(resourceID, r -> RateState.initialState());
@@ -58,4 +57,23 @@ public Optional<Duration> acquirePermission(ResourceID resourceID) {
5857
public void clear(ResourceID resourceID) {
5958
limitData.remove(resourceID);
6059
}
60+
61+
@Override
62+
public void initFrom(RateLimited configuration) {
63+
this.refreshPeriod = Duration.of(configuration.within(),
64+
configuration.unit().toChronoUnit());
65+
this.limitForPeriod = configuration.maxReconciliations();
66+
}
67+
68+
public boolean isActivated() {
69+
return limitForPeriod > 0;
70+
}
71+
72+
public int getLimitForPeriod() {
73+
return limitForPeriod;
74+
}
75+
76+
public Duration getRefreshPeriod() {
77+
return refreshPeriod;
78+
}
6179
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package io.javaoperatorsdk.operator.processing.event.rate;
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 RateLimited {
12+
13+
int maxReconciliations();
14+
15+
int within();
16+
17+
/**
18+
* @return time unit for max delay between reconciliations
19+
*/
20+
TimeUnit unit() default TimeUnit.SECONDS;
21+
}

0 commit comments

Comments
 (0)