Skip to content

Commit 08e5fc7

Browse files
committed
fix: allow for providers to safely shutdown
Signed-off-by: Nicklas Lundin <nicklasl@spotify.com>
1 parent 169415e commit 08e5fc7

File tree

2 files changed

+83
-0
lines changed

2 files changed

+83
-0
lines changed

src/main/java/dev/openfeature/sdk/ProviderRepository.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import java.util.concurrent.ConcurrentHashMap;
1111
import java.util.concurrent.ExecutorService;
1212
import java.util.concurrent.Executors;
13+
import java.util.concurrent.TimeUnit;
1314
import java.util.concurrent.atomic.AtomicReference;
1415
import java.util.function.BiConsumer;
1516
import java.util.function.Consumer;
@@ -277,5 +278,14 @@ public void shutdown() {
277278
.forEach(this::shutdownProvider);
278279
this.stateManagers.clear();
279280
taskExecutor.shutdown();
281+
try {
282+
if (!taskExecutor.awaitTermination(3, TimeUnit.SECONDS)) {
283+
log.warn("Task executor did not terminate before the timeout period had elapsed");
284+
taskExecutor.shutdownNow();
285+
}
286+
} catch (InterruptedException e) {
287+
taskExecutor.shutdownNow();
288+
Thread.currentThread().interrupt();
289+
}
280290
}
281291
}

src/test/java/dev/openfeature/sdk/ProviderRepositoryTest.java

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import java.util.concurrent.ExecutorService;
1616
import java.util.concurrent.Executors;
1717
import java.util.concurrent.Future;
18+
import java.util.concurrent.atomic.AtomicBoolean;
1819
import java.util.function.BiConsumer;
1920
import java.util.function.Consumer;
2021
import java.util.function.Function;
@@ -289,6 +290,78 @@ void shouldRunLambdasOnError() throws Exception {
289290
verify(afterError, timeout(TIMEOUT)).accept(eq(errorFeatureProvider), any());
290291
}
291292
}
293+
294+
@Nested
295+
class GracefulShutdownBehavior {
296+
297+
@Test
298+
@DisplayName("should complete shutdown successfully when executor terminates within timeout")
299+
void shouldCompleteShutdownSuccessfullyWhenExecutorTerminatesWithinTimeout() {
300+
FeatureProvider provider = createMockedProvider();
301+
setFeatureProvider(provider);
302+
303+
assertThatCode(() -> providerRepository.shutdown()).doesNotThrowAnyException();
304+
305+
verify(provider, timeout(TIMEOUT)).shutdown();
306+
}
307+
308+
@Test
309+
@DisplayName("should force shutdown when executor does not terminate within timeout")
310+
void shouldForceShutdownWhenExecutorDoesNotTerminateWithinTimeout() throws Exception {
311+
FeatureProvider provider = createMockedProvider();
312+
AtomicBoolean wasInterrupted = new AtomicBoolean(false);
313+
doAnswer(invocation -> {
314+
try {
315+
Thread.sleep(TIMEOUT);
316+
} catch (InterruptedException e) {
317+
wasInterrupted.set(true);
318+
throw e;
319+
}
320+
return null;
321+
}).when(provider).shutdown();
322+
323+
setFeatureProvider(provider);
324+
325+
assertThatCode(() -> providerRepository.shutdown()).doesNotThrowAnyException();
326+
327+
verify(provider, timeout(TIMEOUT)).shutdown();
328+
// Verify that shutdownNow() interrupted the running shutdown task
329+
await().atMost(Duration.ofSeconds(1))
330+
.untilAsserted(() -> assertThat(wasInterrupted.get()).isTrue());
331+
}
332+
333+
@Test
334+
@DisplayName("should handle interruption during shutdown gracefully")
335+
void shouldHandleInterruptionDuringShutdownGracefully() throws Exception {
336+
FeatureProvider provider = createMockedProvider();
337+
setFeatureProvider(provider);
338+
339+
Thread shutdownThread = new Thread(() -> {
340+
providerRepository.shutdown();
341+
});
342+
343+
shutdownThread.start();
344+
shutdownThread.interrupt();
345+
shutdownThread.join(TIMEOUT);
346+
347+
assertThat(shutdownThread.isAlive()).isFalse();
348+
verify(provider, timeout(TIMEOUT)).shutdown();
349+
}
350+
351+
@Test
352+
@DisplayName("should not hang indefinitely on shutdown")
353+
void shouldNotHangIndefinitelyOnShutdown() {
354+
FeatureProvider provider = createMockedProvider();
355+
setFeatureProvider(provider);
356+
357+
await().alias("shutdown should complete within reasonable time")
358+
.atMost(Duration.ofSeconds(5))
359+
.until(() -> {
360+
providerRepository.shutdown();
361+
return true;
362+
});
363+
}
364+
}
292365
}
293366

294367
@Test

0 commit comments

Comments
 (0)