Fix concurrency permit leak causing deadlock in SimpleAsyncTaskExecutor#35708
Merged
jhoeller merged 1 commit intospring-projects:6.2.xfrom Oct 30, 2025
Merged
Conversation
6dad156 to
5d53b4e
Compare
…TaskExecutor When concurrency limiting is enabled via setConcurrencyLimit() and thread creation fails in doExecute() (e.g., OutOfMemoryError from Thread.start()), the concurrency permit acquired by beforeAccess() is never released because TaskTrackingRunnable.run() never executes. This causes the concurrency count to permanently remain at the limit, causing all subsequent task submissions to block forever in ConcurrencyThrottleSupport.onLimitReached(). Root cause: - beforeAccess() increments concurrencyCount - doExecute() throws Error before thread starts - TaskTrackingRunnable.run() never executes - afterAccess() in finally block never called - Concurrency permit permanently leaked Solution: Wrap doExecute() in try-catch block in the concurrency throttle path and call afterAccess() in catch block to ensure permit is always released, even when thread creation fails. The fix only applies to the concurrency throttle path. The activeThreads-only path does not need fixing because it never calls beforeAccess(), so there is no permit to leak. Test approach: The test simulates thread creation failure and verifies that a subsequent execution does not deadlock. The first execution should fail with some exception (type doesn't matter), and the second execution should complete within timeout if the permit was properly released. Signed-off-by: Park Juhyeong <wngud5957@naver.com>
5d53b4e to
f30f01a
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.
Problem
When concurrency limiting is enabled via
setConcurrencyLimit(), if thread creation fails indoExecute()(for example,OutOfMemoryErrorfromThread.start()), the concurrency permit is never released, causing permanent deadlock.Root Cause
Execution flow when thread creation fails:
beforeAccess()incrementsconcurrencyCount(e.g., 0 → 1)doExecute()callsnewThread(task).start()Thread.start()throwsOutOfMemoryErrorTaskTrackingRunnableobject was created butrun()never executesfinallyblock containingafterAccess()never executesconcurrencyCountremains at 1 permanentlyImpact
After enough failures (equal to
concurrencyLimit), all subsequent task submissions permanently block:The executor becomes permanently deadlocked.
Solution
Why Only This Path?
The
else if (this.activeThreads != null)path does not need fixing:beforeAccess()(different code path)isThrottleActive()returnedfalse, soconcurrencyLimit == -1TaskTrackingRunnable.afterAccess()has guard:if (concurrencyLimit >= 0)concurrencyLimit == -1,afterAccess()becomes no-opTesting
The test reproduces the deadlock scenario:
concurrencyLimitto 1doExecute()to throwOutOfMemoryErrorexecute()call - should fail with some exceptionexecute()call - should not deadlock (the real test)Test approach: We don't care what exception the first call throws (could be
ErrororTaskRejectedException). The critical test is whether the second call completes without timeout, proving the permit was released.Before fix: Second call times out (deadlock)
After fix: Second call completes successfully
Backward Compatibility
Error→TaskRejectedException) provides better error context