Repository navigation
[fix][broker] Prevent automatic TTL expiry from skipping reader messages - #26505
Merged
Merged
Conversation
Readers attach through non-durable subscriptions whose cursor is the reader's own read position. The automatic TTL expiry task advances that cursor's mark-delete position, which drags the read position forward and silently skips unread retained messages. Exclude non-durable subscriptions from both automatic expiry paths. Durable subscription expiry and explicit administrative expiry are unchanged. Fixes apache#26504
lhotari
requested review from
Technoboy-,
dao-jun,
david-streamlio,
merlimat and
nodece
September 8, 2026 20:54
merlimat
approved these changes
Sep 9, 2026
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 #26504
Motivation
A
Readerattaches to a topic through a non-durable subscription, and the cursor of that subscription is the reader's scan position — a reader has no backlog and no acknowledgements.The scheduled TTL task,
PersistentTopic.checkMessageExpiry(), walks every subscription of the topic and force-acknowledges everything older than the TTL. It excludes the compaction cursor and the configuredadditionalSystemCursorNames, but it does not exclude non-durable cursors, so a reader's cursor gets expired like any consumer backlog.For a durable subscription that is exactly what TTL is defined to do. For a reader it silently drops data, because
ManagedCursorImpl.setAcknowledgedPosition()drags the read position along whenever the new mark-delete position reaches or passes it:A reader that is still scanning messages older than the TTL is therefore teleported past everything the expiry task just acknowledged. It gets no error and no warning — it simply resumes after the gap and returns fewer messages.
That is the behaviour reported in #26504 (from discussion #26501): a reader started at
MessageId.earlieston a topic with ~10 year retention and a 2 week TTL returned anywhere between 2,647 and 24,756 of the same ~24.7k messages across ten consecutive runs. The outcome depends only on whether the periodic expiry task (messageExpiryCheckIntervalInMinutes, 5 minutes by default) fires while the reader is still behind the TTL boundary; raising the TTL past the age of the oldest message made the problem disappear. No message was ever deleted — retention covered them all — the reader was just moved past them.Retention, not TTL, is what bounds how long a reader may keep reading. When
ManagedLedgerImplactually trims ledgers,advanceCursorsIfNecessary()force-advances non-durable cursors over the deleted data, so excluding them from TTL expiry cannot let a stale reader pin storage.Modifications
PersistentTopic: add asub.getCursor().isDurable()condition to the subscription filter in both automatic expiry paths —checkMessageExpiryWithSharedPosition()(theManagedLedgerImplpath) andcheckMessageExpiryWithoutSharedPosition()(the fallback for customManagedLedgerimplementations).Durable subscriptions and replicators are unaffected, and only the automatic TTL task changes. The administrative expiry endpoints (
pulsar-admin topics expire-messagesandexpire-messages-all-subscriptions) iterate subscriptions inPersistentTopicsBaseand still act on reader subscriptions, so an operator can still expire them explicitly.Verifying this change
This change added tests and can be verified as follows:
ReaderMessageTTLTest.testReaderRetainsUnreadMessagesDuringExpirypublishes 20 messages to a topic with infinite retention and a 1 second TTL, reads the first message, pauses the reader until every message is older than the TTL, then drivescheckMessageExpiry()until the durable subscription's backlog reaches zero. It asserts that the reader's mark-delete position never passes its read position and that the remaining 19 messages are all still readable, in order.@DataProvider: the shared-position path, and — with aManagedLedgermock that delegates to the real one — the per-subscription fallback path.master(AssertionError: [TTL must not acknowledge unread reader messages]) and pass with the fix.Does this pull request potentially affect one of the following parts:
If the box was checked, please highlight the changes