Repository: apache/rocketmq (Java)
Task type: Bug Fix

Issue:
opt transaction message check
The issue tracker is used for bug reporting purposes **ONLY** whereas feature request needs to follow the [RIP process](https://github.com/apache/rocketmq/wiki/RocketMQ-Improvement-Proposal). To avoid unnecessary duplication, please check whether there is a previous issue before filing a new one.

It is recommended to start a discussion thread in the [mailing lists](http://rocketmq.apache.org/about/contact/) or [github discussions](https://github.com/apache/rocketmq/discussions) in cases of discussing your deployment plan, API clarification, and other non-bug-reporting issues.
We welcome any friendly suggestions, bug fixes, collaboration, and other improvements.

Please ensure that your bug report is clear and self-contained. Otherwise, it would take additional rounds of communication, thus more time, to understand the problem itself.

Generally, fixing an issue goes through the following steps:
1. Understand the issue reported;
1. Reproduce the unexpected behavior locally;
1. Perform root cause analysis to identify the underlying problem;
1. Create test cases to cover the identified problem;
1. Work out a solution to rectify the behavior and make the newly created test cases pass;
1. Make a pull request and go through peer review;

As a result, it would be very helpful yet challenging if you could provide an isolated project reproducing your reported issue. Anyway, please ensure your issue report is informative enough for the community to pick up. At a minimum, include the following hints:

**BUG REPORT**

1. Please describe the issue you observed:
Prevent transactions from being commit or rollback multiple times
If you specify a custom first check time CheckImmunityTimeInSeconds,And the commit/rollback request whose validity period exceeds CheckImmunityTimeInSeconds and is not checked back will be processed and failed

2. Please tell us about your environment:

3. Other information (e.g. detailed explanation, logs, related issues, suggestions on how to fix, etc):

**FEATURE REQUEST**

1. Please describe the feature you are requesting.

2. Provide any additional detail on your proposed use case for this feature.

3. Indicate the importance of this issue to you (blocker, must-have, should-have, nice-to-have). Are you currently using any workarounds to address this issue?

4. If there are some sub-tasks involved, use -[] for each sub-task and create a corresponding issue to map to the sub-task:

- [sub-task1-issue-number](example_sub_issue1_link_here): sub-task1 description here, 
- [sub-task2-issue-number](example_sub_issue2_link_here): sub-task2 description here,
- ...



Discussion:

Solution patch:
diff --git a/broker/src/main/java/org/apache/rocketmq/broker/processor/EndTransactionProcessor.java b/broker/src/main/java/org/apache/rocketmq/broker/processor/EndTransactionProcessor.java
--- a/broker/src/main/java/org/apache/rocketmq/broker/processor/EndTransactionProcessor.java
+++ b/broker/src/main/java/org/apache/rocketmq/broker/processor/EndTransactionProcessor.java
@@ -17,8 +17,10 @@
 package org.apache.rocketmq.broker.processor;
 
 import io.netty.channel.ChannelHandlerContext;
+import org.apache.commons.lang3.StringUtils;
 import org.apache.rocketmq.broker.BrokerController;
 import org.apache.rocketmq.broker.transaction.OperationResult;
+import org.apache.rocketmq.broker.transaction.queue.TransactionalMessageUtil;
 import org.apache.rocketmq.common.TopicFilterType;
 import org.apache.rocketmq.common.constant.LoggerName;
 import org.apache.rocketmq.common.message.MessageAccessor;
@@ -125,6 +127,12 @@ public RemotingCommand processRequest(ChannelHandlerContext ctx, RemotingCommand
         if (MessageSysFlag.TRANSACTION_COMMIT_TYPE == requestHeader.getCommitOrRollback()) {
             result = this.brokerController.getTransactionalMessageService().commitMessage(requestHeader);
             if (result.getResponseCode() == ResponseCode.SUCCESS) {
+                if (rejectCommitOrRollback(requestHeader, result.getPrepareMessage())) {
+                    response.setCode(ResponseCode.ILLEGAL_OPERATION);
+                    LOGGER.warn("Message commit fail [producer end]. currentTimeMillis - bornTime > checkImmunityTime, msgId={},commitLogOffset={}, wait check",
+                            requestHeader.getMsgId(), requestHeader.getCommitLogOffset());
+                    return response;
+                }
                 RemotingCommand res = checkPrepareMessage(result.getPrepareMessage(), requestHeader);
                 if (res.getCode() == ResponseCode.SUCCESS) {
                     MessageExtBrokerInner msgInner = endMessageTransaction(result.getPrepareMessage());
@@ -144,6 +152,12 @@ public RemotingCommand processRequest(ChannelHandlerContext ctx, RemotingCommand
         } else if (MessageSysFlag.TRANSACTION_ROLLBACK_TYPE == requestHeader.getCommitOrRollback()) {
             result = this.brokerController.getTransactionalMessageService().rollbackMessage(requestHeader);
             if (result.getResponseCode() == ResponseCode.SUCCESS) {
+                if (rejectCommitOrRollback(requestHeader, result.getPrepareMessage())) {
+                    response.setCode(ResponseCode.ILLEGAL_OPERATION);
+                    LOGGER.warn("Message rollback fail [producer end]. currentTimeMillis - bornTime > checkImmunityTime, msgId={},commitLogOffset={}, wait check",
+                            requestHeader.getMsgId(), requestHeader.getCommitLogOffset());
+                    return response;
+                }
                 RemotingCommand res = checkPrepareMessage(result.getPrepareMessage(), requestHeader);
                 if (res.getCode() == ResponseCode.SUCCESS) {
                     this.brokerController.getTransactionalMessageService().deletePrepareMessage(result.getPrepareMessage());
@@ -156,6 +170,30 @@ public RemotingCommand processRequest(ChannelHandlerContext ctx, RemotingCommand
         return response;
     }
 
+    /**
+     * If you specify a custom first check time CheckImmunityTimeInSeconds,
+     * And the commit/rollback request whose validity period exceeds CheckImmunityTimeInSeconds and is not checked back will be processed and failed
+     * returns ILLEGAL_OPERATION 604 error
+     * @param requestHeader
+     * @param messageExt
+     * @return
+     */
+    public boolean rejectCommitOrRollback(EndTransactionRequestHeader requestHeader, MessageExt messageExt) {
+        if (requestHeader.getFromTransactionCheck()) {
+            return false;
+        }
+        long transactionTimeout = brokerController.getBrokerConfig().getTransactionTimeOut();
+
+        String checkImmunityTimeStr = messageExt.getUserProperty(MessageConst.PROPERTY_CHECK_IMMUNITY_TIME_IN_SECONDS);
+        if (StringUtils.isNotEmpty(checkImmunityTimeStr)) {
+            long valueOfCurrentMinusBorn = System.currentTimeMillis() - messageExt.getBornTimestamp();
+            long checkImmunityTime = TransactionalMessageUtil.getImmunityTime(checkImmunityTimeStr, transactionTimeout);
+            //Non-check requests that exceed the specified custom first check time fail to return
+            return valueOfCurrentMinusBorn > checkImmunityTime;
+        }
+        return false;
+    }
+
     @Override
     public boolean rejectRequest() {
         return false;
diff --git a/broker/src/main/java/org/apache/rocketmq/broker/transaction/queue/TransactionalMessageUtil.java b/broker/src/main/java/org/apache/rocketmq/broker/transaction/queue/TransactionalMessageUtil.java
--- a/broker/src/main/java/org/apache/rocketmq/broker/transaction/queue/TransactionalMessageUtil.java
+++ b/broker/src/main/java/org/apache/rocketmq/broker/transaction/queue/TransactionalMessageUtil.java
@@ -74,4 +74,20 @@ public static MessageExtBrokerInner buildTransactionalMessageFromHalfMessage(Mes
 
         return msgInner;
     }
+
+    public static long getImmunityTime(String checkImmunityTimeStr, long transactionTimeout) {
+        long checkImmunityTime = 0;
+
+        try {
+            checkImmunityTime = Long.parseLong(checkImmunityTimeStr) * 1000;
+        } catch (Throwable ignored) {
+        }
+
+        //If a custom first check time is set, the minimum check time;
+        //The default check protection period is transactionTimeout
+        if (checkImmunityTime < transactionTimeout) {
+            checkImmunityTime = transactionTimeout;
+        }
+        return checkImmunityTime;
+    }
 }
diff --git a/common/src/main/java/org/apache/rocketmq/common/BrokerConfig.java b/common/src/main/java/org/apache/rocketmq/common/BrokerConfig.java
--- a/common/src/main/java/org/apache/rocketmq/common/BrokerConfig.java
+++ b/common/src/main/java/org/apache/rocketmq/common/BrokerConfig.java
@@ -263,7 +263,7 @@ public class BrokerConfig extends BrokerIdentity {
      * Transaction message check interval.
      */
     @ImportantField
-    private long transactionCheckInterval = 60 * 1000;
+    private long transactionCheckInterval = 30 * 1000;
 
     /**
      * transaction batch op message
diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/ResponseCode.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/ResponseCode.java
--- a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/ResponseCode.java
+++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/ResponseCode.java
@@ -92,6 +92,8 @@ public class ResponseCode extends RemotingSysResponseCode {
 
     public static final int NOT_LEADER_FOR_QUEUE = 501;
 
+    public static final int ILLEGAL_OPERATION = 604;
+
     public static final int RPC_UNKNOWN = -1000;
     public static final int RPC_ADDR_IS_NULL = -1002;
     public static final int RPC_SEND_TO_CHANNEL_FAILED = -1004;


Test patch:
diff --git a/broker/src/test/java/org/apache/rocketmq/broker/processor/EndTransactionProcessorTest.java b/broker/src/test/java/org/apache/rocketmq/broker/processor/EndTransactionProcessorTest.java
--- a/broker/src/test/java/org/apache/rocketmq/broker/processor/EndTransactionProcessorTest.java
+++ b/broker/src/test/java/org/apache/rocketmq/broker/processor/EndTransactionProcessorTest.java
@@ -46,6 +46,8 @@
 import org.mockito.Spy;
 import org.mockito.junit.MockitoJUnitRunner;
 
+import java.nio.charset.StandardCharsets;
+
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.when;
@@ -119,6 +121,22 @@ public void testProcessRequest_RollBack() throws RemotingCommandException {
         assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS);
     }
 
+    @Test
+    public void testProcessRequest_RejectCommitMessage() throws RemotingCommandException {
+        when(transactionMsgService.commitMessage(any(EndTransactionRequestHeader.class))).thenReturn(createRejectResponse());
+        RemotingCommand request = createEndTransactionMsgCommand(MessageSysFlag.TRANSACTION_COMMIT_TYPE, false);
+        RemotingCommand response = endTransactionProcessor.processRequest(handlerContext, request);
+        assertThat(response.getCode()).isEqualTo(ResponseCode.ILLEGAL_OPERATION);
+    }
+
+    @Test
+    public void testProcessRequest_RejectRollBackMessage() throws RemotingCommandException {
+        when(transactionMsgService.rollbackMessage(any(EndTransactionRequestHeader.class))).thenReturn(createRejectResponse());
+        RemotingCommand request = createEndTransactionMsgCommand(MessageSysFlag.TRANSACTION_ROLLBACK_TYPE, false);
+        RemotingCommand response = endTransactionProcessor.processRequest(handlerContext, request);
+        assertThat(response.getCode()).isEqualTo(ResponseCode.ILLEGAL_OPERATION);
+    }
+
     private MessageExt createDefaultMessageExt() {
         MessageExt messageExt = new MessageExt();
         messageExt.setMsgId("12345678");
@@ -149,4 +167,27 @@ private RemotingCommand createEndTransactionMsgCommand(int status, boolean isChe
         request.makeCustomHeaderToNet();
         return request;
     }
+
+    private OperationResult createRejectResponse() {
+        OperationResult response = new OperationResult();
+        response.setPrepareMessage(createRejectMessageExt());
+        response.setResponseCode(ResponseCode.SUCCESS);
+        response.setResponseRemark(null);
+        return response;
+    }
+    private MessageExt createRejectMessageExt() {
+        MessageExt messageExt = new MessageExt();
+        messageExt.setMsgId("12345678");
+        messageExt.setQueueId(0);
+        messageExt.setCommitLogOffset(123456789L);
+        messageExt.setQueueOffset(1234);
+        messageExt.setBody("body".getBytes(StandardCharsets.UTF_8));
+        messageExt.setBornTimestamp(System.currentTimeMillis() - 65 * 1000);
+        MessageAccessor.putProperty(messageExt, MessageConst.PROPERTY_REAL_QUEUE_ID, "0");
+        MessageAccessor.putProperty(messageExt, MessageConst.PROPERTY_TRANSACTION_PREPARED, "true");
+        MessageAccessor.putProperty(messageExt, MessageConst.PROPERTY_PRODUCER_GROUP, "testTransactionGroup");
+        MessageAccessor.putProperty(messageExt, MessageConst.PROPERTY_REAL_TOPIC, "TEST");
+        MessageAccessor.putProperty(messageExt, MessageConst.PROPERTY_CHECK_IMMUNITY_TIME_IN_SECONDS, "60");
+        return messageExt;
+    }
 }
diff --git a/broker/src/test/java/org/apache/rocketmq/broker/transaction/queue/TransactionalMessageUtilTest.java b/broker/src/test/java/org/apache/rocketmq/broker/transaction/queue/TransactionalMessageUtilTest.java
--- a/broker/src/test/java/org/apache/rocketmq/broker/transaction/queue/TransactionalMessageUtilTest.java
+++ b/broker/src/test/java/org/apache/rocketmq/broker/transaction/queue/TransactionalMessageUtilTest.java
@@ -22,6 +22,7 @@
 import org.apache.rocketmq.common.message.MessageExt;
 import org.apache.rocketmq.common.message.MessageExtBrokerInner;
 import org.apache.rocketmq.common.sysflag.MessageSysFlag;
+import org.junit.Assert;
 import org.junit.Test;
 
 import static org.junit.Assert.assertEquals;
@@ -50,4 +51,43 @@ public void testBuildTransactionalMessageFromHalfMessage() {
         assertTrue(MessageSysFlag.check(msgExtInner.getSysFlag(), MessageSysFlag.TRANSACTION_PREPARED_TYPE));
         assertEquals(msgExtInner.getProperty(MessageConst.PROPERTY_PRODUCER_GROUP), halfMessage.getProperty(MessageConst.PROPERTY_PRODUCER_GROUP));
     }
+
+    @Test
+    public void testGetImmunityTime() {
+        long transactionTimeout = 6 * 1000;
+
+        String checkImmunityTimeStr = "1";
+        long immunityTime = TransactionalMessageUtil.getImmunityTime(checkImmunityTimeStr, transactionTimeout);
+        Assert.assertEquals(6 * 1000, immunityTime);
+
+        checkImmunityTimeStr = "5";
+        immunityTime = TransactionalMessageUtil.getImmunityTime(checkImmunityTimeStr, transactionTimeout);
+        Assert.assertEquals(6 * 1000, immunityTime);
+
+        checkImmunityTimeStr = "7";
+        immunityTime = TransactionalMessageUtil.getImmunityTime(checkImmunityTimeStr, transactionTimeout);
+        Assert.assertEquals(7 * 1000, immunityTime);
+
+
+        checkImmunityTimeStr = null;
+        immunityTime = TransactionalMessageUtil.getImmunityTime(checkImmunityTimeStr, transactionTimeout);
+        Assert.assertEquals(6 * 1000, immunityTime);
+
+        checkImmunityTimeStr = "-1";
+        immunityTime = TransactionalMessageUtil.getImmunityTime(checkImmunityTimeStr, transactionTimeout);
+        Assert.assertEquals(6 * 1000, immunityTime);
+
+        checkImmunityTimeStr = "60";
+        immunityTime = TransactionalMessageUtil.getImmunityTime(checkImmunityTimeStr, transactionTimeout);
+        Assert.assertEquals(60 * 1000, immunityTime);
+
+        checkImmunityTimeStr = "100";
+        immunityTime = TransactionalMessageUtil.getImmunityTime(checkImmunityTimeStr, transactionTimeout);
+        Assert.assertEquals(100 * 1000, immunityTime);
+
+
+        checkImmunityTimeStr = "100.5";
+        immunityTime = TransactionalMessageUtil.getImmunityTime(checkImmunityTimeStr, transactionTimeout);
+        Assert.assertEquals(6 * 1000, immunityTime);
+    }
 }
\ No newline at end of file