Fix ByteBuffer concurrency issue in ConcurrentWebSocketSessionDecorator for binary messages #35519
+143
−9
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.
For reference, I'm not good at English, so I wrote this message using a translator. Please understand if my expressions are a bit awkward or if there are any issues.
📝 Summary
With Spring's growing adoption of virtual threads (Project Loom), concurrency levels in applications have increased dramatically. This exposed a critical thread-safety issue in
ConcurrentWebSocketSessionDecorator
when handling binary messages.The Problem: When multiple threads send the same
BinaryMessage
to different WebSocket sessions, they share a singleByteBuffer
whose position gets corrupted during concurrent reads, resulting in data corruption or empty messages.Why This Is a Trap: Developers using binary messages might not be aware that
ByteBuffer
's position is mutable state. It's natural to assume that sending the same message to multiple sessions is safe - after all, we're just "reading" the data. This hidden implementation detail makes this bug easy to accidentally introduce and particularly challenging to diagnose.I was also unaware of this implementation detail and only discovered it after spending two days debugging in production when concurrency levels increased dramatically. I hope this fix saves others from the same frustrating experience.
🔄 Changes
prepareMessage()
method to handle message preparationByteBuffer.duplicate()
forBinaryMessage
to ensure thread-safe position tracking✅ Testing
Added test
concurrentBinaryMessageSharingAcrossSessions()
that reproduces the issue and validates the fix.📊 Impact
🏗️ Why Fix in ConcurrentWebSocketSessionDecorator?
You might wonder why this fix was implemented in
ConcurrentWebSocketSessionDecorator
rather than the baseAbstractWebSocketSession
. The reasoning is architectural:Blocking I/O Handles This Differently: In traditional blocking I/O implementations (like Tomcat's
WsRemoteEndpointImplBase
), thesendMessageBlockInternal
method callspayload.clear()
after sending, resetting the buffer position. This automatic reset prevents position conflicts when reusing the sameBinaryMessage
in sequential operations.Concurrent Decorator's Purpose:
ConcurrentWebSocketSessionDecorator
was specifically designed to handle concurrent access scenarios. When multiple threads access the same message concurrently, there's no automatic buffer reset between operations. It's the appropriate layer to address these concurrency-related ByteBuffer position conflicts.Separation of Concerns: Keeping the fix in the decorator maintains clean separation - the base session handles core functionality while the decorator handles thread-safety concerns. The base implementation can rely on the underlying WebSocket container's behavior (like Tomcat's buffer clearing), while the decorator ensures safety in concurrent scenarios.
This design choice ensures the fix is applied exactly where it's needed - in concurrent scenarios - without adding overhead to single-threaded use cases where the container already handles buffer state management.
🎯 Why This Matters Now
As Spring applications increasingly adopt virtual threads:
This fix ensures WebSocket handling remains robust in the virtual thread era.
Note: This issue only manifests under high concurrency, particularly with virtual threads, making it challenging to detect in traditional testing scenarios.