Skip to content

Commit 4ff3b48

Browse files
committed
Implement app restart on dynamic config change. Resolves #616
1 parent 9c6b622 commit 4ff3b48

File tree

2 files changed

+115
-3
lines changed

2 files changed

+115
-3
lines changed
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
package io.kafbat.ui.service.app;
2+
3+
import io.kafbat.ui.util.ApplicationRestarter;
4+
import io.kafbat.ui.util.DynamicConfigOperations;
5+
import jakarta.annotation.PostConstruct;
6+
import jakarta.annotation.PreDestroy;
7+
import java.io.IOException;
8+
import java.nio.file.ClosedWatchServiceException;
9+
import java.nio.file.FileSystems;
10+
import java.nio.file.Path;
11+
import java.nio.file.StandardWatchEventKinds;
12+
import java.nio.file.WatchEvent;
13+
import java.nio.file.WatchKey;
14+
import java.nio.file.WatchService;
15+
import lombok.RequiredArgsConstructor;
16+
import lombok.extern.slf4j.Slf4j;
17+
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
18+
import org.springframework.stereotype.Service;
19+
20+
@Service
21+
@ConditionalOnProperty(value = {"dynamic.config.enabled", "dynamic.config.autoreload"}, havingValue = "true")
22+
@RequiredArgsConstructor
23+
@Slf4j
24+
public class ConfigReloadService {
25+
26+
private static final String THREAD_NAME = "config-watcher-thread";
27+
private static final long STARTUP_SUPPRESSION_MS = 1000;
28+
private final long appStartedAt = System.currentTimeMillis();
29+
30+
private final DynamicConfigOperations dynamicConfigOperations;
31+
private final ApplicationRestarter restarter;
32+
33+
private WatchService watchService;
34+
private Thread watcherThread;
35+
36+
@PostConstruct
37+
public void init() {
38+
log.debug("Auto reload is enabled, will watch for config changes");
39+
40+
try {
41+
registerWatchService();
42+
startWatching();
43+
} catch (IOException e) {
44+
log.error("Error while registering watch service", e);
45+
}
46+
}
47+
48+
@PreDestroy
49+
public void shutdown() {
50+
try {
51+
if (watchService != null) {
52+
watchService.close();
53+
}
54+
} catch (IOException ignored) {
55+
}
56+
if (watcherThread != null) {
57+
this.watcherThread.interrupt();
58+
}
59+
}
60+
61+
private void registerWatchService() throws IOException {
62+
this.watchService = FileSystems.getDefault().newWatchService();
63+
dynamicConfigOperations.dynamicConfigFilePath()
64+
.getParent()
65+
.register(watchService, StandardWatchEventKinds.ENTRY_MODIFY);
66+
}
67+
68+
private void startWatching() {
69+
watcherThread = new Thread(this::watchLoop, THREAD_NAME);
70+
watcherThread.start();
71+
}
72+
73+
private void watchLoop() {
74+
final var watchedDir = dynamicConfigOperations.dynamicConfigFilePath().getParent();
75+
76+
while (true) {
77+
try {
78+
WatchKey key = watchService.take();
79+
for (WatchEvent<?> event : key.pollEvents()) {
80+
WatchEvent.Kind<?> kind = event.kind();
81+
Path changed = watchedDir.resolve((Path) event.context());
82+
83+
if (kind != StandardWatchEventKinds.ENTRY_MODIFY) {
84+
continue;
85+
}
86+
if (!changed.equals(dynamicConfigOperations.dynamicConfigFilePath())) {
87+
continue;
88+
}
89+
90+
var now = System.currentTimeMillis();
91+
if (now - appStartedAt < STARTUP_SUPPRESSION_MS) {
92+
continue;
93+
}
94+
95+
restart();
96+
}
97+
key.reset();
98+
} catch (ClosedWatchServiceException e) {
99+
log.trace("Watch service closed, exiting watcher thread");
100+
break;
101+
} catch (InterruptedException e) {
102+
Thread.currentThread().interrupt();
103+
break;
104+
}
105+
}
106+
}
107+
108+
private void restart() {
109+
log.info("Application config change detected, restarting");
110+
restarter.requestRestart();
111+
}
112+
113+
114+
}

api/src/main/java/io/kafbat/ui/util/DynamicConfigOperations.java

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,7 @@
3030
import org.springframework.http.codec.multipart.FilePart;
3131
import org.springframework.stereotype.Component;
3232
import org.yaml.snakeyaml.DumperOptions;
33-
import org.yaml.snakeyaml.LoaderOptions;
3433
import org.yaml.snakeyaml.Yaml;
35-
import org.yaml.snakeyaml.constructor.Constructor;
3634
import org.yaml.snakeyaml.introspector.BeanAccess;
3735
import org.yaml.snakeyaml.introspector.Property;
3836
import org.yaml.snakeyaml.introspector.PropertyUtils;
@@ -67,7 +65,7 @@ public boolean dynamicConfigEnabled() {
6765
return "true".equalsIgnoreCase(ctx.getEnvironment().getProperty(DYNAMIC_CONFIG_ENABLED_ENV_PROPERTY));
6866
}
6967

70-
private Path dynamicConfigFilePath() {
68+
public Path dynamicConfigFilePath() {
7169
return Paths.get(
7270
Optional.ofNullable(ctx.getEnvironment().getProperty(DYNAMIC_CONFIG_PATH_ENV_PROPERTY))
7371
.orElse(DYNAMIC_CONFIG_PATH_ENV_PROPERTY_DEFAULT)

0 commit comments

Comments
 (0)