Skip to content

Commit cfe5f3a

Browse files
committed
[#2196] Test demonstrating consumer starvation under narrow configuration
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.
1 parent 3c54283 commit cfe5f3a

1 file changed

Lines changed: 202 additions & 0 deletions

File tree

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
/**
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package org.apache.activemq.bugs;
18+
19+
import static org.junit.Assert.assertEquals;
20+
import static org.junit.Assert.assertTrue;
21+
22+
import java.util.concurrent.CountDownLatch;
23+
import java.util.concurrent.TimeUnit;
24+
import java.util.concurrent.atomic.AtomicInteger;
25+
26+
import jakarta.jms.Connection;
27+
import jakarta.jms.Message;
28+
import jakarta.jms.MessageConsumer;
29+
import jakarta.jms.MessageListener;
30+
import jakarta.jms.MessageProducer;
31+
import jakarta.jms.Session;
32+
33+
import org.apache.activemq.ActiveMQConnectionFactory;
34+
import org.apache.activemq.ActiveMQPrefetchPolicy;
35+
import org.apache.activemq.broker.BrokerService;
36+
import org.apache.activemq.broker.TransportConnector;
37+
import org.apache.activemq.broker.region.Queue;
38+
import org.apache.activemq.broker.region.RegionBroker;
39+
import org.apache.activemq.broker.region.policy.PolicyEntry;
40+
import org.apache.activemq.broker.region.policy.PolicyMap;
41+
import org.apache.activemq.command.ActiveMQQueue;
42+
import org.junit.After;
43+
import org.junit.Before;
44+
import org.junit.Test;
45+
import org.slf4j.Logger;
46+
import org.slf4j.LoggerFactory;
47+
48+
/**
49+
* Reproduces dispatch starvation caused by Queue.doPageInForDispatch()
50+
* gating page-in on dispatchPendingList.size() < maxPageSize.
51+
*
52+
* When the dispatchPendingList fills with messages that no active consumer
53+
* can accept (selector mismatch, message group ownership by a stalled
54+
* consumer, etc.), the page-in gate blocks permanently. Deliverable
55+
* messages remain stuck in the store even though a consumer has available
56+
* prefetch capacity.
57+
*
58+
* The fix adds an alternative gate condition:
59+
* pagedInPendingSize < maxPageSize || getConsumerMessageCountBeforeFull() > 0
60+
* and caps toPageIn to getConsumerMessageCountBeforeFull() when the pending
61+
* list is full, so page-in continues at the rate consumers can absorb.
62+
*/
63+
public class NetworkConsumerPriorityDispatchStarvationTest {
64+
65+
private static final Logger LOG = LoggerFactory.getLogger(NetworkConsumerPriorityDispatchStarvationTest.class);
66+
67+
private static final String QUEUE_NAME = "TEST.STARVATION";
68+
private static final int MAX_PAGE_SIZE = 20;
69+
private static final int TOTAL_MESSAGES = 200;
70+
private static final int APP_PREFETCH = 5;
71+
72+
private BrokerService broker;
73+
private String brokerURL;
74+
75+
@Before
76+
public void setUp() throws Exception {
77+
broker = new BrokerService();
78+
broker.setBrokerName("starvation-test");
79+
broker.setDeleteAllMessagesOnStartup(true);
80+
broker.setUseJmx(false);
81+
broker.setAdvisorySupport(false);
82+
broker.setPersistent(true);
83+
84+
var policyMap = new PolicyMap();
85+
var entry = new PolicyEntry();
86+
entry.setQueue(">");
87+
entry.setMaxPageSize(MAX_PAGE_SIZE);
88+
entry.setUseCache(false);
89+
entry.setOptimizedDispatch(true);
90+
entry.setMemoryLimit(10 * 1024 * 1024);
91+
policyMap.setDefaultEntry(entry);
92+
broker.setDestinationPolicy(policyMap);
93+
94+
broker.getSystemUsage().getMemoryUsage().setLimit(64 * 1024 * 1024);
95+
96+
var connector = broker.addConnector("tcp://0.0.0.0:0");
97+
broker.start();
98+
broker.waitUntilStarted();
99+
brokerURL = connector.getPublishableConnectString();
100+
}
101+
102+
@After
103+
public void tearDown() throws Exception {
104+
if (broker != null) {
105+
broker.stop();
106+
broker.waitUntilStopped();
107+
}
108+
}
109+
110+
/**
111+
* Sends a mix of messages: 1 in 5 has target='app' (matches the app
112+
* consumer's selector), the rest have target='other' (no consumer
113+
* matches). The 'other' messages accumulate in dispatchPendingList
114+
* because no consumer can accept them. Once the list reaches
115+
* maxPageSize, the pre-fix page-in gate blocks permanently, starving
116+
* the app consumer of deliverable messages still in the store.
117+
*
118+
* Expected behavior:
119+
* Pre-fix: app consumer receives only ~5-8 of 40 messages (FAIL)
120+
* Post-fix: app consumer receives all 40 messages (PASS)
121+
*/
122+
@Test(timeout = 60_000)
123+
public void testUndeliverableMessagesClogDispatchBufferAndBlockPageIn() throws Exception {
124+
var appMessageCount = TOTAL_MESSAGES / 5;
125+
126+
produceInterleavedMessages(TOTAL_MESSAGES);
127+
128+
var allReceived = new CountDownLatch(appMessageCount);
129+
var received = new AtomicInteger(0);
130+
131+
var factory = new ActiveMQConnectionFactory(brokerURL);
132+
var prefetchPolicy = new ActiveMQPrefetchPolicy();
133+
prefetchPolicy.setQueuePrefetch(APP_PREFETCH);
134+
factory.setPrefetchPolicy(prefetchPolicy);
135+
136+
try(var conn = factory.createConnection();
137+
var session = conn.createSession(false, Session.AUTO_ACKNOWLEDGE);
138+
var consumer = session.createConsumer(
139+
new ActiveMQQueue(QUEUE_NAME), "target = 'app'")) {
140+
141+
conn.start();
142+
143+
consumer.setMessageListener(new MessageListener() {
144+
@Override
145+
public void onMessage(Message message) {
146+
int count = received.incrementAndGet();
147+
if (count % 10 == 0) {
148+
LOG.info("App consumer received {} / {} messages", count, appMessageCount);
149+
}
150+
allReceived.countDown();
151+
}
152+
});
153+
154+
var drained = allReceived.await(10, TimeUnit.SECONDS);
155+
156+
var brokerQueue = (Queue) ((RegionBroker) broker.getRegionBroker())
157+
.getQueueRegion().getDestinationMap().get(new ActiveMQQueue(QUEUE_NAME));
158+
159+
var finalReceived = received.get();
160+
LOG.info("App consumer received {} / {} messages (drained={})", finalReceived, appMessageCount, drained);
161+
if (brokerQueue != null) {
162+
LOG.info("Queue stats - queueSize: {}, enqueues: {}, dequeues: {}, inflight: {}, dispatched: {}",
163+
brokerQueue.getDestinationStatistics().getMessages().getCount(),
164+
brokerQueue.getDestinationStatistics().getEnqueues().getCount(),
165+
brokerQueue.getDestinationStatistics().getDequeues().getCount(),
166+
brokerQueue.getDestinationStatistics().getInflight().getCount(),
167+
brokerQueue.getDestinationStatistics().getDispatched().getCount());
168+
}
169+
170+
assertTrue(
171+
"DISPATCH STARVATION: app consumer received only " + finalReceived +
172+
" of " + appMessageCount + " deliverable messages. " +
173+
"Undeliverable messages filled the dispatchPendingList to maxPageSize (" +
174+
MAX_PAGE_SIZE + "), blocking page-in of remaining deliverable messages from the store.",
175+
drained);
176+
177+
assertEquals("All app messages should be received", appMessageCount, finalReceived);
178+
}
179+
180+
}
181+
182+
private void produceInterleavedMessages(int count) throws Exception {
183+
var factory = new ActiveMQConnectionFactory(brokerURL);
184+
try(var conn = factory.createConnection();
185+
var session = conn.createSession(false, Session.AUTO_ACKNOWLEDGE);
186+
var producer = session.createProducer(new ActiveMQQueue(QUEUE_NAME))) {
187+
conn.start();
188+
189+
for (int i = 0; i < count; i++) {
190+
var msg = session.createTextMessage("Message-" + i);
191+
if (i % 5 == 0) {
192+
msg.setStringProperty("target", "app");
193+
} else {
194+
msg.setStringProperty("target", "other");
195+
}
196+
producer.send(msg);
197+
}
198+
199+
LOG.info("Produced {} messages ({} app, {} other)", count, count / 5, count - count / 5);
200+
}
201+
}
202+
}

0 commit comments

Comments
 (0)