Learn / Practice
Java LLM practice (10): why is the reader still running after cancellation?
AI-APPS / BUILD · Engineering cases
30 local HTTP connections separate conversation invalidation, reader exit and server work, comparing Future interruption with socket closure.
What was published after this article?
The original conclusions and next-step proposals are retained. These published follow-ups do not imply that every limitation has been resolved.
KEEP EXPLORING
Related content
- Engineering cases
Java retries: three timeouts are not one deadline
Seven local HTTP conditions separate retryability, Retry-After, attempt limits and elapsed-time budgets.
View count unavailable
On this page 9
Pinned code · Raw evidence and audit · Experiment record
Stopping the UI does not necessarily stop the network reader. The previous study protected conversation state from late callbacks. This study replaces the in-memory stream with a real loopback HTTP connection and measures when the reader actually exits.
Three meanings of cancellation
| Layer | Desired effect | Evidence |
|---|---|---|
| Conversation | Old previews and answers cannot update state | Final Session history and events |
| Client transport | The blocked reader terminates | A timestamp and latch inside the worker's finally block |
| Server | Generation or computation stops | Work steps recorded by the local fixture |
A successful Future.cancel(true) attempts interruption; its cancelled status is not proof that the worker has terminated. Java 8 Future reference
A real connection with a deliberately narrow protocol
The Java 8 fixture binds a classic blocking Socket to loopback on an ephemeral port. It serves connection-close HTTP/1.1 with a text/event-stream body. It does not implement TLS, chunked encoding, proxies or HTTP/2. The A6 SSE parser and A7 Session are reused unchanged.
Five conditions × two policies × three repetitions produce 30 local connections: six normal controls and 24 cancellations/deadlines. Both policies invalidate the Session ticket, then call future.cancel(true). Only close-socket additionally closes the connection.
| Condition | Server behavior | Action |
|---|---|---|
| Complete | First text, stop, DONE | None |
| Cancel stalled | First text, then wait for cleanup | Cancel after first preview |
| Cancel dripping | Eight bounded work steps with requested 80ms sleeps | Cancel after first preview |
| Deadline stalled | First text, then silence | Cancel about 200ms after reader start |
| Deadline dripping | Continue eight work steps | Same deadline |
The idle read timeout is 500ms. An 80ms sleep request does not guarantee an exact send interval: scheduling and buffering matter. Recorded time, rather than eight times eighty, is the result.
A cancelled Future can leave a live reader
Time below runs from the beginning of the cancellation action to the worker's finally timestamp. Values are milliseconds: median and minimum–maximum of three observations.
| Condition | Interrupt only | Also close Socket |
|---|---|---|
| Cancel stalled | 507.211 (501.246–509.968) | 0.137 (0.119–0.389) |
| Cancel dripping | 747.739 (740.487–750.737) | 0.176 (0.173–0.181) |
| Deadline stalled | 310.534 (309.455–310.868) | 0.418 (0.402–0.710) |
| Deadline dripping | 532.356 (526.331–543.952) | 0.420 (0.316–0.536) |
The panels use different horizontal scales: compare numbers, not bar lengths. These are fixed local observations on Java 1.8.0_171, not an SLA or a claim about every Java HTTP client.
Interrupt-only stalled runs ended in SocketTimeoutException. Dripping runs read the whole answer, but Session rejected the late completion as STALE_COMPLETE. All twelve fault runs that closed the socket ended in SocketException. Closing a Socket wakes threads blocked in its I/O by throwing that exception. Java 8 Socket reference
Observe worker exit independently
After cancellation, future.get() can report cancellation before the worker returns. The fixture records exit inside the actual worker:
try {
String answer = reader.read(input, text -> session.preview(ticket, text));
session.complete(ticket, answer);
} finally {
exitedAt.set(System.nanoTime());
workerExited.countDown();
}The cancellation side invalidates state before attempting transport cleanup:
session.cancel();
future.cancel(true);
socket.close();
workerExited.await(5, TimeUnit.SECONDS);These are excerpts; the complete fixture also closes resources and joins its server thread. Neither I/O nor waiting belongs inside the Session lock, where it could prevent the cancelling thread from acquiring ownership of the state.
All 24 cancelled runs committed zero history. Each normal run committed one question/answer pair. Identical conversation correctness therefore concealed very different resource-release behavior.
Idle timeout is not a request deadline
A 500ms idle timeout limits a blocking read's wait for data. A stream that keeps supplying data can run much longer overall. The independent 200ms timer does not wait for another SSE event before firing.
This fixture starts its deadline after connection establishment and request writing. DNS, connect and request write are outside the measured boundary. A full-request deadline would need to propagate the remaining budget into those stages and handle cancellation racing with connection creation; this study does not implement that extension.
Client cleanup does not prove remote cancellation
The dripping fixture deliberately continues its eight bounded work steps after a failed write. Both policies therefore leave eight recorded server steps. This demonstrates a missing implication, not a provider policy: a closed client connection alone does not prove remote work has stopped.
No Agnes endpoint or billing data was used. A real provider's cancellation acknowledgement and billing behavior require its protocol and actual evidence. Separate state such as sessionInvalidated, readerExited and a future remoteCancellationAcknowledged is more honest than one overloaded flag.
Reproduce and inspect the evidence
In demos/10-transport-cancellation, prepare Java 8, Maven and Python. Adapt the local Maven executable path in run.py on another machine. The runner uses offline Maven; resolve declared dependencies first on a fresh installation.
python run.py --out evidence/my-run
python audit.py evidence/my-runUse a new output directory. results.json contains all 30 observations and Session events; environment.json identifies the JVM; manifest.json pins source and result hashes. The independent audit checks unique conditions, history counts, timestamp ordering and server steps. It intentionally does not use sub-millisecond exit as a pass threshold.
Inspect one interrupt-only dripping record. Together, futureCancelledAtAction, workerExitedAtAction, outcome and empty history show a cancelled Future, an active reader, a complete protocol response and a rejected stale commit.
What remains to test
This is a classic Socket teaching experiment. SDK connection pools, concurrent requests, completion/cancellation races and cancellation before connect need separate tests. The useful next step is to instrument the actual HTTP client, not turn this narrow fixture into a production SDK.
Fix follow-up · 2026-10-07
2026-10-07 correction: Demo08–10 now validate JSON lexical spelling; each passes 53 offline checks. Maven argument lists preserve output paths containing spaces, and Demo10 audits source fingerprints. The article’s pinned revision and original data remain unchanged; these fixes are not new online model results.
A note to your future selfBefore you move on, keep one thought of your own.
Your private reading note stays in this browser; it is never uploaded or published. Clearing browser data removes it, so export a copy to keep. This article’s translations share the same note.
Next published article:Does TypeScript validate model output? A 32-case comparison with Java →
You are here
BUILD / AI Applications
- ○ Java LLM practice (9): why can a cancelled answer overwrite a new conversation?
- ○ Java LLM practice (10): why is the reader still running after cancellation?
- ○ Does TypeScript validate model output? A 32-case comparison with Java
KEEP EXPLORING
Related content
- LAB / 027
Java LLM practice (10): why is the reader still running after cancellation?
30 local HTTP connections separate conversation invalidation, reader exit and server work, comparing Future interruption with socket closure.
- PROJECT / 002
Java LLM Practice
Reproducible Java conversation, retry and persistence boundaries, alongside attention, cache-cost and quantization-error experiments.
- Engineering cases
Java LLM practice (9): why can a cancelled answer overwrite a new conversation?
18 controlled thread schedules test request ownership for streaming previews, completion and errors, with cancellation and bounded deduplication.
- Engineering cases
Java LLM practice (8): should an interrupted stream enter conversation history?
22 loopback HTTP cases separate incremental preview from committing history, testing UTF-8, SSE framing, cancellation and abnormal endings.
Which way next?
- Java LLM practice (9): why can a cancelled answer overwrite a new conversation? →
18 controlled thread schedules test request ownership for streaming previews, completion and errors, with cancellation and bounded deduplication.
- Java LLM practice (8): should an interrupted stream enter conversation history? →
22 loopback HTTP cases separate incremental preview from committing history, testing UTF-8, SSE framing, cancellation and abnormal endings.
Based on article relationships and published same-topic content, not random recommendations.