Skip to content

Commit 36e285c

Browse files
authored
Use ControllableTimeSource for deterministic TempLocationManagerTest (#10538)
1 parent 25c636c commit 36e285c

File tree

2 files changed

+56
-40
lines changed

2 files changed

+56
-40
lines changed

internal-api/src/main/java/datadog/trace/util/TempLocationManager.java

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
import datadog.environment.SystemProperties;
44
import datadog.trace.api.config.ProfilingConfig;
55
import datadog.trace.api.profiling.ProfilerFlareLogger;
6+
import datadog.trace.api.time.SystemTimeSource;
7+
import datadog.trace.api.time.TimeSource;
68
import datadog.trace.bootstrap.config.provider.ConfigProvider;
79
import datadog.trace.config.inversion.ConfigHelper;
810
import java.io.IOException;
@@ -16,8 +18,6 @@
1618
import java.nio.file.attribute.BasicFileAttributes;
1719
import java.nio.file.attribute.PosixFilePermission;
1820
import java.nio.file.attribute.PosixFilePermissions;
19-
import java.time.Instant;
20-
import java.time.temporal.ChronoUnit;
2121
import java.util.Set;
2222
import java.util.concurrent.CountDownLatch;
2323
import java.util.concurrent.TimeUnit;
@@ -71,25 +71,23 @@ private final class CleanupVisitor implements FileVisitor<Path> {
7171

7272
private Set<String> pidSet;
7373

74-
private final Instant cutoff;
75-
private final Instant timeoutTarget;
74+
private final long cutoffMillis;
75+
private final long timeoutTargetMillis;
7676

7777
private boolean terminated = false;
7878

7979
CleanupVisitor(long timeout, TimeUnit unit) {
80-
this.cutoff = Instant.now().minus(cutoffSeconds, ChronoUnit.SECONDS);
81-
this.timeoutTarget =
82-
timeout > -1
83-
? Instant.now().plus(TimeUnit.MILLISECONDS.convert(timeout, unit), ChronoUnit.MILLIS)
84-
: null;
80+
long now = timeSource.getCurrentTimeMillis();
81+
this.cutoffMillis = now - TimeUnit.SECONDS.toMillis(cutoffSeconds);
82+
this.timeoutTargetMillis = timeout > -1 ? now + unit.toMillis(timeout) : Long.MAX_VALUE;
8583
}
8684

8785
boolean isTerminated() {
8886
return terminated;
8987
}
9088

9189
private boolean isTimedOut() {
92-
return timeoutTarget != null && Instant.now().isAfter(timeoutTarget);
90+
return timeSource.getCurrentTimeMillis() > timeoutTargetMillis;
9391
}
9492

9593
@Override
@@ -133,7 +131,7 @@ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IO
133131
}
134132
cleanupTestHook.visitFile(file, attrs, false);
135133
try {
136-
if (Files.getLastModifiedTime(file).toInstant().isAfter(cutoff)) {
134+
if (Files.getLastModifiedTime(file).toMillis() > cutoffMillis) {
137135
return FileVisitResult.SKIP_SUBTREE;
138136
}
139137
Files.delete(file);
@@ -218,6 +216,7 @@ boolean await(long timeout, TimeUnit unit) throws Throwable {
218216

219217
private final CleanupTask cleanupTask = new CleanupTask();
220218
private final CleanupHook cleanupTestHook;
219+
private final TimeSource timeSource;
221220

222221
/**
223222
* Get the singleton instance of the TempLocationManager. It will run the cleanup task in the
@@ -248,12 +247,21 @@ private TempLocationManager() {
248247
}
249248

250249
TempLocationManager(ConfigProvider configProvider) {
251-
this(configProvider, true, CleanupHook.EMPTY);
250+
this(configProvider, true, CleanupHook.EMPTY, SystemTimeSource.INSTANCE);
252251
}
253252

254253
TempLocationManager(
255254
ConfigProvider configProvider, boolean runStartupCleanup, CleanupHook testHook) {
255+
this(configProvider, runStartupCleanup, testHook, SystemTimeSource.INSTANCE);
256+
}
257+
258+
TempLocationManager(
259+
ConfigProvider configProvider,
260+
boolean runStartupCleanup,
261+
CleanupHook testHook,
262+
TimeSource timeSource) {
256263
cleanupTestHook = testHook;
264+
this.timeSource = timeSource;
257265

258266
Set<String> supportedViews = FileSystems.getDefault().supportedFileAttributeViews();
259267
isPosixFs = supportedViews.contains("posix");

internal-api/src/test/java/datadog/trace/util/TempLocationManagerTest.java

Lines changed: 36 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88
import static org.junit.jupiter.api.Assertions.assertTrue;
99

1010
import datadog.trace.api.config.ProfilingConfig;
11+
import datadog.trace.api.time.ControllableTimeSource;
12+
import datadog.trace.api.time.SystemTimeSource;
13+
import datadog.trace.api.time.TimeSource;
1114
import datadog.trace.bootstrap.config.provider.ConfigProvider;
1215
import java.io.IOException;
1316
import java.nio.file.FileVisitResult;
@@ -16,7 +19,6 @@
1619
import java.nio.file.Paths;
1720
import java.nio.file.attribute.BasicFileAttributes;
1821
import java.nio.file.attribute.PosixFilePermissions;
19-
import java.time.Duration;
2022
import java.util.ArrayList;
2123
import java.util.List;
2224
import java.util.Properties;
@@ -25,7 +27,6 @@
2527
import java.util.concurrent.TimeUnit;
2628
import java.util.concurrent.atomic.AtomicBoolean;
2729
import java.util.concurrent.atomic.AtomicReference;
28-
import java.util.concurrent.locks.LockSupport;
2930
import java.util.stream.Stream;
3031
import org.junit.jupiter.api.Test;
3132
import org.junit.jupiter.params.ParameterizedTest;
@@ -232,23 +233,33 @@ public FileVisitResult postVisitDirectory(Path dir, IOException exc, boolean tim
232233
void testCleanupWithTimeout(boolean shouldSucceed, String section) throws Exception {
233234
/*
234235
* Test that cleanup correctly handles timeout conditions.
235-
* Uses a hook that introduces delays at specific points in the file tree walk.
236+
* Uses a ControllableTimeSource for fully deterministic behavior:
237+
* - Success case: time is never advanced, so timeout never occurs
238+
* - Failure case: time is advanced past timeout on first matching hook call
236239
*
237-
* Timing strategy (more generous for JDK 8 compatibility):
238-
* - Base delay: 100ms (was 10ms - too tight for JDK 8 timer resolution)
239-
* - Success case: 500ms timeout with 100ms delays → plenty of margin
240-
* - Failure case: 20ms timeout with 100ms delays → guaranteed timeout
240+
* This approach is independent of how many times hooks are invoked.
241241
*/
242-
long delayMs = 100;
242+
long timeoutMs = 100;
243+
ControllableTimeSource timeSource = new ControllableTimeSource();
244+
243245
AtomicBoolean withTimeout = new AtomicBoolean(false);
246+
AtomicBoolean timeAdvanced = new AtomicBoolean(false);
244247
TempLocationManager.CleanupHook delayer =
245248
new TempLocationManager.CleanupHook() {
249+
private void maybeAdvanceTime() {
250+
// Only advance time once, and only for failure case
251+
if (!shouldSucceed && timeAdvanced.compareAndSet(false, true)) {
252+
// Advance well past the timeout
253+
timeSource.advance(TimeUnit.MILLISECONDS.toNanos(timeoutMs * 10));
254+
}
255+
}
256+
246257
@Override
247258
public FileVisitResult preVisitDirectory(
248259
Path dir, BasicFileAttributes attrs, boolean timeout) throws IOException {
249260
withTimeout.compareAndSet(false, timeout);
250261
if (section.equals("preVisitDirectory")) {
251-
waitFor(Duration.ofMillis(delayMs));
262+
maybeAdvanceTime();
252263
}
253264
return TempLocationManager.CleanupHook.super.preVisitDirectory(dir, attrs, timeout);
254265
}
@@ -258,7 +269,7 @@ public FileVisitResult visitFileFailed(Path file, IOException exc, boolean timeo
258269
throws IOException {
259270
withTimeout.compareAndSet(false, timeout);
260271
if (section.equals("visitFileFailed")) {
261-
waitFor(Duration.ofMillis(delayMs));
272+
maybeAdvanceTime();
262273
}
263274
return TempLocationManager.CleanupHook.super.visitFileFailed(file, exc, timeout);
264275
}
@@ -268,7 +279,7 @@ public FileVisitResult postVisitDirectory(Path dir, IOException exc, boolean tim
268279
throws IOException {
269280
withTimeout.compareAndSet(false, timeout);
270281
if (section.equals("postVisitDirectory")) {
271-
waitFor(Duration.ofMillis(delayMs));
282+
maybeAdvanceTime();
272283
}
273284
return TempLocationManager.CleanupHook.super.postVisitDirectory(dir, exc, timeout);
274285
}
@@ -278,7 +289,7 @@ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs, boolean t
278289
throws IOException {
279290
withTimeout.compareAndSet(false, timeout);
280291
if (section.equals("visitFile")) {
281-
waitFor(Duration.ofMillis(delayMs));
292+
maybeAdvanceTime();
282293
}
283294
return TempLocationManager.CleanupHook.super.visitFile(file, attrs, timeout);
284295
}
@@ -287,16 +298,15 @@ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs, boolean t
287298
Files.createTempDirectory(
288299
"ddprof-test-",
289300
PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rwx------")));
290-
TempLocationManager instance = instance(baseDir, false, delayer);
301+
TempLocationManager instance = instance(baseDir, false, delayer, timeSource);
291302
Path mytempdir = instance.getTempDir();
292303
Path otherTempdir = mytempdir.getParent().resolve("pid_0000");
293304
Files.createDirectories(otherTempdir);
294305
Files.createFile(mytempdir.resolve("dummy"));
295306
Files.createFile(otherTempdir.resolve("dummy"));
296-
// Success: 500ms timeout (5 * 100ms) with 100ms delays → should complete
297-
// Failure: 20ms timeout (0.2 * 100ms) with 100ms delays → should timeout
298-
boolean rslt =
299-
instance.cleanup((long) (delayMs * (shouldSucceed ? 5 : 0.2d)), TimeUnit.MILLISECONDS);
307+
// Set controllable time AFTER files are created so their modification times are in the past
308+
timeSource.set(TimeUnit.MILLISECONDS.toNanos(System.currentTimeMillis()));
309+
boolean rslt = instance.cleanup(timeoutMs, TimeUnit.MILLISECONDS);
300310
assertEquals(shouldSucceed, rslt);
301311
assertNotEquals(shouldSucceed, withTimeout.get()); // timeout = !shouldSucceed
302312
}
@@ -313,23 +323,21 @@ private static Stream<Arguments> timeoutTestArguments() {
313323

314324
private TempLocationManager instance(
315325
Path baseDir, boolean withStartupCleanup, TempLocationManager.CleanupHook cleanupHook) {
326+
return instance(baseDir, withStartupCleanup, cleanupHook, SystemTimeSource.INSTANCE);
327+
}
328+
329+
private TempLocationManager instance(
330+
Path baseDir,
331+
boolean withStartupCleanup,
332+
TempLocationManager.CleanupHook cleanupHook,
333+
TimeSource timeSource) {
316334
Properties props = new Properties();
317335
props.put(ProfilingConfig.PROFILING_TEMP_DIR, baseDir.toString());
318336
props.put(
319337
ProfilingConfig.PROFILING_UPLOAD_PERIOD,
320338
"0"); // to force immediate cleanup; must be a string value!
321339

322340
return new TempLocationManager(
323-
ConfigProvider.withPropertiesOverride(props), withStartupCleanup, cleanupHook);
324-
}
325-
326-
private void waitFor(Duration timeout) {
327-
long target = System.nanoTime() + timeout.toNanos();
328-
while (System.nanoTime() < target) {
329-
long toSleep = target - System.nanoTime();
330-
if (toSleep > 0) {
331-
LockSupport.parkNanos(toSleep);
332-
}
333-
}
341+
ConfigProvider.withPropertiesOverride(props), withStartupCleanup, cleanupHook, timeSource);
334342
}
335343
}

0 commit comments

Comments
 (0)