[fix][broker] Race condition causes perpetual backlog on internal topics - #25572
Conversation
Topics with compaction enabled — most visibly `__change_events` system topics — accumulate an unbounded `__compaction` subscription backlog that is never drained, polluting backlog metrics across the cluster. The broker disconnects the compaction consumer as part of `PersistentSubscription.resetCursorInternal`, then resets the cursor and sends the success response. This races with the client: `channelInactive` fires on the consumer's `ClientCnx` and fails the in-flight seek future with `ConnectException` before the broker's success response arrives. `AbstractTwoPhaseCompactor.phaseTwoSeekThenLoop` has no retry for this transient failure, so every compaction attempt aborts and the `__compaction` subscription backlog grows without bound. This is most visible on `__change_events` system topics, where compaction is triggered on any non-zero backlog. The seek is idempotent and the cursor is already repositioned server-side, so retry the seek on `ConnectException` with a bounded backoff (100ms -> 1s, 10s budget). Non-transient failures propagate immediately. A unit test reproduces the race via an injection hook and asserts the compactor recovers. Fixes apache#25279 Signed-off-by: Darin Spivey <1874788+darinspivey@users.noreply.github.com>
The injection hook was a `UnaryOperator<CompletableFuture<Void>>`, which forced the call site to invoke `reader.seekAsync(from)` before the test could short-circuit. When the test returned a synthetic failed future, the real seek was still in flight on the underlying `ConsumerImpl`, and the retry's second `seekAsync` call hit "seek operation already in progress". Real disconnects don't produce this state because the failed seek clears the in-flight slot. Change the hook to `BiFunction<RawReader, MessageId, CompletableFuture<Void>>` defaulted to `RawReader::seekAsync`, so the test can return a synthetic failure without invoking the real seek. Signed-off-by: Darin Spivey <1874788+darinspivey@users.noreply.github.com>
|
@merlimat Thanks for your attention and approval on this PR. I've added a commit that hopefully fixes the test injection design for my new test. |
|
The remaining CI failures appear to be out of my control |
when CI fails due to flaky tests, you can add a comment "/pulsarbot rerun" to trigger a retry. |
|
/pulsarbot rerun |
|
Thanks for that tip on personal CI, @lhotari . I'll do that in the future. In the meantime, I meant to sync my fork's master branch, but accidentally sync'd the feature branch. CI will have to be kicked off again, sorry. I won't touch it from here on out. Good to know about "/pulsarbot rerun" too! |
…ics (apache#25572) Signed-off-by: Darin Spivey <1874788+darinspivey@users.noreply.github.com> (cherry picked from commit 60efea1) (cherry picked from commit 8d3b4ad)
…ics (apache#25572) Signed-off-by: Darin Spivey <1874788+darinspivey@users.noreply.github.com> (cherry picked from commit 60efea1) (cherry picked from commit 8d3b4ad)
…ics (apache#25572) Signed-off-by: Darin Spivey <1874788+darinspivey@users.noreply.github.com> Co-authored-by: Darin Spivey <1874788+darinspivey@users.noreply.github.com>
…ics (apache#25572) Signed-off-by: Darin Spivey <1874788+darinspivey@users.noreply.github.com> (cherry picked from commit 60efea1)
Motivation
Topics with compaction enabled — most visibly
__change_eventssystem topics — accumulate an unbounded__compactionsubscription backlog that is never drained, polluting backlog metrics across the cluster.Modifications
The broker disconnects the compaction consumer as part of
PersistentSubscription.resetCursorInternal, then resets the cursor and sends the success response. This races with the client:channelInactivefires on the consumer'sClientCnxand fails the in-flight seek future withConnectExceptionbefore the broker's success response arrives.AbstractTwoPhaseCompactor.phaseTwoSeekThenLoophas no retry for this transient failure, so every compaction attempt aborts and the__compactionsubscription backlog grows without bound. This is most visible on__change_eventssystem topics, where compaction is triggered on any non-zero backlog.The seek is idempotent and the cursor is already repositioned server-side, so retry the seek on
ConnectExceptionwith a bounded backoff (100ms -> 1s, 10s budget). Non-transient failures propagate immediately. A unit test reproduces the race via an injection hook and asserts the compactor recovers.Fixes #25279
Verifying this change
This change added tests and can be verified as follows:
Does this pull request potentially affect one of the following parts:
If the box was checked, please highlight the changes
NONE of these are affected with this change