Skip to content

Commit ff6ff64

Browse files
committed
Add tests for concurrent cleanup
1 parent 5e0ed99 commit ff6ff64

File tree

2 files changed

+144
-6
lines changed

2 files changed

+144
-6
lines changed

dd-java-agent/agent-profiling/profiling-controller/src/main/java/com/datadog/profiling/controller/TempLocationManager.java

Lines changed: 61 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import java.nio.file.FileVisitResult;
88
import java.nio.file.FileVisitor;
99
import java.nio.file.Files;
10+
import java.nio.file.NoSuchFileException;
1011
import java.nio.file.Path;
1112
import java.nio.file.Paths;
1213
import java.nio.file.attribute.BasicFileAttributes;
@@ -31,6 +32,33 @@ private static final class SingletonHolder {
3132
private static final TempLocationManager INSTANCE = new TempLocationManager();
3233
}
3334

35+
interface CleanupHook extends FileVisitor<Path> {
36+
CleanupHook EMPTY = new CleanupHook() {};
37+
38+
@Override
39+
default FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs)
40+
throws IOException {
41+
return null;
42+
}
43+
44+
@Override
45+
default FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
46+
return null;
47+
}
48+
49+
@Override
50+
default FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException {
51+
return null;
52+
}
53+
54+
@Override
55+
default FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
56+
return null;
57+
}
58+
59+
default void onCleanupStart() {}
60+
}
61+
3462
private class CleanupVisitor implements FileVisitor<Path> {
3563
private boolean shouldClean;
3664

@@ -47,6 +75,8 @@ private class CleanupVisitor implements FileVisitor<Path> {
4775
@Override
4876
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs)
4977
throws IOException {
78+
cleanupTestHook.preVisitDirectory(dir, attrs);
79+
5080
if (dir.equals(baseTempDir)) {
5181
return FileVisitResult.CONTINUE;
5282
}
@@ -63,25 +93,40 @@ public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs)
6393

6494
@Override
6595
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
66-
if (Files.getLastModifiedTime(file).toInstant().isAfter(cutoff)) {
67-
return FileVisitResult.SKIP_SUBTREE;
96+
cleanupTestHook.visitFile(file, attrs);
97+
try {
98+
if (Files.getLastModifiedTime(file).toInstant().isAfter(cutoff)) {
99+
return FileVisitResult.SKIP_SUBTREE;
100+
}
101+
Files.delete(file);
102+
} catch (NoSuchFileException ignored) {
103+
// another process has already cleaned it; ignore
68104
}
69-
Files.delete(file);
70105
return FileVisitResult.CONTINUE;
71106
}
72107

73108
@Override
74109
public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException {
75-
if (log.isDebugEnabled()) {
110+
cleanupTestHook.visitFileFailed(file, exc);
111+
// do not log files/directories removed by another process running concurrently
112+
if (!(exc instanceof NoSuchFileException) && log.isDebugEnabled()) {
76113
log.debug("Failed to delete file {}", file, exc);
77114
}
78115
return FileVisitResult.CONTINUE;
79116
}
80117

81118
@Override
82119
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
120+
cleanupTestHook.postVisitDirectory(dir, exc);
121+
if (exc instanceof NoSuchFileException) {
122+
return FileVisitResult.CONTINUE;
123+
}
83124
if (shouldClean) {
84-
Files.delete(dir);
125+
try {
126+
Files.delete(dir);
127+
} catch (NoSuchFileException ignored) {
128+
// another process has already cleaned it; ignore
129+
}
85130
String fileName = dir.getFileName().toString();
86131
// reset the flag only if we are done cleaning the top-level directory
87132
shouldClean = !fileName.startsWith("pid_");
@@ -96,6 +141,8 @@ public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOEx
96141

97142
private final CompletableFuture<Void> cleanupTask;
98143

144+
private final CleanupHook cleanupTestHook;
145+
99146
/**
100147
* Get the singleton instance of the TempLocationManager. It will run the cleanup task in the
101148
* background.
@@ -125,6 +172,12 @@ private TempLocationManager() {
125172
}
126173

127174
TempLocationManager(ConfigProvider configProvider) {
175+
this(configProvider, CleanupHook.EMPTY);
176+
}
177+
178+
TempLocationManager(ConfigProvider configProvider, CleanupHook testHook) {
179+
cleanupTestHook = testHook;
180+
128181
// In order to avoid racy attempts to clean up files which are currently being processed in a
129182
// JVM which is being shut down (the JVMs far in the shutdown routine may not be reported by
130183
// 'jps' but still can be eg. processing JFR chunks) we will not clean up any files not older
@@ -222,6 +275,7 @@ public Path getTempDir(Path subPath, boolean create) {
222275

223276
void cleanup(boolean cleanSelf) {
224277
try {
278+
cleanupTestHook.onCleanupStart();
225279
Files.walkFileTree(baseTempDir, new CleanupVisitor(cleanSelf));
226280
} catch (IOException e) {
227281
if (log.isDebugEnabled()) {
@@ -232,7 +286,8 @@ void cleanup(boolean cleanSelf) {
232286
}
233287
}
234288

235-
private void waitForCleanup() {
289+
// accessible for tests
290+
void waitForCleanup() {
236291
cleanupTask.join();
237292
}
238293
}

dd-java-agent/agent-profiling/profiling-controller/src/test/java/com/datadog/profiling/controller/TempLocationManagerTest.java

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,16 @@
55
import datadog.trace.api.config.ProfilingConfig;
66
import datadog.trace.bootstrap.config.provider.ConfigProvider;
77
import datadog.trace.util.PidHelper;
8+
import java.io.IOException;
9+
import java.nio.file.FileVisitResult;
810
import java.nio.file.Files;
911
import java.nio.file.Path;
1012
import java.nio.file.Paths;
13+
import java.nio.file.attribute.BasicFileAttributes;
1114
import java.nio.file.attribute.PosixFilePermissions;
1215
import java.util.Properties;
1316
import java.util.UUID;
17+
import java.util.concurrent.Phaser;
1418
import org.junit.jupiter.api.Test;
1519
import org.junit.jupiter.params.ParameterizedTest;
1620
import org.junit.jupiter.params.provider.ValueSource;
@@ -105,4 +109,83 @@ void testCleanup(String subPath) throws Exception {
105109
assertFalse(Files.exists(fakeTempDir));
106110
assertTrue(Files.exists(tempDir));
107111
}
112+
113+
@ParameterizedTest
114+
@ValueSource(strings = {"preVisitDirectory", "visitFile", "postVisitDirectory"})
115+
void testConcurrentCleanup(String section) throws Exception {
116+
/* This test simulates concurrent cleanup
117+
It utilizes a special hook to create synchronization points in the filetree walking routine,
118+
allowing to delete the files at various points of execution.
119+
The test makes sure that the cleanup is not interrupted and the file and directory being deleted
120+
stays deleted.
121+
*/
122+
Path baseDir =
123+
Files.createTempDirectory(
124+
"ddprof-test-",
125+
PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rwx------")));
126+
127+
Path fakeTempDir = baseDir.resolve("ddprof/pid_1234/scratch");
128+
Files.createDirectories(fakeTempDir);
129+
Path fakeTempFile = fakeTempDir.resolve("libxxx.so");
130+
Files.createFile(fakeTempFile);
131+
132+
fakeTempDir.toFile().deleteOnExit();
133+
fakeTempFile.toFile().deleteOnExit();
134+
135+
Phaser phaser = new Phaser(2);
136+
137+
TempLocationManager.CleanupHook blocker =
138+
new TempLocationManager.CleanupHook() {
139+
@Override
140+
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs)
141+
throws IOException {
142+
if (section.equals("preVisitDirectory") && dir.equals(fakeTempDir)) {
143+
phaser.arriveAndAwaitAdvance();
144+
phaser.arriveAndAwaitAdvance();
145+
}
146+
return null;
147+
}
148+
149+
@Override
150+
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
151+
throws IOException {
152+
if (section.equals("visitFile") && file.equals(fakeTempFile)) {
153+
phaser.arriveAndAwaitAdvance();
154+
phaser.arriveAndAwaitAdvance();
155+
}
156+
return null;
157+
}
158+
159+
@Override
160+
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
161+
if (section.equals("postVisitDirectory") && dir.equals(fakeTempDir)) {
162+
phaser.arriveAndAwaitAdvance();
163+
phaser.arriveAndAwaitAdvance();
164+
}
165+
return null;
166+
}
167+
};
168+
169+
TempLocationManager mgr = instance(baseDir, blocker);
170+
171+
// wait for the cleanup start
172+
phaser.arriveAndAwaitAdvance();
173+
Files.deleteIfExists(fakeTempFile);
174+
phaser.arriveAndAwaitAdvance();
175+
mgr.waitForCleanup();
176+
177+
assertFalse(Files.exists(fakeTempFile));
178+
assertFalse(Files.exists(fakeTempDir));
179+
}
180+
181+
private TempLocationManager instance(Path baseDir, TempLocationManager.CleanupHook cleanupHook)
182+
throws IOException {
183+
Properties props = new Properties();
184+
props.put(ProfilingConfig.PROFILING_TEMP_DIR, baseDir.toString());
185+
props.put(
186+
ProfilingConfig.PROFILING_UPLOAD_PERIOD,
187+
"0"); // to force immediate cleanup; must be a string value!
188+
189+
return new TempLocationManager(ConfigProvider.withPropertiesOverride(props), cleanupHook);
190+
}
108191
}

0 commit comments

Comments
 (0)