Before Creating the Enhancement Request
Summary
Split out of the review discussion on #10659 (#10659 (comment)). This behavior pre-dates that PR and is left out of its scope on purpose.
When enablePopBufferMerge is on (default off), PopConsumerCache.run() periodically calls cleanupRecords(reviveConsumer), where reviveConsumer is PopConsumerService::revive used as a Consumer<PopConsumerRecord>:
- expired records staged in
removeTreeMap are handed to consumer.accept(record), and the returned CompletableFuture<Boolean> is discarded;
- these records are not added to
writeConsumerRecords, so they are not persisted;
clearStagedRecords() then unconditionally clears the whole staging map.
If the revive future completes exceptionally (for example reviveRetry throwing inside the chain), nobody observes the failure: the record has already been removed from the cache and was never persisted, so the message is never revived. The outer catch in run() only sees synchronous throws, which abort cleanupRecords before the clear and are retried on the next sweep; it cannot see exceptional future completions.
Motivation
A checkpoint of an un-acked POP message silently disappears on an asynchronously failed revive, which means the message is lost for the consumer. The batch revive path handles the same failure by writing a backoff-retry record; the cache path should not be weaker.
Describe the Solution You'd Like
Change the cache callback so it can observe the future: clear a staged record only after a successful revive, and retain it or persist it with backoff on false or exceptional completion. Add a regression test with enablePopBufferMerge=true and an asynchronously failed revive, verifying the checkpoint remains retryable.
Describe Alternatives You've Considered
Blocking on each future inside cleanupRecords before clearStagedRecords(). Simpler to reason about, but it serializes the sweep and changes the timing of the cleanup loop.
Additional Context
Verified semantics behind the analysis: a thenCompose lambda throwing on a completed upstream produces an exceptionally-completed future (no synchronous throw), and an unobserved exceptionally-completed future is silent in the JVM.
Before Creating the Enhancement Request
Summary
Split out of the review discussion on #10659 (#10659 (comment)). This behavior pre-dates that PR and is left out of its scope on purpose.
When
enablePopBufferMergeis on (default off),PopConsumerCache.run()periodically callscleanupRecords(reviveConsumer), wherereviveConsumerisPopConsumerService::reviveused as aConsumer<PopConsumerRecord>:removeTreeMapare handed toconsumer.accept(record), and the returnedCompletableFuture<Boolean>is discarded;writeConsumerRecords, so they are not persisted;clearStagedRecords()then unconditionally clears the whole staging map.If the revive future completes exceptionally (for example
reviveRetrythrowing inside the chain), nobody observes the failure: the record has already been removed from the cache and was never persisted, so the message is never revived. The outer catch inrun()only sees synchronous throws, which abortcleanupRecordsbefore the clear and are retried on the next sweep; it cannot see exceptional future completions.Motivation
A checkpoint of an un-acked POP message silently disappears on an asynchronously failed revive, which means the message is lost for the consumer. The batch revive path handles the same failure by writing a backoff-retry record; the cache path should not be weaker.
Describe the Solution You'd Like
Change the cache callback so it can observe the future: clear a staged record only after a successful revive, and retain it or persist it with backoff on
falseor exceptional completion. Add a regression test withenablePopBufferMerge=trueand an asynchronously failed revive, verifying the checkpoint remains retryable.Describe Alternatives You've Considered
Blocking on each future inside
cleanupRecordsbeforeclearStagedRecords(). Simpler to reason about, but it serializes the sweep and changes the timing of the cleanup loop.Additional Context
Verified semantics behind the analysis: a
thenComposelambda throwing on a completed upstream produces an exceptionally-completed future (no synchronous throw), and an unobserved exceptionally-completed future is silent in the JVM.