Skip to content

[CXF-8926] replace PipedInputStream with TimedBlockingPipe to fix MTO… - #3250

Open
ffang wants to merge 1 commit into
apache:mainfrom
ffang:CXF-8926
Open

[CXF-8926] replace PipedInputStream with TimedBlockingPipe to fix MTO…#3250
ffang wants to merge 1 commit into
apache:mainfrom
ffang:CXF-8926

Conversation

@ffang

@ffang ffang commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

…M attachment stall

Summary

Fixes CXF-8926: a CXF client sending a large request body (e.g. a MTOM attachment) to a slow or stalled server hangs indefinitely with no way to recover.

Root cause

Two conduits are affected by the same underlying problem — the CXF writer thread can be permanently blocked on a request-body write with no effect from receiveTimeout:

HttpClientHTTPConduit — CXF serializes the request body into a PipedOutputStream; the JDK HttpClient drains the connected PipedInputStream on a cached thread-pool thread. When the server reads slowly (or not at all), OS TCP send/receive buffers fill, the HttpClient thread stalls inside the pipe, and PipedOutputStream.write() blocks in awaitSpace() forever. Compounding this, after ~60 s of inactivity the JDK cached-pool idle-reaps the draining thread; the next write then throws java.io.IOException: Read end deadCould not write attachments even though the server never closed the connection.

URLConnectionHTTPConduit — writes directly to HttpURLConnection$StreamingOutputStream; when OS TCP buffers fill the socket write blocks indefinitely. No pipe or thread-reap is involved — it simply hangs.

In both cases receiveTimeout has no effect because it only governs the HTTP response read timeout, not the request-body write path.

Fix

HttpClientHTTPConduit: replace PipedInputStream / PipedOutputStream with a new TimedBlockingPipe — a purpose-built, lock-based circular byte buffer. Key differences from the JDK pipe:

  • No thread-identity check: the JDK idle reap is invisible to it — the write just blocks until the reader resumes or the deadline expires.
  • Write timeout: when receiveTimeout > 0, writes fail with a bounded IOException instead of hanging forever; when receiveTimeout = 0, writes block indefinitely (same as before, but without the Read end dead risk).

URLConnectionHTTPConduit: interpose the same TimedBlockingPipe between the CXF writer and the socket output. A background daemon thread drains the pipe and pushes bytes to the socket; close() on the pipe side waits for the copier to finish before handleResponse() reads the reply.

Tests added (MTOMAttachmentStallTest)

Test Scenario Conduits exercised
testMtomClientDoesNotHangWhenNetworkStalls Raw ServerSocket accepts but never reads; OS TCP buffers saturate Both (default + forceURLConnection)
testMtomClientSucceedsWhenServerResumesAfterStall Real CXF/Jetty server stalls ~75 s mid-stream then resumes; client must complete successfully HttpClientHTTPConduit

testMtomClientDoesNotHangWhenNetworkStalls sets receiveTimeout = 5 s and asserts the client unblocks within 20 s.
testMtomClientSucceedsWhenServerResumesAfterStall sets receiveTimeout = 0 (infinite) and asserts the call succeeds once back-pressure releases — proving TimedBlockingPipe survives the stall that previously caused Read end dead.

@ffang
ffang requested a review from reta July 2, 2026 13:11
@reta

reta commented Jul 22, 2026

Copy link
Copy Markdown
Member

@ffang my apologies, I haven't forgotten about this one, just too many things, I will get to it shortly 🤞

@ffang

ffang commented Jul 22, 2026

Copy link
Copy Markdown
Contributor Author

@ffang my apologies, I haven't forgotten about this one, just too many things, I will get to it shortly 🤞

Thanks @reta , no rush, take your time.

@reta

reta commented Aug 21, 2026

Copy link
Copy Markdown
Member

Sorry @ffang , just found time to get back to it, I am wondering why the request / socket timeouts do not trigger in this case? Did we miss something there? I would expect HttpClient / URLConnection to close the pipe (and stream consequently) upon timeout?

@ffang

ffang commented Aug 21, 2026

Copy link
Copy Markdown
Contributor Author

Sorry @ffang , just found time to get back to it, I am wondering why the request / socket timeouts do not trigger in this case? Did we miss something there? I would expect HttpClient / URLConnection to close the pipe (and stream consequently) upon timeout?

Good question @reta — it's not something we missed, it's a genuine gap in the JDK APIs IMHO(no timeout for request-body write path). connectionTimeout only guards the pre-connect handshake (isConnectionAttemptCompleted); once connectionComplete is true it's never consulted again. receiveTimeout maps to HttpRequest.Builder.timeout() / HttpURLConnection.setReadTimeout(), both of which the JDK scopes strictly to reading the response — neither java.net.http.HttpClient nor HttpURLConnection/Socket expose any write/send-side timeout (Socket only has SO_TIMEOUT for reads, there's no SO_SNDTIMEO equivalent).

Concretely, in HttpClientHTTPConduit the request body publisher drains a PipedInputStream via a plain blocking read on a cached-pool thread, entirely outside HttpClient's async exchange/selector machinery — so even if the response timeout were to fire, it has no handle to interrupt that pipe read; the exchange hasn't reached the response-wait stage yet. In URLConnectionHTTPConduit it's more direct: writes go straight to the connection's raw socket OutputStream, which the JDK never times out under any configuration.

TimedBlockingPipe closes that gap by giving the write side its own deadline, reusing receiveTimeout's value since that's the property users already configure — rather than inventing a new config property for something that's really the same "how long am I willing to wait for this exchange" intent.

Best Regards
Freeman

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.

2 participants