Skip to content

fix: harden classpath-warming lifecycle for expression evaluation#31

Merged
novoj merged 1 commit into
mainfrom
fix/classpath-warming-lifecycle
May 26, 2026
Merged

fix: harden classpath-warming lifecycle for expression evaluation#31
novoj merged 1 commit into
mainfrom
fix/classpath-warming-lifecycle

Conversation

@novoj
Copy link
Copy Markdown
Contributor

@novoj novoj commented May 26, 2026

Closes #30.

Hardens the per-connection classpath discovery/caching that backs expression evaluation, logpoints, conditional breakpoints, and watchers. Surfaced by an agent that mis-diagnosed a post-reconnect io cannot be resolved logpoint failure.

Changes

Problem 1 — first-hit discovery latency on the listener thread

The one-time classpath discovery (~1–3 s, walks the target classloader hierarchy) ran lazily on the first evaluate_expression / logpoint hit. Added a best-effort JdiExpressionEvaluator.prewarmClasspath() and wired JdiEventListener.listen() to warm on the first suspending thread (breakpoint/step/exception/watchpoint) once the hit is recorded. Never throws out of the listener. Safe to drive invokeMethod from here for the same reason logpoint evaluation already is.

Problem 2 — stale compiler config retained across disconnect/reconnect

InMemoryJavaCompiler.jdkPath/classpath were never reset, so a failed discovery on a fresh connection left configureCompilerClasspath returning silently and compile() proceeding against the previous target's classpath. Added InMemoryJavaCompiler.reset(), called in the rediscovery branch before discovery; a failed discovery now leaves the compiler unconfigured (compile() throws "not configured").

Problem 3 — silent discovery failure → cryptic JDT error

configureCompilerClasspath now throws JdiEvaluationException with an actionable message when JDK/classpath discovery fails, instead of logging at ERROR and returning void. All call sites already catch Exception: eval tools return it as an error string; logpoint/condition paths record it as LOGPOINT_ERROR.

Tests

  • InMemoryJavaCompilerConfigureTestreset() clears state and forces the not-configured error.
  • New JdiExpressionEvaluatorConfigureClasspathTest (Mockito) — throw-on-failure, reset-on-failure, never-configure-with-stale-state, short-circuit when cached, configure-on-success, prewarm swallows failures.
  • Full suite: 1013 pass, 0 fail, 2 skip; main compile NullAway-clean.

Three rough edges in the per-connection classpath discovery/caching that
made evaluation/logpoint failures slow and confusing (#30):

- Pre-warm the compiler classpath on the first suspending thread so the
  agent's first evaluate_expression / logpoint hit no longer pays the
  one-time ~1-3s discovery cost on its critical path. Best-effort and
  never throws out of the JDI listener.
- Reset InMemoryJavaCompiler config on reconnect (new reset()), called
  from configureCompilerClasspath before rediscovery, so a failed
  discovery cannot silently compile against the previous target's
  classpath. compile() now refuses with "not configured" instead.
- Surface discovery failure as an actionable JdiEvaluationException
  ("Classpath discovery failed ... application types cannot be resolved")
  instead of logging at ERROR and returning void — previously this
  manifested downstream as a cryptic JDT diagnostic (e.g. "io cannot be
  resolved"). All call sites already catch it: eval tools return it as an
  error string, logpoint/condition paths record LOGPOINT_ERROR.

Tests: InMemoryJavaCompilerConfigureTest gains reset() coverage; new
JdiExpressionEvaluatorConfigureClasspathTest covers throw-on-failure,
reset-on-failure, short-circuit-when-cached, configure-on-success, and
prewarm swallowing failures.
Copilot AI review requested due to automatic review settings May 26, 2026 12:26
Copy link
Copy Markdown

Copilot AI left a comment

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 hardens the expression-evaluation classpath discovery/config lifecycle (used by expression evaluation, logpoints, conditional breakpoints, and watchers) by adding a best-effort pre-warm on first suspension, resetting compiler state across reconnects, and surfacing discovery failures as actionable evaluation exceptions.

Changes:

  • Add InMemoryJavaCompiler.reset() and call it on (re)discovery to prevent stale compiler config from surviving disconnect/reconnect.
  • Make JdiExpressionEvaluator.configureCompilerClasspath(...) throw an actionable JdiEvaluationException when discovery fails, and add prewarmClasspath(...) that never throws.
  • Pre-warm the classpath from the JDI event listener on the first suspending event; add/extend tests for reset + discovery-failure semantics.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
jdwp-mcp-server/src/main/java/one/edee/mcp/jdwp/evaluation/JdiExpressionEvaluator.java Resets compiler/cache on rediscovery, throws on discovery failure, adds best-effort prewarm API.
jdwp-mcp-server/src/main/java/one/edee/mcp/jdwp/evaluation/InMemoryJavaCompiler.java Adds reset() to drop configured state after reconnects.
jdwp-mcp-server/src/main/java/one/edee/mcp/jdwp/JdiEventListener.java Warms classpath on first suspending event set to avoid first-eval latency.
jdwp-mcp-server/src/test/java/one/edee/mcp/jdwp/evaluation/InMemoryJavaCompilerConfigureTest.java Adds coverage for reset() behavior and post-reset “not configured” error.
jdwp-mcp-server/src/test/java/one/edee/mcp/jdwp/evaluation/JdiExpressionEvaluatorConfigureClasspathTest.java New tests for throw-on-failure, reset-on-failure, short-circuit, and prewarm swallow behavior.
Comments suppressed due to low confidence (1)

jdwp-mcp-server/src/main/java/one/edee/mcp/jdwp/evaluation/JdiExpressionEvaluator.java:1144

  • The short-circuit condition getDiscoveredJdkPath() != null is not sufficient to guarantee the compiler is actually configured. JDIConnectionService.discoverClasspath() sets discoveredJdkPath before it checks whether any application classpath entries were found (and returns null when the set is empty), so it’s possible for discoveredJdkPath to be non-null while the classpath is still undiscovered. In that state this method (and prewarmClasspath) will return early forever, leaving the compiler reset/unconfigured and preventing a retry. Consider short-circuiting only when both JDK and classpath are known/configured (e.g., track a “classpath configured” flag / expose cachedClasspath status / or ensure discoverClasspath clears discoveredJdkPath on failure).
        // Self-healing: if JDK path is already set, classpath is already configured for this connection
        if (jdiConnectionService.getDiscoveredJdkPath() != null) {
            return;
        }

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

@novoj novoj merged commit 41df444 into main May 26, 2026
1 of 2 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.

Classpath-warming lifecycle: first-hit latency, stale compiler config, and silent discovery failures

2 participants