[fix][broker] Fix forced topic/namespace deletion hanging or failing when compaction is in progress - #26016
Merged
Conversation
Forced topic/namespace deletion deadlocks with an in-flight compaction (issue apache#24148). PR apache#24366 broke the infinite wait by closing the compactor's reader with an unrecoverable error when the topic gets fenced for deletion, but the deletion still fails every time: asyncDeleteCursorWithCleanCompactionLedger chains the compaction cursor deletion with thenCompose on currentCompaction, so a compaction that completed exceptionally skips the cursor deletion and fails the unsubscribe instead. The failed future is never reset, so every subsequent deletion attempt fails immediately until the topic instance is reloaded, and deleteNamespaceWithRetry in tests times out (apache#22736). Proceed with the compaction cursor deletion once the compaction has finished, regardless of whether it completed normally or exceptionally. Also strengthen testConcurrentCompactionAndTopicDelete to assert that the deletion succeeded instead of only asserting that it completed. Assisted-by: Claude Code (claude-fable-5)
…d conflicting with PR apache#26015 Assisted-by: Claude Code (claude-fable-5)
merlimat
approved these changes
Jun 13, 2026
… a terminal state The CI failure on testForcedNamespaceDeleteWithInflightCompaction exposed a second cause of forced topic/namespace deletion hanging (issue apache#24148). RawConsumerImpl.receiveRawAsync() enqueued a pending receive without checking the consumer state. When a topic/namespace is force-deleted while a compaction is in flight, the compaction's RawReader closes on an unrecoverable error (TopicDoesNotExistException). If phaseTwoLoop issues readNextAsync() right after that close (it runs on a separate scheduler thread), the receive is enqueued after failPendingRawReceives() has already drained the queue, so the future never completes. The compaction future then never completes, and the cursor deletion in asyncDeleteCursorWithCleanCompactionLedger() - which waits on it - blocks forever, hanging the deletion. Re-check the consumer state after enqueueing in receiveRawAsync() and fail the pending receives if the consumer has reached a terminal state. Re-checking after the enqueue closes the race with a concurrent close(), which drains the queue only after moving to a terminal state. Assisted-by: Claude Code (claude-fable-5)
void-ptr974
approved these changes
Jun 13, 2026
void-ptr974
left a comment
Contributor
There was a problem hiding this comment.
Reviewed the patch and did not find a blocking issue. The failed-compaction handling in PersistentTopic and the terminal-state recheck in RawReaderImpl both look consistent with the deletion race being fixed.
LGTM.
lhotari
marked this pull request as draft
June 13, 2026 05:17
lhotari
marked this pull request as ready for review
June 13, 2026 05:53
2 tasks
10 tasks
3 tasks
sandeep-ctds
pushed a commit
to datastax/pulsar
that referenced
this pull request
Jul 31, 2026
…when compaction is in progress (apache#26016) (cherry picked from commit ec56aea)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #24148
Related: #22736
Motivation
Symptom (who is affected)
On a topic that uses compaction, a forced deletion can hang forever or fail on every retry:
admin.topics().delete(topic, true)never completes (or keeps failing), andadmin.namespaces().deleteNamespace(ns, true)never completes — it cascades into deleting the namespace's topics, and the always-compacted__change_eventssystem topic (plus any user topic with a compaction threshold) hits the same problem.This is the long-standing issue reported in #24148 and the flakiness in #22736 (e.g.
deleteNamespaceWithRetrytiming out). In production it shows up as a force-delete that hangs, or that fails on every attempt until the topic is unloaded/reloaded or the broker restarts.The trigger is a compaction that is running when the forced deletion fences the topic. Forcing deletion is expected to abort the in-flight compaction and then delete the topic; instead, the deletion gets stuck on the compaction.
Root causes
Two independent bugs can each wedge a forced deletion. They sit at different stages of the same flow, so depending on timing you hit one or the other.
A failed/aborted compaction permanently poisons the compaction-cursor deletion. When the deletion fences the topic, the compaction aborts and
currentCompactioncompletes exceptionally (the reader-close behavior from [fix][broker]Fix deadlock when compaction and topic deletion execute concurrently #24366). ButasyncDeleteCursorWithCleanCompactionLedger()chained the cursor deletion withcurrentCompaction.thenCompose(...), so an exceptional completion skipped the cursor deletion and failed the unsubscribe instead. That failedcurrentCompactionfuture is never reset (a new compaction can't start while the topic is deleting), so every subsequent delete attempt fails immediately until the topic instance is reloaded.The compaction's reader can hang, so
currentCompactionnever completes at all. The compactor reads the topic through an internalRawReader. When the forced deletion makes that reader hit an unrecoverable error and close, areadNextAsync()issued by the compactor at just the wrong instant was enqueued after the consumer's close had already drained its pending-receive queue — leaving that read future uncompleted forever.currentCompactionthen never completes, and the cursor deletion that waits on it blocks the entire forced deletion indefinitely.What about unloads?
Unloading a topic does not itself hang — unload does not wait on
currentCompaction. However, an unload/fence of a compacting topic is one of the events that can leave a compaction's reader in the hung state of cause 2 (the compaction reader is configured to treat "service not ready" as unrecoverable). The user-visible deadlock is on forced deletion, but the cause-2 fix applies no matter what drove the reader to close — a delete, a namespace delete, or an unload.Modifications
PersistentTopic.asyncDeleteCursorWithCleanCompactionLedger()— proceed with the compaction-cursor deletion oncecurrentCompactionhas finished, whether it completed normally or exceptionally (only an in-flight compaction is awaited). Fixes cause 1.RawReaderImpl.RawConsumerImpl.receiveRawAsync()— after enqueuing a pending receive, re-check the consumer state and fail the pending raw receives if the consumer is in a terminal state (Closing/Closed/Failed). This completes the read future deterministically once the consumer is terminal, regardless of why it closed. Fixes cause 2. Re-checking after the enqueue is what closes the race: every close path moves the consumer to a terminal state before it drains the pending-receive queue, so the recheck either observes the terminal state (and self-drains) or is ordered before the close's drain (which then covers it).testConcurrentCompactionAndTopicDeleteto assert the deletion actually succeeded (deleteTopicFuture.get(...)), instead of only asserting it completed (which is also true when it fails).CompactionTest.testForcedDeleteSucceedsAfterFailedCompaction— forced topic deletion after a deterministically failed compaction (cause 1).CompactionTest.testForcedNamespaceDeleteWithInflightCompaction— end-to-end forced namespace deletion with an in-flight compaction ([Bug] Compaction of __change_events topic is blocking forceful namespace/topic deletion #24148 reproduction).RawReaderTest.testReadNextAsyncCompletesAfterConsumerClosed— a read issued once the consumer is terminal must complete rather than hang (cause 2).Verifying this change
This change added tests and can be verified as follows:
RawReaderTest.testReadNextAsyncCompletesAfterConsumerClosed,CompactionTest.testForcedDeleteSucceedsAfterFailedCompaction, and the strengthenedtestConcurrentCompactionAndTopicDeletewas verified red without its corresponding fix and green with it.CompactionTest.testForcedNamespaceDeleteWithInflightCompactionand the fullCompactionTest/RawReaderTestsuites pass locally.Does this pull request potentially affect one of the following parts:
If the box was checked, please highlight the changes