[fix][client] Fix lookup permit double-release, waiting queue starvation and timeout-response races in ClientCnx#26143
Conversation
|
@congbobo184 |
Denovo1998
left a comment
There was a problem hiding this comment.
I think it's best to let the permit ownership follow the active entry in pendingRequests:
-
Only the path that successfully removes the active lookup from pendingRequests has the right to release or transfer the permit.
-
Timeout, lookup response, write failure, CommandError, and channel close all go through the same lookup cleanup primitive.
-
The helper uses remove(requestId, expectedFuture) and returns whether ownership was successfully obtained.
-
No longer guess whether the permit needs to be released based on the type of future exception.
-
Add permit invariant tests for failed lookup response, active request connection close, and promoted waiting request connection close.
This might resolve timeout starvation, double-release, and the unclear permit ownership issue for promoted waiting requests on certain exception paths at once.
… response handling and semaphore leaks in ClientCnx
@Denovo1998 The core change introduces a unified One implementation note: since For consistency, all response handlers that previously called The |
… response handling and semaphore leaks in ClientCnx
…wait condition in ClientCnx
|
@Denovo1998 PTAL, both comments have been addressed. |
Denovo1998
left a comment
There was a problem hiding this comment.
LGTM. Update the pr description.
@Denovo1998 |
main pr #25038
Motivation
PR #25038 fixed the semaphore leak issue by adding a
TimeoutExceptioncheck in thewhenCompletecallback ofnewLookup(). However, there are several remaining issues:Waiting queue starvation on timeout: When a lookup request times out in
checkRequestTimeout(), the code only callspendingRequests.remove()and relies on thewhenCompletecallback to release the semaphore. While the semaphore is correctly released,getAndRemovePendingLookupRequest()— which is responsible for polling and dispatching the next waiting request — is never invoked on the timeout path. If all concurrent lookup slots time out simultaneously (e.g., during a broker GC pause), waiting requests remain stuck indefinitely.Double-release of lookup semaphore: When a lookup receives a failed response,
getAndRemovePendingLookupRequest()releases the permit (or transfers it to the waiting queue). ThenhandleLookupResponse()completes the future withLookupException, which triggers thewhenCompletecallback registered inClientCnx.newLookup()to callpendingLookupRequestSemaphore.release()again — becauseLookupExceptionis one of the exception types that the callback explicitly releases for. This causesavailablePermits()to exceed its configured initial value.Unclear permit ownership for promoted waiting requests: When a request is promoted from the waiting queue, its permit ownership is ambiguous. If the promoted request's write fails or it times out, it's unclear which path is responsible for releasing the permit.
Race conditions between timeout and response paths: The timeout handler and response handler could concurrently process the same request, leading to spurious
duplicatedResponseCounterincrements and potential double-completion.Modifications
Let the permit ownership follow the active entry in
pendingRequests:Only the path that successfully removes the entry from
pendingRequestsowns the cleanup responsibility. IntroducedremovePendingRequest(requestId, expectedFuture)which usespendingRequests.remove(requestId, expectedFuture)as the single CAS contention point. Only the thread that wins the CAS can complete the future. For lookup requests, the winner additionally releases or transfers the permit.All pending request completion paths go through the same
removePendingRequestprimitive. Timeout, lookup response, write failure,CommandError, channel close, and all other response handlers (ack response, producer success, get schema, get topics, etc.) now callremovePendingRequestuniformly. Only the CAS winner can complete the future. For lookup requests specifically, the winner additionally callsreleasePermitAndDriveWaitingQueue()to either transfer the permit to the next waiting request or release it back topendingLookupRequestSemaphore.The helper uses
remove(requestId, expectedFuture)and returns whether ownership was successfully obtained. AddedpendingLookupRequestIds(aConcurrentHashMap-backed Set) to track which requestIds belong to lookup requests, so thatremovePendingRequestcan correctly identify and release lookup semaphore permits.No longer guess whether the permit needs to be released based on the type of future exception. Removed the
future.whenCompletecallback innewLookup()that previously inferred permit ownership from exception type (ConnectException/LookupException). Permit release is now exclusively handled insideremovePendingRequest.Added permit invariant tests for failed lookup response, active request connection close, and promoted waiting request connection close. New tests:
testFailedLookupResponseReleasesSemaphore,testActiveRequestConnectionCloseReleasesSemaphore,testPromotedWaitingRequestConnectionCloseReleasesSemaphore.This resolves timeout starvation, double-release, and the unclear permit ownership issue for promoted waiting requests on certain exception paths at once.
Verifying this change
This change added tests and can be verified as follows:
testLookupTimeoutReleasesSemaphore— verifies that the semaphore is properly released after a lookup timeout.testLookupTimeoutDrivesWaitingQueue— verifies that when a concurrent lookup times out, the next waiting request in the queue is dispatched.testMultipleLookupTimeoutsNoSemaphoreLeak— verifies that with multiple concurrent and queued lookups all timing out, every request eventually completes and no semaphore permits are leaked.testFailedLookupResponseReleasesSemaphore— verifies that a failedCommandLookupTopicResponsecorrectly releases exactly one permit without over-release.testActiveRequestConnectionCloseReleasesSemaphore— verifies that connection close correctly releases the permit for an active lookup request.testPromotedWaitingRequestConnectionCloseReleasesSemaphore— verifies that connection close correctly releases the permit for a promoted waiting request.Does this pull request potentially affect one of the following parts:
Local workflow:
geniusjoe#1