Skip to content

Commit 546d724

Browse files
author
mtlyzhang
committed
[ISSUE #9328] Fix ConfigManager.persist() not truncating stale data after write
When ConfigManager.persist() writes config data using RandomAccessFile, it does not truncate the file after writing. If the new content is shorter than the previous content, stale trailing bytes remain in the file. After an unexpected power cut during a write, the file may contain a mix of partial new data and old trailing data, producing invalid JSON that causes decode() to fail on restart and leads to NullPointerException. Fix: call randomAccessFile.setLength(data.length) after writing to ensure the file is truncated to the exact size of the new content. Made-with: Cursor
1 parent 9879968 commit 546d724

2 files changed

Lines changed: 63 additions & 4 deletions

File tree

common/src/main/java/org/apache/rocketmq/common/ConfigManager.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,9 @@ public synchronized void persist() {
111111
}
112112

113113
try (RandomAccessFile randomAccessFile = new RandomAccessFile(config, "rw")) {
114-
randomAccessFile.write(jsonString.getBytes(StandardCharsets.UTF_8));
114+
byte[] data = jsonString.getBytes(StandardCharsets.UTF_8);
115+
randomAccessFile.write(data);
116+
randomAccessFile.setLength(data.length);
115117
randomAccessFile.getChannel().force(true);
116118
// sync the directory, ensure that the config file is visible
117119
MixAll.fsyncDirectory(Paths.get(configFile.getParent()));

common/src/test/java/org/apache/rocketmq/common/ConfigManagerTest.java

Lines changed: 60 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@
1717

1818
import java.io.File;
1919
import java.io.PrintWriter;
20+
import java.io.RandomAccessFile;
2021
import java.lang.reflect.Method;
22+
import java.nio.charset.StandardCharsets;
23+
import org.junit.After;
2124
import org.junit.Test;
2225

2326
import static org.junit.Assert.assertEquals;
@@ -27,6 +30,12 @@ public class ConfigManagerTest {
2730
private static final String PATH_FILE = System.getProperty("java.io.tmpdir") + File.separator + "org.apache.rocketmq.common.ConfigManagerTest";
2831
private static final String CONTENT_ENCODE = "Encode content for ConfigManager";
2932

33+
@After
34+
public void cleanup() {
35+
new File(PATH_FILE).delete();
36+
new File(PATH_FILE + ".bak").delete();
37+
}
38+
3039
@Test
3140
public void testLoad() throws Exception {
3241
ConfigManager testConfigManager = buildTestConfigManager();
@@ -42,7 +51,6 @@ public void testLoad() throws Exception {
4251
public void testLoadBak() throws Exception {
4352
ConfigManager testConfigManager = buildTestConfigManager();
4453
File file = createAndWriteFile(testConfigManager.configFilePath() + ".bak");
45-
// invoke private method "loadBak()"
4654
Method declaredMethod = ConfigManager.class.getDeclaredMethod("loadBak");
4755
declaredMethod.setAccessible(true);
4856
Boolean loadBakResult = (Boolean) declaredMethod.invoke(testConfigManager);
@@ -62,6 +70,50 @@ public void testPersist() throws Exception {
6270
assertEquals(CONTENT_ENCODE, MixAll.file2String(file));
6371
}
6472

73+
@Test
74+
public void testPersistTruncatesStaleData() throws Exception {
75+
String configPath = PATH_FILE;
76+
File dir = new File(new File(configPath).getParent());
77+
if (!dir.exists()) {
78+
dir.mkdirs();
79+
}
80+
81+
String longContent = "THIS_IS_A_VERY_LONG_OLD_CONTENT_THAT_SHOULD_BE_TRUNCATED_AFTER_PERSIST";
82+
try (RandomAccessFile raf = new RandomAccessFile(configPath, "rw")) {
83+
raf.write(longContent.getBytes(StandardCharsets.UTF_8));
84+
raf.getChannel().force(true);
85+
}
86+
assertEquals(longContent, MixAll.file2String(new File(configPath)));
87+
88+
ConfigManager testConfigManager = buildTestConfigManager();
89+
testConfigManager.persist();
90+
91+
String afterPersist = MixAll.file2String(new File(configPath));
92+
assertEquals(
93+
"persist() should truncate stale trailing data from previous writes",
94+
CONTENT_ENCODE, afterPersist);
95+
}
96+
97+
@Test
98+
public void testLoadCorruptedFileWithValidBackup() throws Exception {
99+
ConfigManager testConfigManager = buildTestConfigManager();
100+
createAndWriteFile(testConfigManager.configFilePath(), "{corrupted!!!");
101+
createAndWriteFile(testConfigManager.configFilePath() + ".bak", CONTENT_ENCODE);
102+
103+
assertTrue("Should succeed by falling back to valid .bak file",
104+
testConfigManager.load());
105+
}
106+
107+
@Test
108+
public void testLoadFirstTimeNoFilesExist() throws Exception {
109+
ConfigManager testConfigManager = buildTestConfigManager();
110+
new File(testConfigManager.configFilePath()).delete();
111+
new File(testConfigManager.configFilePath() + ".bak").delete();
112+
113+
assertTrue("First-time startup with no files should succeed",
114+
testConfigManager.load());
115+
}
116+
65117
private ConfigManager buildTestConfigManager() {
66118
return new ConfigManager() {
67119
@Override
@@ -87,14 +139,19 @@ public String encode(boolean prettyFormat) {
87139
}
88140

89141
private File createAndWriteFile(String fileName) throws Exception {
142+
return createAndWriteFile(fileName, "TestForConfigManager");
143+
}
144+
145+
private File createAndWriteFile(String fileName, String content) throws Exception {
90146
File file = new File(fileName);
91147
if (file.exists()) {
92148
file.delete();
93149
}
150+
file.getParentFile().mkdirs();
94151
file.createNewFile();
95152
PrintWriter out = new PrintWriter(fileName);
96-
out.write("TestForConfigManager");
153+
out.write(content);
97154
out.close();
98155
return file;
99156
}
100-
}
157+
}

0 commit comments

Comments
 (0)