-
Notifications
You must be signed in to change notification settings - Fork 52
fix: allow for providers to safely shutdown #1744
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,6 +4,7 @@ | |
| import static dev.openfeature.sdk.testutils.stubbing.ConditionStubber.doDelayResponse; | ||
| import static org.assertj.core.api.Assertions.assertThat; | ||
| import static org.assertj.core.api.Assertions.assertThatCode; | ||
| import static org.assertj.core.api.Assertions.assertThatThrownBy; | ||
| import static org.awaitility.Awaitility.await; | ||
| import static org.mockito.ArgumentMatchers.any; | ||
| import static org.mockito.ArgumentMatchers.eq; | ||
|
|
@@ -15,6 +16,7 @@ | |
| import java.util.concurrent.ExecutorService; | ||
| import java.util.concurrent.Executors; | ||
| import java.util.concurrent.Future; | ||
| import java.util.concurrent.atomic.AtomicBoolean; | ||
| import java.util.function.BiConsumer; | ||
| import java.util.function.Consumer; | ||
| import java.util.function.Function; | ||
|
|
@@ -289,6 +291,191 @@ | |
| verify(afterError, timeout(TIMEOUT)).accept(eq(errorFeatureProvider), any()); | ||
| } | ||
| } | ||
|
|
||
| @Nested | ||
| class GracefulShutdownBehavior { | ||
|
|
||
| @Test | ||
| @DisplayName("should complete shutdown successfully when executor terminates within timeout") | ||
| void shouldCompleteShutdownSuccessfullyWhenExecutorTerminatesWithinTimeout() { | ||
| FeatureProvider provider = createMockedProvider(); | ||
| setFeatureProvider(provider); | ||
|
|
||
| assertThatCode(() -> providerRepository.shutdown()).doesNotThrowAnyException(); | ||
|
|
||
| verify(provider, timeout(TIMEOUT)).shutdown(); | ||
| } | ||
|
|
||
| @Test | ||
| @DisplayName("should force shutdown when executor does not terminate within timeout") | ||
| void shouldForceShutdownWhenExecutorDoesNotTerminateWithinTimeout() throws Exception { | ||
| FeatureProvider provider = createMockedProvider(); | ||
| AtomicBoolean wasInterrupted = new AtomicBoolean(false); | ||
| doAnswer(invocation -> { | ||
| try { | ||
| Thread.sleep(TIMEOUT); | ||
|
Check warning on line 316 in src/test/java/dev/openfeature/sdk/ProviderRepositoryTest.java
|
||
| } catch (InterruptedException e) { | ||
| wasInterrupted.set(true); | ||
| throw e; | ||
| } | ||
| return null; | ||
| }) | ||
| .when(provider) | ||
| .shutdown(); | ||
|
|
||
| setFeatureProvider(provider); | ||
|
|
||
| assertThatCode(() -> providerRepository.shutdown()).doesNotThrowAnyException(); | ||
|
|
||
| verify(provider, timeout(TIMEOUT)).shutdown(); | ||
| // Verify that shutdownNow() interrupted the running shutdown task | ||
| await().atMost(Duration.ofSeconds(1)) | ||
| .untilAsserted(() -> assertThat(wasInterrupted.get()).isTrue()); | ||
| } | ||
|
|
||
| @Test | ||
| @DisplayName("should handle interruption during shutdown gracefully") | ||
| void shouldHandleInterruptionDuringShutdownGracefully() throws Exception { | ||
| FeatureProvider provider = createMockedProvider(); | ||
| setFeatureProvider(provider); | ||
|
|
||
| Thread shutdownThread = new Thread(() -> { | ||
| providerRepository.shutdown(); | ||
| }); | ||
|
|
||
| shutdownThread.start(); | ||
| shutdownThread.interrupt(); | ||
| shutdownThread.join(TIMEOUT); | ||
|
|
||
| assertThat(shutdownThread.isAlive()).isFalse(); | ||
| verify(provider, timeout(TIMEOUT)).shutdown(); | ||
| } | ||
|
|
||
| @Test | ||
| @DisplayName("should not hang indefinitely on shutdown") | ||
| void shouldNotHangIndefinitelyOnShutdown() { | ||
| FeatureProvider provider = createMockedProvider(); | ||
| setFeatureProvider(provider); | ||
|
|
||
| await().alias("shutdown should complete within reasonable time") | ||
| .atMost(Duration.ofSeconds(5)) | ||
| .until(() -> { | ||
| providerRepository.shutdown(); | ||
| return true; | ||
| }); | ||
| } | ||
|
|
||
| @Test | ||
| @DisplayName("should handle shutdown during provider initialization") | ||
| void shouldHandleShutdownDuringProviderInitialization() throws Exception { | ||
| FeatureProvider slowInitProvider = createMockedProvider(); | ||
| AtomicBoolean initStarted = new AtomicBoolean(false); | ||
| AtomicBoolean shutdownCalled = new AtomicBoolean(false); | ||
|
|
||
| doAnswer(invocation -> { | ||
| initStarted.set(true); | ||
| Thread.sleep(500); | ||
|
Check warning on line 377 in src/test/java/dev/openfeature/sdk/ProviderRepositoryTest.java
|
||
| return null; | ||
| }) | ||
| .when(slowInitProvider) | ||
| .initialize(any()); | ||
|
|
||
| doAnswer(invocation -> { | ||
| shutdownCalled.set(true); | ||
| return null; | ||
| }) | ||
| .when(slowInitProvider) | ||
| .shutdown(); | ||
|
|
||
| providerRepository.setProvider( | ||
| slowInitProvider, | ||
| mockAfterSet(), | ||
| mockAfterInit(), | ||
| mockAfterShutdown(), | ||
| mockAfterError(), | ||
| false); | ||
|
|
||
| await().atMost(Duration.ofSeconds(1)).untilTrue(initStarted); | ||
|
|
||
| // Call shutdown while initialization is in progress | ||
| assertThatCode(() -> providerRepository.shutdown()).doesNotThrowAnyException(); | ||
|
|
||
| await().atMost(Duration.ofSeconds(1)).untilTrue(shutdownCalled); | ||
| verify(slowInitProvider, times(1)).shutdown(); | ||
| } | ||
|
|
||
| @Test | ||
| @DisplayName("should handle provider replacement during shutdown") | ||
| void shouldHandleProviderReplacementDuringShutdown() throws Exception { | ||
| FeatureProvider oldProvider = createMockedProvider(); | ||
| FeatureProvider newProvider = createMockedProvider(); | ||
| AtomicBoolean oldProviderShutdownCalled = new AtomicBoolean(false); | ||
|
|
||
| doAnswer(invocation -> { | ||
| oldProviderShutdownCalled.set(true); | ||
| return null; | ||
| }) | ||
| .when(oldProvider) | ||
| .shutdown(); | ||
|
|
||
| providerRepository.setProvider( | ||
| oldProvider, mockAfterSet(), mockAfterInit(), mockAfterShutdown(), mockAfterError(), true); | ||
|
|
||
| // Replace provider (this will trigger old provider shutdown in background) | ||
| providerRepository.setProvider( | ||
| newProvider, mockAfterSet(), mockAfterInit(), mockAfterShutdown(), mockAfterError(), false); | ||
|
|
||
| assertThatCode(() -> providerRepository.shutdown()).doesNotThrowAnyException(); | ||
|
|
||
| await().atMost(Duration.ofSeconds(1)).untilTrue(oldProviderShutdownCalled); | ||
| verify(oldProvider, times(1)).shutdown(); | ||
| verify(newProvider, times(1)).shutdown(); | ||
| } | ||
|
|
||
| @Test | ||
| @DisplayName("should prevent adding providers after shutdown has started") | ||
| void shouldPreventAddingProvidersAfterShutdownHasStarted() { | ||
| FeatureProvider provider = createMockedProvider(); | ||
| setFeatureProvider(provider); | ||
|
|
||
| providerRepository.shutdown(); | ||
|
|
||
| FeatureProvider newProvider = createMockedProvider(); | ||
| assertThatThrownBy(() -> providerRepository.setProvider( | ||
|
Check warning on line 444 in src/test/java/dev/openfeature/sdk/ProviderRepositoryTest.java
|
||
| newProvider, | ||
| mockAfterSet(), | ||
| mockAfterInit(), | ||
| mockAfterShutdown(), | ||
| mockAfterError(), | ||
| false)) | ||
| .isInstanceOf(IllegalStateException.class) | ||
| .hasMessageContaining("shutting down"); | ||
| } | ||
|
|
||
| @Test | ||
| @DisplayName("should handle concurrent shutdown calls gracefully") | ||
| void shouldHandleConcurrentShutdownCallsGracefully() { | ||
| FeatureProvider provider = createMockedProvider(); | ||
| setFeatureProvider(provider); | ||
|
|
||
| assertThatCode(() -> { | ||
| Thread t1 = new Thread(() -> providerRepository.shutdown()); | ||
| Thread t2 = new Thread(() -> providerRepository.shutdown()); | ||
| Thread t3 = new Thread(() -> providerRepository.shutdown()); | ||
|
Comment on lines
+462
to
+464
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We cannot be sure that any of these threads execute in parallel. This should be a VMLens test
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks, I'll look into making them VMLens tests instead, just need to find some time :) |
||
|
|
||
| t1.start(); | ||
| t2.start(); | ||
| t3.start(); | ||
|
|
||
| t1.join(TIMEOUT); | ||
| t2.join(TIMEOUT); | ||
| t3.join(TIMEOUT); | ||
| }) | ||
| .doesNotThrowAnyException(); | ||
|
|
||
| verify(provider, times(1)).shutdown(); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| @Test | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How long does the Mocked Provider take to shutdown? Are we sure we interrupt during this time?
Maybe this is a case for a VMLens test