From fc7d48cb93183767a6815e4694218f16ff3fefe4 Mon Sep 17 00:00:00 2001 From: Brice Dutheil Date: Wed, 8 Jul 2026 17:14:31 +0200 Subject: [PATCH 1/4] refactor(agent): Make index generation immune to filesystem unordered iteration `AgentJarIndex.IndexGenerator` used `Files.walkFileTree` traversal order as stable input. But directory entry order is explicitly unspecified, and in CI, the folder creation in `included/` depends on when the parallel Gradle task are finished. This causes a different index file to be generated, and invalidates the input of the `shadowJar`. Note the index remains correct, however this behavior changes the prefix IDs in the index file. This happens because `Files.walkFileTree` rely under the hood on `UnixFileSystemProvider.newDirectoryStream` which uses `readdir(3)` C api which asks the file system via syscall, in particular we can read: > The order in which filenames are read by successive calls to > readdir() depends on the filesystem implementation; it is unlikely > that the names will be sorted in any fashion. https://www.man7.org/linux/man-pages/man3/readdir.3.html https://man7.org/linux/man-pages/man2/getdents.2.html The tool `find` has the same behavior. https://sources.debian.org/src/findutils/4.10.0-3/gl/lib/fts.c/#L1444 --- .../trace/bootstrap/AgentJarIndex.java | 81 +++++++++---------- .../trace/bootstrap/AgentJarIndexTest.java | 58 +++++++++++++ 2 files changed, 96 insertions(+), 43 deletions(-) diff --git a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/AgentJarIndex.java b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/AgentJarIndex.java index 9035465dda3..486079b200e 100644 --- a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/AgentJarIndex.java +++ b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/AgentJarIndex.java @@ -7,18 +7,19 @@ import java.io.DataOutputStream; import java.io.File; import java.io.IOException; -import java.nio.file.FileVisitResult; import java.nio.file.Files; +import java.nio.file.LinkOption; import java.nio.file.Path; import java.nio.file.Paths; -import java.nio.file.SimpleFileVisitor; -import java.nio.file.attribute.BasicFileAttributes; import java.util.ArrayList; import java.util.Arrays; +import java.util.Comparator; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.jar.JarFile; +import java.util.stream.Collectors; +import java.util.stream.Stream; import java.util.zip.ZipEntry; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -90,7 +91,7 @@ public static AgentJarIndex readIndex(JarFile agentJar) { * Generates an index from the contents of the 'build/resources' directory that makes up the agent * jar. */ - static class IndexGenerator extends SimpleFileVisitor { + static class IndexGenerator { private static final Set ignoredFileNames = new HashSet<>(Arrays.asList("MANIFEST.MF", "NOTICE", "LICENSE.renamed")); @@ -102,9 +103,6 @@ static class IndexGenerator extends SimpleFileVisitor { private final List collectedEntryKeys = new ArrayList<>(); private final List collectedPrefixIds = new ArrayList<>(); - private Path prefixRoot; - private int prefixId; - IndexGenerator(Path resourcesDir) { this.resourcesDir = resourcesDir; @@ -112,7 +110,29 @@ static class IndexGenerator extends SimpleFileVisitor { } void buildIndex() throws IOException { - Files.walkFileTree(resourcesDir, this); + try (Stream paths = Files.walk(resourcesDir)) { + List entries = + paths + .filter(path -> Files.isRegularFile(path, LinkOption.NOFOLLOW_LINKS)) + .map(resourcesDir::relativize) + .sorted(Comparator.comparing(IndexGenerator::normalizedPath)) + .collect(Collectors.toList()); + + Set seen = new HashSet<>(); + for (Path entry : entries) { + if (entry.getNameCount() < 2) { + continue; + } + + String prefix = entry.getName(0) + "/"; + int prefixId = prefixIdFor(prefix); + String entryKey = computeEntryKey(entry.subpath(1, entry.getNameCount())); + if (null != entryKey && seen.add(prefixId + "\0" + entryKey)) { + collectedEntryKeys.add(entryKey); + collectedPrefixIds.add(prefixId); + } + } + } for (int i = 0; i < collectedEntryKeys.size(); i++) { prefixTrie.put(collectedEntryKeys.get(i), collectedPrefixIds.get(i)); @@ -152,42 +172,13 @@ private String getPrefix(int prefixId) { return prefixes.get(prefixId - 1); } - @Override - public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) { - if (dir.getParent().equals(resourcesDir)) { - String prefix = dir.getFileName() + "/"; - prefixRoot = dir; - prefixId = 1 + prefixes.indexOf(prefix); - if (prefixId < 1) { - prefixes.add(prefix); - prefixId = prefixes.size(); - } + private int prefixIdFor(String prefix) { + int prefixId = 1 + prefixes.indexOf(prefix); + if (prefixId < 1) { + prefixes.add(prefix); + prefixId = prefixes.size(); } - return FileVisitResult.CONTINUE; - } - - @Override - public FileVisitResult postVisitDirectory(Path dir, IOException exc) { - if (dir.equals(prefixRoot)) { - prefixRoot = null; - } - return FileVisitResult.CONTINUE; - } - - @Override - public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { - if (null != prefixRoot) { - String entryKey = computeEntryKey(prefixRoot.relativize(file)); - if (null != entryKey) { - collectedEntryKeys.add(entryKey); - collectedPrefixIds.add(prefixId); - if (entryKey.endsWith("*")) { - // optimization: wildcard will match everything under here so can skip - return FileVisitResult.SKIP_SIBLINGS; - } - } - } - return FileVisitResult.CONTINUE; + return prefixId; } static String computeEntryKey(Path path) { @@ -216,6 +207,10 @@ static String computeEntryKey(Path path) { return entryKey.replace('/', '.'); } + private static String normalizedPath(Path path) { + return path.toString().replace(File.separatorChar, '/'); + } + public static void main(String[] args) throws IOException { if (args.length < 1) { throw new IllegalArgumentException("Expected: resources-dir"); diff --git a/dd-java-agent/agent-bootstrap/src/test/java/datadog/trace/bootstrap/AgentJarIndexTest.java b/dd-java-agent/agent-bootstrap/src/test/java/datadog/trace/bootstrap/AgentJarIndexTest.java index 1af386972e2..44a7ad017c3 100644 --- a/dd-java-agent/agent-bootstrap/src/test/java/datadog/trace/bootstrap/AgentJarIndexTest.java +++ b/dd-java-agent/agent-bootstrap/src/test/java/datadog/trace/bootstrap/AgentJarIndexTest.java @@ -1,13 +1,18 @@ package datadog.trace.bootstrap; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; +import java.io.BufferedInputStream; +import java.io.DataInputStream; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.Arrays; +import java.util.List; import java.util.jar.JarFile; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; @@ -103,6 +108,25 @@ private static void createFile(Path root, String relativePath) throws IOExceptio Files.createFile(file); } + private static Path writeIndex(Path resourcesDir, Path indexFile) throws IOException { + AgentJarIndex.IndexGenerator generator = new AgentJarIndex.IndexGenerator(resourcesDir); + generator.buildIndex(); + generator.writeIndex(indexFile); + return indexFile; + } + + private static List readPrefixes(Path indexFile) throws IOException { + try (DataInputStream in = + new DataInputStream(new BufferedInputStream(Files.newInputStream(indexFile)))) { + int prefixCount = in.readInt(); + String[] prefixes = new String[prefixCount]; + for (int i = 0; i < prefixCount; i++) { + prefixes[i] = in.readUTF(); + } + return Arrays.asList(prefixes); + } + } + @Test void classEntryNameResolvesBootstrapClassToTopLevel(@TempDir Path tempDir) throws Exception { Path resources = tempDir.resolve("resources"); @@ -199,6 +223,40 @@ void multiplePrefixesAreIndexedIndependently(@TempDir Path tempDir) throws Excep index.classEntryName("com.datadoghq.stats.StatsClient")); } + @Test + void buildIndexWritesPrefixesInSortedOrder(@TempDir Path tempDir) throws Exception { + Path resources = tempDir.resolve("resources"); + createFile(resources, "metrics/com/datadoghq/stats/StatsClient.classdata"); + createFile(resources, "inst/datadog/trace/instrumentation/servlet/ServletAdvice.classdata"); + createFile(resources, "appsec/com/datadog/appsec/Event.classdata"); + + Path indexFile = writeIndex(resources, tempDir.resolve("dd-java-agent.index")); + + assertEquals(Arrays.asList("appsec/", "inst/", "metrics/"), readPrefixes(indexFile)); + } + + @Test + void buildIndexIsIndependentOfDirectoryCreationOrder(@TempDir Path tempDir) throws Exception { + Path buildResources = tempDir.resolve("build-resources"); + createFile(buildResources, "appsec/com/datadog/appsec/Event.classdata"); + createFile(buildResources, "ci-visibility/com/datadog/ci/Visibility.classdata"); + createFile(buildResources, "cws-tls/com/datadog/cws/Tls.classdata"); + createFile( + buildResources, "inst/datadog/trace/instrumentation/servlet/ServletAdvice.classdata"); + + Path deployResources = tempDir.resolve("deploy-resources"); + createFile(deployResources, "cws-tls/com/datadog/cws/Tls.classdata"); + createFile(deployResources, "ci-visibility/com/datadog/ci/Visibility.classdata"); + createFile(deployResources, "appsec/com/datadog/appsec/Event.classdata"); + createFile( + deployResources, "inst/datadog/trace/instrumentation/servlet/ServletAdvice.classdata"); + + Path buildIndex = writeIndex(buildResources, tempDir.resolve("build-dd-java-agent.index")); + Path deployIndex = writeIndex(deployResources, tempDir.resolve("deploy-dd-java-agent.index")); + + assertArrayEquals(Files.readAllBytes(buildIndex), Files.readAllBytes(deployIndex)); + } + @Test void buildIndexWithEmptyResourcesDirProducesWorkingIndex(@TempDir Path tempDir) throws Exception { Path resources = tempDir.resolve("resources"); From 5d8ef7826bc88999a1391ca5fdac83c88fe7de68 Mon Sep 17 00:00:00 2001 From: Brice Dutheil Date: Thu, 9 Jul 2026 10:40:25 +0200 Subject: [PATCH 2/4] test(agent): Replace awkward stream and for loop, with a pure stream variant --- .../trace/bootstrap/AgentJarIndex.java | 38 ++++++++----------- .../trace/bootstrap/AgentJarIndexTest.java | 12 ++++++ 2 files changed, 28 insertions(+), 22 deletions(-) diff --git a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/AgentJarIndex.java b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/AgentJarIndex.java index 486079b200e..8b56c67c123 100644 --- a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/AgentJarIndex.java +++ b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/AgentJarIndex.java @@ -18,7 +18,6 @@ import java.util.List; import java.util.Set; import java.util.jar.JarFile; -import java.util.stream.Collectors; import java.util.stream.Stream; import java.util.zip.ZipEntry; import org.slf4j.Logger; @@ -110,28 +109,23 @@ static class IndexGenerator { } void buildIndex() throws IOException { + Set seen = new HashSet<>(); try (Stream paths = Files.walk(resourcesDir)) { - List entries = - paths - .filter(path -> Files.isRegularFile(path, LinkOption.NOFOLLOW_LINKS)) - .map(resourcesDir::relativize) - .sorted(Comparator.comparing(IndexGenerator::normalizedPath)) - .collect(Collectors.toList()); - - Set seen = new HashSet<>(); - for (Path entry : entries) { - if (entry.getNameCount() < 2) { - continue; - } - - String prefix = entry.getName(0) + "/"; - int prefixId = prefixIdFor(prefix); - String entryKey = computeEntryKey(entry.subpath(1, entry.getNameCount())); - if (null != entryKey && seen.add(prefixId + "\0" + entryKey)) { - collectedEntryKeys.add(entryKey); - collectedPrefixIds.add(prefixId); - } - } + paths + .filter(path -> Files.isRegularFile(path, LinkOption.NOFOLLOW_LINKS)) + .map(resourcesDir::relativize) + .sorted(Comparator.comparing(IndexGenerator::normalizedPath)) + .filter(entry -> entry.getNameCount() >= 2) + .forEach( + entry -> { + String prefix = entry.getName(0) + "/"; + int prefixId = prefixIdFor(prefix); + String entryKey = computeEntryKey(entry.subpath(1, entry.getNameCount())); + if (null != entryKey && seen.add(prefixId + "\0" + entryKey)) { + collectedEntryKeys.add(entryKey); + collectedPrefixIds.add(prefixId); + } + }); } for (int i = 0; i < collectedEntryKeys.size(); i++) { diff --git a/dd-java-agent/agent-bootstrap/src/test/java/datadog/trace/bootstrap/AgentJarIndexTest.java b/dd-java-agent/agent-bootstrap/src/test/java/datadog/trace/bootstrap/AgentJarIndexTest.java index 44a7ad017c3..7ff3028b9cf 100644 --- a/dd-java-agent/agent-bootstrap/src/test/java/datadog/trace/bootstrap/AgentJarIndexTest.java +++ b/dd-java-agent/agent-bootstrap/src/test/java/datadog/trace/bootstrap/AgentJarIndexTest.java @@ -235,6 +235,18 @@ void buildIndexWritesPrefixesInSortedOrder(@TempDir Path tempDir) throws Excepti assertEquals(Arrays.asList("appsec/", "inst/", "metrics/"), readPrefixes(indexFile)); } + @Test + void buildIndexIgnoresTopLevelFilesWhenCollectingPrefixes(@TempDir Path tempDir) + throws Exception { + Path resources = tempDir.resolve("resources"); + createFile(resources, "root-resource.properties"); + createFile(resources, "inst/datadog/trace/instrumentation/servlet/ServletAdvice.classdata"); + + Path indexFile = writeIndex(resources, tempDir.resolve("dd-java-agent.index")); + + assertEquals(Arrays.asList("inst/"), readPrefixes(indexFile)); + } + @Test void buildIndexIsIndependentOfDirectoryCreationOrder(@TempDir Path tempDir) throws Exception { Path buildResources = tempDir.resolve("build-resources"); From 51b20c7435d190f6e9fa0b0a138f7802b9c765ea Mon Sep 17 00:00:00 2001 From: Brice Dutheil Date: Thu, 9 Jul 2026 10:44:52 +0200 Subject: [PATCH 3/4] refactor(agent): Remove normalization for windows Originally, normalization was introduced so that the index would be same across OS, but it's not really improtant for windows. --- .../main/java/datadog/trace/bootstrap/AgentJarIndex.java | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/AgentJarIndex.java b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/AgentJarIndex.java index 8b56c67c123..da3f3412006 100644 --- a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/AgentJarIndex.java +++ b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/AgentJarIndex.java @@ -114,7 +114,7 @@ void buildIndex() throws IOException { paths .filter(path -> Files.isRegularFile(path, LinkOption.NOFOLLOW_LINKS)) .map(resourcesDir::relativize) - .sorted(Comparator.comparing(IndexGenerator::normalizedPath)) + .sorted() .filter(entry -> entry.getNameCount() >= 2) .forEach( entry -> { @@ -201,10 +201,6 @@ static String computeEntryKey(Path path) { return entryKey.replace('/', '.'); } - private static String normalizedPath(Path path) { - return path.toString().replace(File.separatorChar, '/'); - } - public static void main(String[] args) throws IOException { if (args.length < 1) { throw new IllegalArgumentException("Expected: resources-dir"); From 794f5f296ef66ef08426bcd59a7fce44d7698899 Mon Sep 17 00:00:00 2001 From: Brice Dutheil Date: Thu, 9 Jul 2026 11:08:16 +0200 Subject: [PATCH 4/4] refactor(agent): Remove unused `Comparator` import --- .../src/main/java/datadog/trace/bootstrap/AgentJarIndex.java | 1 - 1 file changed, 1 deletion(-) diff --git a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/AgentJarIndex.java b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/AgentJarIndex.java index da3f3412006..5350933897c 100644 --- a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/AgentJarIndex.java +++ b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/AgentJarIndex.java @@ -13,7 +13,6 @@ import java.nio.file.Paths; import java.util.ArrayList; import java.util.Arrays; -import java.util.Comparator; import java.util.HashSet; import java.util.List; import java.util.Set;