Skip to content

[#2196] Improve queue consumer starvation under low configuration#2197

Open
mattrpav wants to merge 2 commits into
apache:mainfrom
mattrpav:amq-gh-2196-queue-consumer-starvation
Open

[#2196] Improve queue consumer starvation under low configuration#2197
mattrpav wants to merge 2 commits into
apache:mainfrom
mattrpav:amq-gh-2196-queue-consumer-starvation

Conversation

@mattrpav

@mattrpav mattrpav commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Run the test before the fix and observe:

2026-07-07 15:56:20,119 [me-limited test] - INFO  PriorityDispatchStarvationTest - Produced 200 messages (40 app, 160 other)
2026-07-07 15:56:30,154 [me-limited test] - INFO  PriorityDispatchStarvationTest - App consumer received 8 / 40 messages (drained=false)
2026-07-07 15:56:30,156 [me-limited test] - INFO  PriorityDispatchStarvationTest - Queue stats - queueSize: 192, enqueues: 200, dequeues: 8, inflight: 0, dispatched: 8

Test run after fix:

2026-07-07 15:59:33,076 [me-limited test] - INFO  PriorityDispatchStarvationTest - Produced 200 messages (40 app, 160 other)
2026-07-07 15:59:33,110 [ Session Task-1] - INFO  PriorityDispatchStarvationTest - App consumer received 10 / 40 messages
2026-07-07 15:59:33,115 [ Session Task-1] - INFO  PriorityDispatchStarvationTest - App consumer received 20 / 40 messages
2026-07-07 15:59:33,119 [ Session Task-1] - INFO  PriorityDispatchStarvationTest - App consumer received 30 / 40 messages
2026-07-07 15:59:33,123 [ Session Task-1] - INFO  PriorityDispatchStarvationTest - App consumer received 40 / 40 messages
2026-07-07 15:59:33,123 [me-limited test] - INFO  PriorityDispatchStarvationTest - App consumer received 40 / 40 messages (drained=true)
2026-07-07 15:59:33,124 [me-limited test] - INFO  PriorityDispatchStarvationTest - Queue stats - queueSize: 160, enqueues: 200, dequeues: 40, inflight: 0, dispatched: 40

edit: Tests passed, except for assembly on an address-in-use bind error. Re-running tests to get a green checkmark

@mattrpav mattrpav self-assigned this Jul 7, 2026
@mattrpav mattrpav changed the title [#2196] Queue consumer starvation under low configuration [#2196] Improve queue consumer starvation under low configuration Jul 7, 2026
@mattrpav mattrpav force-pushed the amq-gh-2196-queue-consumer-starvation branch from b733033 to 607f112 Compare July 7, 2026 21:03
@mattrpav mattrpav requested a review from cshannon July 7, 2026 21:03
@mattrpav mattrpav force-pushed the amq-gh-2196-queue-consumer-starvation branch from 607f112 to a5b8ef9 Compare July 7, 2026 21:11

@cshannon cshannon left a comment

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.

I only took a quick look so far but I don't really understand why you don't just set lazyDispatch to true? The point of that flag is to do exactly this where it will page in what consumers can handle.

if (isLazyDispatch() && !force) {
// Only page in the minimum number of messages which can be
// dispatched immediately.
toPageIn = Math.min(toPageIn, getConsumerMessageCountBeforeFull());

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.

getConsumerMessageCountBeforeFull() is an expensive operation and has to iterate, it should be called one time and not 3 times and result saved

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think also dispatchPendingList trigger maxPageSize each cycle (related).

// dispatched immediately.
toPageIn = Math.min(toPageIn, getConsumerMessageCountBeforeFull());
}
if (pagedInPendingSize >= maxPageSize && !force) {

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.

Shouldn't this be an if/else so you aren't doing the same thing twice?

@mattrpav

mattrpav commented Jul 8, 2026

Copy link
Copy Markdown
Contributor Author

I only took a quick look so far but I don't really understand why you don't just set lazyDispatch to true? The point of that flag is to do exactly this where it will page in what consumers can handle.

LazyDispatch vs this change comes down to when force = true. I think you are right that lazy dispatch would handle the normal consumer, since it looks for force=false scenarios. I'll do a pass through cursor scenarios. iirc the force=true scenarios were the management use cases.

edit: lazyDispatch does not provide relief. The issue is this must also evaluate to true:

if (toPageIn > 0 && (force || (haveRealConsumer() && pagedInPendingSize < maxPageSize))) {

When the pageSize is super low and the pagedInPendingSize is larger, this will evaluate false.

That being said, I'm not convinced taking the penalty to calculate if consumers have space makes sense to put on everyone's queue, every time for this one scenario. I'm leaning towards the best approach is to not starve the queue with a small maxPageSize.

mattrpav added 2 commits July 8, 2026 14:23
…figuration

Test demonstrates that stalled low-priority network consumers with saturated
prefetch throttle a normal-priority app consumer's throughput by 5-20x.
Root cause: Queue.doPageInForDispatch() uses dispatchPendingList.size()
against maxPageSize, so messages stuck behind full network consumers
block page-in for all consumers.

The test assertion (slowdownFactor <= 3.0) is expected to FAIL against
the current code, proving the bug exists. The subsequent fix commit
will make it pass.
…l prefetch

Queue.doPageInForDispatch() used pagedInPendingSize < maxPageSize as the
sole gate for paging in messages from the store. When low-priority network
consumers had saturated prefetch (not acking), their undispatched messages
accumulated in dispatchPendingList, counting against maxPageSize and
blocking page-in for all consumers — including higher-priority app
consumers with available prefetch capacity.

Two changes:
1. When pagedInPendingSize >= maxPageSize, cap toPageIn to what consumers
   can actually accept (getConsumerMessageCountBeforeFull)
2. Allow page-in when any consumer has available prefetch capacity, even
   if pagedInPendingSize >= maxPageSize
@mattrpav mattrpav force-pushed the amq-gh-2196-queue-consumer-starvation branch from a5b8ef9 to 2b73dd1 Compare July 8, 2026 19:23
@cshannon

cshannon commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

I also have concerns that this could page in a lot more into memory and cause a huge memory spike if you are using count before full. The default prefetch is 32k and obviously that should be tuned but it could cause a large increase in paged in messages if there are a lot of consumers even with a prefetch not that high

@cshannon

cshannon commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

I also have concerns that this could page in a lot more into memory and cause a huge memory spike if you are using count before full. The default prefetch is 32k and obviously that should be tuned but it could cause a large increase in paged in messages if there are a lot of consumers even with a prefetch not that high

This might be fine the more i think about it (after all we want to keep prefetch full), i just want to take a closer look as it has been a while since i've looked at all the logic with paging stuff in

@mattrpav

mattrpav commented Jul 8, 2026

Copy link
Copy Markdown
Contributor Author

I also have concerns that this could page in a lot more into memory and cause a huge memory spike if you are using count before full. The default prefetch is 32k and obviously that should be tuned but it could cause a large increase in paged in messages if there are a lot of consumers even with a prefetch not that high

Note: queuePrefetch default is 1,000, but yeah the overall concern is valid.

Its an odd scenario and it looks like it trips up on the somewhat side-ways logic.

memoryUsage: has space
sum of consumer prefetch: has space
maxPageSize: less than the number of paged in messages.

The conditional comparing current pending count vs pageSize is a context switch in the comparison logic. Smells like an apples-to-oranges type deal rotating around counts vs space in memory usage.

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.

3 participants