From b288737a8fc3e4aea068b6156ac641ef1d26e299 Mon Sep 17 00:00:00 2001 From: 10000-ki <10000ki6472@gmail.com> Date: Mon, 5 Aug 2024 00:41:28 +0900 Subject: [PATCH 1/4] feat: support for graceful shutdown based on configuration Support for graceful shutdown based on configuration Signed-off-by: 10000-ki <10000ki6472@gmail.com> Fix naming Signed-off-by: 10000-ki <10000ki6472@gmail.com> Fix lint error Signed-off-by: 10000-ki <10000ki6472@gmail.com> Fix lint error Signed-off-by: 10000-ki <10000ki6472@gmail.com> Fix lint error Signed-off-by: 10000-ki <10000ki6472@gmail.com> Fix test duration Signed-off-by: 10000-ki <10000ki6472@gmail.com> --- .../patterns-and-best-practices/_index.md | 22 ++++++ .../io/javaoperatorsdk/operator/Operator.java | 4 +- .../api/config/ConfigurationService.java | 46 +++++++----- .../config/ConfigurationServiceOverrider.java | 13 ++++ .../ConfigurationServiceOverriderTest.java | 4 + .../operator/GracefulStopIT.java | 2 +- .../GracefulStopWithConfigurationIT.java | 74 +++++++++++++++++++ 7 files changed, 143 insertions(+), 22 deletions(-) create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/GracefulStopWithConfigurationIT.java diff --git a/docs/content/en/docs/patterns-and-best-practices/_index.md b/docs/content/en/docs/patterns-and-best-practices/_index.md index 422f3f7bfe..f4156951ec 100644 --- a/docs/content/en/docs/patterns-and-best-practices/_index.md +++ b/docs/content/en/docs/patterns-and-best-practices/_index.md @@ -120,3 +120,25 @@ might be a permission issue for some resources in another namespace. The `stopOnInformerErrorDuringStartup` has implication on [cache sync timeout](https://github.com/java-operator-sdk/java-operator-sdk/blob/114c4312c32b34688811df8dd7cea275878c9e73/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationService.java#L177-L179) behavior. If true operator will stop on cache sync timeout. if `false`, after the timeout the controller will start reconcile resources even if one or more event source caches did not sync yet. + +## Graceful Shutdown + +We can provide sufficient time for the reconciler to process and complete the currently ongoing events before shutting down. +There are two ways to support the graceful shutdown feature. + +The first method is to directly provide a timeout value to the `stop` method. + +```java +final var operator = new Operator(); + +operator.stop(Duration.ofSeconds(5)); +``` + +The second method is to specify the timeout value by overriding the `ConfigurationService`. + +```java +final var overridden = new ConfigurationServiceOverrider(config) + .withReconciliationTerminationTimeout(Duration.ofSeconds(5)); + +final var operator = new Operator(overridden); +``` diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/Operator.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/Operator.java index d85de6b1e5..9113afaa68 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/Operator.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/Operator.java @@ -164,7 +164,9 @@ public void stop(Duration gracefulShutdownTimeout) throws OperatorException { @Override public void stop() throws OperatorException { - stop(Duration.ZERO); + Duration reconciliationTerminationTimeout = + configurationService.reconciliationTerminationTimeout(); + stop(reconciliationTerminationTimeout); } /** diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationService.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationService.java index 942d770506..4ca41bd0b5 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationService.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationService.java @@ -101,8 +101,8 @@ static ConfigurationService newOverriddenConfigurationService( * * @param reconciler the reconciler we want the configuration of * @param the {@code CustomResource} type associated with the specified reconciler - * @return the {@link ControllerConfiguration} associated with the specified reconciler or {@code - * null} if no configuration exists for the reconciler + * @return the {@link ControllerConfiguration} associated with the specified reconciler or + * {@code null} if no configuration exists for the reconciler */ ControllerConfiguration getConfigurationFor(Reconciler reconciler); @@ -211,7 +211,7 @@ default int concurrentWorkflowExecutorThreads() { /** * Override to provide a custom {@link Metrics} implementation - * + * * @return the {@link Metrics} implementation */ default Metrics getMetrics() { @@ -221,7 +221,7 @@ default Metrics getMetrics() { /** * Override to provide a custom {@link ExecutorService} implementation to change how threads * handle concurrent reconciliations - * + * * @return the {@link ExecutorService} implementation to use for concurrent reconciliation * processing */ @@ -232,7 +232,7 @@ default ExecutorService getExecutorService() { /** * Override to provide a custom {@link ExecutorService} implementation to change how dependent * workflows are processed in parallel - * + * * @return the {@link ExecutorService} implementation to use for dependent workflow processing */ default ExecutorService getWorkflowExecutorService() { @@ -242,7 +242,7 @@ default ExecutorService getWorkflowExecutorService() { /** * Determines whether the associated Kubernetes client should be closed when the associated * {@link io.javaoperatorsdk.operator.Operator} is stopped. - * + * * @return {@code true} if the Kubernetes should be closed on stop, {@code false} otherwise */ default boolean closeClientOnStop() { @@ -252,7 +252,7 @@ default boolean closeClientOnStop() { /** * Override to provide a custom {@link DependentResourceFactory} implementation to change how * {@link io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource} are instantiated - * + * * @return the custom {@link DependentResourceFactory} implementation */ @SuppressWarnings("rawtypes") @@ -264,7 +264,7 @@ default DependentResourceFactory dependentResourceFactory() { * Retrieves the optional {@link LeaderElectionConfiguration} to specify how the associated * {@link io.javaoperatorsdk.operator.Operator} handles leader election to ensure only one * instance of the operator runs on the cluster at any given time - * + * * @return the {@link LeaderElectionConfiguration} */ default Optional getLeaderElectionConfiguration() { @@ -299,6 +299,16 @@ default Duration cacheSyncTimeout() { return Duration.ofMinutes(2); } + /** + * This is the timeout value that allows the reconciliation threads to gracefully shut down. If no + * value is set, the default is immediate shutdown. + * + * @return The duration of time to wait before terminating the reconciliation threads + */ + default Duration reconciliationTerminationTimeout() { + return Duration.ZERO; + } + /** * Handler for an informer stop. Informer stops if there is a non-recoverable error. Like received * a resource that cannot be deserialized. @@ -326,7 +336,7 @@ default Optional getInformerStoppedHandler() { * Override to provide a custom {@link ManagedWorkflowFactory} implementation to change how * {@link io.javaoperatorsdk.operator.processing.dependent.workflow.ManagedWorkflow} are * instantiated - * + * * @return the custom {@link ManagedWorkflowFactory} implementation */ @SuppressWarnings("rawtypes") @@ -336,7 +346,7 @@ default ManagedWorkflowFactory getWorkflowFactory() { /** * Override to provide a custom {@link ExecutorServiceManager} implementation - * + * * @return the custom {@link ExecutorServiceManager} implementation */ default ExecutorServiceManager getExecutorServiceManager() { @@ -353,9 +363,8 @@ default ExecutorServiceManager getExecutorServiceManager() { * SSA based create/update can be still used with the legacy matching, just overriding the match * method of Kubernetes Dependent Resource. * - * @since 4.4.0 - * * @return if SSA should be used for dependent resources + * @since 4.4.0 */ default boolean ssaBasedCreateUpdateMatchForDependentResources() { return true; @@ -383,9 +392,8 @@ default Set> defaultNonSSAResource() { *

* Disable this if you want to react to your own dependent resource updates * - * @since 4.5.0 - * * @return if special annotation should be used for dependent resource to filter events + * @since 4.5.0 */ default boolean previousAnnotationForDependentResourcesEventFiltering() { return true; @@ -400,9 +408,8 @@ default boolean previousAnnotationForDependentResourcesEventFiltering() { * logic, and you want to further minimize the amount of work done / updates issued by the * operator. * - * @since 4.5.0 - * * @return if resource version should be parsed (as integer) + * @since 4.5.0 */ default boolean parseResourceVersionsForEventFilteringAndCaching() { return false; @@ -415,8 +422,8 @@ default boolean parseResourceVersionsForEventFilteringAndCaching() { * * @return {@code true} if Server-Side Apply (SSA) should be used when patching the primary * resources, {@code false} otherwise - * @since 5.0.0 * @see ConfigurationServiceOverrider#withUseSSAToPatchPrimaryResource(boolean) + * @since 5.0.0 */ default boolean useSSAToPatchPrimaryResource() { return true; @@ -427,7 +434,7 @@ default boolean useSSAToPatchPrimaryResource() { * Determines whether resources retrieved from caches such as via calls to * {@link Context#getSecondaryResource(Class)} should be defensively cloned first. *

- * + * *

* Defensive cloning to prevent problematic cache modifications (modifying the resource would * otherwise modify the stored copy in the cache) was transparently done in previous JOSDK @@ -435,10 +442,9 @@ default boolean useSSAToPatchPrimaryResource() { * Server-Side Apply, where you should create a new copy of your resource with only modified * fields, such modifications of these resources are less likely to occur. *

- * + * * @return {@code true} if resources should be defensively cloned before returning them from * caches, {@code false} otherwise - * * @since 5.0.0 */ default boolean cloneSecondaryResourcesWhenGettingFromCache() { diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceOverrider.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceOverrider.java index 8da14aa0c3..e35d843cb1 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceOverrider.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceOverrider.java @@ -32,6 +32,7 @@ public class ConfigurationServiceOverrider { private InformerStoppedHandler informerStoppedHandler; private Boolean stopOnInformerErrorDuringStartup; private Duration cacheSyncTimeout; + private Duration reconciliationTerminationTimeout; private Boolean ssaBasedCreateUpdateMatchForDependentResources; private Set> defaultNonSSAResource; private Boolean previousAnnotationForDependentResources; @@ -127,6 +128,12 @@ public ConfigurationServiceOverrider withCacheSyncTimeout(Duration cacheSyncTime return this; } + public ConfigurationServiceOverrider withReconciliationTerminationTimeout( + Duration reconciliationTerminationTimeout) { + this.reconciliationTerminationTimeout = reconciliationTerminationTimeout; + return this; + } + public ConfigurationServiceOverrider withSSABasedCreateUpdateMatchForDependentResources( boolean value) { this.ssaBasedCreateUpdateMatchForDependentResources = value; @@ -251,6 +258,12 @@ public Duration cacheSyncTimeout() { return overriddenValueOrDefault(cacheSyncTimeout, ConfigurationService::cacheSyncTimeout); } + @Override + public Duration reconciliationTerminationTimeout() { + return overriddenValueOrDefault(reconciliationTerminationTimeout, + ConfigurationService::reconciliationTerminationTimeout); + } + @Override public boolean ssaBasedCreateUpdateMatchForDependentResources() { return overriddenValueOrDefault(ssaBasedCreateUpdateMatchForDependentResources, diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceOverriderTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceOverriderTest.java index 4c374e09f2..0e2b8e9cc2 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceOverriderTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceOverriderTest.java @@ -1,5 +1,6 @@ package io.javaoperatorsdk.operator.api.config; +import java.time.Duration; import java.util.Optional; import java.util.concurrent.Executors; @@ -63,6 +64,7 @@ public R clone(R object) { .withLeaderElectionConfiguration(new LeaderElectionConfiguration("newLease", "newLeaseNS")) .withInformerStoppedHandler((informer, ex) -> { }) + .withReconciliationTerminationTimeout(Duration.ofSeconds(30)) .build(); assertNotEquals(config.closeClientOnStop(), overridden.closeClientOnStop()); @@ -77,6 +79,8 @@ public R clone(R object) { overridden.getLeaderElectionConfiguration()); assertNotEquals(config.getInformerStoppedHandler(), overridden.getLeaderElectionConfiguration()); + assertNotEquals(config.reconciliationTerminationTimeout(), + overridden.reconciliationTerminationTimeout()); } } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/GracefulStopIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/GracefulStopIT.java index 715921ecbb..a835480412 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/GracefulStopIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/GracefulStopIT.java @@ -28,7 +28,7 @@ public class GracefulStopIT { .build(); @Test - void stopsGracefullyWIthTimeout() { + void stopsGracefullyWithTimeout() { testGracefulStop(TEST_1, RECONCILER_SLEEP, 2); } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/GracefulStopWithConfigurationIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/GracefulStopWithConfigurationIT.java new file mode 100644 index 0000000000..dedd9e7a13 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/GracefulStopWithConfigurationIT.java @@ -0,0 +1,74 @@ +package io.javaoperatorsdk.operator; + +import java.time.Duration; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; +import io.javaoperatorsdk.operator.sample.gracefulstop.GracefulStopTestCustomResource; +import io.javaoperatorsdk.operator.sample.gracefulstop.GracefulStopTestCustomResourceSpec; +import io.javaoperatorsdk.operator.sample.gracefulstop.GracefulStopTestReconciler; + +import static io.javaoperatorsdk.operator.sample.gracefulstop.GracefulStopTestReconciler.*; +import static org.assertj.core.api.Assertions.*; +import static org.awaitility.Awaitility.*; + +public class GracefulStopWithConfigurationIT { + + public static final String TEST_1 = "test1"; + + @RegisterExtension + LocallyRunOperatorExtension operator = + LocallyRunOperatorExtension.builder() + .withConfigurationService(o -> o.withCloseClientOnStop(false) + .withReconciliationTerminationTimeout(Duration.ofMillis(RECONCILER_SLEEP))) + .withReconciler(new GracefulStopTestReconciler()) + .build(); + + @Test + void stopsGracefullyWithTimeoutConfiguration() { + testGracefulStop(TEST_1, 2); + } + + private void testGracefulStop(String resourceName, int expectedFinalGeneration) { + var testRes = operator.create(testResource(resourceName)); + await().untilAsserted(() -> { + var r = operator.get(GracefulStopTestCustomResource.class, resourceName); + assertThat(r.getStatus()).isNotNull(); + assertThat(r.getStatus().getObservedGeneration()).isEqualTo(1); + assertThat(operator.getReconcilerOfType(GracefulStopTestReconciler.class) + .getNumberOfExecutions()).isEqualTo(1); + }); + + testRes.getSpec().setValue(2); + operator.replace(testRes); + + await().pollDelay(Duration.ofMillis(50)).untilAsserted( + () -> assertThat(operator.getReconcilerOfType(GracefulStopTestReconciler.class) + .getNumberOfExecutions()).isEqualTo(2)); + + operator.getOperator().stop(); + + await().untilAsserted(() -> { + var r = operator.get(GracefulStopTestCustomResource.class, resourceName); + assertThat(r.getStatus()).isNotNull(); + assertThat(r.getStatus().getObservedGeneration()).isEqualTo(expectedFinalGeneration); + }); + } + + public GracefulStopTestCustomResource testResource(String name) { + GracefulStopTestCustomResource resource = + new GracefulStopTestCustomResource(); + resource.setMetadata( + new ObjectMetaBuilder() + .withName(name) + .withNamespace(operator.getNamespace()) + .build()); + resource.setSpec(new GracefulStopTestCustomResourceSpec()); + resource.getSpec().setValue(1); + return resource; + } + +} From 4152258bc2d4bcc0a542c7512548b471d48e8d20 Mon Sep 17 00:00:00 2001 From: 10000-ki <10000ki6472@gmail.com> Date: Tue, 6 Aug 2024 22:57:59 +0900 Subject: [PATCH 2/4] feat: remove parameter-based graceful shutdown function Signed-off-by: 10000-ki <10000ki6472@gmail.com> --- .../io/javaoperatorsdk/operator/Operator.java | 20 ++--- .../api/config/ConfigurationService.java | 1 + .../operator/GracefulStopIT.java | 23 +++--- .../GracefulStopWithConfigurationIT.java | 74 ------------------- .../operator/InformerRelatedBehaviorITS.java | 3 +- 5 files changed, 20 insertions(+), 101 deletions(-) delete mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/GracefulStopWithConfigurationIT.java diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/Operator.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/Operator.java index 9113afaa68..1283896a42 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/Operator.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/Operator.java @@ -100,7 +100,7 @@ private static ConfigurationService initConfigurationService(KubernetesClient cl @SuppressWarnings("unused") public void installShutdownHook(Duration gracefulShutdownTimeout) { if (!leaderElectionManager.isLeaderElectionEnabled()) { - Runtime.getRuntime().addShutdownHook(new Thread(() -> stop(gracefulShutdownTimeout))); + Runtime.getRuntime().addShutdownHook(new Thread(this::stop)); } else { log.warn("Leader election is on, shutdown hook will not be installed."); } @@ -145,15 +145,18 @@ public synchronized void start() { } } - public void stop(Duration gracefulShutdownTimeout) throws OperatorException { + @Override + public void stop() throws OperatorException { + Duration reconciliationTerminationTimeout = + configurationService.reconciliationTerminationTimeout(); if (!started) { return; } - log.info( - "Operator SDK {} is shutting down...", configurationService.getVersion().getSdkVersion()); + log.info("Operator SDK {} is shutting down...", + configurationService.getVersion().getSdkVersion()); controllerManager.stop(); - configurationService.getExecutorServiceManager().stop(gracefulShutdownTimeout); + configurationService.getExecutorServiceManager().stop(reconciliationTerminationTimeout); leaderElectionManager.stop(); if (configurationService.closeClientOnStop()) { getKubernetesClient().close(); @@ -162,13 +165,6 @@ public void stop(Duration gracefulShutdownTimeout) throws OperatorException { started = false; } - @Override - public void stop() throws OperatorException { - Duration reconciliationTerminationTimeout = - configurationService.reconciliationTerminationTimeout(); - stop(reconciliationTerminationTimeout); - } - /** * Add a registration requests for the specified reconciler with this operator. The effective * registration of the reconciler is delayed till the operator is started. diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationService.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationService.java index 4ca41bd0b5..0bd1607213 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationService.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationService.java @@ -304,6 +304,7 @@ default Duration cacheSyncTimeout() { * value is set, the default is immediate shutdown. * * @return The duration of time to wait before terminating the reconciliation threads + * @since 5.0.0 */ default Duration reconciliationTerminationTimeout() { return Duration.ZERO; diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/GracefulStopIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/GracefulStopIT.java index a835480412..3df200df29 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/GracefulStopIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/GracefulStopIT.java @@ -11,33 +11,28 @@ import io.javaoperatorsdk.operator.sample.gracefulstop.GracefulStopTestCustomResourceSpec; import io.javaoperatorsdk.operator.sample.gracefulstop.GracefulStopTestReconciler; -import static io.javaoperatorsdk.operator.sample.gracefulstop.GracefulStopTestReconciler.RECONCILER_SLEEP; -import static org.assertj.core.api.Assertions.assertThat; -import static org.awaitility.Awaitility.await; +import static io.javaoperatorsdk.operator.sample.gracefulstop.GracefulStopTestReconciler.*; +import static org.assertj.core.api.Assertions.*; +import static org.awaitility.Awaitility.*; public class GracefulStopIT { public static final String TEST_1 = "test1"; - public static final String TEST_2 = "test2"; @RegisterExtension LocallyRunOperatorExtension operator = LocallyRunOperatorExtension.builder() - .withConfigurationService(o -> o.withCloseClientOnStop(false)) + .withConfigurationService(o -> o.withCloseClientOnStop(false) + .withReconciliationTerminationTimeout(Duration.ofMillis(RECONCILER_SLEEP))) .withReconciler(new GracefulStopTestReconciler()) .build(); @Test - void stopsGracefullyWithTimeout() { - testGracefulStop(TEST_1, RECONCILER_SLEEP, 2); + void stopsGracefullyWithTimeoutConfiguration() { + testGracefulStop(TEST_1, 2); } - @Test - void stopsGracefullyWithExpiredTimeout() { - testGracefulStop(TEST_2, RECONCILER_SLEEP / 5, 1); - } - - private void testGracefulStop(String resourceName, int stopTimeout, int expectedFinalGeneration) { + private void testGracefulStop(String resourceName, int expectedFinalGeneration) { var testRes = operator.create(testResource(resourceName)); await().untilAsserted(() -> { var r = operator.get(GracefulStopTestCustomResource.class, resourceName); @@ -54,7 +49,7 @@ private void testGracefulStop(String resourceName, int stopTimeout, int expected () -> assertThat(operator.getReconcilerOfType(GracefulStopTestReconciler.class) .getNumberOfExecutions()).isEqualTo(2)); - operator.getOperator().stop(Duration.ofMillis(stopTimeout)); + operator.getOperator().stop(); await().untilAsserted(() -> { var r = operator.get(GracefulStopTestCustomResource.class, resourceName); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/GracefulStopWithConfigurationIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/GracefulStopWithConfigurationIT.java deleted file mode 100644 index dedd9e7a13..0000000000 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/GracefulStopWithConfigurationIT.java +++ /dev/null @@ -1,74 +0,0 @@ -package io.javaoperatorsdk.operator; - -import java.time.Duration; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.RegisterExtension; - -import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; -import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; -import io.javaoperatorsdk.operator.sample.gracefulstop.GracefulStopTestCustomResource; -import io.javaoperatorsdk.operator.sample.gracefulstop.GracefulStopTestCustomResourceSpec; -import io.javaoperatorsdk.operator.sample.gracefulstop.GracefulStopTestReconciler; - -import static io.javaoperatorsdk.operator.sample.gracefulstop.GracefulStopTestReconciler.*; -import static org.assertj.core.api.Assertions.*; -import static org.awaitility.Awaitility.*; - -public class GracefulStopWithConfigurationIT { - - public static final String TEST_1 = "test1"; - - @RegisterExtension - LocallyRunOperatorExtension operator = - LocallyRunOperatorExtension.builder() - .withConfigurationService(o -> o.withCloseClientOnStop(false) - .withReconciliationTerminationTimeout(Duration.ofMillis(RECONCILER_SLEEP))) - .withReconciler(new GracefulStopTestReconciler()) - .build(); - - @Test - void stopsGracefullyWithTimeoutConfiguration() { - testGracefulStop(TEST_1, 2); - } - - private void testGracefulStop(String resourceName, int expectedFinalGeneration) { - var testRes = operator.create(testResource(resourceName)); - await().untilAsserted(() -> { - var r = operator.get(GracefulStopTestCustomResource.class, resourceName); - assertThat(r.getStatus()).isNotNull(); - assertThat(r.getStatus().getObservedGeneration()).isEqualTo(1); - assertThat(operator.getReconcilerOfType(GracefulStopTestReconciler.class) - .getNumberOfExecutions()).isEqualTo(1); - }); - - testRes.getSpec().setValue(2); - operator.replace(testRes); - - await().pollDelay(Duration.ofMillis(50)).untilAsserted( - () -> assertThat(operator.getReconcilerOfType(GracefulStopTestReconciler.class) - .getNumberOfExecutions()).isEqualTo(2)); - - operator.getOperator().stop(); - - await().untilAsserted(() -> { - var r = operator.get(GracefulStopTestCustomResource.class, resourceName); - assertThat(r.getStatus()).isNotNull(); - assertThat(r.getStatus().getObservedGeneration()).isEqualTo(expectedFinalGeneration); - }); - } - - public GracefulStopTestCustomResource testResource(String name) { - GracefulStopTestCustomResource resource = - new GracefulStopTestCustomResource(); - resource.setMetadata( - new ObjectMetaBuilder() - .withName(name) - .withNamespace(operator.getNamespace()) - .build()); - resource.setSpec(new GracefulStopTestCustomResourceSpec()); - resource.getSpec().setValue(1); - return resource; - } - -} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/InformerRelatedBehaviorITS.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/InformerRelatedBehaviorITS.java index 3a6f4d05e9..a8288db7af 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/InformerRelatedBehaviorITS.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/InformerRelatedBehaviorITS.java @@ -76,7 +76,7 @@ void beforeEach(TestInfo testInfo) { @AfterEach void cleanup() { if (operator != null) { - operator.stop(Duration.ofSeconds(1)); + operator.stop(); } adminClient.resource(dependentConfigMap()).delete(); adminClient.resource(testCustomResource()).delete(); @@ -321,6 +321,7 @@ Operator startOperator(boolean stopOnInformerErrorDuringStartup, boolean addStop co.withKubernetesClient(clientUsingServiceAccount()); co.withStopOnInformerErrorDuringStartup(stopOnInformerErrorDuringStartup); co.withCacheSyncTimeout(Duration.ofMillis(3000)); + co.withReconciliationTerminationTimeout(Duration.ofSeconds(1)); if (addStopHandler) { co.withInformerStoppedHandler((informer, ex) -> replacementStopHandlerCalled = true); } From 200ab7330afe8493f1d608c13c596e7cd92125a9 Mon Sep 17 00:00:00 2001 From: 10000-ki <10000ki6472@gmail.com> Date: Tue, 6 Aug 2024 23:00:54 +0900 Subject: [PATCH 3/4] feat: do not use static import Signed-off-by: 10000-ki <10000ki6472@gmail.com> --- .../java/io/javaoperatorsdk/operator/GracefulStopIT.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/GracefulStopIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/GracefulStopIT.java index 3df200df29..e6a38326d5 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/GracefulStopIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/GracefulStopIT.java @@ -11,9 +11,9 @@ import io.javaoperatorsdk.operator.sample.gracefulstop.GracefulStopTestCustomResourceSpec; import io.javaoperatorsdk.operator.sample.gracefulstop.GracefulStopTestReconciler; -import static io.javaoperatorsdk.operator.sample.gracefulstop.GracefulStopTestReconciler.*; -import static org.assertj.core.api.Assertions.*; -import static org.awaitility.Awaitility.*; +import static io.javaoperatorsdk.operator.sample.gracefulstop.GracefulStopTestReconciler.RECONCILER_SLEEP; +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; public class GracefulStopIT { From 1c7ddfb555fb11af12a48a76d7b68593a4443fc0 Mon Sep 17 00:00:00 2001 From: 10000-ki <10000ki6472@gmail.com> Date: Tue, 6 Aug 2024 23:27:21 +0900 Subject: [PATCH 4/4] feat: edit guide docs Signed-off-by: 10000-ki <10000ki6472@gmail.com> --- .../en/docs/patterns-and-best-practices/_index.md | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/docs/content/en/docs/patterns-and-best-practices/_index.md b/docs/content/en/docs/patterns-and-best-practices/_index.md index f4156951ec..a2b3b716b6 100644 --- a/docs/content/en/docs/patterns-and-best-practices/_index.md +++ b/docs/content/en/docs/patterns-and-best-practices/_index.md @@ -123,18 +123,8 @@ reconcile resources even if one or more event source caches did not sync yet. ## Graceful Shutdown -We can provide sufficient time for the reconciler to process and complete the currently ongoing events before shutting down. -There are two ways to support the graceful shutdown feature. - -The first method is to directly provide a timeout value to the `stop` method. - -```java -final var operator = new Operator(); - -operator.stop(Duration.ofSeconds(5)); -``` - -The second method is to specify the timeout value by overriding the `ConfigurationService`. +You can provide sufficient time for the reconciler to process and complete the currently ongoing events before shutting down. +The configuration is simple. You just need to set an appropriate duration value for `reconciliationTerminationTimeout` using `ConfigurationServiceOverrider`. ```java final var overridden = new ConfigurationServiceOverrider(config)