Skip to content

fix(dedup): bind the control socket before the app starts - #189

Merged
slayerjain merged 5 commits into
mainfrom
fix/java-dedup-bind-before-ready-next
Nov 7, 2026
Merged

slayerjain merged 5 commits into
mainfrom
fix/java-dedup-bind-before-ready-next

Conversation

@slayerjain

Copy link
Copy Markdown
Member

Related Issue

Java dedup lost the coverage of a run's first tests, seen as Ignoring mismatched END command. expected=, actual=test-set-0/test-47 in the Java dedup lanes (once in about 470 runs).

Describe the changes you've made

The bug. premain started a daemon thread and the socket was bound on that thread. Keploy fires tests as soon as the app answers its health check, and nothing ordered the bind before that. When the bind was late, each START and END met a refused dial, and the first END after the bind arrived with no START, so the SDK logged the mismatch and still answered ACK. Those tests had no coverage, and the report came out short: 153 of 200 on the run that failed.

The fix.

  • The control socket is bound in start(), on the caller's thread, not on the accept thread. For -javaagent that is premain, which runs before the app's main, so the socket listens before any app code can answer. For KeployMiddleware it is the filter's init, which the container runs before the filter's first request. An agent attached to a running app binds as it attaches.
  • A bind failure is logged and the app starts without dedup; it never fails the app's start. That includes an Error from loading junixsocket's native library, which now loads in premain.
  • An END with no START is answered IGNORED, not ACK: nothing was published for that test, and ACK said it had been.
  • On the TCP transport the same END gets IGNORED <id> and then ACK. ACK stays because it ends the collector's wait.

Compatibility. A Keploy agent that does not know IGNORED reads it as an answer, as it reads ACK. A k8s collector that does not know the frame skips it and takes the ACK.

CI. The smoke test waited for the socket file before its first command, which is the wait Keploy does not do, so it could not see a late bind. START is now main's first act. A new smoke case runs an app with junixsocket's native library made unloadable and checks that main runs and that the agent says the socket is unavailable. The verify job built with -DskipTests, so the unit tests ran in no pipeline; they now run on each JDK.

Tested.

  • Unit tests: ControlSocketTest (socket bound when start() returns, stale file replaced, bind failure reported, IGNORED on the unix socket) and CoverageTcpClientTest (IGNORED <id> then ACK, in that order, naming the test). Both compare the reply with the literal word: Keploy and k8s-proxy read that word, so it is a contract.
  • Build, unit tests (10 of 10) and the smoke test (7 cases) on JDK 8, 17 and 21. The smoke test fails on each with the bind back on the accept thread, and fails with the bind's catch narrowed to IOException: that JVM aborts in premain before main runs.
  • End to end on the dropwizard sample with the bind held 8 s in premain: 200 covered, no mismatch, no stall.
  • Keploy's Java dedup lanes with this branch built in, both samples: the linux lane on JDK 8, 17 and 21 10 of 10, and the docker lane green, beside 4 busy loops on a loaded host, no mismatch.

Type of change

  • Bug fix (non-breaking change which fixes an issue)

The agent bound /tmp/coverage_control.sock on its background thread, and
nothing ordered that bind before the app's first answer to a health check.
Keploy fires tests as soon as the app answers, so when the bind came late
the first tests' START and END reached no one and their coverage was lost:
a run reported 153 of 200 tests covered. The END that met the socket just
after it came up logged "Ignoring mismatched END command".

start() now binds on the calling thread, which is premain for -javaagent,
before the app's main runs, and hands the bound socket to the accept
thread. A bind failure still leaves the app running without dedup: start()
logs it and returns false, and nothing escapes premain.

An END with no START for that test publishes nothing, yet was answered
ACK, so Keploy took it as delivered and waited for a record that never
came. The unix control socket now answers IGNORED. A Keploy that does not
know the reply reads any answer as taken, as it did before.

Signed-off-by: slayerjain <shubhamkjain@outlook.com>
The smoke harness waited for the control socket file before its first
command. That wait is the race Keploy loses: it fires tests as soon as the
app answers, with no such wait, so the smoke could not see a late bind.
START is now main's first act and nothing waits for the socket, so an agent
that binds it after premain fails the smoke with the jar that ships. The
smoke fails this way on JDK 8, 17 and 21 with the bind put back on the
accept thread.

The verify job built with -DskipTests, so the module's unit tests ran in no
pipeline. It now runs them on each JDK.

Signed-off-by: slayerjain <shubhamkjain@outlook.com>
On the TCP transport an END with no START for it was answered with a bare
ACK and no COV, so the collector could not tell the test had gone unmeasured.
Only the app's own log said so, in a pod the collector never reads. The unix
socket already answers IGNORED for the same END.

The SDK now sends "IGNORED <id>" ahead of the ACK. The ACK stays because it
is what ends the collector's wait: a collector that does not know the frame
skips it and goes on as before, where IGNORED in place of ACK would have cost
it a full ACK timeout on each such test.

Signed-off-by: slayerjain <shubhamkjain@outlook.com>
The agent binds its control socket in premain, so junixsocket's native
library now loads there, before the app's main. Whatever that throws,
premain must not stop the app: start() catches Throwable and runs the
app without dedup. Nothing tested that guard; narrowed to IOException,
it passed the unit tests and the smoke while a JVM that cannot load the
library aborted before main.

The smoke now runs an app with junixsocket's override properties
pointing at a library that does not exist, and checks that main ran and
that the agent said the control socket is unavailable, so the case
cannot pass with the library loaded.

Signed-off-by: slayerjain <shubhamkjain@outlook.com>
Keploy's agent reads an answer starting "IGNORED", and k8s-proxy's
collector a frame starting "IGNORED ", as a command the SDK did not
take. The rows compared the reply with the SDK's own constant, so a
change to the word passed them while both readers would take every
ignored END as taken. They now compare it with the literal.

Signed-off-by: slayerjain <shubhamkjain@outlook.com>
@slayerjain
slayerjain merged commit 8f92303 into main Nov 7, 2026
4 checks passed
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.

1 participant