Skip to content

[fix][broker] Race condition causes perpetual backlog on internal topics - #25572

Merged
merlimat merged 3 commits into
apache:masterfrom
darinspivey:issue_25279
May 1, 2026
Merged

[fix][broker] Race condition causes perpetual backlog on internal topics#25572
merlimat merged 3 commits into
apache:masterfrom
darinspivey:issue_25279

Conversation

@darinspivey

@darinspivey darinspivey commented Apr 23, 2026

Copy link
Copy Markdown
Contributor

Motivation

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.

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: 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 #25279

Verifying this change

  • Make sure that the change passes the CI checks.

This change added tests and can be verified as follows:

  • Added an integration test following existing styles for testing the race condition that happens and the implemented fix for retrying

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

  • Dependencies (add or upgrade a dependency)
  • The public API
  • The schema
  • The default values of configurations
  • The threading model
  • The binary protocol
  • The REST endpoints
  • The admin CLI options
  • The metrics
  • Anything that affects deployment

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>
@darinspivey

Copy link
Copy Markdown
Contributor Author

@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.

@darinspivey

Copy link
Copy Markdown
Contributor Author

The remaining CI failures appear to be out of my control

@lhotari

lhotari commented Apr 30, 2026

Copy link
Copy Markdown
Member

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.
For early stage PRs, it's worth setting up "Personal CI" where you have full control.

@lhotari

lhotari commented Apr 30, 2026

Copy link
Copy Markdown
Member

/pulsarbot rerun

@darinspivey

Copy link
Copy Markdown
Contributor Author

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!

@merlimat
merlimat merged commit 60efea1 into apache:master May 1, 2026
43 checks passed
merlimat pushed a commit that referenced this pull request May 1, 2026
…ics (#25572)

Signed-off-by: Darin Spivey <1874788+darinspivey@users.noreply.github.com>
Co-authored-by: Darin Spivey <1874788+darinspivey@users.noreply.github.com>
(cherry picked from commit 60efea1)
merlimat pushed a commit that referenced this pull request May 1, 2026
…ics (#25572)

Signed-off-by: Darin Spivey <1874788+darinspivey@users.noreply.github.com>

(cherry picked from commit 60efea1)
@darinspivey
darinspivey deleted the issue_25279 branch May 4, 2026 14:11
manas-ctds pushed a commit to datastax/pulsar that referenced this pull request May 5, 2026
…ics (apache#25572)

Signed-off-by: Darin Spivey <1874788+darinspivey@users.noreply.github.com>

(cherry picked from commit 60efea1)
(cherry picked from commit 8d3b4ad)
srinath-ctds pushed a commit to datastax/pulsar that referenced this pull request May 5, 2026
…ics (apache#25572)

Signed-off-by: Darin Spivey <1874788+darinspivey@users.noreply.github.com>

(cherry picked from commit 60efea1)
(cherry picked from commit 8d3b4ad)
poorbarcode pushed a commit to poorbarcode/pulsar that referenced this pull request May 6, 2026
…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>
nodece pushed a commit to ascentstream/pulsar that referenced this pull request May 27, 2026
…ics (apache#25572)

Signed-off-by: Darin Spivey <1874788+darinspivey@users.noreply.github.com>

(cherry picked from commit 60efea1)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug][compaction] Compaction phase 2 always fails with ConnectException because seek races with consumer disconnect

3 participants