Skip to content

Feat/dynamic dedup tcp#184

Open
praagyajain wants to merge 4 commits into
mainfrom
feat/dynamic-dedup-tcp
Open

Feat/dynamic dedup tcp#184
praagyajain wants to merge 4 commits into
mainfrom
feat/dynamic-dedup-tcp

Conversation

@praagyajain

Copy link
Copy Markdown

Related Issue

  • Info about Issue or bug

Closes: #[issue number that will be closed through this PR]

Describe the changes you've made

A clear and concise description of what you have done to successfully close your assigned issue. Any new files? or anything you feel to let us know!

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Code style update (formatting, local variables)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • This change requires a documentation update

How did you test your code changes?

Please describe the tests(if any). Provide instructions how its affecting the coverage.

Describe if there is any unusual behaviour of your code(Write NA if there isn't)

A clear and concise description of it.

Checklist:

  • My code follows the style guidelines of this project.
  • I have performed a self-review of my own code.
  • I have commented my code, particularly in hard-to-understand areas and used java doc.
  • I have made corresponding changes to the documentation.
  • My changes generate no new warnings.
  • I have added tests that prove my fix is effective or that my feature works.
  • New and existing unit tests pass locally with my changes.

Screenshots (if any)

Original Updated
original screenshot updated screenshot

praagyajain and others added 3 commits June 14, 2026 14:49
In k8s the dedup coverage collector runs in the k8s-proxy Deployment pod while
the app JVM is in a separate pod, so the shared-/tmp unix sockets can't reach it.
Add a TCP transport selected by KEPLOY_COVERAGE_ENDPOINT (host:port): the SDK
dials the collector and keeps one bidirectional connection for the whole replay.

Wire protocol (newline-delimited), matching the k8s-proxy collector:
  collector -> SDK : "START <id>" | "END <id>"
  SDK -> collector : "ACK"                        (after START reset)
                     "COV <compact-json>" + "ACK" (after END dump)

- CoverageTcpClient mirrors CommandServer's dispatch but inverts roles (SDK is the
  client); reuses CoverageCollector reset/capture unchanged. COV precedes ACK so the
  collector records the payload before the ACK releases the caller.
- Connect loop retries (collector listens only once replay begins). No read timeout
  on the long-lived idle-between-tests connection.
- Unix transport stays the default for local/docker (unchanged); the worker field is
  generalized to Closeable so stop() is transport-agnostic.

Validated end-to-end: real JVM (this SDK as -javaagent, TCP client) against the
k8s-proxy DedupCoverage TCP server — coverage flows correctly; the score>=90 pair
dedupes (identical line sets), the score=40 case differs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A fresh JVM runs each class's static initializer (<clinit>) on first use.
JaCoCo charged those one-time lines to whichever test ran first, so a test
could look different from its true duplicates by luck of timing -> the
duplicate set flip-flopped run to run (e.g. 2 vs 4).

On the first START command (app fully started), eagerly Class.forName(...,
initialize=true) every indexed application class so all <clinit> lines run
once, then the normal reset clears them. Every test window then captures
only the lines its own request executes -> deterministic dedup. Best-effort
per class; disable via KEPLOY_JAVA_DEDUP_WARMUP_DISABLED=true. Go is
unaffected (AOT, init() runs once at startup before the first reset).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fingerprint each test by the set of executed JaCoCo probes per class
({className -> [probeIdx]}) instead of executed source lines. Each branch
is instrumented as a distinct probe, so the probe set distinguishes which
branch a test took (true vs false) on a shared line — line status and even
branch counts report identically for both paths. The probe set subsumes
line coverage and uses canonical VM class-name keys (no path normalization).
Only capture() changes; the wire payload, collector, store, and enterprise
compute stay generic over map[string][]int. Removed the now-dead line-decode
helpers (Analyzer/CoverageBuilder, executedLines, resolveSourcePath).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings June 29, 2026 09:47

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates the Java dynamic-dedup agent to support a TCP transport mode (intended for k8s / non-shared-/tmp setups) and changes how coverage fingerprints are produced/sent during replay.

Changes:

  • Add dynamic transport selection: unix-socket CommandServer (local/docker) vs TCP client CoverageTcpClient (k8s) based on KEPLOY_COVERAGE_ENDPOINT.
  • Add one-time “warmup” of indexed application classes on first START to reduce <clinit> noise in the first test window.
  • Change coverage capture to emit per-class executed JaCoCo probe indices (instead of source-file executed line numbers).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +713 to 722
boolean[] probes = executionData.getProbes();
Set<Integer> fired = new LinkedHashSet<>();
for (int i = 0; i < probes.length; i++) {
if (probes[i]) {
fired.add(i);
}
}
if (!fired.isEmpty()) {
raw.put(executionData.getName(), fired);
}
Comment on lines +516 to +524
Map<String, List<Integer>> executedLinesByFile = collector.capture();
if (executedLinesByFile.isEmpty()) {
log(Level.FINE, "No Java coverage lines collected for " + command.testId, null);
} else {
// COV must precede ACK: the collector reads lines sequentially,
// so the payload is recorded before the ACK releases the caller.
writeLine(out, "COV " + GSON.toJson(
new DedupPayload(command.testId, executedLinesByFile)));
}
Comment on lines +449 to +452
if (running.get()) {
log(Level.INFO, "Keploy dedup: TCP connect to " + host + ":" + port
+ " failed (" + e.getClass().getSimpleName() + ": " + e.getMessage() + "), retrying", null);
}
- TCP END always emits COV before ACK (even when empty), mirroring the unix
  transport, so the line-oriented collector reads exactly one COV per END and
  cannot desync (Copilot comment).
- Reconnect failures log at INFO only on the first failure, then FINE, to
  avoid ~1/s log spam in k8s; reset on a successful connect (Copilot comment).
- smoke-javaagent.sh asserts the VM class-name key ("smoke/Work") instead of
  the old source-file key ("Work.java"), matching the probe-based fingerprint
  contract; fixes the JDK 8/17/21 smoke jobs.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants