Visitar URL original
Limit the topic browse query on the JDBC store (#2630) by papinifrancesco · Pull Request #2634 · apache/activemq · GitHub
Skip to content

Limit the topic browse query on the JDBC store (#2630) - #2634

Open
papinifrancesco wants to merge 1 commit into
apache:mainfrom
papinifrancesco:jdbc-topic-recover-expired
Open

papinifrancesco wants to merge 1 commit into
apache:mainfrom
papinifrancesco:jdbc-topic-recover-expired

Conversation

@papinifrancesco

@papinifrancesco papinifrancesco commented Oct 2, 2026 •

Copy link
Copy Markdown

Link to #2630

Problem. For stores without recoverExpired() (JDBC), the topic expiry task falls back to Topic.doBrowse(), which calls MessageStore.recover(). The JDBC store runs SELECT ID, MSG FROM ACTIVEMQ_MSGS WHERE CONTAINER=? ORDER BY ID with no limit. Since AMQ-6067 the listener stops reading at the page size, but pgjdbc reads the whole result set inside executeQuery(), so with a large durable subscription backlog every expiry run loads the whole topic into the heap and the broker runs out of memory.

Change.

  • MessageStore.recover(MessageRecoveryListener, int maxReturned): new default method that calls recover(listener), so stores that don't override it are unchanged. ProxyMessageStore and ProxyTopicMessageStore delegate it.
  • JDBCMessageStore overrides it; DefaultJDBCAdapter.doRecover() gets an overload that sets setMaxRows(maxReturned) on the query (0 = no limit, as before).
  • Topic.doBrowse() passes its max.

Browse and expiry results are unchanged; only the number of rows the driver reads is bounded.

Testing.

  • New JDBCTopicBrowseLimitTest (H2).
  • JDBCPersistenceAdapterExpiredMessageTest: its proxy now overrides the new recover(listener, maxReturned), which is what Topic calls; it still checks that hasSpace() is called during expiry.
  • Run locally: JDBCStoreBrokerTest, H2JDBCStoreBrokerTest, JDBCDurableSubscriptionTest, JDBCMessagePriorityTest and JDBCPersistenceAdapterExpiredMessageTest pass.
  • PostgreSQL (reproduction from #2630): patched 6.3.2 and 6.2.10 survive the 200,000-message backlog with a 1 GB heap; unpatched, both run out of memory.

Disclosure: written with an AI assistant (Claude Code, Anthropic); the commit carries a Co-authored-by trailer naming the tool.

🤖 Generated with Claude Code

@jbonofre
jbonofre self-requested a review October 3, 2026 13:10
@cshannon
cshannon self-requested a review October 6, 2026 12:49
if (topicStore != null) {
// a JDBC ack moves the last acked id of the subscription, so expiring a browsed
// message would also ack the non-expired messages before it
final boolean expireFromStore = topicStore.getType() == StoreType.JDBC;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This breaks modularity. There shouldn't be a check for an underlying store type and a subsequent change in logic. If there is a problem with the JDBC ack'n with browsed messages, we should fix that in the JDBC layer.

@mattrpav

mattrpav commented Oct 6, 2026

Copy link
Copy Markdown
Contributor

This is a significant change, and I'm wondering if there is a better way to update this (perhaps add new first-class methods to the store) vs branching logic based on the back-end store type.

Is there a way to address the first part-- add a limit query based on maxBrowsePageSize without dissecting the entire Topic expiry code flow? That would allow us to address the core issue quickly, and then spend some time on the design to optimize expiry processing as a follow-on.

Topic.doBrowse(), also used by the topic expiry task for stores without
recoverExpired(), recovered the topic through MessageStore.recover(). The JDBC
store ran the query with no limit and relied on the listener to stop reading,
but pgjdbc reads the whole result set in executeQuery(), so a large durable
subscription backlog was loaded into the heap on every expiry run.

Add MessageStore.recover(listener, maxReturned), whose default calls
recover(listener), so stores that do not override it are unchanged. The JDBC
store puts the limit on the query (setMaxRows). Topic.doBrowse() passes its
page size. What a browse returns or expires does not change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
papinifrancesco added a commit to papinifrancesco/activemq-jdbc-topic-expiry-oom that referenced this pull request Oct 8, 2026
…rowse-limit.patch)

Upstream asked for a smaller first fix (apache/activemq#2634): only bound the
browse query by the page size. build-patched.sh takes FIX to pick the patch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@papinifrancesco
papinifrancesco force-pushed the jdbc-topic-recover-expired branch from 07cc7f7 to d579453 Compare October 9, 2026 08:14
@papinifrancesco papinifrancesco changed the title Implement recoverExpired() for the JDBC topic store (#2630) Limit the topic browse query on the JDBC store (#2630) Oct 9, 2026
@papinifrancesco

Copy link
Copy Markdown
Author

Thanks @mattrpav, agreed. This PR is now only that fix, one commit on current main:

  • MessageStore.recover(MessageRecoveryListener, int maxReturned): a new default method that calls recover(listener), so KahaDB and the memory store run exactly the code they run today. The proxy stores delegate it.
  • The JDBC store overrides it and applies the limit to the query (setMaxRows). Its listener already stops at the page size (AMQ-6067), so what a browse returns or expires doesn't change; pgjdbc just no longer loads the whole topic first.
  • Topic.doBrowse() passes its page size. No store-type checks; this also covers a JMX browse.

On PostgreSQL, with this patch 6.3.2 and 6.2.10 survive the 200,000-message backlog from #2630 with a 1 GB heap, where unpatched they run out of memory (CI run). New H2 test: JDBCTopicBrowseLimitTest.

One question: you added MessageRecoveryContext (AMQ-9646). Would you rather the JDBC store implement recoverMessages(MessageRecoveryContext) and the browse go through it? A plain overload kept this change small, since JDBC has no implementation of the context's offset and start/end ids yet.

The rest is now #2640: on the JDBC store, expiry acks can lose pending messages or redeliver consumed ones, with tests for both. The previous version of this PR is linked there as one possible fix.

@cshannon: with this version, KahaDB's code path is unchanged.

Disclosure: as before, the change, the tests and the reproduction were written with an AI assistant (Claude Code, Anthropic). The test results come from local runs and the linked CI run.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Backlog

Development

Successfully merging this pull request may close these issues.

2 participants