[ISSUE #10827] fix(broker): spin for the lock on same-attemptId pop orderly retry to avoid empty response - #10828
Conversation
… pop orderly retry to avoid empty response An orderly retry carrying the same attemptId is an idempotent reentrant request, but the old logic fails fast with an empty response on group@topic lock contention. The retry then suspends in long polling, burns the only reentrant opportunity, times out, and the client rotates to a new attemptId, permanently losing reentrancy and blocking the queue head (up to invisibleTime, or ~3h when proxy autoRenew keeps extending nextVisibleTime). Fifo requests with a non-empty attemptId now spin-retry tryLock until the lock is acquired; other requests keep the fail-fast behavior. The lock holder always releases on pop completion, with the lock service's 2-minute expiry sweep as the worst-case backstop, so the spin cannot wait forever; same-attemptId contention is rare and the wait is normally milliseconds. Once the lock is acquired the existing re-pop path runs, fully preserving the reentrant semantics.
RockteMQ-AI
left a comment
There was a problem hiding this comment.
Summary
Reviewed the changes in this PR. Overall the changes look reasonable.
A few observations from the automated analysis:
- Please ensure adequate test coverage for the changes
- Verify backward compatibility if any public APIs are modified
Automated review by github-manager-bot
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## develop #10828 +/- ##
=============================================
- Coverage 48.32% 48.32% -0.01%
- Complexity 13516 13538 +22
=============================================
Files 1380 1380
Lines 101138 101165 +27
Branches 13120 13127 +7
=============================================
+ Hits 48876 48886 +10
+ Misses 46298 46292 -6
- Partials 5964 5987 +23 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
…emptId is registered in OrderInfo Fifo pops with different attemptIds would be blocked by checkBlock even after acquiring the lock, so spinning on contention only wastes request threads while the lock may be held for seconds on slow paths. Restrict the forced lock acquisition to requests whose attemptId is already registered in OrderInfo, i.e. genuine in-flight retries of a previous delivery of the same receive attempt. Add ConsumerOrderInfoManager.isAttemptIdMatched to look up the attemptId across the queues of the topic@group.
|
Thanks for fixing the lost-reentrancy path described in #10827. I traced the current head through POP entry, long-poll wakeup, ACK/change-invisible, lock cleanup, request lifetime, and persisted OrderInfo. The direction is reasonable, but the new synchronous wait couples several P1 correctness and availability risks, so I do not think the busy-spin implementation is safe to merge yet. P1: the unbounded spin can exhaust the shared Pull executor and outlive the request
The loop has no request deadline, channel-active, interruption, cancellation, or Broker-stop check. The timeout check in The premise that every holder releases is not true on all existing paths: after acquisition, a deleted or consume-disabled subscription group returns at Please replace the worker-thread busy-spin with bounded, cancellable asynchronous acquisition or a lock-release notification path. Enforce the original request deadline and channel/service state, and put every successful acquisition under one ownership guard. P1: timeout cleanup is not a safe backstop and can break mutual exclusionThe new comment at There is a second ownership problem:
This can concurrently mutate OrderInfo and consumerOffset across POP, ACK, and ChangeInvisible. The existing lock test even verifies removal while Please do not use removal of a held entry as lock handoff. Fix the leak paths and only remove safely idle entries, or introduce owner/epoch/fencing semantics so an old holder cannot commit or unlock a replacement. Add a regression with old holder + timeout cleanup + waiter + old unlock + third contender, asserting that the critical-section concurrency never exceeds one. P1: waiting consumes the invisible time before delivery
If the wait approaches or exceeds After bounded acquisition, recheck the request deadline and establish popTime at the actual delivery epoch before reading/updating OrderInfo. Add a test that holds the lock longer than invisibleTime and verifies that the returned receipt remains invisible for the full configured duration from actual delivery. P1: a stored matching string does not prove an active retry for the target queue
OrderInfo remains after all messages are ACKed or visibility expires, is cleaned only after up to 24 hours, and is persisted/restored across Broker restart. Therefore fully-ACKed, expired, restarted stale IDs, and an ID stored only on another queue can all enter the unbounded wait. The check is also outside the lock, so the current holder can ACK or replace the OrderInfo while the waiter is spinning. Please make the predicate queue-aware and state-aware: for an explicit queue, require a matching active unacked delivery on that queue; only Suggested validationThe current tests pass, but |
|
Follow-up on the P1 lock-wait issue: Thread.yield() cannot provide the progress guarantees this loop relies on. The JDK contract defines Thread.yield() only as a scheduler hint, and the scheduler is free to ignore it. It does not park the waiter, guarantee that the current lock holder will run, provide fairness, or impose any upper bound on acquisition time. Consequently, while (!tryLock(...)) { Thread.yield(); } can keep a pullMessageExecutor worker RUNNABLE indefinitely; with multiple same-attemptId retries, the scheduler may simply rotate among spinning waiters while they continue consuming CPU. It also adds no deadline, cancellation, interruption, disconnect, or shutdown handling. Please do not use yield as the synchronization/progress mechanism here. Prefer an explicit asynchronous unlock notification/handoff, or at minimum a bounded timed acquisition tied to the request deadline and lifecycle cancellation, followed by state revalidation after acquisition. A regression test should hold the lock beyond the request deadline and assert that the request completes without occupying the worker indefinitely and cannot mutate POP/OrderInfo state after expiration; a multi-waiter test should also verify that the shared executor is not exhausted. |
What is the purpose of the change
Fixes #10827.
An orderly (fifo / PopKv) retry carrying the same attemptId is an idempotent reentrant request, but
PopConsumerService.popAsyncfails fast with an empty response when the group@topic lock is contended. The retry then suspends in long polling, burns the only reentrant opportunity, times out, and the client rotates to a new attemptId — permanently losing reentrancy and blocking the queue head (up to one invisibleTime, or ~3h when proxy autoRenew keeps extending nextVisibleTime). See the issue for the full failure chain, verified step by step against code in a real incident (a patrol system consuming orderly messages through Proxy intermittently hung for a long time).Brief description of the change
Introduce
tryLockForPopinPopConsumerService: a fifo request whose attemptId is already registered in OrderInfo (i.e. a genuine in-flight retry of a previous delivery of the same receive attempt, checked via the newConsumerOrderInfoManager.isAttemptIdMatched) spin-retriestryLockuntil the lock is acquired instead of failing fast; all other requests keep the fail-fast behavior — pops with a different attemptId would be blocked by checkBlock even after acquiring the lock, so spinning would only waste request threads while the lock may be held for seconds on slow paths.Correctness rationale:
whenCompleteunlock), with the lock service's 2-minute expiry sweep as the worst-case backstop, so the spin cannot wait forever.A lock-free read-only replay of the previous OrderInfo batch was evaluated first; it works but is much larger, and further analysis showed the existing re-pop path is already self-consistent (invalidating the receipt handle of the lost delivery is normal at-least-once semantics) — the real defect is only that lock contention leaves the retry empty, hence this minimal fix (one new interface method with a small lookup, no config switch).
Does this pull request affect any existing functionality?
No. Only fifo requests whose attemptId is already registered in OrderInfo change behavior (spin instead of empty response on lock contention); all other requests keep fail-fast.
Verification
PopConsumerServiceLockRetryTest: spin until the lock is acquired and the pop path proceeds when the attemptId is registered; unregistered attemptId and non-fifo keep fail-fast.ConsumerOrderInfoManagerTestcoversisAttemptIdMatched. All 17 existingPopConsumerServiceTestcases pass, checkstyle clean, JDK 8 compile verified.