Skip to content

Conversation

@drghdtjr
Copy link

In Pekko HTTP’s asynchronous execution model, multiple threads may access the same PekkoRouteHolder instance concurrently.
The class previously relied on a non-thread-safe StringBuilder and mutable fields without synchronization, which allowed concurrent route-matching operations to corrupt internal state.

Changes

Added synchronized to all methods that mutate internal state in PekkoRouteHolder.

Ensured that route path updates and unmatched path state updates occur atomically.

Technical Rationale (Alternatives Considered)

  • StringBuffer
    StringBuffer synchronizes individual method calls, but it does not guarantee atomicity across multiple dependent state updates. Since the route path and unmatched path must be updated together to maintain consistency, this approach was insufficient to fully resolve the race condition.

  • Immutable String
    Using immutable String objects was also considered. However, frequent string concatenation during Pekko route matching would result in excessive object creation and increased GC pressure, negatively impacting performance.

  • synchronized methods (chosen approach)
    Synchronizing state-modifying methods ensures consistency across related fields with minimal structural changes. Given that route matching is not a hot path with heavy contention, the performance impact is negligible while fully addressing the race condition.

issue : #15681

@laurit
Copy link
Contributor

laurit commented Jan 14, 2026

Were you able to reproduce the issue? I’d imagine that while synchronizing might fix the reported exceptions it is doubtful whether it is enough to ensure that the code works correctly.

@drghdtjr
Copy link
Author

Hi @laurit,

I have verified the fix with a multi-threaded regression test. Here are the core results:

The Bug Confirmed: Without synchronized, running 100,000 iterations with 50 threads consistently crashed with:

java.lang.ArrayIndexOutOfBoundsException: arraycopy: length -170 is negative

Reason: Concurrent access to ArrayDeque and StringBuilder corrupted internal memory pointers.

The Fix Verified: After applying synchronized, the same stress test passed 100% (100,000/100,000 iterations) without any failures.

Conclusion: This proves that PekkoRouteHolder must be thread-safe to handle Pekko's asynchronous model. All debug logs have been removed, and the test follows project standards.

Please take a look when you have a chance. Thanks!

  • without synchronized
스크린샷 2026-01-15 오전 2 35 11
  • with synchronized
스크린샷 2026-01-15 오전 2 35 21


@Test
void testConcurrency() throws InterruptedException {
/* PekkoRouteHolder holder = new PekkoRouteHolder();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

instead of commenting out the code, you could use a @Disabled annotation like here

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @jaydeluca,

I commented out the test because PekkoRouteHolder() is private, which causes a compilation error.

Is it the preferred practice in this project to change the visibility to package-private for testing, or should I keep it private and find another way?
Thanks!

ExecutorService executor = Executors.newFixedThreadPool(threadCount);
ConcurrentLinkedQueue<Throwable> errors = new ConcurrentLinkedQueue<>();

for (int i = 0; i < iterations; i++) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't really a meaningful test. It is purely a synthetic tests it is not certain that it actually represents the events that lead to the failure. If this test case represented the actual issue, which based on the test would be that the same PekkoRouteHolder instance is used by multiple requests, then adding the synchronization wouldn't really fix the issue. You could verify it in test by adding a call to holder.push and verifying the result of holder.route() after that. With just the added synchronization some other thread might alter the holder state between push and reading the route.

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.

3 participants