Skip to content

Commit 9efbc78

Browse files
committed
fix: allow for providers to safely shutdown
Signed-off-by: Nicklas Lundin <nicklasl@spotify.com>
1 parent bf86db5 commit 9efbc78

File tree

2 files changed

+85
-0
lines changed

2 files changed

+85
-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: 75 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,80 @@ 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+
})
322+
.when(provider)
323+
.shutdown();
324+
325+
setFeatureProvider(provider);
326+
327+
assertThatCode(() -> providerRepository.shutdown()).doesNotThrowAnyException();
328+
329+
verify(provider, timeout(TIMEOUT)).shutdown();
330+
// Verify that shutdownNow() interrupted the running shutdown task
331+
await().atMost(Duration.ofSeconds(1))
332+
.untilAsserted(() -> assertThat(wasInterrupted.get()).isTrue());
333+
}
334+
335+
@Test
336+
@DisplayName("should handle interruption during shutdown gracefully")
337+
void shouldHandleInterruptionDuringShutdownGracefully() throws Exception {
338+
FeatureProvider provider = createMockedProvider();
339+
setFeatureProvider(provider);
340+
341+
Thread shutdownThread = new Thread(() -> {
342+
providerRepository.shutdown();
343+
});
344+
345+
shutdownThread.start();
346+
shutdownThread.interrupt();
347+
shutdownThread.join(TIMEOUT);
348+
349+
assertThat(shutdownThread.isAlive()).isFalse();
350+
verify(provider, timeout(TIMEOUT)).shutdown();
351+
}
352+
353+
@Test
354+
@DisplayName("should not hang indefinitely on shutdown")
355+
void shouldNotHangIndefinitelyOnShutdown() {
356+
FeatureProvider provider = createMockedProvider();
357+
setFeatureProvider(provider);
358+
359+
await().alias("shutdown should complete within reasonable time")
360+
.atMost(Duration.ofSeconds(5))
361+
.until(() -> {
362+
providerRepository.shutdown();
363+
return true;
364+
});
365+
}
366+
}
292367
}
293368

294369
@Test

0 commit comments

Comments
 (0)