Feat/dynamic dedup tcp#184
Conversation
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>
There was a problem hiding this comment.
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 clientCoverageTcpClient(k8s) based onKEPLOY_COVERAGE_ENDPOINT. - Add one-time “warmup” of indexed application classes on first
STARTto 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.
| 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); | ||
| } |
| 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))); | ||
| } |
| 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.
… + bytecode
Option B for whole-test-set coverage: a pure Java tool that reconstructs
ExecutionData from a fired-probe union (the dedupFingerprints we already persist)
plus a build-constant {classId, probeCount} manifest, runs JaCoCo Analyzer over the
app bytecode, and sums line/branch/instr/method counters over all classes.
Reuses the fingerprints verbatim as input; no JaCoCo internal APIs (id + probeCount
come from the SDK's live ExecutionData). CoverageReporterSelfTest cross-checks the
reconstruct-from-union path against JaCoCo's direct .exec analysis — exact match on
the sample app (lines 78/105, branches 22/42, instr 452/525).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: praagyajain <praagyajain2002@gmail.com>
…e (Option B)
After warmup (all app classes loaded), the SDK ships — best-effort, async,
once-per-JVM — a zip of the indexed class bytecode plus a manifest
{vmClassName -> {classId(hex), probeCount}} to k8s-proxy via HTTP multipart
(KEPLOY_BYTECODE_UPLOAD_URL, tagged with KEPLOY_BUILD_TAG). classId/probeCount
come straight from the live ExecutionData, so the offline CoverageReporter needs
no JaCoCo internal APIs. HEAD exists-check uploads at most once per build tag.
Gated by the two envs (webhook-injected); absent => no-op, so dedup-only
deployments are unchanged. Fully non-fatal: never throws into the app or the
per-test dedup path.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: praagyajain <praagyajain2002@gmail.com>
The bytecode/manifest upload targets k8s-proxy's in-cluster HTTPS control port (:8080), whose cert is self-signed and may not chain to the app JVM's truststore. Since the upload is a best-effort, cluster-internal data-plane call (like the raw-TCP coverage collector), install a trust-all SSLSocketFactory for that HTTPS call only. No effect on plain-HTTP endpoints. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: praagyajain <praagyajain2002@gmail.com>
Build a live manifest ({vmClassName -> {classId, probeCount}}) from the proven
per-test capture() path and, when it grows, send the app class bytecode as a
base64 "CLASSES ..." frame on the existing collector TCP connection (:36340)
right after the COV frame. This survives the replay agent's outbound mock
interception, which 502'd the earlier HTTPS upload. k8s-proxy uses this
bytecode + the unioned fingerprints to compute whole-test-set coverage offline.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Reviewed the SDK-side dynamic-dedup changes end to end (the JaCoCo probe capture, the new TCP transport, the bytecode + manifest export, and the offline Executive summary
The core design is sound: probe-set fingerprints for branch-aware dedup, a warmup pass for determinism, and offline coverage reconstruction from a probe union plus a per-class manifest. The self-test proving reconstruction equals JaCoCo's own analysis is a good instinct. But the PR ships a large block of dead code (including a trust-all TLS manager), re-transmits the full bytecode blob once per newly discovered class, and changes an existing wire payload's meaning while keeping its old field name. Several correctness edges (load-time-transformed classes, warmup side effects, COV-on-exception) are unguarded, and there are no automated tests. Findings[HIGH] Entire HTTP bytecode-upload path is dead code, and it carries a trust-all TLS manager. [HIGH] The full class zip is rebuilt and re-sent every time the manifest grows. [HIGH] The per-test payload changed meaning but kept the name [MEDIUM to HIGH] Warmup eagerly runs every app class's static initializer, opt-out and on the START path. [MEDIUM] On a capture exception, COV is not emitted before ACK, breaking the stated one-COV-per-END invariant. [MEDIUM] Offline coverage undercounts classes transformed at load time. [MEDIUM] No automated tests; the self-test is a manual harness that skips the real serialization path. [MEDIUM] All app class bytes are held in memory for the JVM lifetime, and the zip is built fully in memory. [LOW] Dead field. [LOW] Endpoint parsing does not handle IPv6. Missing test cases
Breaking change and operational impact
Final verdictNot ready to merge. The approach is right and the reconstruction idea is validated in principle, but the dead HTTP path (with trust-all TLS), the full-zip resend, and the renamed-but-not-renamed wire payload should be fixed before merge, and the warmup default and COV-on-exception edges need to be made safe. Add at least one automated test through the real serialization path. Once the dead code is gone, the zip is sent once, the payload is renamed and documented, and warmup is opt-in, this is close. |
Related Issue
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
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
NAif there isn't)A clear and concise description of it.
Checklist:
Screenshots (if any)