Repository navigation
Conversation
A ResponseBodyControl call made off the event loop is queued and takes effect later on the loop, in the order the calls were queued. A suspend() queued from another thread could therefore take effect after a resume() that the event loop made in the meantime, for example from a body callback, and leave reads suspended with nothing left to resume them. Record a suspend request when suspend() is called and clear it when resume() is called. A queued suspend() is skipped if, when it would take effect, the most recent call was a resume(). A resume() is always applied, so it is never lost to a suspend() that a callback made after it was queued. Calls made on the event loop take effect before they return, as before. Calls made concurrently on different threads are not ordered beyond that. Document when control calls take effect. Claude Code on behalf of Matthias Kurz Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
11 of 15 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
ResponseBodyControl.suspend()queued from another thread no longer takes effect after aresume()that the event loop made in the meantime, which could leave a response suspended with nothing left to resume it.resume()is still always applied, so it is never lost to asuspend()that a callback makes after it was queued.Problem
NettyResponseBodyControlapplies a call made on the event loop before it returns, and queues a call made on any other thread to the event loop. Queued calls take effect in the order they were queued, without regard to calls the event loop made inline in the meantime.The stall:
suspend(), which is queued.resume(), which takes effect immediately.suspend()then runs and suspends reads again.Nothing calls
resume()after that, so the response stalls until the request timeout. The callback'sresume()was the latest decision, but the oldersuspend()overrode it.This pattern occurs in reactive consumers that suspend from whichever thread finds the buffer full. Play WS hit a variant of it, and avoids it today by suspending only on the event loop.
Change
suspend()records a suspend request before it runs or queues the actual suspension, andresume()clears that request.suspend()orresume()was a resume.resume()is always applied, and calls made on the event loop take effect before they return, as before.This is not "the latest call wins". Only suspensions can be skipped. Skipping a resume instead would reintroduce a lost-wakeup stall: a consumer takes the last buffered part and calls
resume(), a callback on an older view callssuspend()before that resume is applied, and the resume must still win. A test pins this case.Calls made concurrently on different threads are not ordered beyond these rules. For example, with suspend, resume and suspend queued from other threads, the first suspension still takes effect briefly, because the most recent call is a suspend when it runs; the queued resume and the second suspension then follow.
The
ResponseBodyControlJavadoc now states when calls take effect:resume()always takes effect;suspend()from another thread is skipped if the most recent call was a resume;Compatibility
No public API change. Behavior changes only for a
suspend()from another thread whose queued suspension is overtaken by aresume(): it no longer takes effect. All other call orders behave as before.Related
The separate pull request #2359, "feat: add ResponseBodyControl.execute to decide in sequence with body delivery", lets a consumer make its suspend/resume decision on the event loop in the first place. The two changes are independent and merge cleanly with each other.
AI disclosure
Claude Code on behalf of Matthias Kurz. The commit includes
Co-Authored-ByperAGENTS.md.Test plan
New tests, for HTTP/1.1, for HTTP/2, and as unit tests in
NettyResponseBodyControlTest(with Netty auto-read on and off):suspend()from another thread does not undo a laterresume();resume()from another thread is not lost to a latersuspend().NettyResponseBodyControlTestalso covers:suspend()after a skipped one still takes effect.The unit tests also check that the suspension start/end hooks stay balanced and that exactly one read is requested per resume.
Broken variants, each run against the 30 control tests:
main: 5 failures and 1 error (HTTP/2, by timeout). These are the tests that an earliersuspend()must not undo a laterresume(), and the test of a suspension after a skipped one.resume()is dropped for a latersuspend(): 3 failures and 1 error (HTTP/2, by timeout). These are the tests that aresume()must not be lost.The sequence test is not caught by either variant: it pins the documented outcome rather than a specific bug.
Runs:
ResponseBodyControlTest,Http2ResponseBodyControlTestandNettyResponseBodyControlTeston JDK 11: 30 tests passed../mvnw -B -ntp -Dgpg.skip=true clean verifyon JDK 11: 1,803 tests, 0 failures, 0 errors, 22 skipped; Revapi passed. No test-skipping flags were used.-Djvm): 0 failures on each.Cookieheaders on retries,ResponseBodyControl.execute, suspend/resume order, demand-bounded decompression): they merge without conflicts, together and in every pair, and./mvnw -B -ntp -Dgpg.skip=true clean installon JDK 11 passes with 1,853 tests, 0 failures, 0 errors, 22 skipped; Revapi passed.