Skip to content

Commit d1517f6

Browse files
committed
[#2198] PendingSendsRollbackLeakTest showing pendingSends bug
1 parent 3c54283 commit d1517f6

1 file changed

Lines changed: 182 additions & 0 deletions

File tree

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
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.io.File;
23+
import java.lang.reflect.Field;
24+
import java.nio.file.Files;
25+
import java.util.concurrent.atomic.AtomicInteger;
26+
27+
import jakarta.jms.Connection;
28+
import jakarta.jms.DeliveryMode;
29+
import jakarta.jms.MessageProducer;
30+
import jakarta.jms.Session;
31+
32+
import org.apache.activemq.ActiveMQConnectionFactory;
33+
import org.apache.activemq.broker.BrokerService;
34+
import org.apache.activemq.broker.region.Queue;
35+
import org.apache.activemq.command.ActiveMQQueue;
36+
import org.apache.activemq.store.kahadb.KahaDBPersistenceAdapter;
37+
import org.apache.activemq.util.IOHelper;
38+
import org.apache.activemq.util.Wait;
39+
import org.junit.After;
40+
import org.junit.Before;
41+
import org.junit.Test;
42+
import org.slf4j.Logger;
43+
import org.slf4j.LoggerFactory;
44+
45+
/**
46+
* Reproduces the pendingSends counter leak on transaction rollback.
47+
*
48+
* Queue.doMessageSend() increments the pendingSends counter for every send.
49+
* On commit, CursorAddSync.afterCommit() -> messageSent() decrements it.
50+
* On rollback, CursorAddSync.afterRollback() must also decrement it —
51+
* without that, every rolled-back transactional send permanently inflates
52+
* pendingSends.
53+
*
54+
* The leaked counter makes Queue.singlePendingSend() return false forever,
55+
* which disables cursor caching via QueueStorePrefetch.canEnableCash().
56+
* The queue is then stuck in store page-in mode, degrading throughput and
57+
* amplifying duplicateFromStore events.
58+
*/
59+
public class PendingSendsRollbackLeakTest {
60+
61+
private static final Logger LOG = LoggerFactory.getLogger(PendingSendsRollbackLeakTest.class);
62+
private static final String QUEUE_NAME = "TEST.PENDING.SENDS.ROLLBACK";
63+
private static final int ROLLBACK_SEND_COUNT = 5;
64+
65+
private BrokerService broker;
66+
private Connection connection;
67+
private File dataDir;
68+
69+
@Before
70+
public void setUp() throws Exception {
71+
var baseDir = new File(IOHelper.getDefaultDataDirectory());
72+
Files.createDirectories(baseDir.toPath());
73+
dataDir = Files.createTempDirectory(baseDir.toPath(), "PendingSendsRollback-").toFile();
74+
dataDir.deleteOnExit();
75+
76+
broker = new BrokerService();
77+
broker.setDataDirectoryFile(dataDir);
78+
broker.setUseJmx(false);
79+
broker.setDeleteAllMessagesOnStartup(true);
80+
broker.getSystemUsage().getMemoryUsage().setLimit(64 * 1024 * 1024);
81+
82+
var pa = new KahaDBPersistenceAdapter();
83+
pa.setDirectory(new File(dataDir, "kahadb"));
84+
broker.setPersistenceAdapter(pa);
85+
86+
broker.addConnector("tcp://localhost:0");
87+
broker.start();
88+
broker.waitUntilStarted();
89+
90+
var factory = new ActiveMQConnectionFactory(
91+
broker.getTransportConnectors().get(0).getConnectUri());
92+
connection = factory.createConnection();
93+
connection.start();
94+
}
95+
96+
@After
97+
public void tearDown() throws Exception {
98+
if (connection != null) {
99+
connection.close();
100+
}
101+
if (broker != null) {
102+
broker.deleteAllMessages();
103+
broker.stop();
104+
broker.waitUntilStopped();
105+
}
106+
}
107+
108+
@Test(timeout = 60_000)
109+
public void testRollbackDecrementsPendingSends() throws Exception {
110+
var dest = new ActiveMQQueue(QUEUE_NAME);
111+
var queue = (Queue) broker.getDestination(dest);
112+
113+
assertEquals("pendingSends should start at 0", 0, getPendingSends(queue));
114+
assertTrue("singlePendingSend should be true initially", queue.singlePendingSend());
115+
116+
// Send persistent messages in a transaction, then roll back
117+
try(var txSession = connection.createSession(true, Session.SESSION_TRANSACTED);
118+
var producer = txSession.createProducer(dest)) {
119+
120+
producer.setDeliveryMode(DeliveryMode.PERSISTENT);
121+
122+
for (int i = 0; i < ROLLBACK_SEND_COUNT; i++) {
123+
producer.send(txSession.createTextMessage("rollback-msg-" + i));
124+
}
125+
126+
txSession.rollback();
127+
}
128+
129+
// Rollback processing is synchronous with the rollback() call, but
130+
// allow a grace period in case any async completion is in flight.
131+
Wait.waitFor(() -> getPendingSends(queue) == 0, 5000, 10);
132+
133+
int leaked = getPendingSends(queue);
134+
LOG.info("After rollback of {} sends: pendingSends={}, singlePendingSend={}",
135+
ROLLBACK_SEND_COUNT, leaked, queue.singlePendingSend());
136+
137+
assertEquals("PENDING SENDS LEAK: pendingSends should return to 0 after " +
138+
"transaction rollback, but " + leaked + " send(s) leaked. " +
139+
"CursorAddSync.afterRollback() must decrement pendingSends the same " +
140+
"way afterCommit() -> messageSent() does. A leaked counter disables " +
141+
"cursor caching (singlePendingSend() false forever).",
142+
0, getPendingSends(queue));
143+
assertTrue("singlePendingSend should recover after rollback", queue.singlePendingSend());
144+
145+
assertEquals("message count should be 0 after rollback",
146+
0, queue.getDestinationStatistics().getMessages().getCount());
147+
}
148+
149+
@Test(timeout = 60_000)
150+
public void testCommitKeepsPendingSendsBalanced() throws Exception {
151+
var dest = new ActiveMQQueue(QUEUE_NAME + ".COMMIT");
152+
var queue = (Queue) broker.getDestination(dest);
153+
154+
try(var txSession = connection.createSession(true, Session.SESSION_TRANSACTED);
155+
var producer = txSession.createProducer(dest)) {
156+
157+
producer.setDeliveryMode(DeliveryMode.PERSISTENT);
158+
159+
for (int i = 0; i < ROLLBACK_SEND_COUNT; i++) {
160+
producer.send(txSession.createTextMessage("commit-msg-" + i));
161+
}
162+
163+
txSession.commit();
164+
}
165+
166+
assertTrue("pendingSends should return to 0 after commit",
167+
Wait.waitFor(() -> getPendingSends(queue) == 0, 5000, 100));
168+
assertTrue("Queue should have " + ROLLBACK_SEND_COUNT + " messages after commit",
169+
Wait.waitFor(() -> queue.getDestinationStatistics().getMessages().getCount() == ROLLBACK_SEND_COUNT,
170+
5000, 100));
171+
}
172+
173+
private int getPendingSends(Queue queue) {
174+
try {
175+
var f = Queue.class.getDeclaredField("pendingSends");
176+
f.setAccessible(true);
177+
return ((AtomicInteger) f.get(queue)).get();
178+
} catch (ReflectiveOperationException e) {
179+
throw new IllegalStateException("Unable to read Queue.pendingSends", e);
180+
}
181+
}
182+
}

0 commit comments

Comments
 (0)