feat: server-side availability oracle in the operator path (DD-030) - #73
Conversation
Design for bringing the server-side availability oracle to the operator path, where today Agent.premain is a no-op and no valve is mounted, so in-cluster findings are client-side only. The agent's premain instruments StandardHostValve.invoke via ByteBuddy (already a dep) to run the same begin/end + DD-029 two-state + /__basquin boundary the valve does, on any Tomcat image with zero operator plumbing. Builds on the DD-029 classes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LXSdVrJQy6DvELDbtptG5p
Spec refined during planning: (1) agent boundary is opt-in (default off) so the bench path (valve+agent) isn't double-instrumented, operator sets the flag; (2) RequestBoundary is Catalina-free (primitives in / decision out) because agent classes load on the boot loader, which can't see Catalina. Adds the task-by-task implementation plan. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LXSdVrJQy6DvELDbtptG5p
Reviewer finding on Task 1: LoadModeControl.handle() and LoadMode.isLoad() were unguarded ahead of the lock, so a future throw from either would propagate out of onEnter, violating its 'never throws' contract. Wrap that pre-lock section and degrade to LOAD_PASSTHROUGH on any Throwable, same as the existing beginIteration() guard.
…, valve reconciliation)
…oundaryAdvice The refactor to delegate to RequestBoundary dropped the try/catch around the setHeader loop, so a throw from setHeader/isCommitted would propagate out of invoke and fail a request that previously succeeded. Restore the best-effort guard to match the original valve and TomcatBoundaryAdvice.exit parity. Also tighten the DD-030 CHANGELOG entry: beginIteration throwing now degrades that one request to passthrough rather than propagating, which is a deliberate (documented) behavior difference, not "unchanged".
|
@claude please review — server-side oracle in the operator path via agent bytecode (DD-030). Note the two operational heads-ups in the description (rolling re-injection on upgrade; explore targets now serialize). |
ianp94
left a comment
There was a problem hiding this comment.
The design here is right — shared RequestBoundary with correct lock discipline (lock-before-begin, unlock-and-degrade on begin failure, headers computed before unlock), exact preservation of the valve's exception semantics, and the default-off opt-in that keeps the bench path single-boundary. The unit tests cover the state machine well.
But the in-cluster e2e — which this PR itself names as the integration proof — fails, and it exposes two real defects:
1. The driver JVM now crashes on NoClassDefFoundError: net/bytebuddy/matcher/ElementMatcher
Exception in thread "main" java.lang.NoClassDefFoundError: net/bytebuddy/matcher/ElementMatcher
at runner.coverage.CoverageGuidedRun.runSequence(CoverageGuidedRun.java:377)
The new ByteBuddy imports in Agent.premain mean that linking agent.Agent now requires ByteBuddy on the classpath (the verifier resolves the premain method's referenced types when the class links). The driver/runner bundles the agent classes but not ByteBuddy, so the first driver call into Agent kills the campaign — phase=Failed, coveragePct=<none>, no corpus ConfigMap; every campaign-stage e2e failure cascades from this one error.
Suggested fix: move the ByteBuddy installation into a separate class (e.g. agent.BoundaryInstaller) and invoke it reflectively from inside the flag check in premain (Class.forName("agent.BoundaryInstaller")...). Then Agent itself never references ByteBuddy types, the driver links it exactly as before, and a missing-ByteBuddy environment degrades gracefully inside the existing try/catch. Bundling ByteBuddy into the runner jar would also work but is heavier and fixes only this occurrence, not the coupling.
2. The agent boundary never installed on the target — /__basquin/* returns Tomcat's 404 page
drift=<!doctype html>...HTTP Status 404 – Not Found...Apache Tomcat/9.0.120
The app is healthy (200s, agents loaded, coverage endpoint up), so premain ran — but StandardHostValve was never instrumented. This is precisely the watch-item the PR description flags: Advice.to(TomcatBoundaryAdvice.class) failing to locate the advice class-file bytes for a class on the boot classpath. The e2e has now demonstrated the default lookup does not work in exactly the environment this PR exists to serve, so the documented ofSystemLoader() fallback needs to be the implementation, not a contingency — e.g.:
Advice.to(TypeDescription.ForLoadedType.of(TomcatBoundaryAdvice.class),
ClassFileLocator.ForClassLoader.ofSystemLoader())Please also grab the target pod's catalina stdout on the next e2e run to confirm which premain message fired (installed vs NOT installed: ...) — if it's the latter, the exception text will confirm the locator diagnosis; if premain printed installed yet requests still bypass the boundary, that's a different bug (matcher never firing) and worth knowing before re-pushing.
Minor, non-blocking
BasquinValve.invokereturns early on the control path without callingRequestBoundary.onExit, while the advice path does call it. Benign today (everyonEnterpath resets the thread-local), but callingonExitin both keeps the enter/exit contract uniform and the thread-local clean between pooled-thread requests.- Unshaded ByteBuddy on the boot classpath will shadow any app-bundled ByteBuddy version (parent-first delegation). Pre-existing, but worth a backlog note now that premain actually exercises it.
Happy to approve once the e2e is green — the two fixes above are both small and well-localized.
…oader The in-cluster e2e caught two integration bugs the unit/build checks can't: 1. The coverage-driver (runner) image bundles the agent classes but NOT ByteBuddy, so loading Agent (for client-side beginIteration) failed verification: NoClassDefFoundError net/bytebuddy/matcher/ElementMatcher. Fix: move the ByteBuddy install into a new BoundaryInstaller, called reflectively from premain, so Agent names no net.bytebuddy.* type. The runner loads Agent cleanly; BoundaryInstaller loads only in a target JVM (flag set), whose agent jar bundles ByteBuddy. 2. The target boundary silently no-op'd (/__basquin 404) even though 'installed' printed: ByteBuddy's default ClassFileLocator (derived from the advice class's boot loader) couldn't read the advice bytes from a -Xbootclasspath/a entry, so the StandardHostValve transform was swallowed. Fix: locate the advice via ClassFileLocator.ForClassLoader.ofSystemLoader() (the -javaagent jar is on the system classpath) + a boot fallback, and add an errors-only AgentBuilder listener so a future transform failure is loud, not silent. Verified: Agent.class has 0 bytebuddy refs; runnerJar bytebuddy-free with Agent+BoundaryInstaller; main jar bundles bytebuddy+both classes, no Catalina; full suite 95/95. Runtime instrumentation reproven by the in-cluster e2e. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LXSdVrJQy6DvELDbtptG5p
|
Re-reviewed after e9be5f5. Issue 1 is fixed and verified — the campaign now completes with non-zero coverage and an emitted corpus ConfigMap, so the reflective Issue 2 is still present: the re-run e2e still gets Tomcat's own 404 for The blocker for diagnosing further is that the e2e failure dump never shows the target pod's Basquin startup lines. The job log contains app exception stacks but no
Please add to $K -n "$NS" logs "$apod" -c jpetstore --tail=400 2>/dev/null | grep -iE 'basquin|bytebuddy' | sed 's/^/ /' || trueso the next run pinpoints which path fired. Worth also reproducing locally before the next push — If it turns out to be failure mode 3 (Catalina types unresolvable from the advice's pool at transform time), consider making the advice Catalina-free like Keeping my request-changes until the DD-030 e2e assertions are green. |
Round-2 e2e still 404'd on /__basquin: the errors-only listener (added last commit) revealed the real cause — NoClassDefFoundError: org/apache/catalina/ connector/Response during the StandardHostValve transform. Advice.to(Class) reflects on the boot-loaded advice Class, forcing its Request/Response parameter types to resolve via the BOOT loader, which can't see Catalina. Fix: describe the advice from a non-loaded TypePool built on the TARGET class's loader (Catalina's) inside the transform callback — that loader sees Catalina (its own) AND the advice (via its boot parent), so both resolve. Also set net.bytebuddy.experimental=true so images on a very recent JDK (24/25), which 1.14.12 otherwise rejects, still instrument. Reproduced locally against real Tomcat with the operator's exact injection (agent-only, no valve): /__basquin/drift -> 200 CSV and mode?to=load -> ok:load on Tomcat 9 (javax, JDK17 AND Java25) and Tomcat 10.1 (jakarta). Full suite 95/95. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LXSdVrJQy6DvELDbtptG5p
ianp94
left a comment
There was a problem hiding this comment.
Approving — all checks green, including the in-cluster e2e, whose DD-030 assertions now prove the whole chain: premain → reflective BoundaryInstaller → target-loader TypePool resolution → instrumented StandardHostValve serving /__basquin/drift (real CSV: heap/threads/epoch) and /__basquin/mode (ok:explore) on a valve-less operator target.
The three review rounds landed exactly the right fixes:
- Driver decoupled from ByteBuddy (
BoundaryInstallerreached only reflectively) — campaign runs are healthy again. - Advice resolved via the target class's loader — the cross-loader silent no-op is gone, and the errors-only listener ensures any future transform failure is loud.
- Valve header-write guard + doc fixes from the earlier nits.
Two small follow-ups worth a backlog note (non-blocking):
- Add the target-pod
grep -iE 'basquin|bytebuddy'log dump toe2e.sh's DD-030 failure path — this round trip would have been one run shorter with it. - Unshaded ByteBuddy on the boot classpath will shadow any app-bundled ByteBuddy (parent-first); now that premain exercises it, shading/relocation deserves a ticket.
Nice work on the turnaround — the final design (Catalina-free RequestBoundary, glue-only advice/valve, opt-in flag) is clean and well-documented.
What & why
The operator path had no server-side availability oracle. The operator injected
-javaagent:basquin-agent.jar, butAgent.premainwas a no-op stub and no valve was mounted — soAgent.beginIteration()/endIteration()were never called in the target JVM. In-cluster explore campaigns got real coverage (JaCoCo TCP from the target) but their heap/thread/latency findings came from the driver JVM (client-side), not the app. The project's core thesis — heap retention and thread/executor leaks inside the app under test — was inert in Kubernetes.This PR brings the server-side oracle (and DD-029 lock-free load) to the operator path.
How
The agent installs the request boundary itself via bytecode — no valve mount, no
context.xml/lib/surgery, works on any Tomcat image:agent/RequestBoundary.java(new) — the three-state boundary (/__basquincontrol · load passthrough · explore lock+begin/end), extracted from the valve. Catalina-free (agent classes load on the boot loader, which can't see Catalina), so it'sString-in / decision-out and unit-tested with no fakes.agent/TomcatBoundaryAdvice.java+Agent.premain(new) — a ByteBuddy advice inlined intoorg.apache.catalina.core.StandardHostValve.invoke, installed only when-Dbasquin.boundary=agent(default off).BasquinValve— refactored to delegate toRequestBoundary(bench/manual path; behavior preserved, namespace-free per DD-011).-Dbasquin.boundary=agent(it mounts no valve, so it opts in). One line./__basquin/drift(CSV) + mode control are live on the valve-less operator target — closing the loop the removed DD-029 assertion left open.Opt-in default-off matters: the bench path runs the valve and the agent; if the agent boundary were always on it would double-count. The operator opts in; the bench path doesn't.
-Dbasquin.boundary=agentis part ofbuildAgentArgs, which feedsspecHash, so every already-injectedBasquinTargetre-injects once on upgrade → a one-time rolling restart of instrumented Deployments. Expected and correct.ITERATION_LOCK(concurrency capped at 1) — the intended cost of trustworthy per-request server-side deltas (DD-005/010). Load mode (DD-029) stays lock-free.Verification
RequestBoundarystate machine (5 tests) + full JVM suite (95/95); valve stays namespace-free (javap= 0); agent jar bundles ByteBuddy, not Catalina.premaininstruments a real Tomcat and serves/__basquinfrom a valve-less operator target. (Known watch-item: the ByteBuddyClassFileLocatorfinding the boot-classpath advice bytes at premain is proven only by this e2e; a documentedofSystemLoader()fallback exists if it ever no-ops.)Depends on
The merged DD-029 work (#70) —
LoadMode/LoadModeControl.Spec:
docs/superpowers/specs/2026-07-21-operator-server-side-boundary-design.mdPlan:
docs/superpowers/plans/2026-07-21-operator-server-side-boundary.md🤖 Generated with Claude Code