[fix] Fix cursor deadlock when skipping non-recoverable entries during mark-delete persistence - #26570
Merged
Conversation
Assisted-by: Codex/GPT-5
merlimat
approved these changes
Sep 13, 2026
lhotari
approved these changes
Sep 13, 2026
lhotari
left a comment
Member
There was a problem hiding this comment.
LGTM. Thanks for fixing this lock inversion. The iterable deletion keeps acknowledgement updates under the cursor write lock and releases it before mark-delete persistence acquires pendingMarkDeleteOps; the skipped range and final acknowledgement behavior remain unchanged.
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.
Motivation
ManagedCursorImpluses two locks in the mark-delete path:ReentrantReadWriteLockprotects the mark-delete position, read position, individual deletion ranges, and batch-index acknowledgement state.pendingMarkDeleteOpsmonitor protects the mark-delete persistence queue and cursor metadata-ledger transitions.Normal mark-delete persistence acquires these locks in this order:
Before this change,
skipNonRecoverableEntriesused the opposite order:This creates a deadlock when normal acknowledgement persistence and automatic skipping after a non-recoverable read failure run concurrently:
The write lock held by T2 prevents T1 from acquiring the read lock. T1 meanwhile owns the monitor required by T2, so neither thread can make progress. The cursor mark-delete position can then stop advancing and acknowledgement operations can remain incomplete.
Modifications
skipNonRecoverableEntries.asyncDeleteoverload in one call.asyncDelete, which already checksinternalIsMessageDeletedwhile holding the cursor write lock.The resulting sequence is:
The cursor write lock is therefore released before the mark-delete persistence path attempts to acquire
pendingMarkDeleteOps. This removes the nestedcursor lock -> pendingMarkDeleteOpsedge that completed the deadlock cycle. The final acknowledgement state is unchanged, while the range now results in one mark-delete persistence attempt instead of one attempt per entry.Verifying this change
This change adds a regression test that holds
pendingMarkDeleteOps, startsskipNonRecoverableEntries, waits until the skip thread reachesinternalAsyncMarkDelete, and verifies that the cursor read lock is still available. The old implementation fails this assertion because the skip thread retains the cursor write lock while waiting for the monitor.Verified with:
./gradlew :managed-ledger:checkstyleMain :managed-ledger:checkstyleTest :managed-ledger:test --tests org.apache.bookkeeper.mledger.impl.ManagedCursorTest.testSkipNonRecoverableEntries --tests org.apache.bookkeeper.mledger.impl.ManagedCursorTest.testSkipNonRecoverableEntriesDoesNotHoldCursorLockWhilePersistingDoes this pull request potentially affect one of the following parts:
The threading-model impact is limited to removing nested cursor-lock acquisition from the non-recoverable-entry skip path.