Skip to content

Commit 8d7d2e4

Browse files
authored
Make agent index generation stable irrespective of filesystem iteration order (#11889)
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 test(agent): Replace awkward stream and for loop, with a pure stream variant 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. refactor(agent): Remove unused `Comparator` import Merge branch 'master' into bdu/non-determistic-index-file-walking Co-authored-by: brice.dutheil <brice.dutheil@datadoghq.com>
1 parent cde4fbf commit 8d7d2e4

2 files changed

Lines changed: 97 additions & 43 deletions

File tree

dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/AgentJarIndex.java

Lines changed: 27 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,17 @@
77
import java.io.DataOutputStream;
88
import java.io.File;
99
import java.io.IOException;
10-
import java.nio.file.FileVisitResult;
1110
import java.nio.file.Files;
11+
import java.nio.file.LinkOption;
1212
import java.nio.file.Path;
1313
import java.nio.file.Paths;
14-
import java.nio.file.SimpleFileVisitor;
15-
import java.nio.file.attribute.BasicFileAttributes;
1614
import java.util.ArrayList;
1715
import java.util.Arrays;
1816
import java.util.HashSet;
1917
import java.util.List;
2018
import java.util.Set;
2119
import java.util.jar.JarFile;
20+
import java.util.stream.Stream;
2221
import java.util.zip.ZipEntry;
2322
import org.slf4j.Logger;
2423
import org.slf4j.LoggerFactory;
@@ -90,7 +89,7 @@ public static AgentJarIndex readIndex(JarFile agentJar) {
9089
* Generates an index from the contents of the 'build/resources' directory that makes up the agent
9190
* jar.
9291
*/
93-
static class IndexGenerator extends SimpleFileVisitor<Path> {
92+
static class IndexGenerator {
9493
private static final Set<String> ignoredFileNames =
9594
new HashSet<>(Arrays.asList("MANIFEST.MF", "NOTICE", "LICENSE.renamed"));
9695

@@ -102,17 +101,31 @@ static class IndexGenerator extends SimpleFileVisitor<Path> {
102101
private final List<String> collectedEntryKeys = new ArrayList<>();
103102
private final List<Integer> collectedPrefixIds = new ArrayList<>();
104103

105-
private Path prefixRoot;
106-
private int prefixId;
107-
108104
IndexGenerator(Path resourcesDir) {
109105
this.resourcesDir = resourcesDir;
110106

111107
prefixTrie.put("datadog.*", 0);
112108
}
113109

114110
void buildIndex() throws IOException {
115-
Files.walkFileTree(resourcesDir, this);
111+
Set<String> seen = new HashSet<>();
112+
try (Stream<Path> paths = Files.walk(resourcesDir)) {
113+
paths
114+
.filter(path -> Files.isRegularFile(path, LinkOption.NOFOLLOW_LINKS))
115+
.map(resourcesDir::relativize)
116+
.sorted()
117+
.filter(entry -> entry.getNameCount() >= 2)
118+
.forEach(
119+
entry -> {
120+
String prefix = entry.getName(0) + "/";
121+
int prefixId = prefixIdFor(prefix);
122+
String entryKey = computeEntryKey(entry.subpath(1, entry.getNameCount()));
123+
if (null != entryKey && seen.add(prefixId + "\0" + entryKey)) {
124+
collectedEntryKeys.add(entryKey);
125+
collectedPrefixIds.add(prefixId);
126+
}
127+
});
128+
}
116129

117130
for (int i = 0; i < collectedEntryKeys.size(); i++) {
118131
prefixTrie.put(collectedEntryKeys.get(i), collectedPrefixIds.get(i));
@@ -152,42 +165,13 @@ private String getPrefix(int prefixId) {
152165
return prefixes.get(prefixId - 1);
153166
}
154167

155-
@Override
156-
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {
157-
if (dir.getParent().equals(resourcesDir)) {
158-
String prefix = dir.getFileName() + "/";
159-
prefixRoot = dir;
160-
prefixId = 1 + prefixes.indexOf(prefix);
161-
if (prefixId < 1) {
162-
prefixes.add(prefix);
163-
prefixId = prefixes.size();
164-
}
165-
}
166-
return FileVisitResult.CONTINUE;
167-
}
168-
169-
@Override
170-
public FileVisitResult postVisitDirectory(Path dir, IOException exc) {
171-
if (dir.equals(prefixRoot)) {
172-
prefixRoot = null;
173-
}
174-
return FileVisitResult.CONTINUE;
175-
}
176-
177-
@Override
178-
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
179-
if (null != prefixRoot) {
180-
String entryKey = computeEntryKey(prefixRoot.relativize(file));
181-
if (null != entryKey) {
182-
collectedEntryKeys.add(entryKey);
183-
collectedPrefixIds.add(prefixId);
184-
if (entryKey.endsWith("*")) {
185-
// optimization: wildcard will match everything under here so can skip
186-
return FileVisitResult.SKIP_SIBLINGS;
187-
}
188-
}
168+
private int prefixIdFor(String prefix) {
169+
int prefixId = 1 + prefixes.indexOf(prefix);
170+
if (prefixId < 1) {
171+
prefixes.add(prefix);
172+
prefixId = prefixes.size();
189173
}
190-
return FileVisitResult.CONTINUE;
174+
return prefixId;
191175
}
192176

193177
static String computeEntryKey(Path path) {

dd-java-agent/agent-bootstrap/src/test/java/datadog/trace/bootstrap/AgentJarIndexTest.java

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
11
package datadog.trace.bootstrap;
22

3+
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
34
import static org.junit.jupiter.api.Assertions.assertEquals;
45
import static org.junit.jupiter.api.Assertions.assertNotNull;
56
import static org.junit.jupiter.api.Assertions.assertNull;
67

8+
import java.io.BufferedInputStream;
9+
import java.io.DataInputStream;
710
import java.io.IOException;
811
import java.nio.file.Files;
912
import java.nio.file.Path;
1013
import java.nio.file.Paths;
14+
import java.util.Arrays;
15+
import java.util.List;
1116
import java.util.jar.JarFile;
1217
import java.util.zip.ZipEntry;
1318
import java.util.zip.ZipOutputStream;
@@ -103,6 +108,25 @@ private static void createFile(Path root, String relativePath) throws IOExceptio
103108
Files.createFile(file);
104109
}
105110

111+
private static Path writeIndex(Path resourcesDir, Path indexFile) throws IOException {
112+
AgentJarIndex.IndexGenerator generator = new AgentJarIndex.IndexGenerator(resourcesDir);
113+
generator.buildIndex();
114+
generator.writeIndex(indexFile);
115+
return indexFile;
116+
}
117+
118+
private static List<String> readPrefixes(Path indexFile) throws IOException {
119+
try (DataInputStream in =
120+
new DataInputStream(new BufferedInputStream(Files.newInputStream(indexFile)))) {
121+
int prefixCount = in.readInt();
122+
String[] prefixes = new String[prefixCount];
123+
for (int i = 0; i < prefixCount; i++) {
124+
prefixes[i] = in.readUTF();
125+
}
126+
return Arrays.asList(prefixes);
127+
}
128+
}
129+
106130
@Test
107131
void classEntryNameResolvesBootstrapClassToTopLevel(@TempDir Path tempDir) throws Exception {
108132
Path resources = tempDir.resolve("resources");
@@ -199,6 +223,52 @@ void multiplePrefixesAreIndexedIndependently(@TempDir Path tempDir) throws Excep
199223
index.classEntryName("com.datadoghq.stats.StatsClient"));
200224
}
201225

226+
@Test
227+
void buildIndexWritesPrefixesInSortedOrder(@TempDir Path tempDir) throws Exception {
228+
Path resources = tempDir.resolve("resources");
229+
createFile(resources, "metrics/com/datadoghq/stats/StatsClient.classdata");
230+
createFile(resources, "inst/datadog/trace/instrumentation/servlet/ServletAdvice.classdata");
231+
createFile(resources, "appsec/com/datadog/appsec/Event.classdata");
232+
233+
Path indexFile = writeIndex(resources, tempDir.resolve("dd-java-agent.index"));
234+
235+
assertEquals(Arrays.asList("appsec/", "inst/", "metrics/"), readPrefixes(indexFile));
236+
}
237+
238+
@Test
239+
void buildIndexIgnoresTopLevelFilesWhenCollectingPrefixes(@TempDir Path tempDir)
240+
throws Exception {
241+
Path resources = tempDir.resolve("resources");
242+
createFile(resources, "root-resource.properties");
243+
createFile(resources, "inst/datadog/trace/instrumentation/servlet/ServletAdvice.classdata");
244+
245+
Path indexFile = writeIndex(resources, tempDir.resolve("dd-java-agent.index"));
246+
247+
assertEquals(Arrays.asList("inst/"), readPrefixes(indexFile));
248+
}
249+
250+
@Test
251+
void buildIndexIsIndependentOfDirectoryCreationOrder(@TempDir Path tempDir) throws Exception {
252+
Path buildResources = tempDir.resolve("build-resources");
253+
createFile(buildResources, "appsec/com/datadog/appsec/Event.classdata");
254+
createFile(buildResources, "ci-visibility/com/datadog/ci/Visibility.classdata");
255+
createFile(buildResources, "cws-tls/com/datadog/cws/Tls.classdata");
256+
createFile(
257+
buildResources, "inst/datadog/trace/instrumentation/servlet/ServletAdvice.classdata");
258+
259+
Path deployResources = tempDir.resolve("deploy-resources");
260+
createFile(deployResources, "cws-tls/com/datadog/cws/Tls.classdata");
261+
createFile(deployResources, "ci-visibility/com/datadog/ci/Visibility.classdata");
262+
createFile(deployResources, "appsec/com/datadog/appsec/Event.classdata");
263+
createFile(
264+
deployResources, "inst/datadog/trace/instrumentation/servlet/ServletAdvice.classdata");
265+
266+
Path buildIndex = writeIndex(buildResources, tempDir.resolve("build-dd-java-agent.index"));
267+
Path deployIndex = writeIndex(deployResources, tempDir.resolve("deploy-dd-java-agent.index"));
268+
269+
assertArrayEquals(Files.readAllBytes(buildIndex), Files.readAllBytes(deployIndex));
270+
}
271+
202272
@Test
203273
void buildIndexWithEmptyResourcesDirProducesWorkingIndex(@TempDir Path tempDir) throws Exception {
204274
Path resources = tempDir.resolve("resources");

0 commit comments

Comments
 (0)