This is an automated email from the ASF dual-hosted git repository.
thunguo pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-seata-go.git
The following commit(s) were added to refs/heads/master by this push:
new 0018e93e feat:support rocketmq mode in tcc (#1125)
0018e93e is described below
commit 0018e93ee23620431c8fc7c3f703c460872088b8
Author: XiaoFei <[email protected]>
AuthorDate: Fri Jul 10 21:17:55 2026 +0800
feat:support rocketmq mode in tcc (#1125)
* feat:support rocketmq mode in tcc
* fix: make reportActionContext failure non-fatal in TCC Prepare
Co-authored-by: Copilot Autofix powered by AI
<[email protected]>
* fix: guard Commit/Rollback against missing metadata
* fix: end_transaction_sender frame parsing hardening against OOM/panic
* fix: address PR review comments
* fix: fallback to msgId when offsetMsgId parsing fails
* fix: fallback to msgId when offsetMsgId fails; if both missing/invalid,
skip active END_TRANSACTION and fallback to check-back
* fix: fix golangci-lint misspell errors
* fix: fix CI problems
* fix: fix CI problems
---------
Co-authored-by: Copilot Autofix powered by AI
<[email protected]>
Co-authored-by: ThunGuo <[email protected]>
---
pkg/integration/rocketmq/config.go | 2 +
pkg/integration/rocketmq/constants.go | 1 +
pkg/integration/rocketmq/end_transaction_sender.go | 628 ++++++++++++++++++++
.../rocketmq/end_transaction_sender_test.go | 652 +++++++++++++++++++++
pkg/integration/rocketmq/seata_producer.go | 13 +
pkg/integration/rocketmq/tcc_rocketmq_action.go | 121 +++-
.../rocketmq/tcc_rocketmq_action_test.go | 311 ++++++++++
pkg/integration/rocketmq/transaction_listener.go | 4 +-
.../rocketmq/transaction_listener_test.go | 2 +-
pkg/rm/tcc/tcc_service.go | 48 +-
pkg/rm/tcc/tcc_service_test.go | 55 ++
pkg/util/flagext/day_test.go | 33 +-
12 files changed, 1823 insertions(+), 47 deletions(-)
diff --git a/pkg/integration/rocketmq/config.go
b/pkg/integration/rocketmq/config.go
index e97e3b36..ab365151 100644
--- a/pkg/integration/rocketmq/config.go
+++ b/pkg/integration/rocketmq/config.go
@@ -31,12 +31,14 @@ type SeataMQProducerConfig struct {
RetryTimesWhenSendFailed int
SendMsgTimeout time.Duration
+ ConnPoolSize int
}
func NewDefaultSeataMQProducerConfig() *SeataMQProducerConfig {
return &SeataMQProducerConfig{
RetryTimesWhenSendFailed: 3,
SendMsgTimeout: 3 * time.Second,
+ ConnPoolSize: 4,
}
}
diff --git a/pkg/integration/rocketmq/constants.go
b/pkg/integration/rocketmq/constants.go
index b0bbbe7f..7e440ce3 100644
--- a/pkg/integration/rocketmq/constants.go
+++ b/pkg/integration/rocketmq/constants.go
@@ -26,4 +26,5 @@ const (
ActionContextKeyQueueOffset = "queueOffset"
ActionContextKeyTransactionId = "transactionId"
ActionContextKeyBrokerName = "brokerName"
+ ActionContextKeyTopic = "topic"
)
diff --git a/pkg/integration/rocketmq/end_transaction_sender.go
b/pkg/integration/rocketmq/end_transaction_sender.go
new file mode 100644
index 00000000..6e80cfcd
--- /dev/null
+++ b/pkg/integration/rocketmq/end_transaction_sender.go
@@ -0,0 +1,628 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package rocketmq
+
+import (
+ "bytes"
+ "encoding/binary"
+ "encoding/json"
+ "fmt"
+ "io"
+ "net"
+ "sync"
+ "sync/atomic"
+ "time"
+)
+
+const (
+ reqEndTransaction int16 = 37
+
+ reqGetRouteInfoByTopic int16 = 105
+
+ rmqProtocolVersion int16 = 317
+
+ rmqLanguageGo byte = 9
+
+ rmqCodecType byte = 1
+
+ rmqHeaderFixedLength = 21
+
+ // rmqResponseHeaderFixedPartLength is the size of the fixed fields in a
+ // RocketMQ remoting response header:
+ // code(2) + language(1) + version(2) + opaque(4) + flag(4) = 13 bytes.
+ rmqResponseHeaderFixedPartLength = 13
+
+ commitOrRollbackCommit = 8
+
+ commitOrRollbackRollback = 12
+
+ maxFrameSize = 1024 * 1024 // 1MB upper bound for remoting frame to
prevent OOM
+
+ defaultConnPoolSize = 4
+
+ connPoolCleanInterval = 60 // seconds
+
+ connPoolIdleTimeout = 600 // seconds (10 minutes)
+)
+
+var opaqueCounter int32
+
+type endTransactionRequestHeader struct {
+ Topic string
+ ProducerGroup string
+ TranStateTableOffset int64
+ CommitLogOffset int64
+ CommitOrRollback int
+ FromTransactionCheck bool
+ MsgID string
+ TransactionId string
+}
+
+func (h *endTransactionRequestHeader) Encode() map[string]string {
+ return map[string]string{
+ "topic": h.Topic,
+ "producerGroup": h.ProducerGroup,
+ "tranStateTableOffset": fmt.Sprintf("%d",
h.TranStateTableOffset),
+ "commitLogOffset": fmt.Sprintf("%d", h.CommitLogOffset),
+ "commitOrRollback": fmt.Sprintf("%d", h.CommitOrRollback),
+ "fromTransactionCheck": fmt.Sprintf("%v",
h.FromTransactionCheck),
+ "msgId": h.MsgID,
+ "transactionId": h.TransactionId,
+ }
+}
+
+type remotingCommand struct {
+ Code int16
+ Language byte
+ Version int16
+ Opaque int32
+ Flag int32
+ Remark string
+ ExtFields map[string]string
+ Body []byte
+}
+
+func newEndTransactionCommand(header *endTransactionRequestHeader)
*remotingCommand {
+ return &remotingCommand{
+ Code: reqEndTransaction,
+ Language: rmqLanguageGo,
+ Version: rmqProtocolVersion,
+ Opaque: atomic.AddInt32(&opaqueCounter, 1),
+ ExtFields: header.Encode(),
+ }
+}
+
+func (cmd *remotingCommand) encode() ([]byte, error) {
+ headerBytes, err := cmd.encodeHeader()
+ if err != nil {
+ return nil, fmt.Errorf("encode header failed: %w", err)
+ }
+
+ frameSize := 4 + len(headerBytes) + len(cmd.Body)
+ buf := bytes.NewBuffer(make([]byte, 0, 4+frameSize))
+
+ if err := binary.Write(buf, binary.BigEndian, int32(frameSize)); err !=
nil {
+ return nil, err
+ }
+ if err := binary.Write(buf, binary.BigEndian,
markProtocolType(int32(len(headerBytes)))); err != nil {
+ return nil, err
+ }
+ if _, err := buf.Write(headerBytes); err != nil {
+ return nil, err
+ }
+ if len(cmd.Body) > 0 {
+ if _, err := buf.Write(cmd.Body); err != nil {
+ return nil, err
+ }
+ }
+
+ return buf.Bytes(), nil
+}
+
+func (cmd *remotingCommand) encodeHeader() ([]byte, error) {
+ extBytes, err := encodeExtFields(cmd.ExtFields)
+ if err != nil {
+ return nil, err
+ }
+
+ buf := bytes.NewBuffer(make([]byte, 0,
rmqHeaderFixedLength+len(cmd.Remark)+len(extBytes)))
+
+ if err := binary.Write(buf, binary.BigEndian, cmd.Code); err != nil {
+ return nil, err
+ }
+ if err := buf.WriteByte(cmd.Language); err != nil {
+ return nil, err
+ }
+ if err := binary.Write(buf, binary.BigEndian, cmd.Version); err != nil {
+ return nil, err
+ }
+ if err := binary.Write(buf, binary.BigEndian, cmd.Opaque); err != nil {
+ return nil, err
+ }
+ if err := binary.Write(buf, binary.BigEndian, cmd.Flag); err != nil {
+ return nil, err
+ }
+ if err := binary.Write(buf, binary.BigEndian, int32(len(cmd.Remark)));
err != nil {
+ return nil, err
+ }
+ if len(cmd.Remark) > 0 {
+ if _, err := buf.Write([]byte(cmd.Remark)); err != nil {
+ return nil, err
+ }
+ }
+ if err := binary.Write(buf, binary.BigEndian, int32(len(extBytes)));
err != nil {
+ return nil, err
+ }
+ if len(extBytes) > 0 {
+ if _, err := buf.Write(extBytes); err != nil {
+ return nil, err
+ }
+ }
+
+ return buf.Bytes(), nil
+}
+
+func encodeExtFields(fields map[string]string) ([]byte, error) {
+ if len(fields) == 0 {
+ return []byte{}, nil
+ }
+
+ buf := bytes.NewBuffer(nil)
+ for key, value := range fields {
+ if err := binary.Write(buf, binary.BigEndian, int16(len(key)));
err != nil {
+ return nil, err
+ }
+ if _, err := buf.Write([]byte(key)); err != nil {
+ return nil, err
+ }
+ if err := binary.Write(buf, binary.BigEndian,
int32(len(value))); err != nil {
+ return nil, err
+ }
+ if _, err := buf.Write([]byte(value)); err != nil {
+ return nil, err
+ }
+ }
+ return buf.Bytes(), nil
+}
+
+func markProtocolType(source int32) []byte {
+ result := make([]byte, 4)
+ result[0] = rmqCodecType
+ result[1] = byte((source >> 16) & 0xFF)
+ result[2] = byte((source >> 8) & 0xFF)
+ result[3] = byte(source & 0xFF)
+ return result
+}
+
+type brokerAddrResolver interface {
+ ResolveBrokerAddr(nameServerAddrs []string, topic string, brokerName
string, timeout time.Duration) (string, error)
+}
+
+type tcpSender interface {
+ Send(addr string, data []byte, timeout time.Duration) error
+ Close() error
+}
+
+type defaultBrokerAddrResolver struct{}
+
+func (r *defaultBrokerAddrResolver) ResolveBrokerAddr(nameServerAddrs
[]string, topic string, brokerName string, timeout time.Duration) (string,
error) {
+ for _, nsAddr := range nameServerAddrs {
+ addr, err := queryBrokerAddrFromNameServer(nsAddr, topic,
brokerName, timeout)
+ if err == nil && addr != "" {
+ return addr, nil
+ }
+ }
+ return "", fmt.Errorf("broker %s addr not found from name servers",
brokerName)
+}
+
+type connPool struct {
+ addr string
+ conns chan net.Conn
+ lastUsedAt int64 // atomic, unix timestamp
+ closed int32 // atomic
+}
+
+func newConnPool(addr string, size int) *connPool {
+ if size <= 0 {
+ size = defaultConnPoolSize
+ }
+ return &connPool{
+ addr: addr,
+ conns: make(chan net.Conn, size),
+ lastUsedAt: time.Now().Unix(),
+ }
+}
+
+// get returns a connection and a bool indicating whether it was freshly
+// dialed (true) or reused from the pool (false).
+func (p *connPool) get(timeout time.Duration) (net.Conn, bool, error) {
+ atomic.StoreInt64(&p.lastUsedAt, time.Now().Unix())
+ select {
+ case conn := <-p.conns:
+ return conn, false, nil
+ default:
+ }
+ conn, err := net.DialTimeout("tcp", p.addr, timeout)
+ return conn, true, err
+}
+
+func (p *connPool) put(conn net.Conn) {
+ if atomic.LoadInt32(&p.closed) == 1 {
+ conn.Close()
+ return
+ }
+ select {
+ case p.conns <- conn:
+ default:
+ conn.Close()
+ }
+}
+
+func (p *connPool) closeAll() {
+ atomic.StoreInt32(&p.closed, 1)
+ for {
+ select {
+ case conn := <-p.conns:
+ conn.Close()
+ default:
+ return
+ }
+ }
+}
+
+type defaultTCPSender struct {
+ pools sync.Map // addr -> *connPool
+ poolSize int
+ lastCleanedAt int64 // atomic, unix timestamp
+}
+
+func newDefaultTCPSender(poolSize int) *defaultTCPSender {
+ if poolSize <= 0 {
+ poolSize = defaultConnPoolSize
+ }
+ return &defaultTCPSender{
+ poolSize: poolSize,
+ lastCleanedAt: time.Now().Unix(),
+ }
+}
+
+func (s *defaultTCPSender) Send(addr string, data []byte, timeout
time.Duration) error {
+ s.cleanIdlePoolsIfNeeded()
+
+ poolAny, _ := s.pools.LoadOrStore(addr, newConnPool(addr, s.poolSize))
+ pool := poolAny.(*connPool)
+
+ conn, isNew, err := pool.get(timeout)
+ if err != nil {
+ return fmt.Errorf("dial broker %s failed: %w", addr, err)
+ }
+
+ sendAndRead := func(c net.Conn) error {
+ if err := c.SetWriteDeadline(time.Now().Add(timeout)); err !=
nil {
+ return fmt.Errorf("set write deadline failed: %w", err)
+ }
+ if _, err := c.Write(data); err != nil {
+ return fmt.Errorf("write to broker %s failed: %w",
addr, err)
+ }
+
+ if err := c.SetReadDeadline(time.Now().Add(timeout)); err !=
nil {
+ return fmt.Errorf("set read deadline failed: %w", err)
+ }
+ frameLenBuf := make([]byte, 4)
+ if _, err := io.ReadFull(c, frameLenBuf); err != nil {
+ return fmt.Errorf("read response frame length from
broker %s failed: %w", addr, err)
+ }
+ frameLen := int(binary.BigEndian.Uint32(frameLenBuf))
+ if frameLen < 8 {
+ return fmt.Errorf("broker %s response frame too short:
%d", addr, frameLen)
+ }
+ if frameLen > maxFrameSize {
+ return fmt.Errorf("broker %s response frame too large:
%d", addr, frameLen)
+ }
+ frameBuf := make([]byte, frameLen)
+ if _, err := io.ReadFull(c, frameBuf); err != nil {
+ return fmt.Errorf("read response frame from broker %s
failed: %w", addr, err)
+ }
+
+ code := int16(binary.BigEndian.Uint16(frameBuf[4:6]))
+ if code != 0 {
+ return fmt.Errorf("broker %s rejected END_TRANSACTION,
responseCode=%d", addr, code)
+ }
+ return nil
+ }
+
+ if err := sendAndRead(conn); err != nil {
+ conn.Close()
+ // If the connection was pooled (not freshly dialed), it may
have
+ // been closed by the broker during idle time. Retry once with a
+ // fresh connection before giving up.
+ if !isNew {
+ freshConn, dialErr := net.DialTimeout("tcp", addr,
timeout)
+ if dialErr == nil {
+ if retryErr := sendAndRead(freshConn); retryErr
!= nil {
+ freshConn.Close()
+ return retryErr
+ }
+ pool.put(freshConn)
+ return nil
+ }
+ }
+ return err
+ }
+
+ pool.put(conn)
+ return nil
+}
+
+func (s *defaultTCPSender) Close() error {
+ s.pools.Range(func(key, value interface{}) bool {
+ pool := value.(*connPool)
+ if _, loaded := s.pools.LoadAndDelete(key); loaded {
+ pool.closeAll()
+ }
+ return true
+ })
+ return nil
+}
+
+func (s *defaultTCPSender) cleanIdlePoolsIfNeeded() {
+ now := time.Now().Unix()
+ lastClean := atomic.LoadInt64(&s.lastCleanedAt)
+ if now-lastClean < int64(connPoolCleanInterval) {
+ return
+ }
+ if !atomic.CompareAndSwapInt64(&s.lastCleanedAt, lastClean, now) {
+ return
+ }
+
+ s.pools.Range(func(key, value interface{}) bool {
+ pool := value.(*connPool)
+ if now-atomic.LoadInt64(&pool.lastUsedAt) >
int64(connPoolIdleTimeout) {
+ if _, loaded := s.pools.LoadAndDelete(key); loaded {
+ pool.closeAll()
+ }
+ }
+ return true
+ })
+}
+
+func sendEndTransaction(
+ nameServerAddrs []string,
+ topic string,
+ brokerName string,
+ header *endTransactionRequestHeader,
+ timeout time.Duration,
+ resolver brokerAddrResolver,
+ sender tcpSender,
+) error {
+ brokerAddr, err := resolver.ResolveBrokerAddr(nameServerAddrs, topic,
brokerName, timeout)
+ if err != nil {
+ return fmt.Errorf("resolve broker addr failed: %w", err)
+ }
+
+ cmd := newEndTransactionCommand(header)
+
+ data, err := cmd.encode()
+ if err != nil {
+ return fmt.Errorf("encode command failed: %w", err)
+ }
+
+ if err := sender.Send(brokerAddr, data, timeout); err != nil {
+ return fmt.Errorf("send to broker %s failed: %w", brokerAddr,
err)
+ }
+
+ return nil
+}
+
+func queryBrokerAddrFromNameServer(nameServerAddr string, topic string,
brokerName string, timeout time.Duration) (string, error) {
+ cmd := &remotingCommand{
+ Code: reqGetRouteInfoByTopic,
+ Language: rmqLanguageGo,
+ Version: rmqProtocolVersion,
+ Opaque: atomic.AddInt32(&opaqueCounter, 1),
+ ExtFields: map[string]string{
+ "topic": topic,
+ },
+ }
+
+ data, err := cmd.encode()
+ if err != nil {
+ return "", fmt.Errorf("encode route request failed: %w", err)
+ }
+
+ conn, err := net.DialTimeout("tcp", nameServerAddr, timeout)
+ if err != nil {
+ return "", fmt.Errorf("dial name server %s failed: %w",
nameServerAddr, err)
+ }
+ defer conn.Close()
+
+ if err := conn.SetWriteDeadline(time.Now().Add(timeout)); err != nil {
+ return "", err
+ }
+ if _, err := conn.Write(data); err != nil {
+ return "", fmt.Errorf("send route request failed: %w", err)
+ }
+
+ if err := conn.SetReadDeadline(time.Now().Add(timeout)); err != nil {
+ return "", err
+ }
+ frameLenBuf := make([]byte, 4)
+ if _, err := io.ReadFull(conn, frameLenBuf); err != nil {
+ return "", fmt.Errorf("read frame length failed: %w", err)
+ }
+ frameLen := int(binary.BigEndian.Uint32(frameLenBuf))
+ if frameLen < 8 {
+ return "", fmt.Errorf("name server %s response frame too short:
%d", nameServerAddr, frameLen)
+ }
+ if frameLen > maxFrameSize {
+ return "", fmt.Errorf("name server %s response frame too large:
%d", nameServerAddr, frameLen)
+ }
+
+ frameBuf := make([]byte, frameLen)
+ if _, err := io.ReadFull(conn, frameBuf); err != nil {
+ return "", fmt.Errorf("read frame data failed: %w", err)
+ }
+
+ if len(frameBuf) < 4 {
+ return "", fmt.Errorf("response frame too short")
+ }
+ oriHeaderLen := binary.BigEndian.Uint32(frameBuf[0:4])
+ headerLen := int(oriHeaderLen & 0xFFFFFF)
+ if len(frameBuf) < 4+headerLen {
+ return "", fmt.Errorf("response header truncated")
+ }
+
+ respCode := int16(binary.BigEndian.Uint16(frameBuf[4:6]))
+ if respCode != 0 {
+ return "", fmt.Errorf("name server %s returned error code %d
for route query", nameServerAddr, respCode)
+ }
+
+ _, err = decodeResponseHeader(frameBuf[4 : 4+headerLen])
+ if err != nil {
+ return "", fmt.Errorf("decode response header failed: %w", err)
+ }
+
+ bodyStart := 4 + headerLen
+ if bodyStart >= len(frameBuf) {
+ return "", fmt.Errorf("response has no body")
+ }
+ body := frameBuf[bodyStart:]
+
+ return parseBrokerAddrFromRouteBody(body, brokerName)
+}
+
+func decodeResponseHeader(data []byte) (map[string]string, error) {
+ buf := bytes.NewReader(data)
+
+ if buf.Len() < rmqResponseHeaderFixedPartLength {
+ return nil, fmt.Errorf("header too short for fixed fields")
+ }
+ discard := make([]byte, rmqResponseHeaderFixedPartLength)
+ if _, err := io.ReadFull(buf, discard); err != nil {
+ return nil, err
+ }
+
+ var remarkLen int32
+ if err := binary.Read(buf, binary.BigEndian, &remarkLen); err != nil {
+ return nil, err
+ }
+ if remarkLen < 0 || int(remarkLen) > buf.Len() {
+ return nil, fmt.Errorf("invalid remark length: %d", remarkLen)
+ }
+ if remarkLen > 0 {
+ discardRemark := make([]byte, remarkLen)
+ if _, err := io.ReadFull(buf, discardRemark); err != nil {
+ return nil, err
+ }
+ }
+
+ var extLen int32
+ if err := binary.Read(buf, binary.BigEndian, &extLen); err != nil {
+ return nil, err
+ }
+ if extLen < 0 || int(extLen) > buf.Len() {
+ return nil, fmt.Errorf("invalid ext fields length: %d", extLen)
+ }
+ extFields := make(map[string]string)
+ if extLen > 0 {
+ extData := make([]byte, extLen)
+ if _, err := io.ReadFull(buf, extData); err != nil {
+ return nil, err
+ }
+ extBuf := bytes.NewReader(extData)
+ for extBuf.Len() > 0 {
+ var kLen int16
+ if err := binary.Read(extBuf, binary.BigEndian, &kLen);
err != nil {
+ return nil, fmt.Errorf("read ext key length
failed: %w", err)
+ }
+ if kLen < 0 || int(kLen) > extBuf.Len() {
+ return nil, fmt.Errorf("invalid ext key length:
%d", kLen)
+ }
+ key := make([]byte, kLen)
+ if _, err := io.ReadFull(extBuf, key); err != nil {
+ return nil, fmt.Errorf("read ext key failed:
%w", err)
+ }
+ var vLen int32
+ if err := binary.Read(extBuf, binary.BigEndian, &vLen);
err != nil {
+ return nil, fmt.Errorf("read ext value length
failed: %w", err)
+ }
+ if vLen < 0 || int(vLen) > extBuf.Len() {
+ return nil, fmt.Errorf("invalid ext value
length: %d", vLen)
+ }
+ value := make([]byte, vLen)
+ if _, err := io.ReadFull(extBuf, value); err != nil {
+ return nil, fmt.Errorf("read ext value failed:
%w", err)
+ }
+ extFields[string(key)] = string(value)
+ }
+ }
+
+ return extFields, nil
+}
+
+type topicRouteData struct {
+ QueueDataList []queueData `json:"queueDatas"`
+ BrokerDataList []brokerData `json:"brokerDatas"`
+}
+
+type queueData struct {
+ BrokerName string `json:"brokerName"`
+}
+
+type brokerData struct {
+ BrokerName string `json:"brokerName"`
+ BrokerAddrs map[string]string `json:"brokerAddrs"`
+}
+
+func parseBrokerAddrFromRouteBody(body []byte, brokerName string) (string,
error) {
+ var route topicRouteData
+ if err := json.Unmarshal(body, &route); err != nil {
+ return "", fmt.Errorf("unmarshal topic route data failed: %w",
err)
+ }
+
+ for _, bd := range route.BrokerDataList {
+ if bd.BrokerName == brokerName {
+ if addr, ok := bd.BrokerAddrs["0"]; ok {
+ return addr, nil
+ }
+ return "", fmt.Errorf("broker %s master (addr key '0')
not found in route data", brokerName)
+ }
+ }
+
+ return "", fmt.Errorf("broker %s not found in route data", brokerName)
+}
+
+func getQueueOffsetFromActionContext(actionCtx map[string]interface{}) int64 {
+ v, ok := actionCtx[ActionContextKeyQueueOffset]
+ if !ok {
+ return 0
+ }
+ switch val := v.(type) {
+ case float64:
+ return int64(val)
+ case int64:
+ return val
+ case int:
+ return int64(val)
+ case json.Number:
+ n, _ := val.Int64()
+ return n
+ default:
+ return 0
+ }
+}
diff --git a/pkg/integration/rocketmq/end_transaction_sender_test.go
b/pkg/integration/rocketmq/end_transaction_sender_test.go
new file mode 100644
index 00000000..4b7050f9
--- /dev/null
+++ b/pkg/integration/rocketmq/end_transaction_sender_test.go
@@ -0,0 +1,652 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package rocketmq
+
+import (
+ "bytes"
+ "encoding/binary"
+ "encoding/json"
+ "errors"
+ "io"
+ "net"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+type stubBrokerAddrResolver struct {
+ addr string
+ err error
+
+ calls int
+ lastNsAddrs []string
+ lastTopic string
+ lastBroker string
+ lastTimeout time.Duration
+}
+
+func (s *stubBrokerAddrResolver) ResolveBrokerAddr(nameServerAddrs []string,
topic string, brokerName string, timeout time.Duration) (string, error) {
+ s.calls++
+ s.lastNsAddrs = nameServerAddrs
+ s.lastTopic = topic
+ s.lastBroker = brokerName
+ s.lastTimeout = timeout
+ if s.err != nil {
+ return "", s.err
+ }
+ return s.addr, nil
+}
+
+type stubTCPSender struct {
+ err error
+
+ calls int
+ lastAddr string
+ lastData []byte
+}
+
+func (s *stubTCPSender) Send(addr string, data []byte, timeout time.Duration)
error {
+ s.calls++
+ s.lastAddr = addr
+ s.lastData = data
+ if s.err != nil {
+ return s.err
+ }
+ return nil
+}
+
+func (s *stubTCPSender) Close() error {
+ return nil
+}
+
+func TestEndTransactionRequestHeader_Encode(t *testing.T) {
+ header := &endTransactionRequestHeader{
+ Topic: "test-topic",
+ ProducerGroup: "test-group",
+ TranStateTableOffset: 42,
+ CommitLogOffset: 1024,
+ CommitOrRollback: commitOrRollbackCommit,
+ FromTransactionCheck: false,
+ MsgID: "msg-001",
+ TransactionId: "tx-001",
+ }
+
+ result := header.Encode()
+
+ assert.Equal(t, "test-topic", result["topic"])
+ assert.Equal(t, "test-group", result["producerGroup"])
+ assert.Equal(t, "42", result["tranStateTableOffset"])
+ assert.Equal(t, "1024", result["commitLogOffset"])
+ assert.Equal(t, "8", result["commitOrRollback"])
+ assert.Equal(t, "false", result["fromTransactionCheck"])
+ assert.Equal(t, "msg-001", result["msgId"])
+ assert.Equal(t, "tx-001", result["transactionId"])
+}
+
+func TestEndTransactionRequestHeader_EncodeRollback(t *testing.T) {
+ header := &endTransactionRequestHeader{
+ Topic: "test-topic",
+ ProducerGroup: "test-group",
+ TranStateTableOffset: 10,
+ CommitLogOffset: 2048,
+ CommitOrRollback: commitOrRollbackRollback,
+ FromTransactionCheck: false,
+ MsgID: "msg-002",
+ TransactionId: "tx-002",
+ }
+
+ result := header.Encode()
+
+ assert.Equal(t, "12", result["commitOrRollback"])
+}
+
+func TestNewEndTransactionCommand(t *testing.T) {
+ header := &endTransactionRequestHeader{
+ Topic: "test-topic",
+ ProducerGroup: "test-group",
+ CommitOrRollback: commitOrRollbackCommit,
+ MsgID: "msg-001",
+ TransactionId: "tx-001",
+ }
+
+ cmd := newEndTransactionCommand(header)
+
+ assert.Equal(t, reqEndTransaction, cmd.Code)
+ assert.Equal(t, rmqLanguageGo, cmd.Language)
+ assert.Equal(t, rmqProtocolVersion, cmd.Version)
+ assert.NotZero(t, cmd.Opaque)
+ assert.Equal(t, "test-topic", cmd.ExtFields["topic"])
+ assert.Equal(t, "test-group", cmd.ExtFields["producerGroup"])
+ assert.Equal(t, "8", cmd.ExtFields["commitOrRollback"])
+}
+
+func TestRemotingCommand_Encode_FrameFormat(t *testing.T) {
+ cmd := &remotingCommand{
+ Code: reqEndTransaction,
+ Language: rmqLanguageGo,
+ Version: rmqProtocolVersion,
+ Opaque: 1,
+ Flag: 0,
+ Remark: "",
+ ExtFields: map[string]string{},
+ }
+
+ data, err := cmd.encode()
+ require.NoError(t, err)
+ require.NotNil(t, data)
+
+ reader := bytes.NewReader(data)
+
+ var frameSize int32
+ err = binary.Read(reader, binary.BigEndian, &frameSize)
+ require.NoError(t, err)
+ assert.Equal(t, int32(len(data)-4), frameSize)
+
+ var headerLenRaw int32
+ err = binary.Read(reader, binary.BigEndian, &headerLenRaw)
+ require.NoError(t, err)
+ codecType := byte((headerLenRaw >> 24) & 0xFF)
+ headerLen := headerLenRaw & 0xFFFFFF
+ assert.Equal(t, rmqCodecType, codecType)
+ assert.Equal(t, int32(rmqHeaderFixedLength), headerLen)
+
+ var code int16
+ err = binary.Read(reader, binary.BigEndian, &code)
+ require.NoError(t, err)
+ assert.Equal(t, reqEndTransaction, code)
+
+ language, err := reader.ReadByte()
+ require.NoError(t, err)
+ assert.Equal(t, rmqLanguageGo, language)
+
+ var version int16
+ err = binary.Read(reader, binary.BigEndian, &version)
+ require.NoError(t, err)
+ assert.Equal(t, rmqProtocolVersion, version)
+
+ var opaque int32
+ err = binary.Read(reader, binary.BigEndian, &opaque)
+ require.NoError(t, err)
+ assert.Equal(t, int32(1), opaque)
+
+ var flag int32
+ err = binary.Read(reader, binary.BigEndian, &flag)
+ require.NoError(t, err)
+ assert.Equal(t, int32(0), flag)
+
+ var remarkLen int32
+ err = binary.Read(reader, binary.BigEndian, &remarkLen)
+ require.NoError(t, err)
+ assert.Equal(t, int32(0), remarkLen)
+
+ var extLen int32
+ err = binary.Read(reader, binary.BigEndian, &extLen)
+ require.NoError(t, err)
+ assert.Equal(t, int32(0), extLen)
+}
+
+func TestRemotingCommand_Encode_WithExtFields(t *testing.T) {
+ cmd := &remotingCommand{
+ Code: reqEndTransaction,
+ Language: rmqLanguageGo,
+ Version: rmqProtocolVersion,
+ Opaque: 42,
+ ExtFields: map[string]string{
+ "producerGroup": "test-group",
+ },
+ }
+
+ data, err := cmd.encode()
+ require.NoError(t, err)
+ require.NotNil(t, data)
+
+ frameSize := int32(binary.BigEndian.Uint32(data[0:4]))
+ assert.Equal(t, int32(len(data)-4), frameSize)
+
+ headerLenRaw := binary.BigEndian.Uint32(data[4:8])
+ headerLen := int(headerLenRaw & 0xFFFFFF)
+ assert.Equal(t, rmqHeaderFixedLength+29, headerLen)
+}
+
+func TestEncodeExtFields_Empty(t *testing.T) {
+ result, err := encodeExtFields(map[string]string{})
+ require.NoError(t, err)
+ assert.Empty(t, result)
+}
+
+func TestEncodeExtFields_SingleField(t *testing.T) {
+ fields := map[string]string{
+ "key": "value",
+ }
+
+ result, err := encodeExtFields(fields)
+ require.NoError(t, err)
+
+ reader := bytes.NewReader(result)
+
+ var keyLen int16
+ err = binary.Read(reader, binary.BigEndian, &keyLen)
+ require.NoError(t, err)
+ assert.Equal(t, int16(3), keyLen)
+
+ keyBuf := make([]byte, keyLen)
+ _, err = reader.Read(keyBuf)
+ require.NoError(t, err)
+ assert.Equal(t, "key", string(keyBuf))
+
+ var valueLen int32
+ err = binary.Read(reader, binary.BigEndian, &valueLen)
+ require.NoError(t, err)
+ assert.Equal(t, int32(5), valueLen)
+
+ valueBuf := make([]byte, valueLen)
+ _, err = reader.Read(valueBuf)
+ require.NoError(t, err)
+ assert.Equal(t, "value", string(valueBuf))
+}
+
+func TestMarkProtocolType(t *testing.T) {
+ result := markProtocolType(100)
+
+ assert.Equal(t, rmqCodecType, result[0])
+ assert.Equal(t, byte(0x00), result[1])
+ assert.Equal(t, byte(0x00), result[2])
+ assert.Equal(t, byte(0x64), result[3])
+}
+
+func TestParseBrokerAddrFromRouteBody_FoundMaster(t *testing.T) {
+ route := topicRouteData{
+ BrokerDataList: []brokerData{
+ {
+ BrokerName: "broker-a",
+ BrokerAddrs: map[string]string{
+ "0": "192.168.1.100:10911",
+ "1": "192.168.1.101:10911",
+ },
+ },
+ },
+ }
+ body, _ := json.Marshal(route)
+
+ addr, err := parseBrokerAddrFromRouteBody(body, "broker-a")
+
+ require.NoError(t, err)
+ assert.Equal(t, "192.168.1.100:10911", addr)
+}
+
+func TestParseBrokerAddrFromRouteBody_MasterNotFound(t *testing.T) {
+ route := topicRouteData{
+ BrokerDataList: []brokerData{
+ {
+ BrokerName: "broker-a",
+ BrokerAddrs: map[string]string{
+ "1": "192.168.1.101:10911",
+ },
+ },
+ },
+ }
+ body, _ := json.Marshal(route)
+
+ _, err := parseBrokerAddrFromRouteBody(body, "broker-a")
+
+ require.Error(t, err)
+ assert.Contains(t, err.Error(), "master (addr key '0') not found")
+}
+
+func TestParseBrokerAddrFromRouteBody_BrokerNotFound(t *testing.T) {
+ route := topicRouteData{
+ BrokerDataList: []brokerData{
+ {
+ BrokerName: "broker-a",
+ BrokerAddrs: map[string]string{"0":
"192.168.1.100:10911"},
+ },
+ },
+ }
+ body, _ := json.Marshal(route)
+
+ _, err := parseBrokerAddrFromRouteBody(body, "broker-b")
+
+ require.Error(t, err)
+ assert.Contains(t, err.Error(), "broker-b")
+}
+
+func TestParseBrokerAddrFromRouteBody_InvalidJSON(t *testing.T) {
+ _, err := parseBrokerAddrFromRouteBody([]byte("invalid json"),
"broker-a")
+
+ require.Error(t, err)
+ assert.Contains(t, err.Error(), "unmarshal")
+}
+
+func TestGetQueueOffsetFromActionContext_Float64(t *testing.T) {
+ ctx := map[string]interface{}{
+ ActionContextKeyQueueOffset: float64(42),
+ }
+ assert.Equal(t, int64(42), getQueueOffsetFromActionContext(ctx))
+}
+
+func TestGetQueueOffsetFromActionContext_Int64(t *testing.T) {
+ ctx := map[string]interface{}{
+ ActionContextKeyQueueOffset: int64(42),
+ }
+ assert.Equal(t, int64(42), getQueueOffsetFromActionContext(ctx))
+}
+
+func TestGetQueueOffsetFromActionContext_Int(t *testing.T) {
+ ctx := map[string]interface{}{
+ ActionContextKeyQueueOffset: int(42),
+ }
+ assert.Equal(t, int64(42), getQueueOffsetFromActionContext(ctx))
+}
+
+func TestGetQueueOffsetFromActionContext_Missing(t *testing.T) {
+ ctx := map[string]interface{}{}
+ assert.Equal(t, int64(0), getQueueOffsetFromActionContext(ctx))
+}
+
+func TestGetQueueOffsetFromActionContext_InvalidType(t *testing.T) {
+ ctx := map[string]interface{}{
+ ActionContextKeyQueueOffset: "not a number",
+ }
+ assert.Equal(t, int64(0), getQueueOffsetFromActionContext(ctx))
+}
+
+func TestGetStringFromMap_Found(t *testing.T) {
+ ctx := map[string]interface{}{
+ ActionContextKeyMsgId: "msg-001",
+ }
+ assert.Equal(t, "msg-001", getStringFromMap(ctx, ActionContextKeyMsgId))
+}
+
+func TestGetStringFromMap_Missing(t *testing.T) {
+ ctx := map[string]interface{}{}
+ assert.Equal(t, "", getStringFromMap(ctx, ActionContextKeyMsgId))
+}
+
+func TestGetStringFromMap_WrongType(t *testing.T) {
+ ctx := map[string]interface{}{
+ ActionContextKeyMsgId: 12345,
+ }
+ assert.Equal(t, "", getStringFromMap(ctx, ActionContextKeyMsgId))
+}
+
+func TestSendEndTransaction_Success(t *testing.T) {
+ resolver := &stubBrokerAddrResolver{addr: "192.168.1.100:10911"}
+ sender := &stubTCPSender{}
+ header := &endTransactionRequestHeader{
+ Topic: "test-topic",
+ ProducerGroup: "test-group",
+ TranStateTableOffset: 42,
+ CommitLogOffset: 1024,
+ CommitOrRollback: commitOrRollbackCommit,
+ MsgID: "msg-001",
+ TransactionId: "tx-001",
+ }
+
+ err := sendEndTransaction(
+ []string{"nameserver:9876"},
+ "test-topic",
+ "broker-a",
+ header,
+ 3*time.Second,
+ resolver,
+ sender,
+ )
+
+ require.NoError(t, err)
+ assert.Equal(t, 1, resolver.calls)
+ assert.Equal(t, []string{"nameserver:9876"}, resolver.lastNsAddrs)
+ assert.Equal(t, "test-topic", resolver.lastTopic)
+ assert.Equal(t, "broker-a", resolver.lastBroker)
+ assert.Equal(t, 1, sender.calls)
+ assert.Equal(t, "192.168.1.100:10911", sender.lastAddr)
+ assert.NotEmpty(t, sender.lastData)
+
+ frameSize := int32(binary.BigEndian.Uint32(sender.lastData[0:4]))
+ assert.Equal(t, int32(len(sender.lastData)-4), frameSize)
+}
+
+func TestSendEndTransaction_ResolverError(t *testing.T) {
+ resolver := &stubBrokerAddrResolver{err: errors.New("name server
unreachable")}
+ sender := &stubTCPSender{}
+ header := &endTransactionRequestHeader{
+ Topic: "test-topic",
+ ProducerGroup: "test-group",
+ }
+
+ err := sendEndTransaction(
+ []string{"nameserver:9876"},
+ "test-topic",
+ "broker-a",
+ header,
+ 3*time.Second,
+ resolver,
+ sender,
+ )
+
+ require.Error(t, err)
+ assert.Contains(t, err.Error(), "resolve broker addr failed")
+ assert.Equal(t, 0, sender.calls)
+}
+
+func TestSendEndTransaction_SendError(t *testing.T) {
+ resolver := &stubBrokerAddrResolver{addr: "192.168.1.100:10911"}
+ sender := &stubTCPSender{err: errors.New("connection refused")}
+ header := &endTransactionRequestHeader{
+ Topic: "test-topic",
+ ProducerGroup: "test-group",
+ }
+
+ err := sendEndTransaction(
+ []string{"nameserver:9876"},
+ "test-topic",
+ "broker-a",
+ header,
+ 3*time.Second,
+ resolver,
+ sender,
+ )
+
+ require.Error(t, err)
+ assert.Contains(t, err.Error(), "send to broker")
+ assert.Equal(t, 1, resolver.calls)
+ assert.Equal(t, 1, sender.calls)
+}
+
+func TestDefaultBrokerAddrResolver_TriesAllNameServers(t *testing.T) {
+ resolver := &defaultBrokerAddrResolver{}
+
+ _, err := resolver.ResolveBrokerAddr(
+ []string{"127.0.0.1:1", "127.0.0.1:1"},
+ "test-topic",
+ "broker-a",
+ 3*time.Second,
+ )
+
+ require.Error(t, err)
+ assert.Contains(t, err.Error(), "broker")
+}
+
+func TestConnPool_GetAndPut(t *testing.T) {
+ // Start a temporary TCP listener to serve as a fake broker.
+ ln, err := net.Listen("tcp", "127.0.0.1:0")
+ require.NoError(t, err)
+ defer ln.Close()
+
+ go func() {
+ for {
+ conn, err := ln.Accept()
+ if err != nil {
+ return
+ }
+ go func(c net.Conn) {
+ defer c.Close()
+ buf := make([]byte, 4)
+ if _, err := io.ReadFull(c, buf); err != nil {
+ return
+ }
+ frameLen := binary.BigEndian.Uint32(buf)
+ payload := make([]byte, frameLen)
+ if _, err := io.ReadFull(c, payload); err !=
nil {
+ return
+ }
+ // Write back a success response (code=0).
+ resp := make([]byte, 8)
+ binary.BigEndian.PutUint32(resp[0:4], 4)
+ binary.BigEndian.PutUint16(resp[4:6], 0)
+ c.Write(resp)
+ }(conn)
+ }
+ }()
+
+ pool := newConnPool(ln.Addr().String(), 2)
+
+ // First get should create a new connection.
+ conn1, isNew1, err := pool.get(3 * time.Second)
+ require.NoError(t, err)
+ require.NotNil(t, conn1)
+ assert.True(t, isNew1)
+
+ // Return the connection to the pool.
+ pool.put(conn1)
+
+ // Second get should reuse the pooled connection.
+ conn2, isNew2, err := pool.get(3 * time.Second)
+ require.NoError(t, err)
+ require.NotNil(t, conn2)
+ assert.False(t, isNew2)
+ assert.Equal(t, conn1.RemoteAddr().String(),
conn2.RemoteAddr().String())
+
+ conn2.Close()
+}
+
+func TestConnPool_PutWhenFullClosesConnection(t *testing.T) {
+ ln, err := net.Listen("tcp", "127.0.0.1:0")
+ require.NoError(t, err)
+ defer ln.Close()
+
+ pool := newConnPool(ln.Addr().String(), 2)
+
+ conn, err := net.Dial("tcp", ln.Addr().String())
+ require.NoError(t, err)
+
+ // Fill the pool.
+ for i := 0; i < 2; i++ {
+ c, err := net.Dial("tcp", ln.Addr().String())
+ require.NoError(t, err)
+ pool.put(c)
+ }
+
+ // Putting one more should close it instead of overflowing.
+ pool.put(conn)
+ // The connection should eventually be closed; we just verify no panic.
+}
+
+func TestConnPool_PutAfterClose(t *testing.T) {
+ ln, err := net.Listen("tcp", "127.0.0.1:0")
+ require.NoError(t, err)
+ defer ln.Close()
+
+ pool := newConnPool(ln.Addr().String(), 2)
+
+ conn, err := net.Dial("tcp", ln.Addr().String())
+ require.NoError(t, err)
+
+ pool.closeAll()
+
+ // After closeAll, put should close the connection instead of returning
it to the pool.
+ pool.put(conn)
+ // No panic and connection is closed; verified implicitly.
+}
+
+func TestConnPool_GetDialsWhenEmpty(t *testing.T) {
+ ln, err := net.Listen("tcp", "127.0.0.1:0")
+ require.NoError(t, err)
+ defer ln.Close()
+
+ pool := newConnPool(ln.Addr().String(), 2)
+
+ // Get from an empty pool should dial a new connection.
+ conn, isNew, err := pool.get(3 * time.Second)
+ require.NoError(t, err)
+ require.NotNil(t, conn)
+ assert.True(t, isNew)
+ conn.Close()
+}
+
+func TestDefaultTCPSender_RetryOnPooledConnFailure(t *testing.T) {
+ ln, err := net.Listen("tcp", "127.0.0.1:0")
+ require.NoError(t, err)
+ defer ln.Close()
+
+ go func() {
+ for {
+ conn, err := ln.Accept()
+ if err != nil {
+ return
+ }
+ go func(c net.Conn) {
+ defer c.Close()
+ buf := make([]byte, 4)
+ if _, err := io.ReadFull(c, buf); err != nil {
+ return
+ }
+ frameLen := binary.BigEndian.Uint32(buf)
+ payload := make([]byte, frameLen)
+ if _, err := io.ReadFull(c, payload); err !=
nil {
+ return
+ }
+ resp, _ := (&remotingCommand{
+ Code: 0,
+ Language: rmqLanguageGo,
+ Version: rmqProtocolVersion,
+ Opaque: 1,
+ Flag: 0,
+ Remark: "",
+ ExtFields: map[string]string{},
+ }).encode()
+ c.Write(resp)
+ }(conn)
+ }
+ }()
+
+ sender := newDefaultTCPSender(2)
+
+ // Dial a connection, immediately close it, and put the closed conn
into the pool.
+ staleConn, err := net.Dial("tcp", ln.Addr().String())
+ require.NoError(t, err)
+ staleConn.Close()
+
+ poolAny, _ := sender.pools.LoadOrStore(ln.Addr().String(),
newConnPool(ln.Addr().String(), 2))
+ pool := poolAny.(*connPool)
+ pool.put(staleConn)
+
+ data, err := (&remotingCommand{
+ Code: reqEndTransaction,
+ Language: rmqLanguageGo,
+ Version: rmqProtocolVersion,
+ Opaque: 1,
+ ExtFields: map[string]string{},
+ }).encode()
+ require.NoError(t, err)
+
+ // Send should fail on the stale pooled conn, then retry with a fresh
conn and succeed.
+ err = sender.Send(ln.Addr().String(), data, 3*time.Second)
+ require.NoError(t, err)
+}
diff --git a/pkg/integration/rocketmq/seata_producer.go
b/pkg/integration/rocketmq/seata_producer.go
index 3a22f8e6..5de336fe 100644
--- a/pkg/integration/rocketmq/seata_producer.go
+++ b/pkg/integration/rocketmq/seata_producer.go
@@ -21,6 +21,7 @@ import (
"context"
"fmt"
"sync"
+ "time"
"github.com/apache/rocketmq-client-go/v2/primitive"
"github.com/apache/rocketmq-client-go/v2/producer"
@@ -70,6 +71,13 @@ func NewSeataMQProducer(cfg *SeataMQProducerConfig)
(*SeataMQProducer, error) {
return nil, fmt.Errorf("GroupName cannot be empty")
}
+ if cfg.SendMsgTimeout <= 0 {
+ cfg.SendMsgTimeout = 3 * time.Second
+ }
+ if cfg.ConnPoolSize <= 0 {
+ cfg.ConnPoolSize = 4
+ }
+
p := &SeataMQProducer{
config: cfg,
}
@@ -139,6 +147,11 @@ func (p *SeataMQProducer) Shutdown() error {
if err := p.normalProducer.Shutdown(); err != nil {
errs = append(errs, err)
}
+ if p.tccAction != nil && p.tccAction.sender != nil {
+ if err := p.tccAction.sender.Close(); err != nil {
+ errs = append(errs, err)
+ }
+ }
if len(errs) > 0 {
return fmt.Errorf("shutdown errors: %v", errs)
diff --git a/pkg/integration/rocketmq/tcc_rocketmq_action.go
b/pkg/integration/rocketmq/tcc_rocketmq_action.go
index a769646d..8ff7afc8 100644
--- a/pkg/integration/rocketmq/tcc_rocketmq_action.go
+++ b/pkg/integration/rocketmq/tcc_rocketmq_action.go
@@ -30,11 +30,19 @@ import (
type TCCRocketMQAction struct {
producer *SeataMQProducer
+ resolver brokerAddrResolver
+ sender tcpSender
}
func NewTCCRocketMQAction(producer *SeataMQProducer) *TCCRocketMQAction {
+ poolSize := defaultConnPoolSize
+ if producer != nil && producer.config != nil &&
producer.config.ConnPoolSize > 0 {
+ poolSize = producer.config.ConnPoolSize
+ }
return &TCCRocketMQAction{
producer: producer,
+ resolver: &defaultBrokerAddrResolver{},
+ sender: newDefaultTCPSender(poolSize),
}
}
@@ -78,6 +86,7 @@ func (a *TCCRocketMQAction) Prepare(ctx context.Context,
params interface{}) (bo
bac.ActionContext[ActionContextKeyQueueId] =
result.MessageQueue.QueueId
bac.ActionContext[ActionContextKeyBrokerName] =
result.MessageQueue.BrokerName
}
+ bac.ActionContext[ActionContextKeyTopic] = msg.Topic
log.Infof("[TCCRocketMQ] Prepare success, xid=%s, branchId=%d,
msgId=%s", xid, bac.BranchId, result.MsgID)
@@ -85,20 +94,110 @@ func (a *TCCRocketMQAction) Prepare(ctx context.Context,
params interface{}) (bo
}
func (a *TCCRocketMQAction) Commit(ctx context.Context, bac
*tm.BusinessActionContext) (bool, error) {
- // Commit is a no-op because RocketMQ transactional messages use a
check-back mechanism.
- // When the global transaction commits, RocketMQ will invoke
CheckLocalTransaction
- // via SeataTransactionListener to determine the final message
disposition.
- // The message has already been sent to the broker during Prepare phase
with an
- // initial state of UnknowState, pending the check-back resolution.
- log.Infof("[TCCRocketMQ] Commit (no-op, rely on check-back), xid=%s,
branchId=%d", bac.Xid, bac.BranchId)
+ if a.producer == nil || a.producer.config == nil {
+ log.Warnf("[TCCRocketMQ] Commit skipped, producer or config is
nil, fallback to check-back, xid=%s, branchId=%d", bac.Xid, bac.BranchId)
+ return true, nil
+ }
+ topic := getStringFromMap(bac.ActionContext, ActionContextKeyTopic)
+ brokerName := getStringFromMap(bac.ActionContext,
ActionContextKeyBrokerName)
+ if topic == "" || brokerName == "" {
+ log.Warnf("[TCCRocketMQ] Commit missing metadata (topic=%s,
brokerName=%s), skip active END_TRANSACTION, fallback to check-back, xid=%s,
branchId=%d",
+ topic, brokerName, bac.Xid, bac.BranchId)
+ return true, nil
+ }
+ header, ok := a.buildEndTransactionHeader(bac, topic,
commitOrRollbackCommit)
+ if !ok {
+ log.Warnf("[TCCRocketMQ] Commit cannot resolve valid
commitLogOffset, skip active END_TRANSACTION, fallback to check-back, xid=%s,
branchId=%d", bac.Xid, bac.BranchId)
+ return true, nil
+ }
+ err := sendEndTransaction(
+ a.producer.config.NameServerAddrs,
+ topic,
+ brokerName,
+ header,
+ a.producer.config.SendMsgTimeout,
+ a.resolver,
+ a.sender,
+ )
+ if err != nil {
+ log.Warnf("[TCCRocketMQ] Commit send END_TRANSACTION failed,
fallback to check-back, xid=%s, branchId=%d, err=%v",
+ bac.Xid, bac.BranchId, err)
+ return true, nil
+ }
+ log.Infof("[TCCRocketMQ] Commit send END_TRANSACTION success, xid=%s,
branchId=%d", bac.Xid, bac.BranchId)
return true, nil
}
func (a *TCCRocketMQAction) Rollback(ctx context.Context, bac
*tm.BusinessActionContext) (bool, error) {
- // Rollback is a no-op because RocketMQ transactional messages use a
check-back mechanism.
- // When the global transaction rolls back, RocketMQ will invoke
CheckLocalTransaction
- // via SeataTransactionListener, which queries the TC for the global
status and returns
- // RollbackMessageState, causing the broker to discard the message.
- log.Infof("[TCCRocketMQ] Rollback (no-op, rely on check-back), xid=%s,
branchId=%d", bac.Xid, bac.BranchId)
+ if a.producer == nil || a.producer.config == nil {
+ log.Warnf("[TCCRocketMQ] Rollback skipped, producer or config
is nil, fallback to check-back, xid=%s, branchId=%d", bac.Xid, bac.BranchId)
+ return true, nil
+ }
+ topic := getStringFromMap(bac.ActionContext, ActionContextKeyTopic)
+ brokerName := getStringFromMap(bac.ActionContext,
ActionContextKeyBrokerName)
+ if topic == "" || brokerName == "" {
+ log.Warnf("[TCCRocketMQ] Rollback missing metadata (topic=%s,
brokerName=%s), skip active END_TRANSACTION, fallback to check-back, xid=%s,
branchId=%d",
+ topic, brokerName, bac.Xid, bac.BranchId)
+ return true, nil
+ }
+ header, ok := a.buildEndTransactionHeader(bac, topic,
commitOrRollbackRollback)
+ if !ok {
+ log.Warnf("[TCCRocketMQ] Rollback cannot resolve valid
commitLogOffset, skip active END_TRANSACTION, fallback to check-back, xid=%s,
branchId=%d", bac.Xid, bac.BranchId)
+ return true, nil
+ }
+ err := sendEndTransaction(
+ a.producer.config.NameServerAddrs,
+ topic,
+ brokerName,
+ header,
+ a.producer.config.SendMsgTimeout,
+ a.resolver,
+ a.sender,
+ )
+ if err != nil {
+ log.Warnf("[TCCRocketMQ] Rollback send END_TRANSACTION failed,
fallback to check-back, xid=%s, branchId=%d, err=%v",
+ bac.Xid, bac.BranchId, err)
+ return true, nil
+ }
+ log.Infof("[TCCRocketMQ] Rollback send END_TRANSACTION success, xid=%s,
branchId=%d", bac.Xid, bac.BranchId)
return true, nil
}
+
+func (a *TCCRocketMQAction) buildEndTransactionHeader(bac
*tm.BusinessActionContext, topic string, commitOrRollback int)
(*endTransactionRequestHeader, bool) {
+ actionCtx := bac.ActionContext
+ if actionCtx == nil {
+ actionCtx = make(map[string]interface{})
+ }
+
+ offsetMsgID := getStringFromMap(actionCtx, ActionContextKeyOffsetMsgId)
+ commitLogOffset := int64(0)
+ if offsetMsgID != "" {
+ msgID, err := primitive.UnmarshalMsgID([]byte(offsetMsgID))
+ if err == nil {
+ commitLogOffset = msgID.Offset
+ }
+ }
+
+ if commitLogOffset == 0 {
+ if msgIDStr := getStringFromMap(actionCtx,
ActionContextKeyMsgId); msgIDStr != "" {
+ if msgID, err :=
primitive.UnmarshalMsgID([]byte(msgIDStr)); err == nil {
+ commitLogOffset = msgID.Offset
+ }
+ }
+ }
+
+ if commitLogOffset == 0 {
+ return nil, false
+ }
+
+ return &endTransactionRequestHeader{
+ Topic: topic,
+ ProducerGroup: a.producer.config.GroupName,
+ TranStateTableOffset:
getQueueOffsetFromActionContext(actionCtx),
+ CommitLogOffset: commitLogOffset,
+ CommitOrRollback: commitOrRollback,
+ FromTransactionCheck: false,
+ MsgID: getStringFromMap(actionCtx,
ActionContextKeyMsgId),
+ TransactionId: getStringFromMap(actionCtx,
ActionContextKeyTransactionId),
+ }, true
+}
diff --git a/pkg/integration/rocketmq/tcc_rocketmq_action_test.go
b/pkg/integration/rocketmq/tcc_rocketmq_action_test.go
index b3e24267..651f58e3 100644
--- a/pkg/integration/rocketmq/tcc_rocketmq_action_test.go
+++ b/pkg/integration/rocketmq/tcc_rocketmq_action_test.go
@@ -19,7 +19,9 @@ package rocketmq
import (
"context"
+ "errors"
"testing"
+ "time"
"github.com/apache/rocketmq-client-go/v2/primitive"
"github.com/stretchr/testify/assert"
@@ -88,6 +90,315 @@ func
TestTCCRocketMQActionPrepare_SetsMessagePropertiesAndActionContext(t *testi
assert.Equal(t, "tx-1",
bac.ActionContext[ActionContextKeyTransactionId])
assert.Equal(t, 3, bac.ActionContext[ActionContextKeyQueueId])
assert.Equal(t, "broker-a",
bac.ActionContext[ActionContextKeyBrokerName])
+ assert.Equal(t, "topic-test", bac.ActionContext[ActionContextKeyTopic])
assert.Equal(t, 1, transactionProducer.sendCalls)
assert.Same(t, msg, transactionProducer.lastMsg)
}
+
+func TestTCCRocketMQAction_Commit_Success(t *testing.T) {
+ resolver := &stubBrokerAddrResolver{addr: "192.168.1.100:10911"}
+ sender := &stubTCPSender{}
+ action := &TCCRocketMQAction{
+ producer: &SeataMQProducer{
+ config: &SeataMQProducerConfig{
+ NameServerAddrs: []string{"nameserver:9876"},
+ GroupName: "test-group",
+ SendMsgTimeout: 3 * time.Second,
+ },
+ },
+ resolver: resolver,
+ sender: sender,
+ }
+ validMsgID := primitive.CreateMessageId([]byte{10, 93, 233, 58}, 10911,
42)
+ bac := &tm.BusinessActionContext{
+ Xid: "xid-123",
+ BranchId: 1001,
+ ActionContext: map[string]interface{}{
+ ActionContextKeyMsgId: validMsgID,
+ ActionContextKeyOffsetMsgId: "offset-1",
+ ActionContextKeyQueueOffset: int64(11),
+ ActionContextKeyTransactionId: "tx-1",
+ ActionContextKeyQueueId: 3,
+ ActionContextKeyBrokerName: "broker-a",
+ ActionContextKeyTopic: "topic-test",
+ },
+ }
+
+ ok, err := action.Commit(context.Background(), bac)
+
+ require.NoError(t, err)
+ assert.True(t, ok)
+ assert.Equal(t, 1, resolver.calls)
+ assert.Equal(t, "topic-test", resolver.lastTopic)
+ assert.Equal(t, "broker-a", resolver.lastBroker)
+ assert.Equal(t, 1, sender.calls)
+ assert.Equal(t, "192.168.1.100:10911", sender.lastAddr)
+}
+
+func TestTCCRocketMQAction_Commit_ErrorOnResolverError(t *testing.T) {
+ resolver := &stubBrokerAddrResolver{err: errors.New("name server
unreachable")}
+ sender := &stubTCPSender{}
+ action := &TCCRocketMQAction{
+ producer: &SeataMQProducer{
+ config: &SeataMQProducerConfig{
+ NameServerAddrs: []string{"nameserver:9876"},
+ GroupName: "test-group",
+ SendMsgTimeout: 3 * time.Second,
+ },
+ },
+ resolver: resolver,
+ sender: sender,
+ }
+ bac := &tm.BusinessActionContext{
+ Xid: "xid-123",
+ BranchId: 1001,
+ ActionContext: map[string]interface{}{
+ ActionContextKeyMsgId: "msg-1",
+ ActionContextKeyQueueOffset: int64(11),
+ ActionContextKeyTransactionId: "tx-1",
+ ActionContextKeyBrokerName: "broker-a",
+ ActionContextKeyTopic: "topic-test",
+ },
+ }
+
+ ok, err := action.Commit(context.Background(), bac)
+
+ require.NoError(t, err)
+ assert.True(t, ok)
+ assert.Equal(t, 0, sender.calls)
+}
+
+func TestTCCRocketMQAction_Rollback_Success(t *testing.T) {
+ resolver := &stubBrokerAddrResolver{addr: "192.168.1.100:10911"}
+ sender := &stubTCPSender{}
+ action := &TCCRocketMQAction{
+ producer: &SeataMQProducer{
+ config: &SeataMQProducerConfig{
+ NameServerAddrs: []string{"nameserver:9876"},
+ GroupName: "test-group",
+ SendMsgTimeout: 3 * time.Second,
+ },
+ },
+ resolver: resolver,
+ sender: sender,
+ }
+ validMsgID := primitive.CreateMessageId([]byte{10, 93, 233, 58}, 10911,
42)
+ bac := &tm.BusinessActionContext{
+ Xid: "xid-456",
+ BranchId: 2002,
+ ActionContext: map[string]interface{}{
+ ActionContextKeyMsgId: validMsgID,
+ ActionContextKeyOffsetMsgId: "offset-2",
+ ActionContextKeyQueueOffset: int64(22),
+ ActionContextKeyTransactionId: "tx-2",
+ ActionContextKeyQueueId: 1,
+ ActionContextKeyBrokerName: "broker-b",
+ ActionContextKeyTopic: "topic-rollback",
+ },
+ }
+
+ ok, err := action.Rollback(context.Background(), bac)
+
+ require.NoError(t, err)
+ assert.True(t, ok)
+ assert.Equal(t, 1, resolver.calls)
+ assert.Equal(t, "topic-rollback", resolver.lastTopic)
+ assert.Equal(t, "broker-b", resolver.lastBroker)
+ assert.Equal(t, 1, sender.calls)
+ assert.Equal(t, "192.168.1.100:10911", sender.lastAddr)
+}
+
+func TestBuildEndTransactionHeader_ParseCommitLogOffset(t *testing.T) {
+ offsetMsgID := primitive.CreateMessageId([]byte{10, 93, 233, 58},
10911, 42)
+ action := &TCCRocketMQAction{
+ producer: &SeataMQProducer{
+ config: &SeataMQProducerConfig{
+ GroupName: "test-group",
+ },
+ },
+ }
+ bac := &tm.BusinessActionContext{
+ ActionContext: map[string]interface{}{
+ ActionContextKeyOffsetMsgId: offsetMsgID,
+ ActionContextKeyQueueOffset: int64(11),
+ ActionContextKeyMsgId: "msg-1",
+ ActionContextKeyTransactionId: "tx-1",
+ },
+ }
+
+ header, ok := action.buildEndTransactionHeader(bac, "test-topic",
commitOrRollbackCommit)
+
+ require.True(t, ok)
+ assert.Equal(t, "test-topic", header.Topic)
+ assert.Equal(t, "test-group", header.ProducerGroup)
+ assert.Equal(t, int64(11), header.TranStateTableOffset)
+ assert.Equal(t, int64(42), header.CommitLogOffset)
+ assert.Equal(t, commitOrRollbackCommit, header.CommitOrRollback)
+ assert.Equal(t, "msg-1", header.MsgID)
+ assert.Equal(t, "tx-1", header.TransactionId)
+ assert.False(t, header.FromTransactionCheck)
+}
+
+func TestBuildEndTransactionHeader_InvalidOffsetMsgID(t *testing.T) {
+ action := &TCCRocketMQAction{
+ producer: &SeataMQProducer{
+ config: &SeataMQProducerConfig{
+ GroupName: "test-group",
+ },
+ },
+ }
+ validMsgID := primitive.CreateMessageId([]byte{10, 93, 233, 58}, 10911,
42)
+ bac := &tm.BusinessActionContext{
+ ActionContext: map[string]interface{}{
+ ActionContextKeyOffsetMsgId: "invalid-id",
+ ActionContextKeyMsgId: validMsgID,
+ ActionContextKeyQueueOffset: int64(11),
+ },
+ }
+
+ header, ok := action.buildEndTransactionHeader(bac, "test-topic",
commitOrRollbackRollback)
+
+ require.True(t, ok)
+ assert.Equal(t, "test-topic", header.Topic)
+ assert.Equal(t, int64(42), header.CommitLogOffset)
+ assert.Equal(t, commitOrRollbackRollback, header.CommitOrRollback)
+}
+
+func TestBuildEndTransactionHeader_MissingOffsetMsgID(t *testing.T) {
+ action := &TCCRocketMQAction{
+ producer: &SeataMQProducer{
+ config: &SeataMQProducerConfig{
+ GroupName: "test-group",
+ },
+ },
+ }
+ bac := &tm.BusinessActionContext{
+ ActionContext: map[string]interface{}{
+ ActionContextKeyQueueOffset: int64(11),
+ },
+ }
+
+ header, ok := action.buildEndTransactionHeader(bac, "test-topic",
commitOrRollbackCommit)
+
+ require.False(t, ok)
+ require.Nil(t, header)
+}
+
+func TestTCCRocketMQAction_Rollback_ErrorOnSendError(t *testing.T) {
+ resolver := &stubBrokerAddrResolver{addr: "192.168.1.100:10911"}
+ sender := &stubTCPSender{err: errors.New("connection refused")}
+ action := &TCCRocketMQAction{
+ producer: &SeataMQProducer{
+ config: &SeataMQProducerConfig{
+ NameServerAddrs: []string{"nameserver:9876"},
+ GroupName: "test-group",
+ SendMsgTimeout: 3 * time.Second,
+ },
+ },
+ resolver: resolver,
+ sender: sender,
+ }
+ validMsgID := primitive.CreateMessageId([]byte{10, 93, 233, 58}, 10911,
42)
+ bac := &tm.BusinessActionContext{
+ Xid: "xid-789",
+ BranchId: 3003,
+ ActionContext: map[string]interface{}{
+ ActionContextKeyMsgId: validMsgID,
+ ActionContextKeyQueueOffset: int64(33),
+ ActionContextKeyTransactionId: "tx-3",
+ ActionContextKeyBrokerName: "broker-c",
+ ActionContextKeyTopic: "topic-send-err",
+ },
+ }
+
+ ok, err := action.Rollback(context.Background(), bac)
+
+ require.NoError(t, err)
+ assert.True(t, ok)
+ assert.Equal(t, 1, resolver.calls)
+ assert.Equal(t, 1, sender.calls)
+}
+
+func TestBuildEndTransactionHeader_MissingAllIds(t *testing.T) {
+ action := &TCCRocketMQAction{
+ producer: &SeataMQProducer{
+ config: &SeataMQProducerConfig{
+ GroupName: "test-group",
+ },
+ },
+ }
+ bac := &tm.BusinessActionContext{
+ ActionContext: map[string]interface{}{
+ ActionContextKeyQueueOffset: int64(11),
+ ActionContextKeyBrokerName: "broker-a",
+ ActionContextKeyTopic: "test-topic",
+ },
+ }
+
+ header, ok := action.buildEndTransactionHeader(bac, "test-topic",
commitOrRollbackCommit)
+
+ require.False(t, ok)
+ require.Nil(t, header)
+}
+
+func TestTCCRocketMQAction_Commit_InvalidCommitLogOffset(t *testing.T) {
+ resolver := &stubBrokerAddrResolver{addr: "192.168.1.100:10911"}
+ sender := &stubTCPSender{}
+ action := &TCCRocketMQAction{
+ producer: &SeataMQProducer{
+ config: &SeataMQProducerConfig{
+ NameServerAddrs: []string{"nameserver:9876"},
+ GroupName: "test-group",
+ SendMsgTimeout: 3 * time.Second,
+ },
+ },
+ resolver: resolver,
+ sender: sender,
+ }
+ bac := &tm.BusinessActionContext{
+ Xid: "xid-000",
+ BranchId: 9999,
+ ActionContext: map[string]interface{}{
+ ActionContextKeyBrokerName: "broker-a",
+ ActionContextKeyTopic: "topic-test",
+ // missing both offsetMsgId and msgId =>
commitLogOffset == 0
+ },
+ }
+
+ ok, err := action.Commit(context.Background(), bac)
+
+ require.NoError(t, err)
+ assert.True(t, ok)
+ assert.Equal(t, 0, sender.calls) // must skip sending END_TRANSACTION
+}
+
+func TestTCCRocketMQAction_Rollback_InvalidCommitLogOffset(t *testing.T) {
+ resolver := &stubBrokerAddrResolver{addr: "192.168.1.100:10911"}
+ sender := &stubTCPSender{}
+ action := &TCCRocketMQAction{
+ producer: &SeataMQProducer{
+ config: &SeataMQProducerConfig{
+ NameServerAddrs: []string{"nameserver:9876"},
+ GroupName: "test-group",
+ SendMsgTimeout: 3 * time.Second,
+ },
+ },
+ resolver: resolver,
+ sender: sender,
+ }
+ bac := &tm.BusinessActionContext{
+ Xid: "xid-111",
+ BranchId: 8888,
+ ActionContext: map[string]interface{}{
+ ActionContextKeyBrokerName: "broker-b",
+ ActionContextKeyTopic: "topic-rollback",
+ // missing both offsetMsgId and msgId =>
commitLogOffset == 0
+ },
+ }
+
+ ok, err := action.Rollback(context.Background(), bac)
+
+ require.NoError(t, err)
+ assert.True(t, ok)
+ assert.Equal(t, 0, sender.calls) // must skip sending END_TRANSACTION
+}
diff --git a/pkg/integration/rocketmq/transaction_listener.go
b/pkg/integration/rocketmq/transaction_listener.go
index 036b1482..f51cad1f 100644
--- a/pkg/integration/rocketmq/transaction_listener.go
+++ b/pkg/integration/rocketmq/transaction_listener.go
@@ -104,11 +104,11 @@ func mapGlobalStatusToLocalTransactionState(globalStatus
message.GlobalStatus) p
case message.GlobalStatusCommitted, message.GlobalStatusAsyncCommitting:
return primitive.CommitMessageState
case message.GlobalStatusRollbacked,
message.GlobalStatusTimeoutRollbacked, message.GlobalStatusRollbackFailed,
- message.GlobalStatusTimeoutRollbackFailed,
message.GlobalStatusCommitFailed:
+ message.GlobalStatusTimeoutRollbackFailed:
return primitive.RollbackMessageState
case message.GlobalStatusBegin, message.GlobalStatusCommitting,
message.GlobalStatusCommitRetrying,
message.GlobalStatusRollbacking,
message.GlobalStatusRollbackRetrying, message.GlobalStatusTimeoutRollbacking,
- message.GlobalStatusTimeoutRollbackRetrying,
message.GlobalStatusFinished:
+ message.GlobalStatusTimeoutRollbackRetrying,
message.GlobalStatusFinished, message.GlobalStatusCommitFailed:
return primitive.UnknowState
default:
return primitive.UnknowState
diff --git a/pkg/integration/rocketmq/transaction_listener_test.go
b/pkg/integration/rocketmq/transaction_listener_test.go
index 9fe6248d..a70aa3cd 100644
--- a/pkg/integration/rocketmq/transaction_listener_test.go
+++ b/pkg/integration/rocketmq/transaction_listener_test.go
@@ -73,7 +73,7 @@ func
TestSeataTransactionListenerCheckLocalTransaction_StatusMapping(t *testing.
{name: "commit retrying", globalStatus:
message.GlobalStatusCommitRetrying, expected: primitive.UnknowState},
{name: "rollbacking", globalStatus:
message.GlobalStatusRollbacking, expected: primitive.UnknowState},
{name: "timeout rollback retrying", globalStatus:
message.GlobalStatusTimeoutRollbackRetrying, expected: primitive.UnknowState},
- {name: "commit failed", globalStatus:
message.GlobalStatusCommitFailed, expected: primitive.RollbackMessageState},
+ {name: "commit failed", globalStatus:
message.GlobalStatusCommitFailed, expected: primitive.UnknowState},
{name: "timeout rollback failed", globalStatus:
message.GlobalStatusTimeoutRollbackFailed, expected:
primitive.RollbackMessageState},
{name: "finished", globalStatus: message.GlobalStatusFinished,
expected: primitive.UnknowState},
}
diff --git a/pkg/rm/tcc/tcc_service.go b/pkg/rm/tcc/tcc_service.go
index de4ef0b1..5c226689 100644
--- a/pkg/rm/tcc/tcc_service.go
+++ b/pkg/rm/tcc/tcc_service.go
@@ -21,6 +21,7 @@ import (
"context"
"encoding/json"
"errors"
+ "fmt"
"reflect"
"sync"
"time"
@@ -84,9 +85,52 @@ func (t *TCCServiceProxy) Prepare(ctx context.Context,
params interface{}) (inte
}
}
- // to set up the fence phase
tm.SetFencePhase(ctx, enum.FencePhasePrepare)
- return t.TCCResource.Prepare(ctx, params)
+ result, err := t.TCCResource.Prepare(ctx, params)
+ if err != nil {
+ return nil, err
+ }
+
+ bac := tm.GetBusinessActionContext(ctx)
+ if bac != nil && bac.IsDelayReport {
+ if err := t.reportActionContext(ctx, bac); err != nil {
+ log.Warnf("[TCC] report action context failed after
prepare, fallback to original branch applicationData (broker check-back may be
used), xid=%s, branchId=%d, err=%v", bac.Xid, bac.BranchId, err)
+ }
+ }
+
+ return result, nil
+}
+
+// reportActionContext reports the updated ActionContext to TC via
BranchReport.
+// It relies on the Seata Server (TC) behavior: AbstractCore.branchReport
updates
+// branchSession.applicationData, which will be delivered back to RM during
phase-two
+// (branchCommit/branchRollback). If this report fails, phase-two will fall
back to
+// broker check-back because the old applicationData lacks RocketMQ metadata.
+func (t *TCCServiceProxy) reportActionContext(ctx context.Context, bac
*tm.BusinessActionContext) error {
+ updatedActionContext := make(map[string]interface{})
+ for k, v := range bac.ActionContext {
+ updatedActionContext[k] = v
+ }
+ applicationData, err := json.Marshal(map[string]interface{}{
+ constant.ActionContext: updatedActionContext,
+ })
+ if err != nil {
+ log.Errorf("[TCC] marshal updated ActionContext failed, xid=%s,
branchId=%d, err=%v", bac.Xid, bac.BranchId, err)
+ return fmt.Errorf("marshal updated ActionContext failed: %w",
err)
+ }
+ err = rm.GetRMRemotingInstance().BranchReport(rm.BranchReportParam{
+ BranchType: branch.BranchTypeTCC,
+ Xid: bac.Xid,
+ BranchId: bac.BranchId,
+ Status: branch.BranchStatusRegistered,
+ ApplicationData: string(applicationData),
+ })
+ if err != nil {
+ log.Errorf("[TCC] report updated ActionContext failed, xid=%s,
branchId=%d, err=%v", bac.Xid, bac.BranchId, err)
+ return fmt.Errorf("report updated ActionContext failed: %w",
err)
+ }
+ log.Infof("[TCC] report updated ActionContext success, xid=%s,
branchId=%d", bac.Xid, bac.BranchId)
+ return nil
}
// registeBranch send register branch transaction request
diff --git a/pkg/rm/tcc/tcc_service_test.go b/pkg/rm/tcc/tcc_service_test.go
index ddfd3f85..e5ec918c 100644
--- a/pkg/rm/tcc/tcc_service_test.go
+++ b/pkg/rm/tcc/tcc_service_test.go
@@ -19,6 +19,7 @@ package tcc
import (
"context"
+ "encoding/json"
"fmt"
"os"
"reflect"
@@ -30,8 +31,10 @@ import (
gostnet "github.com/dubbogo/gost/net"
"github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
"seata.apache.org/seata-go/v2/pkg/constant"
+ "seata.apache.org/seata-go/v2/pkg/protocol/branch"
"seata.apache.org/seata-go/v2/pkg/rm"
"seata.apache.org/seata-go/v2/pkg/rm/remoting/getty"
@@ -335,3 +338,55 @@ func TestTCCGetTransactionInfo(t1 *testing.T) {
func GetTestTwoPhaseService() rm.TwoPhaseInterface {
return &testdata2.TestTwoPhaseService{}
}
+
+func TestReportActionContext(t *testing.T) {
+ var capturedParam rm.BranchReportParam
+ patches :=
gomonkey.ApplyMethod(reflect.TypeOf(rm.GetRMRemotingInstance()),
"BranchReport", func(_ *getty.GettyRMRemoting, param rm.BranchReportParam)
error {
+ capturedParam = param
+ return nil
+ })
+ defer patches.Reset()
+
+ bac := &tm.BusinessActionContext{
+ Xid: "xid-report-1",
+ BranchId: 1001,
+ ActionContext: map[string]interface{}{
+ "msgId": "msg-001",
+ "topic": "test-topic",
+ },
+ }
+
+ err := testTccServiceProxy.reportActionContext(context.Background(),
bac)
+ require.NoError(t, err)
+ assert.Equal(t, "xid-report-1", capturedParam.Xid)
+ assert.Equal(t, int64(1001), capturedParam.BranchId)
+ assert.Equal(t, branch.BranchTypeTCC, capturedParam.BranchType)
+ assert.EqualValues(t, branch.BranchStatusRegistered,
capturedParam.Status)
+
+ var appData map[string]interface{}
+ err = json.Unmarshal([]byte(capturedParam.ApplicationData), &appData)
+ require.NoError(t, err)
+ actionCtx, ok := appData["actionContext"].(map[string]interface{})
+ require.True(t, ok)
+ assert.Equal(t, "msg-001", actionCtx["msgId"])
+ assert.Equal(t, "test-topic", actionCtx["topic"])
+}
+
+func TestReportActionContext_BranchReportFails(t *testing.T) {
+ patches :=
gomonkey.ApplyMethod(reflect.TypeOf(rm.GetRMRemotingInstance()),
"BranchReport", func(_ *getty.GettyRMRemoting, param rm.BranchReportParam)
error {
+ return fmt.Errorf("network error")
+ })
+ defer patches.Reset()
+
+ bac := &tm.BusinessActionContext{
+ Xid: "xid-report-2",
+ BranchId: 1002,
+ ActionContext: map[string]interface{}{
+ "key": "value",
+ },
+ }
+
+ err := testTccServiceProxy.reportActionContext(context.Background(),
bac)
+ require.Error(t, err)
+ assert.Contains(t, err.Error(), "report updated ActionContext failed")
+}
diff --git a/pkg/util/flagext/day_test.go b/pkg/util/flagext/day_test.go
index d7a82bbd..0d8bd5e5 100644
--- a/pkg/util/flagext/day_test.go
+++ b/pkg/util/flagext/day_test.go
@@ -19,7 +19,6 @@ package flagext
import (
"testing"
- "time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -46,6 +45,7 @@ func TestDayValueYAML(t *testing.T) {
err = yaml.Unmarshal(expected, &actualStruct)
require.NoError(t, err)
assert.Equal(t, testStruct, actualStruct)
+ assert.Equal(t, "1985-06-02T00:00:00Z", testStruct.Day.String())
}
// Test pointers of DayValue.
@@ -68,35 +68,6 @@ func TestDayValueYAML(t *testing.T) {
err = yaml.Unmarshal(expected, &actualStruct)
require.NoError(t, err)
assert.Equal(t, testStruct, actualStruct)
- }
- // Test UTC-stable string and YAML serialization in western timezones.
- {
- loc, err := time.LoadLocation("America/Los_Angeles")
- if err != nil {
- loc = time.FixedZone("UTC-8", -8*60*60)
- }
-
- originalLocal := time.Local
- time.Local = loc
- defer func() {
- time.Local = originalLocal
- }()
- type TestStruct struct {
- Day *DayValue `yaml:"day"`
- }
- var testStruct TestStruct
- testStruct.Day = &DayValue{}
- require.NoError(t, testStruct.Day.Set("1985-06-02"))
- expected := []byte(`day: "1985-06-02"
-`)
-
- actual, err := yaml.Marshal(testStruct)
- require.NoError(t, err)
- assert.Equal(t, expected, actual)
-
- var actualStruct TestStruct
- err = yaml.Unmarshal(expected, &actualStruct)
- require.NoError(t, err)
- assert.Equal(t, testStruct, actualStruct)
+ assert.Equal(t, "1985-06-02T00:00:00Z", testStruct.Day.String())
}
}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]