Skip to content

Commit 6785567

Browse files
Stabilize java dedup coverage collection
Signed-off-by: Asish Kumar <officialasishkumar@gmail.com>
1 parent c0fb66e commit 6785567

2 files changed

Lines changed: 69 additions & 41 deletions

File tree

README.md

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,10 @@ KeployDedupAgent.start();
7373

7474
### 3. Run the App with the JaCoCo Java Agent
7575

76-
The dedup agent reads coverage in-process via JaCoCo's runtime API (`org.jacoco.agent.rt.RT.getAgent()`), so all you need is to attach the JaCoCo Java agent — no TCP server flags, no port choices:
76+
The dedup agent reads coverage in-process via JaCoCo's runtime API (`org.jacoco.agent.rt.RT.getAgent()`), so attaching the JaCoCo Java agent is the only runtime requirement in the common cases below:
77+
78+
- Maven/Gradle dev runs where application classes are under `target/classes` or `build/classes/java/main`
79+
- packaged `java -jar` runs where the application classes live inside the executable jar
7780

7881
```bash
7982
java -javaagent:/path/to/jacocoagent.jar -jar your-app.jar
@@ -112,13 +115,9 @@ keploy dedup --rm
112115

113116
## Docker and Restricted Docker
114117

115-
Java dedup works in native, Docker, and restricted Docker environments as long as the following conditions are met:
116-
117-
- host `/tmp` is bind-mounted into the container as `/tmp`
118-
- `/tmp` remains writable so the Unix sockets can be created
119-
- if the SDK falls back to TCP, the JaCoCo TCP port is reachable from the Java process
118+
Java dedup works in native, Docker, and restricted Docker environments as long as `/tmp` is shared and writable between Keploy Enterprise and the Java process. In Docker Compose flows, Enterprise can inject that shared `/tmp` mount when it rewrites the Compose file for replay.
120119

121-
The `/tmp` bind mount is required because Keploy Enterprise and the Java SDK communicate over these Unix sockets:
120+
Keploy Enterprise and the Java SDK communicate over these Unix sockets:
122121

123122
- `/tmp/coverage_control.sock`
124123
- `/tmp/coverage_data.sock`
@@ -129,8 +128,8 @@ Without a shared `/tmp`, dedup will not work inside containers because Enterpris
129128

130129
- `KEPLOY_JACOCO_HOST`: JaCoCo TCP host used when the in-process runtime API is unavailable. Default: `127.0.0.1`
131130
- `KEPLOY_JACOCO_PORT`: JaCoCo TCP port used when the in-process runtime API is unavailable. Default: `36320`
132-
- `KEPLOY_JAVA_CLASS_DIRS`: optional comma-separated class or jar locations to analyze for executed lines
133-
- `KEPLOY_JAVA_CLASSPATH_FALLBACK`: scans classpath directories and jars if no class roots are found. Default: `false`
131+
- `KEPLOY_JAVA_CLASS_DIRS`: optional comma-separated class or jar locations to analyze for executed lines when your build output lives outside the standard locations
132+
- `KEPLOY_JAVA_CLASSPATH_FALLBACK`: scans the full classpath if standard class roots and the executable jar do not provide application classes. Default: `false`
134133
- `KEPLOY_JAVA_DEDUP_DISABLED`: disables the Java dedup agent when set to `true`, `1`, or `yes`
135134

136135
## Sample

keploy-sdk/src/main/java/io/keploy/dedup/KeployDedupAgent.java

Lines changed: 61 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,6 @@
4141
import java.util.List;
4242
import java.util.Map;
4343
import java.util.Set;
44-
import java.util.concurrent.ExecutorService;
45-
import java.util.concurrent.Executors;
46-
import java.util.concurrent.ThreadFactory;
4744
import java.util.concurrent.atomic.AtomicBoolean;
4845
import java.util.jar.JarEntry;
4946
import java.util.jar.JarFile;
@@ -197,7 +194,6 @@ private static final class CommandServer implements Runnable, Closeable {
197194

198195
private final CoverageCollector collector;
199196
private final CoveragePublisher publisher;
200-
private final ExecutorService workers;
201197
private final AtomicBoolean running = new AtomicBoolean(true);
202198
private final Object testCaseLock = new Object();
203199
private volatile AFUNIXServerSocket serverSocket;
@@ -206,7 +202,6 @@ private static final class CommandServer implements Runnable, Closeable {
206202
CommandServer(CoverageCollector collector, CoveragePublisher publisher) {
207203
this.collector = collector;
208204
this.publisher = publisher;
209-
this.workers = Executors.newCachedThreadPool(new NamedDaemonFactory("keploy-java-dedup-worker"));
210205
}
211206

212207
@Override
@@ -221,13 +216,7 @@ public void run() {
221216

222217
while (running.get()) {
223218
try {
224-
final Socket socket = localServer.accept();
225-
workers.execute(new Runnable() {
226-
@Override
227-
public void run() {
228-
handle(socket);
229-
}
230-
});
219+
handle(localServer.accept());
231220
} catch (IOException e) {
232221
if (running.get()) {
233222
log(Level.SEVERE, "Failed to accept Java dedup coverage command", e);
@@ -238,7 +227,6 @@ public void run() {
238227
STARTED.set(false);
239228
log(Level.SEVERE, "Java dedup control socket server is unavailable", t);
240229
} finally {
241-
workers.shutdownNow();
242230
deleteSocketFile(controlSocket);
243231
}
244232
}
@@ -269,6 +257,7 @@ private void dispatch(CoverageCommand command, OutputStream outputStream) throws
269257
if (command.action == CommandAction.START) {
270258
activeTestId = command.testId;
271259
collector.reset();
260+
writeAck(outputStream);
272261
return;
273262
}
274263

@@ -317,7 +306,6 @@ public void close() {
317306
log(Level.FINE, "Failed to close Java dedup control socket", e);
318307
}
319308
}
320-
workers.shutdownNow();
321309
}
322310
}
323311

@@ -578,18 +566,54 @@ private InProcessAgent(Object agent, Method getExecutionData, Method reset) {
578566
}
579567

580568
static InProcessAgent locate() throws ReflectiveOperationException {
581-
Class<?> rtClass = Class.forName("org.jacoco.agent.rt.RT");
569+
ReflectiveOperationException firstFailure = null;
570+
for (ClassLoader loader : candidateLoaders()) {
571+
try {
572+
InProcessAgent agent = locateWithLoader(loader);
573+
if (agent != null) {
574+
return agent;
575+
}
576+
} catch (ReflectiveOperationException e) {
577+
if (firstFailure == null) {
578+
firstFailure = e;
579+
}
580+
}
581+
}
582+
583+
if (firstFailure != null) {
584+
throw firstFailure;
585+
}
586+
throw new ClassNotFoundException("org.jacoco.agent.rt.RT");
587+
}
588+
589+
private static InProcessAgent locateWithLoader(ClassLoader loader) throws ReflectiveOperationException {
590+
Class<?> rtClass = Class.forName("org.jacoco.agent.rt.RT", true, loader);
582591
Object resolved = rtClass.getMethod("getAgent").invoke(null);
583592
if (resolved == null) {
584593
return null;
585594
}
595+
586596
Method getExecutionData = resolved.getClass().getMethod("getExecutionData", boolean.class);
587597
Method reset = resolved.getClass().getMethod("reset");
588598
getExecutionData.setAccessible(true);
589599
reset.setAccessible(true);
590600
return new InProcessAgent(resolved, getExecutionData, reset);
591601
}
592602

603+
private static List<ClassLoader> candidateLoaders() {
604+
List<ClassLoader> loaders = new ArrayList<>(3);
605+
addLoader(loaders, ClassLoader.getSystemClassLoader());
606+
addLoader(loaders, Thread.currentThread().getContextClassLoader());
607+
addLoader(loaders, KeployDedupAgent.class.getClassLoader());
608+
return loaders;
609+
}
610+
611+
private static void addLoader(List<ClassLoader> loaders, ClassLoader candidate) {
612+
if (candidate != null && !loaders.contains(candidate)) {
613+
loaders.add(candidate);
614+
}
615+
}
616+
593617
byte[] getExecutionData(boolean resetCounters) throws IOException {
594618
try {
595619
return (byte[]) getExecutionData.invoke(agent, resetCounters);
@@ -643,6 +667,9 @@ private List<ClassEntry> entries() {
643667
private List<ClassEntry> loadEntries() {
644668
LinkedHashMap<String, ClassEntry> collected = new LinkedHashMap<>();
645669
scanRoots(applicationRoots(), collected);
670+
if (collected.isEmpty()) {
671+
scanRoots(executableArchiveRoots(), collected);
672+
}
646673
if (collected.isEmpty() && isClasspathFallbackEnabled()) {
647674
scanRoots(classpathRoots(), collected);
648675
}
@@ -679,6 +706,25 @@ private boolean isClasspathFallbackEnabled() {
679706
"keploy.java.classpath.fallback", "false"));
680707
}
681708

709+
private List<File> executableArchiveRoots() {
710+
LinkedHashSet<File> roots = new LinkedHashSet<>();
711+
String classpath = System.getProperty("java.class.path", "");
712+
if (classpath.trim().isEmpty()) {
713+
return new ArrayList<>(roots);
714+
}
715+
716+
String[] parts = classpath.split(Pattern.quote(File.pathSeparator));
717+
if (parts.length != 1) {
718+
return new ArrayList<>(roots);
719+
}
720+
721+
File file = new File(parts[0].trim());
722+
if (file.isFile() && file.getName().endsWith(".jar")) {
723+
roots.add(file);
724+
}
725+
return new ArrayList<>(roots);
726+
}
727+
682728
private String[] configuredRoots(String configured) {
683729
if (configured.indexOf(',') >= 0) {
684730
return configured.split(",");
@@ -841,21 +887,4 @@ private ClassEntry(String className, String location, byte[] bytes) {
841887
}
842888
}
843889

844-
private static final class NamedDaemonFactory implements ThreadFactory {
845-
846-
private final String prefix;
847-
private int counter;
848-
849-
private NamedDaemonFactory(String prefix) {
850-
this.prefix = prefix;
851-
}
852-
853-
@Override
854-
public synchronized Thread newThread(Runnable runnable) {
855-
counter++;
856-
Thread thread = new Thread(runnable, prefix + "-" + counter);
857-
thread.setDaemon(true);
858-
return thread;
859-
}
860-
}
861890
}

0 commit comments

Comments
 (0)