Fix busy-spin in SubscriberInputStream await() - #37159
Open
flex-donggyu wants to merge 1 commit into
Open
Conversation
LockSupport.park() can return without resume() having set the parked thread reference to READY, either spuriously or when the thread is interrupted. The reference then still points to the current thread, so the compareAndSet in the await() loop can never succeed again and the loop neither parks nor exits, spinning on CPU until the upstream emits the next signal. On virtual threads this is particularly harmful since a spinning thread never yields its carrier. Once as many requests spin as there are carrier threads, unrelated virtual thread tasks are no longer scheduled. The interrupt case is reachable through Spring MVC async request handling, where both a timeout and a client disconnect cancel the task with interruption. Park again when the reference is still owned by the current thread after a spurious wakeup. On interruption, clear the reference and propagate the cancellation, unless data has arrived concurrently, in which case the data is delivered first and the interrupt status is preserved for the next call. Closes spring-projectsgh-37159 Signed-off-by: donggyu <donggyu@flex.team>
flex-donggyu
force-pushed
the
fix-subscriber-input-stream-await-spin
branch
from
August 20, 2026 08:17
582211e to
493fb15
Compare
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.
SubscriberInputStream.await()parks the reader withLockSupport.park()after storing the current thread inparkedThread. Whenpark()returns withoutresume()having setREADY— spurious wakeup or interrupt — the reference still points to the current thread, so the loop can neither exit nor park again (compareAndSet(null, ...)always fails). The thread spins at 100% CPU until the next upstream signal. And sincepark()doesn't consume the interrupt flag, every laterawait()call on that thread spins the same way.The interrupt case is easy to hit through Spring MVC async handling: both the async timeout and a client disconnect cancel the task with
Future.cancel(true)(CallableInterceptorChain#cancelTask). WithStreamingResponseBodyproxying a slow upstream, the worker is usually parked inawait()at that moment.On virtual threads a spinning thread never yields its carrier, so a few cancelled streaming requests can starve the whole scheduler — we traced a production outage (liveness probe kills) to this.
Reproduced on JDK 21 and 24: after the interrupt the thread stays
RUNNABLE, burning a full core (~13M loop iterations in 200ms).Fix
Applied to both near-duplicate classes (spring-web and spring-core), with a regression test that fails on the old code.
Related: #35978 fixed a separate defect in
resume()in this class; theawait()interrupt path wasn't covered by it.