Skip to content

Capture backend server logs for conformance sampling flake#5867

Draft
jhrozek wants to merge 10 commits into
mainfrom
conformance-no-debug-probe
Draft

Capture backend server logs for conformance sampling flake#5867
jhrozek wants to merge 10 commits into
mainfrom
conformance-no-debug-probe

Conversation

@jhrozek

@jhrozek jhrozek commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Summary

The tools-call-sampling conformance scenario intermittently times out at
60s in CI. This PR started as a probe to drop --debug (on the theory that
debug logging masked the flake), but the debug proxy log from PR #5839's
failing run disproves that theory and redirects the investigation:

  • Add stateless routing to streamable MCP proxy #5839 failed with --debug on — debug does not reliably mask it.
  • The debug trace localizes the stall precisely: after initialize,
    notifications/initialized, the tools/call POST (SSE stream) and the
    standalone GET SSE stream all succeed, the session goes silent for 58s
    (only the 10s health pinger runs), and the client never sends its
    sampling-response POST
    . i.e. the client never received the server's
    sampling/createMessage request.
  • The backend stays healthy throughout; no proxy error is logged.

So --debug is our best signal, not a mask. This PR therefore:

  1. Restores --debug (reverts this branch's original change).
  2. Adds capture of the Node backend container's own logs to the
    conformance harness cleanup(). The proxy log cannot show whether the
    backend emitted sampling/createMessage (SSE bodies are not logged), so
    the backend log is the one missing artifact that splits the two remaining
    hypotheses:
    • backend emitted it -> proxy/SSE-relay race (ToolHive bug)
    • backend never emitted it -> backend-side (ToolHive exonerated)

The new files (backend-logs.txt, backend-docker-logs.txt) land in
test/conformance/results/ and are uploaded with the existing
conformance-results artifact automatically.

Type of change

  • Test / CI infrastructure

Test plan

  • Verified the failing Add stateless routing to streamable MCP proxy #5839 run's conformance-results artifact:
    sampling checks.json shows the 60s -32001 timeout; proxy-logs.txt
    (debug) shows the 58s silence and the abandoned session.
  • Compared against a passing run (29699112641): continuous ~15s suite,
    no gap, round-trip completes inline.
  • Next CI flake on this branch should now also capture the backend logs.

Generated with Claude Code

DRAFT probe, not for merge. The tools-call-sampling conformance flake stopped
appearing on runs that carry `thv run --debug` (4/4 debug runs passed vs a >50%
base fail rate), suggesting the debug log I/O perturbs the timing race and masks
the failure.

Drop `--debug` while keeping the proxy-log capture (at default INFO level) to
test that hypothesis: if this reliably fails tools-call-sampling again, --debug
was masking it, and the captured INFO-level proxy logs (which still record a
session/-32001 event) give a non-perturbing root-cause path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@github-actions github-actions Bot added the size/XS Extra small PR: < 100 lines changed label Jul 17, 2026
@codecov

codecov Bot commented Jul 17, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 71.52%. Comparing base (38b03b6) to head (6c11633).
⚠️ Report is 14 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #5867      +/-   ##
==========================================
+ Coverage   71.28%   71.52%   +0.23%     
==========================================
  Files         693      695       +2     
  Lines       70572    71005     +433     
==========================================
+ Hits        50307    50785     +478     
+ Misses      16633    16566      -67     
- Partials     3632     3654      +22     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Debug logs from PR #5839's failing run showed --debug does NOT mask the
sampling flake: the run failed with --debug on, and the trace localized
the stall to a missing server->client sampling/createMessage delivery
(the client never sends its sampling-response POST). So restore --debug
(it is our best signal, not a mask) and add the one artifact still
missing: the Node backend container's own logs, which reveal whether the
backend emitted the sampling request (-> proxy relay race) or not
(-> backend-side). SSE bodies are not in the proxy log, so this is the
only way to split those two legs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@github-actions github-actions Bot added size/XS Extra small PR: < 100 lines changed and removed size/XS Extra small PR: < 100 lines changed labels Jul 20, 2026
@jhrozek jhrozek changed the title Probe: drop --debug from conformance proxy (test flake masking) Capture backend server logs for conformance sampling flake Jul 20, 2026
@github-actions github-actions Bot added size/XS Extra small PR: < 100 lines changed and removed size/XS Extra small PR: < 100 lines changed labels Jul 20, 2026
The backend/proxy logs cannot show whether the backend actually emitted the
server->client sampling/createMessage on the SSE stream (SSE bodies are not
logged, and the Node reference server may not log its own writes). A packet
capture shows the bytes on both legs and definitively splits the remaining
fork: sampling/createMessage present on backend->proxy but absent on
proxy->client => ToolHive relay bug; absent on both => backend never emitted.

Start tcpdump before the suite and stop it in cleanup(). `-i any` covers the
loopback and docker-bridge legs (the backend is a docker-published port via
docker-proxy); full snaplen is required to see SSE payloads; -U flushes per
packet. Best-effort and non-fatal when tcpdump/passwordless sudo are absent
(local macOS). The pcap lands in the results dir already uploaded as an
artifact, so no workflow change is needed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@github-actions github-actions Bot added size/XS Extra small PR: < 100 lines changed and removed size/XS Extra small PR: < 100 lines changed labels Jul 20, 2026
The pcap showed the backend never emits sampling/createMessage and that
ToolHive fronts the workload with a Squid ingress proxy (createIngressSquidContainer)
plus the Go transparent proxy. It is unresolved whether that proxy stack causes
or merely aggravates the client-ordering race that starves the server->client
sampling stream.

Add compare-sampling.sh: run the conformance suite N times against the
reference container DIRECTLY (no thv) vs THROUGH the thv stack (Go proxy ->
Squid ingress), and tally the tools-call-sampling failure rate. If it only
flakes through the stack, ToolHive owns it (likely the Squid ingress). Wire it
as a diagnostic step in the conformance job (draft-only) and bump the job
timeout to 30m to accommodate the extra runs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@github-actions github-actions Bot added size/S Small PR: 100-299 lines changed and removed size/XS Extra small PR: < 100 lines changed labels Jul 20, 2026
The direct-vs-stack comparison proves the ToolHive stack triggers the
sampling flake but can't say whether the Squid ingress or the Go
transparent proxy widens the tools/call-vs-GET-SSE race. Add a middle
tier that hits the ingress Squid's published host port directly, dropping
the Go proxy from the path, so a failing CI run localizes the culprit:
DIRECT green + SQUID flaky => Squid; SQUID green + STACK bad => Go proxy.
Switch the tally to the single tools-call-sampling scenario so three
tiers stay affordable in CI.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@github-actions github-actions Bot added size/S Small PR: 100-299 lines changed and removed size/S Small PR: 100-299 lines changed labels Jul 20, 2026
The 3-tier comparison ran isolated `--scenario tools-call-sampling`, which
completes in ~1s and never reproduces the 60s hang (the flake only manifests
in the full-suite context), and its result grep matched the suite-summary
format, so every tier reported UNKNOWN (0/N). Run the full `active` suite per
iteration (which does reproduce), keep the suite-summary grep, and drop N to 4
so 3 tiers fit the job timeout.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@github-actions github-actions Bot added size/S Small PR: 100-299 lines changed and removed size/S Small PR: 100-299 lines changed labels Jul 20, 2026
The flake only reproduces on the FIRST suite run against a freshly created
stack; the previous comparison reused one warm stack across iterations and
so came back all-green (inconclusive). Give every iteration its own fresh
stack, and measure SQUID and STACK from SEPARATE fresh stacks (sharing one
would leave the second tier warm and hide the flake). This is what lets CI
localize whether the Squid ingress or the Go transparent proxy widens the
tools/call-vs-GET-SSE race. Costs 2N thv runs; N defaults to 3 to fit the
30m job budget.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@github-actions github-actions Bot added size/S Small PR: 100-299 lines changed and removed size/S Small PR: 100-299 lines changed labels Jul 20, 2026
The tools-call-sampling conformance flake was localized to the ingress
Squid: on a cold stack the first GET /mcp SSE stream pays inline DNS + TCP
connect latency to the upstream while a later tools/call POST reuses a
warmed path, so the POST reaches the server and runs its handler before
the standalone GET SSE stream is established. A server-initiated request
(sampling) then has no stream to ride and the client times out at 60s.

Add standby=2 to the ingress cache_peer so Squid keeps warm idle upstream
connections; the first GET is forwarded without inline connect latency,
removing the GET-vs-POST reorder. Config-only, no ACL change. Bump the
diagnostic compare to N=8 to validate the SQUID tier goes green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@github-actions github-actions Bot added size/S Small PR: 100-299 lines changed and removed size/S Small PR: 100-299 lines changed labels Jul 20, 2026
On a cold start, the transparent proxy forwarded requests over a bare
shared http.DefaultTransport whose per-host idle pool starts empty, so
the first upstream connection paid inline DNS+TCP dial latency. The
client's standalone GET (SSE stream) and tools/call POST are forwarded
on separate connections, so a cold dial on the GET could delay stream
establishment past the point where the backend's tool handler sends a
server-to-client request on it, causing a client timeout.

Clone http.DefaultTransport (never mutate the shared global) with a
small idle pool, and pre-warm it at startup with a burst of
side-effect-free GET requests to the upstream. This is the Go-proxy
analog of the Squid ingress cache_peer standby=2 fix for the
squid->backend hop.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@github-actions github-actions Bot removed the size/S Small PR: 100-299 lines changed label Jul 20, 2026
@github-actions github-actions Bot added the size/S Small PR: 100-299 lines changed label Jul 20, 2026
@github-actions github-actions Bot added size/S Small PR: 100-299 lines changed and removed size/S Small PR: 100-299 lines changed labels Jul 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/S Small PR: 100-299 lines changed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant