diff --git a/activemq-client/src/main/java/org/apache/activemq/command/ActiveMQTextMessage.java b/activemq-client/src/main/java/org/apache/activemq/command/ActiveMQTextMessage.java
index 0ace802c8ce..aa5a38989a7 100644
--- a/activemq-client/src/main/java/org/apache/activemq/command/ActiveMQTextMessage.java
+++ b/activemq-client/src/main/java/org/apache/activemq/command/ActiveMQTextMessage.java
@@ -68,13 +68,17 @@ public String getJMSXMimeType() {
return "jms/text-message";
}
+ // add sync
@Override
- public void setText(String text) throws MessageNotWriteableException {
+ public synchronized void compress() throws IOException {
+ super.compress();
+ }
+
+ @Override
+ public synchronized void setText(String text) throws MessageNotWriteableException {
checkReadOnlyBody();
- synchronized (this) {
- this.text = text;
- setContent(null);
- }
+ this.text = text;
+ setContent(null);
}
// Synchronize this to prevent setting content if another mutation operation
@@ -137,15 +141,13 @@ public void beforeMarshall(WireFormat wireFormat) throws IOException {
storeContentAndClear();
}
+ // always lock to simplify things because if this method is being called
+ // it's right before send so it's very likely to need to mutate state
+ // This should generally be uncontested lock so it will be fast
@Override
- public void storeContentAndClear() {
- // always lock to simplify things because if this method is being called
- // it's right before send so it's very likely to need to mutate state
- // This should generally be uncontested lock so it will be fast
- synchronized (this) {
- storeContent();
- text = null;
- }
+ public synchronized void storeContentAndClear() {
+ storeContent();
+ text = null;
}
@Override
@@ -178,15 +180,12 @@ public void storeContent() {
// see https://issues.apache.org/activemq/browse/AMQ-2103
// and https://issues.apache.org/activemq/browse/AMQ-2966
@Override
- public void clearUnMarshalledState() throws JMSException {
- super.clearUnMarshalledState();
- if (this.text != null) {
- synchronized (this) {
- // This is volatile but another locking makes sure another thread
- // isn't attempting to read this value to marshal to the content at the
- // same time
- this.text = null;
- }
+ public synchronized void clearUnMarshalledState() throws JMSException {
+ // Double check this under lock, we should only clear if
+ // the text/properties are marshaled
+ if (isMarshalled()) {
+ super.clearUnMarshalledState();
+ this.text = null;
}
}
@@ -208,11 +207,9 @@ public synchronized boolean isContentMarshalled() {
* due to some internal error.
*/
@Override
- public void clearBody() throws JMSException {
- synchronized (this) {
- super.clearBody();
- this.text = null;
- }
+ public synchronized void clearBody() throws JMSException {
+ super.clearBody();
+ this.text = null;
}
@Override
diff --git a/activemq-client/src/test/java/org/apache/activemq/command/ActiveMQTextMessageContentRaceTest.java b/activemq-client/src/test/java/org/apache/activemq/command/ActiveMQMessageContentRaceTest.java
similarity index 63%
rename from activemq-client/src/test/java/org/apache/activemq/command/ActiveMQTextMessageContentRaceTest.java
rename to activemq-client/src/test/java/org/apache/activemq/command/ActiveMQMessageContentRaceTest.java
index 698eb43c69d..9e2b10b4206 100644
--- a/activemq-client/src/test/java/org/apache/activemq/command/ActiveMQTextMessageContentRaceTest.java
+++ b/activemq-client/src/test/java/org/apache/activemq/command/ActiveMQMessageContentRaceTest.java
@@ -21,13 +21,21 @@
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
+import jakarta.jms.JMSException;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
+import org.junit.Assume;
import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -35,11 +43,11 @@
* Validates the fix for the race condition between getText(),
* storeContentAndClear() and copy() in ActiveMQTextMessage that could cause
* the message body to become permanently null.
- *
+ *
* The race existed because those methods performed multi-step
* read-modify-clear operations on two fields (text and content) without a
* common monitor:
- *
+ *
* Thread A (getText): Thread B (storeContentAndClear):
* 1. reads content -> non-null 1. storeContent(): content non-null,
* 2. decodes content -> text skips encoding text -> content
@@ -47,35 +55,83 @@
*
* Result: content=null (cleared by A), text=null (cleared by B).
* The body was permanently lost.
- *
+ *
* Real-world trigger: on the broker side, a transport thread calls
* beforeMarshall() -> storeContentAndClear() while concurrently an XPath
* selector evaluator (JAXPXPathEvaluator) or JMX browse calls getText() on
* the same Message instance, and the network bridge calls copy() on it. The
* broker dispatches the same Message object to multiple consumers without
* copying it.
- *
+ *
* The fix synchronizes the state transitions on the message instance
* itself: getText() decodes+clears under {@code synchronized(this)} (with a
* double-checked volatile read of text for the fast path),
* storeContentAndClear() stores+clears under the same monitor, copy() takes
* the monitor while snapshotting both fields, and content/text are volatile.
+ *
+ * Note:
+ * This issue is specific to ActiveMQTextMessage because it is the only message
+ * type that will only store either the unmarshalled data (text) or the
+ * bytes, but not both. Other message types do not clear the content after unmarshaling.
+ * This means that if multiple threads try and do a conversion on a message like Map
+ * or Object message, it may convert twice, but you won't get null. The other
+ * message types only clear unmarshaled content at certain points in the broker
+ * if reduceMemoryFootprint is true.
+ *
+ * This test also runs the relevant race condition tests on other message types
+ * to confirm they do not suffer from the same null body issue.
*/
-public class ActiveMQTextMessageContentRaceTest {
+@RunWith(Parameterized.class)
+public class ActiveMQMessageContentRaceTest {
- private static final Logger LOG = LoggerFactory.getLogger(ActiveMQTextMessageContentRaceTest.class);
+ private static final Logger LOG = LoggerFactory.getLogger(ActiveMQMessageContentRaceTest.class);
private static final String TEST_TEXT = "Hello, World! This is a test message body.";
private static final int ITERATIONS = 20_000;
+ private final byte messageType;
+
+ public ActiveMQMessageContentRaceTest(byte messageType) {
+ this.messageType = messageType;
+ }
+
+ @Parameters
+ public static Collection