Skip to content

Commit 5f22e9a

Browse files
author
Joey Yang
committed
fix configMap ..data directory watch bug, and make up mechanism
1 parent 9ca5156 commit 5f22e9a

File tree

6 files changed

+96
-15
lines changed

6 files changed

+96
-15
lines changed

README-zh.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,14 +42,14 @@ Maven
4242
<dependency>
4343
<groupId>top.code2life</groupId>
4444
<artifactId>spring-boot-dynamic-config</artifactId>
45-
<version>1.0.4</version>
45+
<version>1.0.5</version>
4646
</dependency>
4747
```
4848

4949
Gradle
5050

5151
```groovy
52-
implementation 'top.code2life:spring-boot-dynamic-config:1.0.4'
52+
implementation 'top.code2life:spring-boot-dynamic-config:1.0.5'
5353
```
5454

5555
### 步骤二:在代码中添加 @DynamicConfig 注解

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,15 +41,15 @@ Maven
4141

4242
<dependency>
4343
<groupId>top.code2life</groupId>
44-
<artifactId>spring-boot-dynamic-config</artifactId>
45-
<version>1.0.4</version>
44+
<artifactId>spring-boot-dynamic-config</artifactId>
45+
<version>1.0.5</version>
4646
</dependency>
4747
```
4848

4949
Gradle
5050

5151
```groovy
52-
implementation 'top.code2life:spring-boot-dynamic-config:1.0.4'
52+
implementation 'top.code2life:spring-boot-dynamic-config:1.0.5'
5353
```
5454

5555
### Step2. Add @DynamicConfig Annotation

build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ java {
1616
}
1717

1818
group = 'top.code2life'
19-
version = '1.0.4'
19+
version = '1.0.5'
2020
sourceCompatibility = JavaVersion.VERSION_1_8
2121

2222
repositories {

example/build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,5 +27,5 @@ dependencies {
2727
annotationProcessor 'org.projectlombok:lombok:1.18.20'
2828
implementation 'org.springframework.boot:spring-boot-starter-web'
2929

30-
implementation 'top.code2life:spring-boot-dynamic-config:1.0.4'
30+
implementation 'top.code2life:spring-boot-dynamic-config:1.0.5'
3131
}

src/main/java/top/code2life/config/DynamicConfigPropertiesWatcher.java

Lines changed: 60 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,16 @@
1414
import org.springframework.util.StringUtils;
1515

1616
import javax.annotation.PostConstruct;
17+
import java.io.File;
1718
import java.io.IOException;
1819
import java.nio.file.*;
1920
import java.util.Arrays;
2021
import java.util.HashMap;
2122
import java.util.List;
2223
import java.util.Map;
2324
import java.util.concurrent.Executors;
25+
import java.util.concurrent.TimeUnit;
26+
import java.util.stream.Stream;
2427

2528
/**
2629
* Enhance PropertySource when spring.config.location is specified, it will start directory-watch,
@@ -33,7 +36,10 @@
3336
@ConditionalOnProperty("spring.config.location")
3437
public class DynamicConfigPropertiesWatcher implements DisposableBean {
3538

39+
private static final long SYMBOL_LINK_POLLING_INTERVAL = 5000;
40+
private static final long NORMAL_FILE_POLLING_INTERVAL = 90000;
3641
private static final String FILE_COLON_SYMBOL = "file:";
42+
private static final String SYMBOL_LINK_DIR = "..data";
3743
private static final String FILE_SOURCE_CONFIGURATION_PATTERN = "^.*Config\\sresource.*file.*$";
3844
private static final String FILE_SOURCE_CONFIGURATION_PATTERN_LEGACY = "^.+Config:\\s\\[file:.*$";
3945
private static final Map<String, PropertySourceMeta> PROPERTY_SOURCE_META_MAP = new HashMap<>(8);
@@ -43,6 +49,7 @@ public class DynamicConfigPropertiesWatcher implements DisposableBean {
4349
private final List<PropertySourceLoader> propertyLoaders;
4450

4551
private WatchService watchService;
52+
private long symbolicLinkModifiedTime = 0;
4653

4754
@Value("${spring.config.location:}")
4855
private String configLocation;
@@ -108,13 +115,15 @@ private void startWatchDir() {
108115
log.info("start watching configuration directory: {}", configLocation);
109116
watchService = FileSystems.getDefault().newWatchService();
110117
Paths.get(configLocation).register(watchService, StandardWatchEventKinds.ENTRY_MODIFY, StandardWatchEventKinds.ENTRY_CREATE);
118+
checkChangesWithPeriod();
111119
WatchKey key;
112120
while ((key = watchService.take()) != null) {
113121
// avoid receiving two ENTRY_MODIFY events: file modified and timestamp updated
114122
Thread.sleep(50);
115123
for (WatchEvent<?> event : key.pollEvents()) {
116124
Path path = (Path) event.context();
117-
reloadChangedFile(path);
125+
reloadChangedFile(path, false);
126+
118127
}
119128
key.reset();
120129
}
@@ -126,7 +135,55 @@ private void startWatchDir() {
126135
}
127136
}
128137

129-
private void reloadChangedFile(Path path) {
138+
@SuppressWarnings("AlibabaThreadPoolCreation")
139+
private void checkChangesWithPeriod() throws IOException {
140+
Path symLinkPath = Paths.get(configLocation, SYMBOL_LINK_DIR);
141+
boolean hasDotDataLinkFile = new File(configLocation, SYMBOL_LINK_DIR).exists();
142+
if (hasDotDataLinkFile) {
143+
log.info("ConfigMap/Secret mode detected, will polling symbolic link instead.");
144+
symbolicLinkModifiedTime = Files.getLastModifiedTime(symLinkPath, LinkOption.NOFOLLOW_LINKS).toMillis();
145+
Executors.newSingleThreadScheduledExecutor(r -> new Thread(r, "config-watcher-polling")).scheduleWithFixedDelay(
146+
this::checkSymbolicLink, SYMBOL_LINK_POLLING_INTERVAL, SYMBOL_LINK_POLLING_INTERVAL, TimeUnit.MILLISECONDS);
147+
} else {
148+
// longer check for all config files, make up mechanism if WatchService doesn't work
149+
Executors.newSingleThreadScheduledExecutor(r -> new Thread(r, "config-watcher-polling")).scheduleWithFixedDelay(
150+
this::reloadAllConfigFiles, NORMAL_FILE_POLLING_INTERVAL, NORMAL_FILE_POLLING_INTERVAL, TimeUnit.MILLISECONDS);
151+
}
152+
}
153+
154+
private void checkSymbolicLink() {
155+
try {
156+
Path symLinkPath = Paths.get(configLocation, SYMBOL_LINK_DIR);
157+
long tmp = Files.getLastModifiedTime(symLinkPath, LinkOption.NOFOLLOW_LINKS).toMillis();
158+
if (tmp != symbolicLinkModifiedTime) {
159+
reloadAllConfigFiles(true);
160+
symbolicLinkModifiedTime = tmp;
161+
}
162+
} catch (IOException ex) {
163+
log.warn("could not check symbolic link of config dir: {}", ex.getMessage());
164+
}
165+
}
166+
167+
private void reloadAllConfigFiles() {
168+
reloadAllConfigFiles(false);
169+
}
170+
171+
private void reloadAllConfigFiles(boolean forceReload) {
172+
try (Stream<Path> paths = Files.walk(Paths.get(configLocation))) {
173+
paths.forEach((path) -> {
174+
if (!Files.isDirectory(path)) {
175+
reloadChangedFile(path, forceReload);
176+
}
177+
});
178+
} catch (IOException e) {
179+
log.warn("can not walk through config directory: {}", e.getMessage());
180+
}
181+
}
182+
183+
private void reloadChangedFile(Path path, boolean forceReload) {
184+
if (SYMBOL_LINK_DIR.equals(path.toString())) {
185+
return;
186+
}
130187
Path absPath = null;
131188
try {
132189
absPath = Paths.get(configLocation, path.toString());
@@ -143,7 +200,7 @@ private void reloadChangedFile(Path path) {
143200
}
144201
long currentModTs = Files.getLastModifiedTime(absPath).toMillis();
145202
long mdt = propertySourceMeta.getLastModifyTime();
146-
if (mdt != currentModTs) {
203+
if (forceReload || mdt != currentModTs) {
147204
doReloadConfigFile(propertySourceMeta, absPathStr, currentModTs);
148205
}
149206
} catch (Exception ex) {

src/test/java/top/code2life/config/DynamicConfigPropertiesWatcherTest.java

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package top.code2life.config;
22

3+
import org.junit.jupiter.api.Assertions;
34
import org.junit.jupiter.api.Test;
45
import org.springframework.beans.BeansException;
56
import org.springframework.beans.factory.annotation.Autowired;
@@ -10,6 +11,10 @@
1011

1112
import java.io.File;
1213
import java.io.PrintWriter;
14+
import java.nio.file.Files;
15+
import java.nio.file.LinkOption;
16+
17+
import static top.code2life.config.DynamicConfigTests.CONFIG_LOCATION;
1318

1419
/**
1520
* @author Code2Life
@@ -37,24 +42,43 @@ public void testBeanLoaded() throws Exception {
3742
}
3843

3944
@Test
40-
public void testFileWatch() {
45+
public void testSymbolLinkWatch() throws Exception {
4146
DynamicConfigPropertiesWatcher watcher = new DynamicConfigPropertiesWatcher(environment, eventPublisher);
42-
watcher.destroy();
43-
// should not throw any exception
47+
File file = new File(CONFIG_LOCATION, "..data");
48+
try (PrintWriter writer = new PrintWriter(file)) {
49+
writer.write("test-symbolic-link");
50+
watcher.setConfigLocation(CONFIG_LOCATION);
51+
watcher.watchConfigDirectory();
52+
}
53+
Thread.sleep(1000);
54+
long mdt1 = Files.getLastModifiedTime(file.toPath(), LinkOption.NOFOLLOW_LINKS).toMillis();
55+
try (PrintWriter writer = new PrintWriter(file)) {
56+
writer.write("test-symbolic-link-2");
57+
}
58+
long mdt2 = Files.getLastModifiedTime(file.toPath(), LinkOption.NOFOLLOW_LINKS).toMillis();
59+
Assertions.assertTrue(mdt1 != mdt2);
60+
Thread.sleep(7000);
4461
}
4562

4663
@Test
4764
public void testWatchUnRecognizedFile() throws Exception {
4865
DynamicConfigPropertiesWatcher watcher = new DynamicConfigPropertiesWatcher(environment, eventPublisher);
4966
// watch nothing since config.location not set
5067
watcher.watchConfigDirectory();
51-
watcher.setConfigLocation(DynamicConfigTests.CONFIG_LOCATION);
68+
watcher.setConfigLocation(CONFIG_LOCATION);
5269
watcher.watchConfigDirectory();
53-
File file = new File(DynamicConfigTests.CONFIG_LOCATION, "unknown.txt");
70+
File file = new File(CONFIG_LOCATION, "unknown.txt");
5471
try (PrintWriter writer = new PrintWriter(file)) {
5572
writer.write("test-data");
5673
}
5774
Thread.sleep(500);
5875
// should not throw any exception
5976
}
77+
78+
@Test
79+
public void testFileWatch() {
80+
DynamicConfigPropertiesWatcher watcher = new DynamicConfigPropertiesWatcher(environment, eventPublisher);
81+
watcher.destroy();
82+
// should not throw any exception
83+
}
6084
}

0 commit comments

Comments
 (0)