RocketMQ - send transaction message

1. Principle of transaction message

The implementation principle of RocketMQ transaction message is based on two-stage commit and scheduled transaction status check to determine whether the message is finally committed or rolled back.

2. Transaction message start

TransactionListener transactionListener = new TransactionListenerImpl();
TransactionMQProducer producer = new TransactionMQProducer("trans_producer");
ExecutorService executorService = new ThreadPoolExecutor(2, 5,
		100, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(2000), new ThreadFactory() {
	@Override
	public Thread newThread(Runnable r) {
		Thread thread = new Thread(r);
		thread.setName("client-transaction-msg-check-thread");
		return thread;
	}
});
// The thread pool that executes the transaction listening task
producer.setExecutorService(executorService);
producer.setTransactionListener(transactionListener);
producer.start();

2.1 transaction producer startup

Call org.apache.rocketmq.client.producer.TransactionMQProducer#start: first, initialize the basic configuration of the transaction producer, then call org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl#start: complete the startup of the producer. The logic here is the same as the default producer startup logic.

public void start() throws MQClientException {
    // Initialize the transaction message environment and set the thread pool to listen for transaction messages
    this.defaultMQProducerImpl.initTransactionEnv();
    // Start the default producer process
    super.start();
}

public void initTransactionEnv() {
	// Thread pool initialization for transaction state execution
	TransactionMQProducer producer = (TransactionMQProducer) this.defaultMQProducer;
	if (producer.getExecutorService() != null) {
		this.checkExecutor = producer.getExecutorService();
	} else {
		this.checkRequestQueue = new LinkedBlockingQueue<Runnable>(2000);
		this.checkExecutor = new ThreadPoolExecutor(
			1,
			1,
			1000 * 60,
			TimeUnit.MILLISECONDS,
			this.checkRequestQueue);
	}
}

3 send transaction message

org.apache.rocketmq.client.producer.TransactionMQProducer#sendMessageInTransaction: the transaction producer's interface for sending messages. First, check whether the event listener is registered. If it is empty, throw the legacy directly to the. Finally, call org.apache.rocketmq.client.impl.producer.defaultmqproducer impl#sendmessageintransaction: send synchronously by default

public TransactionSendResult sendMessageInTransaction(final Message msg, final Object arg) throws MQClientException {
	if (null == this.transactionListener) {
		throw new MQClientException("TransactionListener is null", null);
	}
	return this.defaultMQProducerImpl.sendMessageInTransaction(msg, transactionListener, arg);
}

3.1 transaction message sending process

  1. org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl#sendMessageInTransaction: to send a transaction message, first check whether the transaction executor exists, then check whether the message is legal, and then add a message to the attribute: TRAN_MSG:true,PGROUP:groupName.

    // If the transaction executor is empty, an exception is thrown
    if (null == tranExecuter) {
    	throw new MQClientException("tranExecutor is null", null);
    }
    // Check message
    Validators.checkMessage(msg, this.defaultMQProducer);
    SendResult sendResult = null;
    // Set msg attribute, TRAN_MSG:true
    MessageAccessor.putProperty(msg, MessageConst.PROPERTY_TRANSACTION_PREPARED, "true");
    // PGROUP:groupName
    MessageAccessor.putProperty(msg, MessageConst.PROPERTY_PRODUCER_GROUP, this.defaultMQProducer.getProducerGroup());
    try {
    
  2. Then call org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl#send): the sending message logic of the default producer is analyzed above. For the transaction message, the main message is to set the type of message to Trans_. Msg_ Half message, then call org.apache.rocketmq.broker.transaction.queue.TransactionalMessageServiceImpl#prepareMessage: broker to receive the message, then process the prepare message. Other logic is the same as general messages.

    // Transaction message processing
    final int tranType = MessageSysFlag.getTransactionValue(msg.getSysFlag());
    // Transaction commit or no operation
    if (tranType == MessageSysFlag.TRANSACTION_NOT_TYPE
    	|| tranType == MessageSysFlag.TRANSACTION_COMMIT_TYPE) {
    	// Delay delivery, and ensure that the maximum delay level is maxDelayLevel
    	if (msg.getDelayTimeLevel() > 0) {
    		if (msg.getDelayTimeLevel() > this.defaultMessageStore.getScheduleMessageService().getMaxDelayLevel()) {
    			msg.setDelayTimeLevel(this.defaultMessageStore.getScheduleMessageService().getMaxDelayLevel());
    		}
    		// When a message is stored, the delayed message entry Topic is SCHEDULE_TOPIC_XXXX
    		topic = ScheduleMessageService.SCHEDULE_TOPIC;
    		// The delay level is fixed mapped with the message queue number: queueId = delayLevel - 1
    		queueId = ScheduleMessageService.delayLevel2QueueId(msg.getDelayTimeLevel());
    
    		// Back up the real topic and message queue id into the attribute
    		MessageAccessor.putProperty(msg, MessageConst.PROPERTY_REAL_TOPIC, msg.getTopic());
    		MessageAccessor.putProperty(msg, MessageConst.PROPERTY_REAL_QUEUE_ID, String.valueOf(msg.getQueueId()));
    		msg.setPropertiesString(MessageDecoder.messageProperties2String(msg.getProperties()));
    		// Set the modified topic and queueId to ensure that messages of the same message level will be placed in the same queue
    		msg.setTopic(topic);
    		msg.setQueueId(queueId);
    	}
    }
    
  3. After obtaining the message sending result of the previous step, perform the corresponding operation.

    1. After sending successfully, set the transactionId of the message, and then execute org.apache.rocketmq.client.producer.transactionlistener#executelocal transaction: the logic of the local transaction in the transaction listener. Get the local transaction status of the transaction message. This is to ensure that the local transaction and the business code are in the same transaction, that is, the transaction consistency on the sender side.
    2. Sending failed: set the transaction status to ROLLBACK_MESSAGE.
    // Initialize local transaction status to unknown
    LocalTransactionState localTransactionState = LocalTransactionState.UNKNOW;
    Throwable localException = null;
    switch (sendResult.getSendStatus()) {
    	case SEND_OK: {
    		try {
    			// Set transaction id
    			if (sendResult.getTransactionId() != null) {
    				msg.putUserProperty("__transactionId__", sendResult.getTransactionId());
    			}
    			// Gets the transaction id of the message client
    			String transactionId = msg.getProperty(MessageConst.PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX);
    			if (null != transactionId && !"".equals(transactionId)) {
    				msg.setTransactionId(transactionId);
    			}
    			// 2. If the message is sent successfully, process the local transaction unit associated with the message and listen to the transaction message implemented by the client itself
    			localTransactionState = tranExecuter.executeLocalTransaction(msg, arg);
    			// If the returned local transaction state is not commit, it will be rolled back
    			if (null == localTransactionState) {
    				localTransactionState = LocalTransactionState.UNKNOW;
    			}
    			if (localTransactionState != LocalTransactionState.COMMIT_MESSAGE) {
    				log.info("executeLocalTransactionBranch return {}", localTransactionState);
    				log.info(msg.toString());
    			}
    		} catch (Throwable e) {
    			log.info("executeLocalTransactionBranch exception", e);
    			log.info(msg.toString());
    			localException = e;
    		}
    	}
    	break;
    	case FLUSH_DISK_TIMEOUT:
    	case FLUSH_SLAVE_TIMEOUT:
    	case SLAVE_NOT_AVAILABLE:
    		localTransactionState = LocalTransactionState.ROLLBACK_MESSAGE;
    		break;
    	default:
    		break;
    }
    
  4. Call org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl#endTransaction: end the transaction, and perform commit, rollback or temporarily not processing operations according to the transaction status obtained in the previous step. Finally, send a request to the broker to end the transaction. According to the transaction status, commit or rollback the progress of the commitlog file and whether to delete the prepare message.

    public void endTransaction(
    	final SendResult sendResult,
    	final LocalTransactionState localTransactionState,
    	final Throwable localException) throws RemotingException, MQBrokerException, InterruptedException, UnknownHostException {
    	//1. Find the Prepared message according to sendResult, which contains the ID of the transaction message
    	//2. Update the final status of the message according to localTransaction
    	final MessageId id;
    	if (sendResult.getOffsetMsgId() != null) {
    		id = MessageDecoder.decodeMessageId(sendResult.getOffsetMsgId());
    	} else {
    		id = MessageDecoder.decodeMessageId(sendResult.getMsgId());
    	}
    	String transactionId = sendResult.getTransactionId();
    	// Find the broker address to send the message request
    	final String brokerAddr = this.mQClientFactory.findBrokerAddressInPublish(sendResult.getMessageQueue().getBrokerName());
    	EndTransactionRequestHeader requestHeader = new EndTransactionRequestHeader();
    	requestHeader.setTransactionId(transactionId);
    	// commilog file offset
    	requestHeader.setCommitLogOffset(id.getOffset());
    	switch (localTransactionState) {
    		case COMMIT_MESSAGE:
    			requestHeader.setCommitOrRollback(MessageSysFlag.TRANSACTION_COMMIT_TYPE);
    			break;
    		case ROLLBACK_MESSAGE:
    			requestHeader.setCommitOrRollback(MessageSysFlag.TRANSACTION_ROLLBACK_TYPE);
    			break;
    		case UNKNOW:
    			requestHeader.setCommitOrRollback(MessageSysFlag.TRANSACTION_NOT_TYPE);
    			break;
    		default:
    			break;
    	}
    	requestHeader.setProducerGroup(this.defaultMQProducer.getProducerGroup());
    	requestHeader.setTranStateTableOffset(sendResult.getQueueOffset());
    	requestHeader.setMsgId(sendResult.getMsgId());
    	String remark = localException != null ? ("executeLocalTransactionBranch exception: " + localException.toString()) : null;
    	// Send request, one-way send
    	this.mQClientFactory.getMQClientAPIImpl().endTransactionOneway(brokerAddr, requestHeader, remark,
    		this.defaultMQProducer.getSendMsgTimeout());
    }
    

3 commit or rollback transaction

The commit or rollback of a transaction depends on the result returned after the execution of the local transaction logic. When the result is COMMIT_MESSAGE, ROLLBACK_ When message or unknown, different processing will be carried out.

3.1 transaction processing

No matter what the result of the local transaction is, the client will send a one-way request to the broker of the master node. After receiving the request, the broker will forward it to EndTransactionProcessor for processing according to the request code. Call org.apache.rocketmq.broker.processor.EndTransactionProcessor#processRequest to process the transaction results.

  1. Check whether the role of store is in save mode. If yes, return directly.

    // If the store is in the slave mode, it returns directly
    if (BrokerRole.SLAVE == brokerController.getMessageStoreConfig().getBrokerRole()) {
    	response.setCode(ResponseCode.SLAVE_NOT_AVAILABLE);
    	LOGGER.warn("Message store is slave mode, so end transaction is forbidden. ");
    	return response;
    }
    
  2. If the transaction status is TRANSACTION_NOT_TYPE, null will be returned directly without processing

    switch (requestHeader.getCommitOrRollback()) {
    	// Return null without operation
    	case MessageSysFlag.TRANSACTION_NOT_TYPE: {
    		LOGGER.warn("The producer[{}] end transaction in sending message,  and it's pending status."
    				+ "RequestHeader: {} Remark: {}",
    			RemotingHelper.parseChannelRemoteAddr(ctx.channel()),
    			requestHeader.toString(),
    			request.getRemark());
    		return null;
    	}
            .........
            .........
    }
    
  3. If the transaction status is TRANSACTION_COMMIT_TYPE: indicates that the local transaction is executed successfully. First, obtain the prepare message from the commitlog file according to the offset. If the prepare message is obtained successfully, check whether the prepare message is legal. Finally, use the prepare message to restore the original topic, queueId and other information to build the message, send the final message and delete the prepare message.

    if (MessageSysFlag.TRANSACTION_COMMIT_TYPE == requestHeader.getCommitOrRollback()) {
    	// Get the prepare message according to the offset from the commitlog file
    	result = this.brokerController.getTransactionalMessageService().commitMessage(requestHeader);
    	// Get prepare message succeeded
    	if (result.getResponseCode() == ResponseCode.SUCCESS) {
    		// Check whether the prepare message is legal
    		RemotingCommand res = checkPrepareMessage(result.getPrepareMessage(), requestHeader);
    		if (res.getCode() == ResponseCode.SUCCESS) {
    			// Why use the prepare message? Because the prepare message will not be consumed, it will exist until the final message is sent,
    			// Use the prepare message, re-use the original topic and queueId to build the message and send the message. Finally, delete the prepare message,
    			MessageExtBrokerInner msgInner = endMessageTransaction(result.getPrepareMessage());
    			msgInner.setSysFlag(MessageSysFlag.resetTransactionValue(msgInner.getSysFlag(), requestHeader.getCommitOrRollback()));
    			msgInner.setQueueOffset(requestHeader.getTranStateTableOffset());
    			msgInner.setPreparedTransactionOffset(requestHeader.getCommitLogOffset());
    			msgInner.setStoreTimestamp(result.getPrepareMessage().getStoreTimestamp());
    			// Sending the final message is actually restoring the previously modified message
    			RemotingCommand sendResult = sendFinalMessage(msgInner);
    			// Delete the prepare message, and then
    			if (sendResult.getCode() == ResponseCode.SUCCESS) {
    				this.brokerController.getTransactionalMessageService().deletePrepareMessage(result.getPrepareMessage());
    			}
    			return sendResult;
    		}
    		return res;
    	}
    }
    
  4. If the transaction status is TRANSACTION_ROLLBACK_TYPE: indicates that the local transaction failed to execute. First, obtain the prepare message from the commitlog file according to the offset. If the prepare message is obtained successfully, check whether the prepare message is legal. Finally, delete the prepare message directly. Here is to ensure that the first stage is atomic.

    if (MessageSysFlag.TRANSACTION_ROLLBACK_TYPE == requestHeader.getCommitOrRollback()) {
    	// Get the prepare message according to the offset from the commitlog file
    	result = this.brokerController.getTransactionalMessageService().rollbackMessage(requestHeader);
    	// Get prepare message succeeded
    	if (result.getResponseCode() == ResponseCode.SUCCESS) {
    		// Check whether the prepare message is legal
    		RemotingCommand res = checkPrepareMessage(result.getPrepareMessage(), requestHeader);
    		if (res.getCode() == ResponseCode.SUCCESS) {
    			// If the rollback operation is performed, the prepare message will be deleted directly.
    			this.brokerController.getTransactionalMessageService().deletePrepareMessage(result.getPrepareMessage());
    		}
    		return res;
    	}
    }
    
  5. The deletion mentioned in the above two transaction States is not a real deletion. Call org.apache.rocketmq.broker.transaction.queue.TransactionalMessageServiceImpl#deletePrepareMessage: store the prepare message in RMQ_ SYS_ TRANS_ OP_ HALF_ In topic topic, it indicates that the transaction message (message in prepare status) has been processed, which provides a search basis for the transaction backcheck of unprocessed transactions.

    public boolean deletePrepareMessage(MessageExt msgExt) {
    	if (this.transactionalMessageBridge.putOpMessage(msgExt, TransactionalMessageUtil.REMOVETAG)) {
    		log.info("Transaction op message write successfully. messageId={}, queueId={} msgExt:{}", msgExt.getMsgId(), msgExt.getQueueId(), msgExt);
    		return true;
    	} else {
    		log.error("Transaction op message write failed. messageId is {}, queueId is {}", msgExt.getMsgId(), msgExt.getQueueId());
    		return false;
    	}
    }
    

    3.2 transaction review

    RocketMQ detects RMQ through the TransactionalMessageCheckService thread_ SYS_ TRANS_ For messages in the half topic topic, the detection frequency of the transaction status of the message transaction check service is 1 minute by default. The default value can be changed by setting the transactionchecklninterval in the broker.conf file, and the unit is milliseconds.

    1. The transaction status here is TRANSACTION_COMMIT_TYPE,TRANSACTION_ ROLLBACK_ The logic of type has been processed when the status is transaction_ NOT_ If the type and prepare messages have not been deleted, they will be sent to the transaction callback service TransactionalMessageCheckService to check the transaction status, that is, the user-defined transaction callback interface. This service is an org.apache.rocketmq.broker.brokercontroller#initialtransaction transaction. It is a transaction callback service started when the broker controller is initialized.

      public void start() {
      	// started defaults to false
      	if (started.compareAndSet(false, true)) {
      		// Run run method
      		super.start();
      		this.brokerController.getTransactionalMessageService().open();
      	}
      }
      public void run() {
      	log.info("Start transaction check service thread!");
      	// The default check interval is 60 * 1000 = 1s
      	long checkInterval = brokerController.getBrokerConfig().getTransactionCheckInterval();
      	while (!this.isStopped()) {
      		// implement
      		this.waitForRunning(checkInterval);
      	}
      	log.info("End transaction check service thread!");
      }
      
    2. org.apache.rocketmq.broker.transaction.TransactionalMessageCheckService#onWaitEnd: check the transaction status of the message.

    protected void onWaitEnd() {
    //The transaction expiration time is 3s by default
    long timeout = brokerController.getBrokerConfig().getTransactionTimeOut();
    //The maximum number of transaction backchecks is 5 by default
    int checkMax = brokerController.getBrokerConfig().getTransactionCheckMax();
    long begin = System.currentTimeMillis();
    log.info("Begin to check prepare message, begin time:{}", begin);
    this.brokerController.getTransactionalMessageService().check(timeout, checkMax, this.brokerController.getTransactionalMessageCheckListener());
    log.info("End to check prepare message, consumed time:{}", System.currentTimeMillis() - begin);
    }

    3. `org.apache.rocketmq.broker.transaction.queue.TransactionalMessageServiceImpl#Check `: the core method to check the transaction status of messages through the topic ` RMQ_SYS_TRANS_HALF_TOPIC ` get the message queue, that is, the queue where the prepare message is located.
    
    ```java
    // The subject that handles the prepare message
    String topic = MixAll.RMQ_SYS_TRANS_HALF_TOPIC;
    // Get the routing information and message queue of topic from the broker
    Set<MessageQueue> msgQueues = transactionalMessageBridge.fetchMessageQueues(topic);
    if (msgQueues == null || msgQueues.size() == 0) {
    	log.warn("The queue of topic is empty :" + topic);
    	return;
    }
    
    1. Traverse the obtained message queue to obtain the prepared message that has been processed, with the subject of SYS_TRANS_OP_HALF_TOPIC, that is, the first stage is a message whose transaction status is commit and rollback. Then get the prepare message progress and the processed prepare message progress on the same message queue respectively.

      for (MessageQueue messageQueue : msgQueues) {
      	long startTime = System.currentTimeMillis();
      	// Gets the message subject RMQ sys of the processed prepare message_ TRANS_ OP_ HALF_ TOPIC
      	MessageQueue opQueue = getOpQueue(messageQueue);
      	// Get the consumption progress of prepare message
      	long halfOffset = transactionalMessageBridge.fetchConsumeOffset(messageQueue);
      	// Gets the message progress of the processed prepare message
      	long opOffset = transactionalMessageBridge.fetchConsumeOffset(opQueue);
      	log.info("Before check, the queue={} msgOffset={} opOffset={}", messageQueue, halfOffset, opOffset);
      	if (halfOffset < 0 || opOffset < 0) {
      		log.error("MessageQueue: {} illegal offset read: {}, op offset: {},skip this queue", messageQueue,
      			halfOffset, opOffset);
      		continue;
      	}
      	.......
      	.......
      }
      
    2. Call org.apache.rocketmq.broker.transaction.queue.TransactionalMessageServiceImpl#fillOpRemoveMap: pull 32 messages from opQueue to judge whether the message has been processed. If it has been processed, you don't need to send transaction status check back request again to avoid repeated transaction check back.

      List<Long> doneOpOffset = new ArrayList<>();
      HashMap<Long, Long> removeMap = new HashMap<>();
      // Pull 32 messages from the processed queue according to the current processing progress to judge whether the message has been processed,
      // If it is processed, it is unnecessary to send the transaction status check back request again to avoid repeated transaction check back
      PullResult pullResult = fillOpRemoveMap(removeMap, opQueue, opOffset, halfOffset, doneOpOffset);
      // The pull message is empty. Continue to try to pull the next consumption queue
      if (null == pullResult) {
      	log.error("The queue={} check msgOffset={} with opOffset={} failed, pullResult is null",
      		messageQueue, halfOffset, opOffset);
      	continue;
      }
      
      private PullResult fillOpRemoveMap(HashMap<Long, Long> removeMap,
      	MessageQueue opQueue, long pullOffsetOfOp, long miniOffset, List<Long> doneOpOffset) {
      	// Pull 32 messages from opQueue
      	PullResult pullResult = pullOpMsg(opQueue, pullOffsetOfOp, 32);
      	if (null == pullResult) {
      		return null;
      	}
      	// Pull message status
      	if (pullResult.getPullStatus() == PullStatus.OFFSET_ILLEGAL
      		|| pullResult.getPullStatus() == PullStatus.NO_MATCHED_MSG) {
      		log.warn("The miss op offset={} in queue={} is illegal, pullResult={}", pullOffsetOfOp, opQueue,
      			pullResult);
      		transactionalMessageBridge.updateConsumeOffset(opQueue, pullResult.getNextBeginOffset());
      		return pullResult;
      	} else if (pullResult.getPullStatus() == PullStatus.NO_NEW_MSG) {
      		log.warn("The miss op offset={} in queue={} is NO_NEW_MSG, pullResult={}", pullOffsetOfOp, opQueue,
      			pullResult);
      		return pullResult;
      	}
      	List<MessageExt> opMsg = pullResult.getMsgFoundList();
      	if (opMsg == null) {
      		log.warn("The miss op offset={} in queue={} is empty, pullResult={}", pullOffsetOfOp, opQueue, pullResult);
      		return pullResult;
      	}
      	// Pull the processed prepare message successfully
      	for (MessageExt opMessageExt : opMsg) {
      		Long queueOffset = getLong(new String(opMessageExt.getBody(), TransactionalMessageUtil.charset));
      		log.info("Topic: {} tags: {}, OpOffset: {}, HalfOffset: {}", opMessageExt.getTopic(),
      			opMessageExt.getTags(), opMessageExt.getQueueOffset(), queueOffset);
      		if (TransactionalMessageUtil.REMOVETAG.equals(opMessageExt.getTags())) {
      			if (queueOffset < miniOffset) {
      				// A collection of messages that have been processed
      				doneOpOffset.add(opMessageExt.getQueueOffset());
      			} else {
      				// Message set to be removed key:halfOffset, value: opOffset
      				removeMap.put(queueOffset, opMessageExt.getQueueOffset());
      			}
      		} else {
      			log.error("Found a illegal tag in opMessageExt= {} ", opMessageExt);
      		}
      	}
      	// Progress of op messages to be removed
      	log.debug("Remove map: {}", removeMap);
      	// Message progress completed
      	log.debug("Done op list: {}", doneOpOffset);
      	return pullResult;
      }
      
    3. Next, the pullResult result is processed. If the offset of the prepare message has been processed, it is removed from the collection and the next operation is performed.

      // Number of times to get an empty message
      int getMessageNullCount = 1;
      // Currently processing RMQ_ SYS_ TRANS_ HALF_ Latest progress of topic #queueld
      long newOffset = halfOffset;
      // The queue offset of the currently processed message. The subject is still RMQ_SYS_TRANS_HALF_TOPIC
      long i = halfOffset;
      while (true) {
      	// If the processing time is greater than 1min, it will stop
      	if (System.currentTimeMillis() - startTime > MAX_PROCESS_TIME_LIMIT) {
      		log.info("Queue={} process time reach max={}", messageQueue, MAX_PROCESS_TIME_LIMIT);
      		break;
      	}
      	// The current message has been processed. Remove it from the collection and continue processing the next message
      	if (removeMap.containsKey(i)) {
      		log.info("Half offset {} has been committed/rolled back", i);
      		removeMap.remove(i);
      	} else {
      		.....
      		.....
      	}
      	newOffset = i + 1;
          i++;
      }
      
    4. When the offset of the prepare message has not been processed, obtain the prepare message according to the offset and check whether the message is legal.

      // Get the message from the consumption queue according to the message queue offset
      GetResult getResult = getHalfMsg(messageQueue, i);
      MessageExt msgExt = getResult.getMsg();
      // The message is empty
      if (msgExt == null) {
      	// When the pull message is empty, it will be retried once by default. If it exceeds, the transaction status check of the message queue will be ended
      	if (getMessageNullCount++ > MAX_RETRY_COUNT_WHEN_HALF_NULL) {
      		break;
      	}
      	// No new news, just break
      	if (getResult.getPullResult().getPullStatus() == PullStatus.NO_NEW_MSG) {
      		log.info("No new msg, the miss offset={} in={}, continue check={}, pull result={}", i,
      			messageQueue, getMessageNullCount, getResult.getPullResult());
      		break;
      	} else {
      		// i set to the next offset and pull again
      		log.info("Illegal offset, the miss offset={} in={}, continue check={}, pull result={}",
      			i, messageQueue, getMessageNullCount, getResult.getPullResult());
      		i = getResult.getPullResult().getNextBeginOffset();
      		newOffset = i;
      		continue;
      	}
      }
      
    5. This means that the message has been checked to determine whether the message needs to be discarded or skipped. Discard criteria: if the number of message lookbacks is greater than the maximum allowed number of lookbacks, the attribute transaction of the message will be discarded_ CHECK_ Times plus 1, the default value is 5. Skip standard: the transaction message exceeds the expiration time of the file, and the default value is 72 hours. If you are sure to discard or skip, execute the next obtained message. Continue to judge whether the storage time of the message is greater than the current time. If it is greater than the current time, it means that the message is added after the backcheck. break directly.

      // When the message is not empty, judge whether the message needs to be discarded (swallowed, discarded, not processed) or skipped
      // Judge discarding: if the number of message lookbacks is greater than the maximum allowed number of lookbacks, the attribute transaction of the message will be discarded_ CHECK_ Times plus 1, the default is 5
      // Judge skip: the transaction message exceeds the expiration time of the file. The default value is 72 hours
      if (needDiscard(msgExt, transactionCheckMax) || needSkip(msgExt)) {
      	listener.resolveDiscardMsg(msgExt);
      	newOffset = i + 1;
      	i++;
      	continue;
      }
      // Storage time > = start time, which indicates the messages added later. It is not processed here. break
      if (msgExt.getStoreTimestamp() >= startTime) {
      	log.info("Fresh stored. the miss offset={}, check it later, store={}", i,
      		new Date(msgExt.getStoreTimestamp()));
      	break;
      }
      
    6. Check whether the current message can execute the back check request, and judge the storage time of the message, the time of the first check method, the storage time and the immediate detection message.

      // The time when the message has been stored, which is the current time of the system minus the time stamp of the message storage
      long valueOfCurrentMinusBorn = System.currentTimeMillis() - msgExt.getBornTimestamp();
      // checkImmunityTime: the time to immediately detect the transaction message. transactionTimeout: the timeout time of the transaction message, which is 3s by default
      long checkImmunityTime = transactionTimeout;
      // The latest time of the message transaction message backcheck request. It is valid only when the backcheck message is received within this time. It is null by default
      String checkImmunityTimeStr = msgExt.getUserProperty(MessageConst.PROPERTY_CHECK_IMMUNITY_TIME_IN_SECONDS);
      if (null != checkImmunityTimeStr) {
      	checkImmunityTime = getImmunityTime(checkImmunityTimeStr, transactionTimeout);
      	//Message stored time < check now time, wait for next retry
      	if (valueOfCurrentMinusBorn < checkImmunityTime) {
      		// If true is returned, the message is skipped
      		if (checkPrepareQueueOffset(removeMap, doneOpOffset, msgExt, checkImmunityTime)) {
      			newOffset = i + 1;
      			i++;
      			continue;
      		}
      	}
      } else {
      	// checkImmunityTime=transactionTimeout, the newly arrived message is not in the scope of this time. break directly
      	if ((0 <= valueOfCurrentMinusBorn) && (valueOfCurrentMinusBorn < checkImmunityTime)) {
      		log.info("New arrived, the miss offset={}, check it later checkImmunity={}, born={}", i,
      			checkImmunityTime, new Date(msgExt.getBornTimestamp()));
      		break;
      	}
      }
      
    7. Here, to indicate the final check, whether it is necessary to send the transaction back to the message, and confirm that after executing the request, call the asynchronous thread pool to send the transaction request.

      • If there are no processed messages in the processed message queue and the application transaction end time has been exceeded, that is, transactionTimeout
      • The processed message queue is not empty, and the storage time of the last message is greater than transactionTimeout.
      // Determine whether to send a transaction query back message:
      // 1. If there is no processed message in the processed message queue and the application transaction end time has been exceeded, that is, transactionTimeout
      // 2. The processed message queue is not empty, and the storage time of the last message is greater than transactionTimeout.
      boolean isNeedCheck = (opMsg == null && valueOfCurrentMinusBorn > checkImmunityTime)
      	|| (opMsg != null && (opMsg.get(opMsg.size() - 1).getBornTimestamp() - startTime > transactionTimeout))
      	|| (valueOfCurrentMinusBorn <= -1);
      
      if (isNeedCheck) {
      	// Put the message into RMQ first_ SYS_ TRANS_ HALF_ Topic topic, send successfully, true
      	if (!putBackHalfMsgQueue(msgExt, i)) {
      		continue;
      	}
      	// Call asynchronous thread pool to send transaction lookup command
      	listener.resolveHalfMsg(msgExt);
      } else {
      	// Obtain the processed message progress again for subsequent filtering of duplicate processing data
      	pullResult = fillOpRemoveMap(removeMap, opQueue, pullResult.getNextBeginOffset(), halfOffset, doneOpOffset);
      	log.info("The miss offset:{} in messageQueue:{} need to get more opMsg, result is:{}", i,
      		messageQueue, pullResult);
      	continue;
      }
      
    8. Finally, update the prepare message consumption queue and the processed prepare message queue.

      // Indicates that there are multiple messages. Update the consumption queue
      if (newOffset != halfOffset) {
      	transactionalMessageBridge.updateConsumeOffset(messageQueue, newOffset);
      }
      // Recalculate the consumption progress of processed messages
      long newOpOffset = calculateOpOffset(doneOpOffset, opOffset);
      // Indicates that the processed message progress is increased and the consumption queue is updated
      if (newOpOffset != opOffset) {
      	transactionalMessageBridge.updateConsumeOffset(opQueue, newOpOffset);
      }
      

3.3 send transaction back query

org.apache.rocketmq.broker.transaction.AbstractTransactionalMessageCheckListener#resolveHalfMsg: asynchronous thread pool sends backlookup request, org.apache.rocketmq.broker.transaction.AbstractTransactionalMessageCheckListener#sendCheckMessage: sends backlookup request.

public void sendCheckMessage(MessageExt msgExt) throws Exception {
	CheckTransactionStateRequestHeader checkTransactionStateRequestHeader = new CheckTransactionStateRequestHeader();
	// Encapsulate the callback request header
	checkTransactionStateRequestHeader.setCommitLogOffset(msgExt.getCommitLogOffset());
	checkTransactionStateRequestHeader.setOffsetMsgId(msgExt.getMsgId());
	checkTransactionStateRequestHeader.setMsgId(msgExt.getUserProperty(MessageConst.PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX));
	checkTransactionStateRequestHeader.setTransactionId(checkTransactionStateRequestHeader.getMsgId());
	checkTransactionStateRequestHeader.setTranStateTableOffset(msgExt.getQueueOffset());
	msgExt.setTopic(msgExt.getUserProperty(MessageConst.PROPERTY_REAL_TOPIC));
	msgExt.setQueueId(Integer.parseInt(msgExt.getUserProperty(MessageConst.PROPERTY_REAL_QUEUE_ID)));
	msgExt.setStoreSize(0);
	// Get production group id
	String groupId = msgExt.getProperty(MessageConst.PROPERTY_PRODUCER_GROUP);
	// Obtain the available producers according to the producer group id
	Channel channel = brokerController.getProducerManager().getAvaliableChannel(groupId);
	if (channel != null) {
		// When processing the transaction check back, the method org.apache.rocketmq.client.producer.TransactionListener.checkLocalTransaction defined by the producer will be called
		// According to the committed transaction results, another phase of transaction processing is performed, including commit, rollback or no processing.
		brokerController.getBroker2Client().checkProducerTransactionState(groupId, channel, checkTransactionStateRequestHeader, msgExt);
	} else {
		LOGGER.warn("Check transaction failed, channel is null. groupId={}", groupId);
	}
}

Tags: Java message queue RocketMQ

Posted on Fri, 03 Dec 2021 05:12:45 -0500 by dey.souvik007