Skip to content

Commit 624ee9f

Browse files
chore: suggestions to the fix (#325)
1 parent ae955be commit 624ee9f

2 files changed

Lines changed: 76 additions & 2 deletions

File tree

src/main/java/io/getunleash/util/UnleashScheduledExecutorImpl.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,15 +54,17 @@ public Future<Void> scheduleOnce(Runnable runnable) {
5454
}
5555

5656
@Override
57-
public void shutdown() {
57+
public synchronized void shutdown() {
5858
this.scheduledThreadPoolExecutor.shutdown();
59+
this.executorService.shutdown();
5960
INSTANCE = null;
6061
LOG.info("Shutdown - Scheduled Executor");
6162
}
6263

6364
@Override
64-
public void shutdownNow() {
65+
public synchronized void shutdownNow() {
6566
this.scheduledThreadPoolExecutor.shutdownNow();
67+
this.executorService.shutdownNow();
6668
INSTANCE = null;
6769
LOG.info("Shutdown Now - Scheduled Executor");
6870
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package io.getunleash;
2+
3+
import io.getunleash.util.UnleashConfig;
4+
import org.junit.jupiter.api.*;
5+
6+
import java.lang.reflect.Field;
7+
import java.util.concurrent.ScheduledThreadPoolExecutor;
8+
9+
import static org.assertj.core.api.Assertions.assertThat;
10+
11+
class HotReloadSchedulerReuseTest {
12+
13+
14+
private UnleashConfig baseConfig() {
15+
return UnleashConfig.builder()
16+
.appName("hot-reload-test-app")
17+
.instanceId("A")
18+
.unleashAPI("http://localhost") // never hit
19+
.synchronousFetchOnInitialisation(false)
20+
.fetchTogglesInterval(100)
21+
.sendMetricsInterval(100)
22+
.build();
23+
}
24+
25+
private ScheduledThreadPoolExecutor currentGlobalExecutor() throws Exception {
26+
Class<?> execClazz = Class.forName("io.getunleash.util.UnleashScheduledExecutorImpl");
27+
Field instance = execClazz.getDeclaredField("INSTANCE");
28+
instance.setAccessible(true);
29+
Object inst = instance.get(null);
30+
if (inst == null) return null;
31+
32+
Field pool = execClazz.getDeclaredField("scheduledThreadPoolExecutor");
33+
pool.setAccessible(true);
34+
return (ScheduledThreadPoolExecutor) pool.get(inst);
35+
}
36+
37+
@Test
38+
void secondClientDoesNotReuseSchedulerExecutor() throws Exception {
39+
// 1) Create first client; let it schedule background tasks
40+
DefaultUnleash first = new DefaultUnleash(
41+
baseConfig()
42+
);
43+
44+
// Let it initialize/schedule
45+
Thread.sleep(150);
46+
47+
// Snapshot the global executor
48+
ScheduledThreadPoolExecutor execBeforeShutdown = currentGlobalExecutor();
49+
assertThat(execBeforeShutdown).isNotNull();
50+
assertThat(execBeforeShutdown.isShutdown()).isFalse();
51+
52+
// 2) Simulate app stop (DevTools restart) by shutting the client
53+
first.shutdown();
54+
55+
// After shutdown, the global executor instance is properly reset to null
56+
ScheduledThreadPoolExecutor execAfterShutdown = currentGlobalExecutor();
57+
assertThat(execAfterShutdown).isNull();
58+
59+
// 3) "Reloaded app": create a second client in the same JVM (statics still around)
60+
DefaultUnleash second = new DefaultUnleash(
61+
baseConfig()
62+
);
63+
64+
// 4) Assert that the second client creates a fresh executor (not reusing the terminated one)
65+
ScheduledThreadPoolExecutor execUsedBySecond = currentGlobalExecutor();
66+
assertThat(execUsedBySecond).isNotNull();
67+
assertThat(execUsedBySecond.isShutdown()).isFalse(); // it's a new one
68+
69+
// Clean up
70+
second.shutdown();
71+
}
72+
}

0 commit comments

Comments
 (0)