[ISSUE #10658] Fix PopConsumerService.revive aborting the whole batch on a single failed record#10659
Conversation
… batch on a single failed record revive(PopConsumerRecord) chains getMessageAsync(record).thenCompose(...) with no exceptionally handler, and the batch revive(AtomicLong, int) awaits all per-record futures via CompletableFuture.allOf(...).join() with no surrounding try-catch. As a result a single failing record aborts the entire batch: writeRecords(failureList), deleteRecords(consumerRecords) and the currentTime advance are all skipped and revive() throws. If one record keeps failing (e.g. a persistently unreachable remote), revive gets stuck reprocessing the same batch forever, and the retries for the other healthy records in that batch are never persisted (head-of-line blocking). A record can fail in two ways: (1) the returned future completes exceptionally (e.g. reviveRetry throwing inside thenCompose, or a decode failure in the escape bridge), and (2) revive(record) throws synchronously before it returns a future (e.g. DefaultMessageStore.getMessageAsync is completedFuture(getMessage(...)) and getMessage throws). Handle both: add an exceptionally handler to revive(record) that logs and downgrades an async failure to a failed revive (returns false); and in the batch loop, catch a synchronous throw from revive(record) and turn it into completedFuture(false) instead of rethrowing and aborting the batch (the semaphore permit is released by the whenComplete stage, and acquire()'s InterruptedException is handled separately). Either way a single record's failure only affects that record, and writeRecords/deleteRecords/ currentTime still run. Add PopConsumerServiceTest#reviveShouldNotAbortBatchWhenGetMessageFailsExceptionally (async exceptional completion) and reviveShouldIsolateSynchronousReadFailureAndNotBlockHealthyRecords (synchronous throw plus head-of-line blocking); both fail before the fix and pass after.
RockteMQ-AI
left a comment
There was a problem hiding this comment.
Review by github-manager-bot
Summary
Fixes head-of-line blocking in PopConsumerService.revive(AtomicLong, int) where a single failing record would abort the entire batch via CompletableFuture.allOf(...).join(), preventing healthy records from being persisted and retried.
Findings
-
[Info]
PopConsumerService.java:564-569— The.exceptionally()handler correctly downgrades async read failures tofalse, routing the record through the existingfailureListbackoff-retry path. Clean approach that reuses existing retry infrastructure. -
[Info]
PopConsumerService.java:597-614— Good separation ofsemaphore.acquire()InterruptedExceptionhandling from therevive(record)sync-failure catch. Restoring the interrupt flag withThread.currentThread().interrupt()before wrapping inRuntimeExceptionis the correct pattern. -
[Info]
PopConsumerService.java:610-613— The sync-failure catch createsCompletableFuture.completedFuture(false)so the record enters thefailureListpath. The semaphore permit is correctly released by the downstreamwhenCompletestage sincecompletedFuturetriggers it immediately. -
[Info]
PopConsumerServiceTest.java— Two well-targeted tests covering both failure modes (async exceptionally and sync throw). Both verify that batch bookkeeping (writeRecords/deleteRecords/scanExpiredRecords) still runs after a failure, which is the key invariant this fix protects.
Assessment
This is a solid, minimal fix for a classic batch-processing head-of-line blocking problem. The error isolation is correct for both async and sync failure paths, semaphore management is sound, and the tests directly validate the fix. Scope is appropriately limited to the newer popkv path (popConsumerKVServiceEnable), not touching the legacy PopBufferMergeService.
Automated review by github-manager-bot
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## develop #10659 +/- ##
=============================================
- Coverage 48.35% 48.22% -0.14%
+ Complexity 13510 13468 -42
=============================================
Files 1380 1380
Lines 101044 101051 +7
Branches 13094 13094
=============================================
- Hits 48863 48728 -135
- Misses 46225 46340 +115
- Partials 5956 5983 +27 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
RockteMQ-AI
left a comment
There was a problem hiding this comment.
Review by github-manager-bot
Summary
Fixes PopConsumerService.revive so that a single failed record (async or sync) does not abort the entire revive batch. Adds an .exceptionally() handler to the per-record future chain and catches synchronous failures from revive(record) in the batch loop.
Findings
- [Critical]
PopConsumerService.java:564-568— The new.exceptionally()handler correctly catches async failures (e.g., remote read via escape bridge failing) and returnsfalseinstead of propagating the exception. This preventsallOf().join()from throwing and skipping thewriteRecords/deleteRecordscleanup. - [Warning]
PopConsumerService.java:597-608— The synchronous failure handling creates aCompletableFuture.completedFuture(false)as a fallback. This is correct, but note that thesemaphore.acquire()is now in a separate try-catch before the future creation. Ifrevive(record)throws synchronously, the semaphore permit was already acquired but the.whenCompletestage on the fallback future will release it. Verify that thethenAcceptcallback attached later (atfutureList.add(future.thenAccept(...))) also properly releases the semaphore in all paths. - [Info]
PopConsumerService.java:594-596— Good fix for theInterruptedExceptionhandling: properly restores the interrupt flag withThread.currentThread().interrupt()before throwing. - [Info] Tests are comprehensive:
reviveShouldNotAbortBatchWhenGetMessageFailsExceptionallyandreviveShouldIsolateSynchronousReadFailureAndNotBlockHealthyRecordsdirectly validate both failure modes.
Suggestions
Consider adding a brief comment on the fallback future explaining why it's needed (the synchronous throw path that bypasses the .exceptionally() chain).
Verdict
Solid fix for a critical reliability issue. The batch processing pattern is now resilient to both async and sync failures.
Automated review by github-manager-bot
Which Issue(s) This PR Fixes
Fixes #10658
Brief Description
In the popkv pop consumer (
PopConsumerService), the batchrevive(AtomicLong, int)awaits all per-record futures withCompletableFuture.allOf(...).join()and has no per-record error isolation, so a single failing record aborts the whole batch (writeRecords(failureList),deleteRecords(consumerRecords)and thecurrentTimeadvance are skipped, andrevive()throws). A persistently failing record therefore blocks the whole batch forever, and the healthy records' retries in that batch are never persisted (head-of-line blocking).A record can fail two ways, both handled here:
revive(PopConsumerRecord)gets an.exceptionally(...)handler that logs and downgrades the failure to a "failed revive" (false), so it goes through the existingfailureListbackoff-retry path instead of propagating throughallOf(...).join().revive(record)(e.g.getMessageAsyncthrowing before it returns a future) is caught and turned intocompletedFuture(false)instead of being rethrown and aborting the batch. The semaphore permit is released by thewhenCompletestage;acquire()'sInterruptedExceptionis handled separately.Either way a single record's failure only affects that record, and
writeRecords/deleteRecords/currentTimestill run.Note: this is the newer popkv pop path (
PopConsumerService, gated bypopConsumerKVServiceEnable), which is disabled by default; it does not touch the legacyPopBufferMergeService.How Did You Test This Change?
Added two tests in
PopConsumerServiceTest, both failing before the fix and passing after:reviveShouldNotAbortBatchWhenGetMessageFailsExceptionally— a single record whose read completes exceptionally; assertsrevivedoes not throw and the record is consumed (batch bookkeeping still runs).reviveShouldIsolateSynchronousReadFailureAndNotBlockHealthyRecords— two records, one whose read throws synchronously and one healthy; assertsrevivereturns 2 and both original records are consumed (scanExpiredRecordsreturns 0), i.e. the failed record is isolated and the healthy record is not head-of-line blocked.Full
PopConsumerServiceTestpasses (19/19), andmvn -pl broker validatereports 0 checkstyle violations.