This is an automated email from the ASF dual-hosted git repository.

cshannon pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/activemq.git


The following commit(s) were added to refs/heads/main by this push:
     new b131c3d266 Fix TextMessage body thread safety (#2188)
b131c3d266 is described below

commit b131c3d2661ad28236a5ac18826e7d88cd3cb976
Author: Christopher L. Shannon <[email protected]>
AuthorDate: Thu Jul 9 09:05:47 2026 -0400

    Fix TextMessage body thread safety (#2188)
    
    This fixes the rare null body issues seen on the broker when using text
    messages by using synchronization when marshaling or unmarshaling the
    body into a String to prevent a race condition from causing the body to
    end up null. This has been optimized to use volatiles and double checked
    locking to avoid synchronization unless needed.
---
 .../activemq/command/ActiveMQTextMessage.java      | 124 ++++++++++++++-------
 .../java/org/apache/activemq/command/Message.java  |   8 +-
 2 files changed, 90 insertions(+), 42 deletions(-)

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 a5df4164f0..0ace802c8c 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
@@ -45,20 +45,19 @@ public class ActiveMQTextMessage extends ActiveMQMessage 
implements TextMessage
 
     public static final byte DATA_STRUCTURE_TYPE = 
CommandTypes.ACTIVEMQ_TEXT_MESSAGE;
 
-    protected String text;
+    // This is package scope (instead of private) for testing purposes
+    volatile String text;
 
     @Override
     public Message copy() {
         ActiveMQTextMessage copy = new ActiveMQTextMessage();
-        copy(copy);
+        synchronized (this) {
+            super.copy(copy);
+            copy.text = text;
+        }
         return copy;
     }
 
-    private void copy(ActiveMQTextMessage copy) {
-        super.copy(copy);
-        copy.text = text;
-    }
-
     @Override
     public byte getDataStructureType() {
         return DATA_STRUCTURE_TYPE;
@@ -72,19 +71,35 @@ public class ActiveMQTextMessage extends ActiveMQMessage 
implements TextMessage
     @Override
     public void setText(String text) throws MessageNotWriteableException {
         checkReadOnlyBody();
-        this.text = text;
-        setContent(null);
+        synchronized (this) {
+            this.text = text;
+            setContent(null);
+        }
+    }
+
+    // Synchronize this to prevent setting content if another mutation 
operation
+    // is happening concurrently.
+    @Override
+    public synchronized void setContent(ByteSequence content) {
+        super.setContent(content);
     }
 
     @Override
     public String getText() throws JMSException {
-        ByteSequence content = getContent();
+        String text = this.text;
 
-        if (text == null && content != null) {
-            text = decodeContent(content);
-            setContent(null);
-            setCompressed(false);
+        if (text == null) {
+            synchronized (this) {
+                text = this.text;
+                // Double-checked locking, re-check under lock if we need to 
decode
+                if (text == null && content != null) {
+                    this.text = text = decodeContent(content);
+                    setContent(null);
+                    setCompressed(false);
+                }
+            }
         }
+
         return text;
     }
 
@@ -124,27 +139,36 @@ public class ActiveMQTextMessage extends ActiveMQMessage 
implements TextMessage
 
     @Override
     public void storeContentAndClear() {
-        storeContent();
-        text=null;
+        // 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;
+        }
     }
 
     @Override
     public void storeContent() {
         try {
-            ByteSequence content = getContent();
-            String text = this.text;
-            if (content == null && text != null) {
-                ByteArrayOutputStream bytesOut = new ByteArrayOutputStream();
-                OutputStream os = bytesOut;
-                ActiveMQConnection connection = getConnection();
-                if (connection != null && connection.isUseCompression()) {
-                    compressed = true;
-                    os = new DeflaterOutputStream(os);
+            // Content is volatile so if it's not null we can skip and do 
nothing
+            if (content == null) {
+                synchronized (this) {
+                    // Double-checked locking, re-check state under lock
+                    if (content == null && text != null) {
+                        ByteArrayOutputStream bytesOut = new 
ByteArrayOutputStream();
+                        OutputStream os = bytesOut;
+                        ActiveMQConnection connection = getConnection();
+                        if (connection != null && 
connection.isUseCompression()) {
+                            compressed = true;
+                            os = new DeflaterOutputStream(os);
+                        }
+                        DataOutputStream dataOut = new DataOutputStream(os);
+                        MarshallingSupport.writeUTF8(dataOut, text);
+                        dataOut.close();
+                        setContent(bytesOut.toByteSequence());
+                    }
                 }
-                DataOutputStream dataOut = new DataOutputStream(os);
-                MarshallingSupport.writeUTF8(dataOut, text);
-                dataOut.close();
-                setContent(bytesOut.toByteSequence());
             }
         } catch (IOException e) {
             throw new RuntimeException(e);
@@ -156,11 +180,19 @@ public class ActiveMQTextMessage extends ActiveMQMessage 
implements TextMessage
     @Override
     public void clearUnMarshalledState() throws JMSException {
         super.clearUnMarshalledState();
-        this.text = null;
+        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;
+            }
+        }
     }
 
+    // We need to sync because both variables need to be read independently
     @Override
-    public boolean isContentMarshalled() {
+    public synchronized boolean isContentMarshalled() {
         return content != null || text == null;
     }
 
@@ -177,19 +209,31 @@ public class ActiveMQTextMessage extends ActiveMQMessage 
implements TextMessage
      */
     @Override
     public void clearBody() throws JMSException {
-        super.clearBody();
-        this.text = null;
+        synchronized (this) {
+            super.clearBody();
+            this.text = null;
+        }
     }
 
     @Override
     public int getSize() {
-        String text = this.text;
-        if (size == 0 && content == null && text != null) {
-            size = getMinimumMessageSize();
-            if (marshalledProperties != null) {
-                size += marshalledProperties.getLength();
+        int size = this.size;
+        if (size == 0) {
+            synchronized (this) {
+                size = this.size;
+                String text = this.text;
+                ByteSequence content = getContent();
+                if (size == 0 && content == null && text != null) {
+                    size = getMinimumMessageSize();
+                    ByteSequence marshalledProperties = 
this.marshalledProperties;
+                    if (marshalledProperties != null) {
+                        size += marshalledProperties.getLength();
+                    }
+                    size += text.length() * 2;
+                    this.size = size;
+                }
+                return super.getSize();
             }
-            size += text.length() * 2;
         }
         return super.getSize();
     }
@@ -198,7 +242,7 @@ public class ActiveMQTextMessage extends ActiveMQMessage 
implements TextMessage
     public String toString() {
         try {
             String text = this.text;
-            if( text == null ) {
+            if (text == null) {
                 text = decodeContent(getContent());
             }
             if (text != null) {
diff --git 
a/activemq-client/src/main/java/org/apache/activemq/command/Message.java 
b/activemq-client/src/main/java/org/apache/activemq/command/Message.java
index 06e0aebeea..11ccd306fa 100644
--- a/activemq-client/src/main/java/org/apache/activemq/command/Message.java
+++ b/activemq-client/src/main/java/org/apache/activemq/command/Message.java
@@ -81,12 +81,12 @@ public abstract class Message extends BaseCommand 
implements MarshallAware, Mess
     protected boolean compressed;
     protected String userID;
 
-    protected ByteSequence content;
+    protected volatile ByteSequence content;
     protected volatile ByteSequence marshalledProperties;
     protected DataStructure dataStructure;
     protected int redeliveryCounter;
 
-    protected int size;
+    protected volatile int size;
     protected Map<String, Object> properties;
     protected boolean readOnlyProperties;
     protected boolean readOnlyBody;
@@ -715,14 +715,18 @@ public abstract class Message extends BaseCommand 
implements MarshallAware, Mess
     @Override
        public int getSize() {
         int minimumMessageSize = getMinimumMessageSize();
+        ByteSequence content = this.content;
+        int size = this.size;
         if (size < minimumMessageSize || size == 0) {
             size = minimumMessageSize;
+            ByteSequence marshalledProperties = this.marshalledProperties;
             if (marshalledProperties != null) {
                 size += marshalledProperties.getLength();
             }
             if (content != null) {
                 size += content.getLength();
             }
+            this.size = size;
         }
         return size;
     }


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]
For further information, visit: https://activemq.apache.org/contact


Reply via email to