Fix concurrency permit leak causing deadlock in SimpleAsyncTaskExecutor #35708
+71
−1
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