diff --git a/docs/documentation/features.md b/docs/documentation/features.md index 91778ee275..27a369e4c1 100644 --- a/docs/documentation/features.md +++ b/docs/documentation/features.md @@ -346,12 +346,18 @@ Users can override it by implementing their own [`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) . -To configure the default rate limiter use `@ControllerConfiguration` annotation. The following -configuration limits -each resource to reconcile at most twice within a 3 second interval: +To configure the default rate limiter use the `@LimitingRateOverPeriod` annotation on your +`Reconciler` class. The following configuration limits each resource to reconcile at most twice +within a 3 second interval: -`@ControllerConfiguration(rateLimit = @RateLimit(limitForPeriod = 2,refreshPeriod = 3,refreshPeriodTimeUnit = TimeUnit.SECONDS))` -. +```java + +@LimitingRateOverPeriod(maxReconciliations = 2, within = 3, unit = TimeUnit.SECONDS) +@ControllerConfiguration +public class MyReconciler implements Reconciler { + +} +``` Thus, if a given resource was reconciled twice in one second, no further reconciliation for this resource will happen before two seconds have elapsed. Note that, since rate is limited on a diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AnnotationConfigurable.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AnnotationConfigurable.java new file mode 100644 index 0000000000..53ee608568 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AnnotationConfigurable.java @@ -0,0 +1,7 @@ +package io.javaoperatorsdk.operator.api.config; + +import java.lang.annotation.Annotation; + +public interface AnnotationConfigurable { + void initFrom(C configuration); +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AnnotationControllerConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AnnotationControllerConfiguration.java index 560a79e5be..6164fd6a4d 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AnnotationControllerConfiguration.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AnnotationControllerConfiguration.java @@ -1,5 +1,7 @@ package io.javaoperatorsdk.operator.api.config; +import java.lang.annotation.Annotation; +import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.time.Duration; import java.util.Arrays; @@ -27,7 +29,6 @@ import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource; import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResourceConfig; import io.javaoperatorsdk.operator.processing.dependent.workflow.Condition; -import io.javaoperatorsdk.operator.processing.event.rate.PeriodRateLimiter; import io.javaoperatorsdk.operator.processing.event.rate.RateLimiter; import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilter; import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilters; @@ -35,6 +36,7 @@ import io.javaoperatorsdk.operator.processing.event.source.filter.VoidOnAddFilter; import io.javaoperatorsdk.operator.processing.event.source.filter.VoidOnDeleteFilter; import io.javaoperatorsdk.operator.processing.event.source.filter.VoidOnUpdateFilter; +import io.javaoperatorsdk.operator.processing.retry.Retry; import static io.javaoperatorsdk.operator.api.reconciler.Constants.DEFAULT_NAMESPACES_SET; @@ -154,12 +156,39 @@ public Optional reconciliationMaxInterval() { @Override public RateLimiter getRateLimiter() { - if (annotation.rateLimit() != null) { - return new PeriodRateLimiter(Duration.of(annotation.rateLimit().refreshPeriod(), - annotation.rateLimit().refreshPeriodTimeUnit().toChronoUnit()), - annotation.rateLimit().limitForPeriod()); - } else { - return io.javaoperatorsdk.operator.api.config.ControllerConfiguration.super.getRateLimiter(); + final Class rateLimiterClass = annotation.rateLimiter(); + return instantiateAndConfigureIfNeeded(rateLimiterClass, RateLimiter.class); + } + + @Override + public Retry getRetry() { + final Class retryClass = annotation.retry(); + return instantiateAndConfigureIfNeeded(retryClass, Retry.class); + } + + @SuppressWarnings("unchecked") + private T instantiateAndConfigureIfNeeded(Class targetClass, + Class expectedType) { + try { + final Constructor constructor = targetClass.getDeclaredConstructor(); + constructor.setAccessible(true); + final var instance = constructor.newInstance(); + if (instance instanceof AnnotationConfigurable) { + AnnotationConfigurable configurable = (AnnotationConfigurable) instance; + final Class configurationClass = + (Class) Utils.getFirstTypeArgumentFromSuperClassOrInterface( + targetClass, AnnotationConfigurable.class); + final var configAnnotation = reconciler.getClass().getAnnotation(configurationClass); + if (configAnnotation != null) { + configurable.initFrom(configAnnotation); + } + } + return instance; + } catch (InstantiationException | IllegalAccessException | InvocationTargetException + | NoSuchMethodException e) { + throw new OperatorException("Couldn't instantiate " + expectedType.getSimpleName() + " '" + + targetClass.getName() + "' for '" + getName() + + "' reconciler. You need to provide an accessible no-arg constructor.", e); } } @@ -280,7 +309,7 @@ private String getName(Dependent dependent, Class d return name; } - @SuppressWarnings("rawtypes") + @SuppressWarnings({"rawtypes", "unchecked"}) private Object createKubernetesResourceConfig(Class dependentType) { Object config; diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfiguration.java index 339e06f894..037703c355 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfiguration.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfiguration.java @@ -8,16 +8,17 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.ReconcilerUtils; import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceSpec; -import io.javaoperatorsdk.operator.processing.event.rate.PeriodRateLimiter; +import io.javaoperatorsdk.operator.processing.event.rate.LinearRateLimiter; import io.javaoperatorsdk.operator.processing.event.rate.RateLimiter; import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilter; import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilters; import io.javaoperatorsdk.operator.processing.retry.GenericRetry; +import io.javaoperatorsdk.operator.processing.retry.GradualRetry; import io.javaoperatorsdk.operator.processing.retry.Retry; public interface ControllerConfiguration extends ResourceConfiguration { - RateLimiter DEFAULT_RATE_LIMITER = new PeriodRateLimiter(); + RateLimiter DEFAULT_RATE_LIMITER = LinearRateLimiter.deactivatedRateLimiter(); default String getName() { return ReconcilerUtils.getDefaultReconcilerName(getAssociatedReconcilerClassName()); @@ -34,15 +35,20 @@ default boolean isGenerationAware() { String getAssociatedReconcilerClassName(); default Retry getRetry() { - return GenericRetry.fromConfiguration(getRetryConfiguration()); // NOSONAR + final var configuration = getRetryConfiguration(); + return !RetryConfiguration.DEFAULT.equals(configuration) + ? GenericRetry.fromConfiguration(configuration) + : GenericRetry.DEFAULT; // NOSONAR } /** - * Use getRetry instead. + * Use {@link #getRetry()} instead. * * @return configuration for retry. + * @deprecated provide your own {@link Retry} implementation or use the {@link GradualRetry} + * annotation instead */ - @Deprecated + @Deprecated(forRemoval = true) default RetryConfiguration getRetryConfiguration() { return RetryConfiguration.DEFAULT; } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationOverrider.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationOverrider.java index 440a076428..82bd30a371 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationOverrider.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationOverrider.java @@ -106,14 +106,17 @@ public ControllerConfigurationOverrider watchingAllNamespaces() { return this; } - public ControllerConfigurationOverrider withRetry(Retry retry) { - this.retry = retry; + /** + * @deprecated Use {@link #withRetry(Retry)} instead + */ + @Deprecated(forRemoval = true) + public ControllerConfigurationOverrider withRetry(RetryConfiguration retry) { + this.retry = GenericRetry.fromConfiguration(retry); return this; } - @Deprecated - public ControllerConfigurationOverrider withRetry(RetryConfiguration retry) { - this.retry = GenericRetry.fromConfiguration(retry); + public ControllerConfigurationOverrider withRetry(Retry retry) { + this.retry = retry; return this; } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DefaultControllerConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DefaultControllerConfiguration.java index 9cc6fd1608..98cc433f63 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DefaultControllerConfiguration.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DefaultControllerConfiguration.java @@ -10,6 +10,7 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceSpec; +import io.javaoperatorsdk.operator.processing.event.rate.LinearRateLimiter; import io.javaoperatorsdk.operator.processing.event.rate.RateLimiter; import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilter; import io.javaoperatorsdk.operator.processing.retry.Retry; @@ -60,7 +61,8 @@ public DefaultControllerConfiguration( ? ControllerConfiguration.super.getRetry() : retry; this.resourceEventFilter = resourceEventFilter; - this.rateLimiter = rateLimiter; + this.rateLimiter = + rateLimiter != null ? rateLimiter : LinearRateLimiter.deactivatedRateLimiter(); this.dependents = dependents != null ? dependents : Collections.emptyList(); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DefaultRetryConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DefaultRetryConfiguration.java index 4e891b3fd3..40fbb38aa7 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DefaultRetryConfiguration.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DefaultRetryConfiguration.java @@ -1,4 +1,5 @@ package io.javaoperatorsdk.operator.api.config; public class DefaultRetryConfiguration implements RetryConfiguration { + } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/RetryConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/RetryConfiguration.java index dee54c3e8d..b293c7e33f 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/RetryConfiguration.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/RetryConfiguration.java @@ -1,5 +1,12 @@ package io.javaoperatorsdk.operator.api.config; +import io.javaoperatorsdk.operator.processing.retry.GradualRetry; + +/** + * @deprecated specify your own {@link io.javaoperatorsdk.operator.processing.retry.Retry} + * implementation or use {@link GradualRetry} annotation instead + */ +@Deprecated(forRemoval = true) public interface RetryConfiguration { RetryConfiguration DEFAULT = new DefaultRetryConfiguration(); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ControllerConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ControllerConfiguration.java index 3d86c74779..a21dbd3214 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ControllerConfiguration.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ControllerConfiguration.java @@ -9,10 +9,14 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.api.reconciler.dependent.Dependent; +import io.javaoperatorsdk.operator.processing.event.rate.LinearRateLimiter; +import io.javaoperatorsdk.operator.processing.event.rate.RateLimiter; import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilter; import io.javaoperatorsdk.operator.processing.event.source.filter.VoidGenericFilter; import io.javaoperatorsdk.operator.processing.event.source.filter.VoidOnAddFilter; import io.javaoperatorsdk.operator.processing.event.source.filter.VoidOnUpdateFilter; +import io.javaoperatorsdk.operator.processing.retry.GenericRetry; +import io.javaoperatorsdk.operator.processing.retry.Retry; @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE}) @@ -92,8 +96,6 @@ ReconciliationMaxInterval reconciliationMaxInterval() default @ReconciliationMax interval = 10); - RateLimit rateLimit() default @RateLimit; - /** * Optional list of {@link Dependent} configurations which associate a resource type to a * {@link io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource} implementation @@ -101,4 +103,20 @@ ReconciliationMaxInterval reconciliationMaxInterval() default @ReconciliationMax * @return the list of {@link Dependent} configurations */ Dependent[] dependents() default {}; + + /** + * Optional {@link Retry} implementation for the associated controller to use. + * + * @return the class providing the {@link Retry} implementation to use, needs to provide an + * accessible no-arg constructor. + */ + Class retry() default GenericRetry.class; + + /** + * Optional {@link RateLimiter} implementation for the associated controller to use. + * + * @return the class providing the {@link RateLimiter} implementation to use, needs to provide an + * accessible no-arg constructor. + */ + Class rateLimiter() default LinearRateLimiter.class; } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/RateLimit.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/RateLimit.java deleted file mode 100644 index 6c4feb2b2a..0000000000 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/RateLimit.java +++ /dev/null @@ -1,23 +0,0 @@ -package io.javaoperatorsdk.operator.api.reconciler; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; -import java.util.concurrent.TimeUnit; - -import io.javaoperatorsdk.operator.processing.event.rate.PeriodRateLimiter; - -@Retention(RetentionPolicy.RUNTIME) -@Target({ElementType.TYPE}) -public @interface RateLimit { - - int limitForPeriod() default PeriodRateLimiter.NO_LIMIT_PERIOD; - - int refreshPeriod() default PeriodRateLimiter.DEFAULT_REFRESH_PERIOD_SECONDS; - - /** - * @return time unit for max delay between reconciliations - */ - TimeUnit refreshPeriodTimeUnit() default TimeUnit.SECONDS; -} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/rate/PeriodRateLimiter.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/rate/LinearRateLimiter.java similarity index 53% rename from operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/rate/PeriodRateLimiter.java rename to operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/rate/LinearRateLimiter.java index caa6075ee7..4eb6758874 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/rate/PeriodRateLimiter.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/rate/LinearRateLimiter.java @@ -6,38 +6,37 @@ import java.util.Map; import java.util.Optional; +import io.javaoperatorsdk.operator.api.config.AnnotationConfigurable; import io.javaoperatorsdk.operator.processing.event.ResourceID; /** - * A Simple rate limiter that limits the number of permission for a time interval. + * A simple rate limiter that limits the number of permission for a time interval. */ -public class PeriodRateLimiter implements RateLimiter { +public class LinearRateLimiter + implements RateLimiter, AnnotationConfigurable { - public static final int DEFAULT_REFRESH_PERIOD_SECONDS = 10; - public static final int DEFAULT_LIMIT_FOR_PERIOD = 3; - public static final Duration DEFAULT_REFRESH_PERIOD = - Duration.ofSeconds(DEFAULT_REFRESH_PERIOD_SECONDS); - - /** To turn off rate limiting set limit fod period to a non-positive number */ + /** To turn off rate limiting set limit for period to a non-positive number */ public static final int NO_LIMIT_PERIOD = -1; private Duration refreshPeriod; - private int limitForPeriod; + private int limitForPeriod = NO_LIMIT_PERIOD; - private Map limitData = new HashMap<>(); + private final Map limitData = new HashMap<>(); - public PeriodRateLimiter() { - this(DEFAULT_REFRESH_PERIOD, DEFAULT_LIMIT_FOR_PERIOD); + public static LinearRateLimiter deactivatedRateLimiter() { + return new LinearRateLimiter(); } - public PeriodRateLimiter(Duration refreshPeriod, int limitForPeriod) { + LinearRateLimiter() {} + + public LinearRateLimiter(Duration refreshPeriod, int limitForPeriod) { this.refreshPeriod = refreshPeriod; this.limitForPeriod = limitForPeriod; } @Override public Optional acquirePermission(ResourceID resourceID) { - if (limitForPeriod <= 0) { + if (!isActivated()) { return Optional.empty(); } var actualState = limitData.computeIfAbsent(resourceID, r -> RateState.initialState()); @@ -58,4 +57,23 @@ public Optional acquirePermission(ResourceID resourceID) { public void clear(ResourceID resourceID) { limitData.remove(resourceID); } + + @Override + public void initFrom(RateLimited configuration) { + this.refreshPeriod = Duration.of(configuration.within(), + configuration.unit().toChronoUnit()); + this.limitForPeriod = configuration.maxReconciliations(); + } + + public boolean isActivated() { + return limitForPeriod > 0; + } + + public int getLimitForPeriod() { + return limitForPeriod; + } + + public Duration getRefreshPeriod() { + return refreshPeriod; + } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/rate/RateLimited.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/rate/RateLimited.java new file mode 100644 index 0000000000..7f425b73d9 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/rate/RateLimited.java @@ -0,0 +1,21 @@ +package io.javaoperatorsdk.operator.processing.event.rate; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.concurrent.TimeUnit; + +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE}) +public @interface RateLimited { + + int maxReconciliations(); + + int within(); + + /** + * @return time unit for max delay between reconciliations + */ + TimeUnit unit() default TimeUnit.SECONDS; +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/retry/GenericRetry.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/retry/GenericRetry.java index 7804586fa3..3c67b87966 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/retry/GenericRetry.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/retry/GenericRetry.java @@ -1,25 +1,28 @@ package io.javaoperatorsdk.operator.processing.retry; +import io.javaoperatorsdk.operator.api.config.AnnotationConfigurable; import io.javaoperatorsdk.operator.api.config.RetryConfiguration; -public class GenericRetry implements Retry { - private int maxAttempts = RetryConfiguration.DEFAULT_MAX_ATTEMPTS; - private long initialInterval = RetryConfiguration.DEFAULT_INITIAL_INTERVAL; - private double intervalMultiplier = RetryConfiguration.DEFAULT_MULTIPLIER; - private long maxInterval = -1; +public class GenericRetry implements Retry, AnnotationConfigurable { + private int maxAttempts = GradualRetry.DEFAULT_MAX_ATTEMPTS; + private long initialInterval = GradualRetry.DEFAULT_INITIAL_INTERVAL; + private double intervalMultiplier = GradualRetry.DEFAULT_MULTIPLIER; + private long maxInterval = GradualRetry.DEFAULT_MAX_INTERVAL; + + public static final Retry DEFAULT = new GenericRetry(); public static GenericRetry defaultLimitedExponentialRetry() { - return new GenericRetry(); + return (GenericRetry) DEFAULT; } public static GenericRetry noRetry() { return new GenericRetry().setMaxAttempts(0); } - public static GenericRetry every10second10TimesRetry() { - return new GenericRetry().withLinearRetry().setMaxAttempts(10).setInitialInterval(10000); - } - + /** + * @deprecated Use the {@link GradualRetry} annotation instead + */ + @Deprecated(forRemoval = true) public static Retry fromConfiguration(RetryConfiguration configuration) { return configuration == null ? defaultLimitedExponentialRetry() : new GenericRetry() @@ -29,6 +32,11 @@ public static Retry fromConfiguration(RetryConfiguration configuration) { .setMaxInterval(configuration.getMaxInterval()); } + + public static GenericRetry every10second10TimesRetry() { + return new GenericRetry().withLinearRetry().setMaxAttempts(10).setInitialInterval(10000); + } + @Override public GenericRetryExecution initExecution() { return new GenericRetryExecution(this); @@ -83,4 +91,14 @@ public GenericRetry withLinearRetry() { this.intervalMultiplier = 1; return this; } + + @Override + public void initFrom(GradualRetry configuration) { + this.initialInterval = configuration.initialInterval(); + this.maxAttempts = configuration.maxAttempts(); + this.intervalMultiplier = configuration.intervalMultiplier(); + this.maxInterval = configuration.maxInterval() == GradualRetry.UNSET_VALUE + ? GradualRetry.DEFAULT_MAX_INTERVAL + : configuration.maxInterval(); + } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/retry/GradualRetry.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/retry/GradualRetry.java new file mode 100644 index 0000000000..66f6372b32 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/retry/GradualRetry.java @@ -0,0 +1,28 @@ +package io.javaoperatorsdk.operator.processing.retry; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface GradualRetry { + + int DEFAULT_MAX_ATTEMPTS = 5; + long DEFAULT_INITIAL_INTERVAL = 2000L; + double DEFAULT_MULTIPLIER = 1.5D; + + long DEFAULT_MAX_INTERVAL = (long) (GradualRetry.DEFAULT_INITIAL_INTERVAL * Math.pow( + GradualRetry.DEFAULT_MULTIPLIER, GradualRetry.DEFAULT_MAX_ATTEMPTS)); + + long UNSET_VALUE = Long.MAX_VALUE; + + int maxAttempts() default DEFAULT_MAX_ATTEMPTS; + + long initialInterval() default DEFAULT_INITIAL_INTERVAL; + + double intervalMultiplier() default DEFAULT_MULTIPLIER; + + long maxInterval() default UNSET_VALUE; +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/retry/Retry.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/retry/Retry.java index 3500a196f8..78ec6f189c 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/retry/Retry.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/retry/Retry.java @@ -1,5 +1,6 @@ package io.javaoperatorsdk.operator.processing.retry; +@FunctionalInterface public interface Retry { RetryExecution initExecution(); diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/EventProcessorTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/EventProcessorTest.java index 788325ebe8..bcb7f3e739 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/EventProcessorTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/EventProcessorTest.java @@ -17,7 +17,7 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.api.config.RetryConfiguration; import io.javaoperatorsdk.operator.api.monitoring.Metrics; -import io.javaoperatorsdk.operator.processing.event.rate.PeriodRateLimiter; +import io.javaoperatorsdk.operator.processing.event.rate.LinearRateLimiter; import io.javaoperatorsdk.operator.processing.event.rate.RateLimiter; import io.javaoperatorsdk.operator.processing.event.source.controller.ControllerResourceEventSource; import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceAction; @@ -258,7 +258,7 @@ void startProcessedMarkedEventReceivedBefore() { var crID = new ResourceID("test-cr", TEST_NAMESPACE); eventProcessor = spy(new EventProcessor(reconciliationDispatcherMock, eventSourceManagerMock, "Test", null, - new PeriodRateLimiter(), + LinearRateLimiter.deactivatedRateLimiter(), metricsMock)); when(controllerResourceEventSourceMock.get(eq(crID))) .thenReturn(Optional.of(testCustomResource())); diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/rate/PeriodRateLimiterTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/rate/LinearRateLimiterTest.java similarity index 85% rename from operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/rate/PeriodRateLimiterTest.java rename to operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/rate/LinearRateLimiterTest.java index 90ad8447cf..a9cd48bef4 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/rate/PeriodRateLimiterTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/rate/LinearRateLimiterTest.java @@ -9,14 +9,14 @@ import static org.assertj.core.api.Assertions.assertThat; -class PeriodRateLimiterTest { +class LinearRateLimiterTest { public static final Duration REFRESH_PERIOD = Duration.ofMillis(300); ResourceID resourceID = ResourceID.fromResource(TestUtils.testCustomResource()); @Test void acquirePermissionForNewResource() { - var rl = new PeriodRateLimiter(REFRESH_PERIOD, 2); + var rl = new LinearRateLimiter(REFRESH_PERIOD, 2); var res = rl.acquirePermission(resourceID); assertThat(res).isEmpty(); res = rl.acquirePermission(resourceID); @@ -28,7 +28,7 @@ void acquirePermissionForNewResource() { @Test void returnsMinimalDurationToAcquirePermission() { - var rl = new PeriodRateLimiter(REFRESH_PERIOD, 1); + var rl = new LinearRateLimiter(REFRESH_PERIOD, 1); var res = rl.acquirePermission(resourceID); assertThat(res).isEmpty(); @@ -40,7 +40,7 @@ void returnsMinimalDurationToAcquirePermission() { @Test void resetsPeriodAfterLimit() throws InterruptedException { - var rl = new PeriodRateLimiter(REFRESH_PERIOD, 1); + var rl = new LinearRateLimiter(REFRESH_PERIOD, 1); var res = rl.acquirePermission(resourceID); assertThat(res).isEmpty(); res = rl.acquirePermission(resourceID); @@ -55,7 +55,7 @@ void resetsPeriodAfterLimit() throws InterruptedException { @Test void rateLimitCanBeTurnedOff() { - var rl = new PeriodRateLimiter(REFRESH_PERIOD, PeriodRateLimiter.NO_LIMIT_PERIOD); + var rl = new LinearRateLimiter(REFRESH_PERIOD, LinearRateLimiter.NO_LIMIT_PERIOD); var res = rl.acquirePermission(resourceID); diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/CustomResourceSelectorTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/CustomResourceSelectorTest.java index ed073f950c..f0f0bb0e62 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/CustomResourceSelectorTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/CustomResourceSelectorTest.java @@ -20,8 +20,11 @@ import io.javaoperatorsdk.operator.api.config.ConfigurationServiceProvider; import io.javaoperatorsdk.operator.api.config.DefaultControllerConfiguration; import io.javaoperatorsdk.operator.api.config.Version; -import io.javaoperatorsdk.operator.api.reconciler.*; -import io.javaoperatorsdk.operator.processing.event.rate.PeriodRateLimiter; +import io.javaoperatorsdk.operator.api.reconciler.Constants; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; +import io.javaoperatorsdk.operator.api.reconciler.Reconciler; +import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; import static org.assertj.core.api.Assertions.assertThat; @@ -137,7 +140,7 @@ public MyConfiguration() { super(MyController.class.getCanonicalName(), "mycontroller", null, Constants.NO_VALUE_SET, false, null, null, null, null, TestCustomResource.class, null, null, null, null, - new PeriodRateLimiter(), null); + null, null); } } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/runtime/AnnotationControllerConfigurationTest.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/runtime/AnnotationControllerConfigurationTest.java index de54d2f3f4..731c7b7cd3 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/runtime/AnnotationControllerConfigurationTest.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/runtime/AnnotationControllerConfigurationTest.java @@ -1,5 +1,10 @@ package io.javaoperatorsdk.operator.config.runtime; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.time.Duration; import java.util.List; import java.util.Optional; import java.util.Set; @@ -9,6 +14,7 @@ import io.fabric8.kubernetes.api.model.ConfigMap; import io.javaoperatorsdk.operator.OperatorException; +import io.javaoperatorsdk.operator.api.config.AnnotationConfigurable; import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceSpec; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; @@ -19,10 +25,17 @@ import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent; import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource; import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResourceConfig; +import io.javaoperatorsdk.operator.processing.event.rate.LinearRateLimiter; +import io.javaoperatorsdk.operator.processing.event.rate.RateLimited; +import io.javaoperatorsdk.operator.processing.retry.GenericRetry; +import io.javaoperatorsdk.operator.processing.retry.GradualRetry; +import io.javaoperatorsdk.operator.processing.retry.Retry; +import io.javaoperatorsdk.operator.processing.retry.RetryExecution; import io.javaoperatorsdk.operator.sample.readonly.ReadOnlyDependent; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -39,6 +52,7 @@ void defaultValuesShouldBeConsistent() { assertNull(unannotated.labelSelector()); } + @SuppressWarnings("rawtypes") private KubernetesDependentResourceConfig extractDependentKubernetesResourceConfig( io.javaoperatorsdk.operator.api.config.ControllerConfiguration configuration, int index) { return (KubernetesDependentResourceConfig) configuration.getDependentResources().get(index) @@ -47,6 +61,7 @@ private KubernetesDependentResourceConfig extractDependentKubernetesResourceConf } @Test + @SuppressWarnings("rawtypes") void getDependentResources() { var configuration = new AnnotationControllerConfiguration<>(new NoDepReconciler()); var dependents = configuration.getDependentResources(); @@ -114,6 +129,46 @@ && findByNameOptional(dependents, DependentResource.defaultNameFor(ReadOnlyDepen .isPresent()); } + + @Test + void checkDefaultRateAndRetryConfigurations() { + var config = new AnnotationControllerConfiguration<>(new NoDepReconciler()); + final var retry = assertInstanceOf(GenericRetry.class, config.getRetry()); + assertEquals(GradualRetry.DEFAULT_MAX_ATTEMPTS, retry.getMaxAttempts()); + assertEquals(GradualRetry.DEFAULT_MULTIPLIER, retry.getIntervalMultiplier()); + assertEquals(GradualRetry.DEFAULT_INITIAL_INTERVAL, retry.getInitialInterval()); + assertEquals(GradualRetry.DEFAULT_MAX_INTERVAL, retry.getMaxInterval()); + + final var limiter = assertInstanceOf(LinearRateLimiter.class, config.getRateLimiter()); + assertFalse(limiter.isActivated()); + } + + @Test + void configuringRateAndRetryViaAnnotationsShouldWork() { + var config = + new AnnotationControllerConfiguration<>(new ConfigurableRateLimitAndRetryReconciler()); + final var retry = config.getRetry(); + final var testRetry = assertInstanceOf(TestRetry.class, retry); + assertEquals(12, testRetry.getValue()); + + final var rateLimiter = assertInstanceOf(LinearRateLimiter.class, config.getRateLimiter()); + assertEquals(7, rateLimiter.getLimitForPeriod()); + assertEquals(Duration.ofSeconds(3), rateLimiter.getRefreshPeriod()); + } + + @Test + void checkingRetryingGraduallyWorks() { + var config = new AnnotationControllerConfiguration<>(new CheckRetryingGraduallyConfiguration()); + final var retry = config.getRetry(); + final var genericRetry = assertInstanceOf(GenericRetry.class, retry); + assertEquals(CheckRetryingGraduallyConfiguration.INITIAL_INTERVAL, + genericRetry.getInitialInterval()); + assertEquals(CheckRetryingGraduallyConfiguration.MAX_ATTEMPTS, genericRetry.getMaxAttempts()); + assertEquals(CheckRetryingGraduallyConfiguration.INTERVAL_MULTIPLIER, + genericRetry.getIntervalMultiplier()); + assertEquals(CheckRetryingGraduallyConfiguration.MAX_INTERVAL, genericRetry.getMaxInterval()); + } + @ControllerConfiguration(namespaces = OneDepReconciler.CONFIGURED_NS, dependents = @Dependent(type = ReadOnlyDependent.class)) private static class OneDepReconciler implements Reconciler { @@ -202,4 +257,62 @@ public UpdateControl reconcile(ConfigMap resource, Context return null; } } + + public static class TestRetry implements Retry, AnnotationConfigurable { + private int value; + + public TestRetry() {} + + @Override + public RetryExecution initExecution() { + return null; + } + + public int getValue() { + return value; + } + + @Override + public void initFrom(TestRetryConfiguration configuration) { + value = configuration.value(); + } + } + + @Target(ElementType.TYPE) + @Retention(RetentionPolicy.RUNTIME) + private @interface TestRetryConfiguration { + int value() default 42; + } + + @TestRetryConfiguration(12) + @RateLimited(maxReconciliations = 7, within = 3) + @ControllerConfiguration(retry = TestRetry.class) + private static class ConfigurableRateLimitAndRetryReconciler implements Reconciler { + + @Override + public UpdateControl reconcile(ConfigMap resource, Context context) + throws Exception { + return UpdateControl.noUpdate(); + } + } + + @GradualRetry( + maxAttempts = CheckRetryingGraduallyConfiguration.MAX_ATTEMPTS, + initialInterval = CheckRetryingGraduallyConfiguration.INITIAL_INTERVAL, + intervalMultiplier = CheckRetryingGraduallyConfiguration.INTERVAL_MULTIPLIER, + maxInterval = CheckRetryingGraduallyConfiguration.MAX_INTERVAL) + @ControllerConfiguration + private static class CheckRetryingGraduallyConfiguration implements Reconciler { + + public static final int MAX_ATTEMPTS = 7; + public static final int INITIAL_INTERVAL = 1000; + public static final int INTERVAL_MULTIPLIER = 2; + public static final int MAX_INTERVAL = 60000; + + @Override + public UpdateControl reconcile(ConfigMap resource, Context context) + throws Exception { + return UpdateControl.noUpdate(); + } + } } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/ratelimit/RateLimitReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/ratelimit/RateLimitReconciler.java index b4a77a8fc0..19a63952ef 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/ratelimit/RateLimitReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/ratelimit/RateLimitReconciler.java @@ -3,11 +3,16 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; -import io.javaoperatorsdk.operator.api.reconciler.*; - -@ControllerConfiguration(rateLimit = @RateLimit(limitForPeriod = 1, - refreshPeriod = RateLimitReconciler.REFRESH_PERIOD, - refreshPeriodTimeUnit = TimeUnit.MILLISECONDS)) +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; +import io.javaoperatorsdk.operator.api.reconciler.Reconciler; +import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; +import io.javaoperatorsdk.operator.processing.event.rate.RateLimited; + +@RateLimited(maxReconciliations = 1, + within = RateLimitReconciler.REFRESH_PERIOD, + unit = TimeUnit.MILLISECONDS) +@ControllerConfiguration public class RateLimitReconciler implements Reconciler { diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java index 2aa1674564..ca6710e049 100644 --- a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java @@ -3,7 +3,6 @@ import java.util.HashMap; import java.util.Map; import java.util.Objects; -import java.util.concurrent.TimeUnit; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; @@ -19,16 +18,32 @@ import io.fabric8.kubernetes.client.KubernetesClient; import io.javaoperatorsdk.operator.ReconcilerUtils; import io.javaoperatorsdk.operator.api.config.informer.InformerConfiguration; -import io.javaoperatorsdk.operator.api.reconciler.*; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; +import io.javaoperatorsdk.operator.api.reconciler.ErrorStatusHandler; +import io.javaoperatorsdk.operator.api.reconciler.ErrorStatusUpdateControl; +import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; +import io.javaoperatorsdk.operator.api.reconciler.EventSourceInitializer; +import io.javaoperatorsdk.operator.api.reconciler.Reconciler; +import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; +import io.javaoperatorsdk.operator.processing.event.rate.RateLimited; import io.javaoperatorsdk.operator.processing.event.source.EventSource; import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource; -import static io.javaoperatorsdk.operator.sample.Utils.*; +import static io.javaoperatorsdk.operator.sample.Utils.configMapName; +import static io.javaoperatorsdk.operator.sample.Utils.createStatus; +import static io.javaoperatorsdk.operator.sample.Utils.deploymentName; +import static io.javaoperatorsdk.operator.sample.Utils.handleError; +import static io.javaoperatorsdk.operator.sample.Utils.isValidHtml; +import static io.javaoperatorsdk.operator.sample.Utils.makeDesiredIngress; +import static io.javaoperatorsdk.operator.sample.Utils.serviceName; +import static io.javaoperatorsdk.operator.sample.Utils.setInvalidHtmlErrorMessage; +import static io.javaoperatorsdk.operator.sample.Utils.simulateErrorIfRequested; import static io.javaoperatorsdk.operator.sample.WebPageManagedDependentsReconciler.SELECTOR; /** Shows how to implement reconciler using the low level api directly. */ -@ControllerConfiguration(rateLimit = @RateLimit(limitForPeriod = 2, refreshPeriod = 3, - refreshPeriodTimeUnit = TimeUnit.SECONDS)) +@RateLimited(maxReconciliations = 2, within = 3) +@ControllerConfiguration public class WebPageReconciler implements Reconciler, ErrorStatusHandler, EventSourceInitializer {