This is an automated email from the ASF dual-hosted git repository.
ssshr-66 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 baabdf73 feature: add RocketMQ TCC integration (#1054)
baabdf73 is described below
commit baabdf73cf6a99fffef3a539bcc46e229b513866
Author: CAICAII <[email protected]>
AuthorDate: Sat Jun 6 03:41:11 2026 +0800
feature: add RocketMQ TCC integration (#1054)
* feature: add RocketMQ TCC integration
- Implement TCCRocketMQAction with Prepare/Commit/Rollback
- Add SeataMQProducer wrapper for transaction message sending
- Add SeataTransactionListener for broker check-back
- Add producer factory with singleton pattern
- Add XID/BranchId propagation via message properties
* fix: address PR review comments
- Add safe type assertion in CheckLocalTransaction
- Replace sync.Once with mutex for retry support
- Remove duplicate ParseTCCResource call
- Add separate normalProducer for non-transactional messages
- Document Commit/Rollback delegation model
- Add explicit handling for intermediate global statuses
- Remove unused ActionContextKeySendResult constant
* fix: resolve MessageFuture race condition causing sync timeout
* test: harden rocketmq tcc integration
* fix: treat finished global status as unknown
* refactor: improve rocketmq integration readability
* test: strengthen rocketmq producer coverage
---------
Co-authored-by: 福來|Fulai <[email protected]>
Co-authored-by: ThunGuo <[email protected]>
Co-authored-by: flypiggyNo3 <[email protected]>
Co-authored-by: ssshr-66 <[email protected]>
---
go.mod | 7 +-
go.sum | 41 +++++
pkg/constant/context.go | 4 +
pkg/integration/rocketmq/config.go | 57 ++++++
.../rocketmq/constants.go} | 27 +--
pkg/integration/rocketmq/producer_factory.go | 80 +++++++++
pkg/integration/rocketmq/producer_factory_test.go | 170 ++++++++++++++++++
pkg/integration/rocketmq/seata_producer.go | 193 +++++++++++++++++++++
pkg/integration/rocketmq/seata_producer_test.go | 177 +++++++++++++++++++
pkg/integration/rocketmq/tcc_rocketmq_action.go | 104 +++++++++++
.../rocketmq/tcc_rocketmq_action_test.go | 93 ++++++++++
pkg/integration/rocketmq/test_helpers_test.go | 96 ++++++++++
pkg/integration/rocketmq/transaction_listener.go | 116 +++++++++++++
.../rocketmq/transaction_listener_test.go | 157 +++++++++++++++++
pkg/protocol/message/other_message.go | 2 +-
pkg/protocol/message/other_message_test.go | 4 +-
pkg/remoting/getty/getty_remoting.go | 6 +-
pkg/remoting/getty/getty_remoting_test.go | 46 +++++
18 files changed, 1358 insertions(+), 22 deletions(-)
diff --git a/go.mod b/go.mod
index 39b5adc9..decb9e08 100644
--- a/go.mod
+++ b/go.mod
@@ -6,6 +6,7 @@ require (
dubbo.apache.org/dubbo-go/v3 v3.0.4
github.com/DATA-DOG/go-sqlmock v1.5.0
github.com/apache/dubbo-getty v1.5.0
+ github.com/apache/rocketmq-client-go/v2 v2.1.2
github.com/arana-db/parser v0.2.17
github.com/bluele/gcache v0.0.2
github.com/dsnet/compress v0.0.1
@@ -84,6 +85,7 @@ require (
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd //
indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/mschoch/smat v0.2.0 // indirect
+ github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
github.com/pingcap/errors v0.11.5-0.20210425183316-da1aaba5fb63 //
indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
@@ -92,8 +94,12 @@ require (
github.com/prometheus/procfs v0.7.3 // indirect
github.com/satori/go.uuid v1.2.1-0.20181028125025-b2ce2384e17b //
indirect
github.com/shirou/gopsutil/v3 v3.22.2 // indirect
+ github.com/sirupsen/logrus v1.7.0 // indirect
github.com/stoewer/go-strcase v1.2.0 // indirect
github.com/stretchr/objx v0.5.0 // indirect
+ github.com/tidwall/gjson v1.13.0 // indirect
+ github.com/tidwall/match v1.1.1 // indirect
+ github.com/tidwall/pretty v1.2.0 // indirect
github.com/tklauser/go-sysconf v0.3.10 // indirect
github.com/tklauser/numcpus v0.4.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
@@ -111,7 +117,6 @@ require (
)
require (
- github.com/BurntSushi/toml v1.1.0 // indirect
github.com/go-playground/validator/v10 v10.14.0 // indirect
github.com/klauspost/compress v1.15.11
github.com/mattn/go-colorable v0.1.8 // indirect
diff --git a/go.sum b/go.sum
index e1eb50e9..141da943 100644
--- a/go.sum
+++ b/go.sum
@@ -74,6 +74,8 @@ github.com/apache/dubbo-go-hessian2 v1.9.1/go.mod
h1:xQUjE7F8PX49nm80kChFvepA/Av
github.com/apache/dubbo-go-hessian2 v1.9.3/go.mod
h1:xQUjE7F8PX49nm80kChFvepA/AvqAZ0oh/UaB6+6pBE=
github.com/apache/dubbo-go-hessian2 v1.11.4
h1:u8spEogpaZ82/k3bmv1PLYnha/wiwsIme15FuZZgkiI=
github.com/apache/dubbo-go-hessian2 v1.11.4/go.mod
h1:QP9Tc0w/B/mDopjusebo/c7GgEfl6Lz8jeuFg8JA6yw=
+github.com/apache/rocketmq-client-go/v2 v2.1.2
h1:yt73olKe5N6894Dbm+ojRf/JPiP0cxfDNNffKwhpJVg=
+github.com/apache/rocketmq-client-go/v2 v2.1.2/go.mod
h1:6I6vgxHR3hzrvn+6n/4mrhS+UTulzK/X9LB2Vk1U5gE=
github.com/apache/thrift v0.12.0/go.mod
h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
github.com/apache/thrift v0.13.0/go.mod
h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
github.com/arana-db/parser v0.2.17
h1:4wNfSgza2N3pjpwR5jmWLvu4L6Sme6EtoLuZOgwWlsU=
@@ -170,6 +172,7 @@ github.com/davecgh/go-spew v1.1.0/go.mod
h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/davecgh/go-spew v1.1.1
h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod
h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod
h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
+github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod
h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod
h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/dsnet/compress v0.0.1
h1:PlZu0n3Tuv04TzpfPbrnI0HW/YwodEXDS+oPKahKF0Q=
github.com/dsnet/compress v0.0.1/go.mod
h1:Aw8dCMJ7RioblQeTqt88akK31OvO8Dhf5JflhBbQEHo=
@@ -189,6 +192,7 @@ github.com/eapache/go-xerial-snappy
v0.0.0-20180814174437-776d5712da21/go.mod h1
github.com/eapache/queue v1.1.0/go.mod
h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
github.com/edsrzf/mmap-go v1.0.0/go.mod
h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
github.com/emicklei/go-restful/v3 v3.10.1/go.mod
h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
+github.com/emirpasic/gods v1.12.0/go.mod
h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o=
github.com/envoyproxy/go-control-plane v0.6.9/go.mod
h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod
h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane
v0.9.1-0.20191026205805-5f8ba28d4473/go.mod
h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
@@ -255,11 +259,13 @@ github.com/go-playground/universal-translator
v0.18.1/go.mod h1:xekY+UJKNuX9WP91
github.com/go-playground/validator/v10 v10.11.0/go.mod
h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU=
github.com/go-playground/validator/v10 v10.14.0
h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
github.com/go-playground/validator/v10 v10.14.0/go.mod
h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
+github.com/go-redis/redis/v8 v8.11.5/go.mod
h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
github.com/go-resty/resty/v2 v2.7.0/go.mod
h1:9PWDzw47qPphMRFfhsyk0NnSgvluHcljSMVIq3w7q0I=
github.com/go-sql-driver/mysql v1.4.0/go.mod
h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-sql-driver/mysql v1.6.0
h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
github.com/go-sql-driver/mysql v1.6.0/go.mod
h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-stack/stack v1.8.0/go.mod
h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
+github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod
h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/go-test/deep v1.0.2-0.20181118220953-042da051cf31/go.mod
h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
github.com/go-test/deep v1.0.2/go.mod
h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
github.com/goccy/go-json v0.10.2
h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
@@ -343,6 +349,7 @@ github.com/google/pprof
v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hf
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod
h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod
h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod
h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod
h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/renameio v0.1.0/go.mod
h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.0.0/go.mod
h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.1.1/go.mod
h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
@@ -352,6 +359,7 @@ github.com/google/uuid v1.3.0/go.mod
h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+
github.com/googleapis/gax-go/v2 v2.0.4/go.mod
h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod
h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod
h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
+github.com/gopherjs/gopherjs v0.0.0-20190910122728-9d188e94fb99
h1:twflg0XRTjwKpxb/jFExr4HGq6on2dEOmnL6FV+fgPw=
github.com/gopherjs/gopherjs v0.0.0-20190910122728-9d188e94fb99/go.mod
h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/context v1.1.1/go.mod
h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/mux v1.6.2/go.mod
h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
@@ -436,6 +444,7 @@ github.com/hjson/hjson-go/v4 v4.0.0/go.mod
h1:KaYt3bTw3zhBjYqnXkYywcYctk0A2nxeEF
github.com/hpcloud/tail v1.0.0/go.mod
h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/hudl/fargo v1.3.0/go.mod
h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod
h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
+github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod
h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/inconshreveable/mousetrap v1.0.0/go.mod
h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/influxdata/influxdb1-client
v0.0.0-20191209144304-8bf82d3c094d/go.mod
h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo=
github.com/jackc/pgpassfile v1.0.0
h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
@@ -467,6 +476,7 @@ github.com/json-iterator/go v1.1.12
h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr
github.com/json-iterator/go v1.1.12/go.mod
h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod
h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod
h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
+github.com/jtolds/gls v4.20.0+incompatible
h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod
h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/julienschmidt/httprouter v1.2.0/go.mod
h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/julienschmidt/httprouter v1.3.0/go.mod
h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
@@ -580,6 +590,8 @@ github.com/nats-io/nkeys v0.1.3/go.mod
h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxzi
github.com/nats-io/nuid v1.0.1/go.mod
h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod
h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/npillmayer/nestext v0.1.3/go.mod
h1:h2lrijH8jpicr25dFY+oAJLyzlya6jhnuG+zWp9L0Uk=
+github.com/nxadm/tail v1.4.4/go.mod
h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
+github.com/nxadm/tail v1.4.8/go.mod
h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
github.com/oklog/oklog v0.3.2/go.mod
h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs=
github.com/oklog/run v1.0.0/go.mod
h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
github.com/oklog/ulid v1.3.1/go.mod
h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
@@ -587,7 +599,15 @@ github.com/olekukonko/tablewriter
v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:v
github.com/oliveagle/jsonpath v0.0.0-20180606110733-2e52cf6e6852/go.mod
h1:eqOVx5Vwu4gd2mmMZvVZsgIqNSaW3xxRThUJ0k/TPk4=
github.com/onsi/ginkgo v1.6.0/go.mod
h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0/go.mod
h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/ginkgo v1.12.1/go.mod
h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
+github.com/onsi/ginkgo v1.16.4/go.mod
h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
+github.com/onsi/ginkgo v1.16.5/go.mod
h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
+github.com/onsi/ginkgo/v2 v2.0.0/go.mod
h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c=
github.com/onsi/gomega v1.4.3/go.mod
h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
+github.com/onsi/gomega v1.7.1/go.mod
h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
+github.com/onsi/gomega v1.10.1/go.mod
h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
+github.com/onsi/gomega v1.17.0/go.mod
h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
+github.com/onsi/gomega v1.18.1/go.mod
h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs=
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod
h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
github.com/opentracing-contrib/go-observer
v0.0.0-20170622124052-a52f23424492/go.mod
h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis=
github.com/opentracing/basictracer-go v1.0.0/go.mod
h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74=
@@ -601,6 +621,8 @@ github.com/openzipkin/zipkin-go v0.2.2/go.mod
h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnh
github.com/pact-foundation/pact-go v1.0.4/go.mod
h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod
h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pascaldekloe/goe v0.1.0/go.mod
h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
+github.com/patrickmn/go-cache v2.1.0+incompatible
h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
+github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod
h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
github.com/pborman/uuid v1.2.0/go.mod
h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
github.com/pelletier/go-toml v1.2.0/go.mod
h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pelletier/go-toml v1.7.0/go.mod
h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE=
@@ -702,11 +724,15 @@ github.com/shirou/gopsutil/v3 v3.22.2
h1:wCrArWFkHYIdDxx/FSfF5RB4dpJYW6t7rcp3+zL
github.com/shirou/gopsutil/v3 v3.22.2/go.mod
h1:WapW1AOOPlHyXr+yOyw3uYx36enocrtSoSBy0L5vUHY=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod
h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.2.0/go.mod
h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
+github.com/sirupsen/logrus v1.4.0/go.mod
h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2/go.mod
h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.6.0/go.mod
h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
+github.com/sirupsen/logrus v1.7.0
h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM=
github.com/sirupsen/logrus v1.7.0/go.mod
h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
+github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d
h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod
h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod
h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
+github.com/smartystreets/goconvey v1.6.4
h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
github.com/smartystreets/goconvey v1.6.4/go.mod
h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/soheilhy/cmux v0.1.4/go.mod
h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
github.com/soheilhy/cmux v0.1.5-0.20210205191134-5ec6847320e5/go.mod
h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0=
@@ -745,6 +771,12 @@ github.com/stretchr/testify v1.8.2/go.mod
h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o
github.com/stretchr/testify v1.8.3
h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
github.com/stretchr/testify v1.8.3/go.mod
h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/subosito/gotenv v1.2.0/go.mod
h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
+github.com/tidwall/gjson v1.13.0
h1:3TFY9yxOQShrvmjdM76K+jc66zJeT6D3/VFFYCGQf7M=
+github.com/tidwall/gjson v1.13.0/go.mod
h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
+github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
+github.com/tidwall/match v1.1.1/go.mod
h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
+github.com/tidwall/pretty v1.2.0
h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
+github.com/tidwall/pretty v1.2.0/go.mod
h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tklauser/go-sysconf v0.3.6/go.mod
h1:MkWzOF4RMCshBAMXuhXJs64Rte09mITnppBXY/rYEFI=
github.com/tklauser/go-sysconf v0.3.9/go.mod
h1:11DU/5sG7UexIrp/O6g35hrWzu0JxlwQ3LSFUzyeuhs=
github.com/tklauser/go-sysconf v0.3.10
h1:IJ1AZGZRWbY8T5Vfk04D9WOA5WSejdflXxP03OUqALw=
@@ -810,6 +842,7 @@ go.opentelemetry.io/proto/otlp v0.7.0/go.mod
h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqe
go.uber.org/atomic v1.3.2/go.mod
h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.4.0/go.mod
h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.5.0/go.mod
h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
+go.uber.org/atomic v1.5.1/go.mod
h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/atomic v1.6.0/go.mod
h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/atomic v1.7.0/go.mod
h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
@@ -930,6 +963,7 @@ golang.org/x/net v0.0.0-20200421231249-e086a090c8fd/go.mod
h1:qpuaurCH72eLCgpAm/
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod
h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod
h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod
h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod
h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod
h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod
h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod
h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
@@ -940,6 +974,7 @@ golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod
h1:sp8m0HH+o8qH0wwXwY
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod
h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod
h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod
h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8=
+golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod
h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod
h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod
h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211105192438-b53810dc28af/go.mod
h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
@@ -989,6 +1024,7 @@ golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod
h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod
h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod
h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod
h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod
h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod
h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod
h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod
h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -996,6 +1032,7 @@ golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod
h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod
h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod
h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod
h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod
h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod
h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod
h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod
h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -1023,6 +1060,7 @@ golang.org/x/sys
v0.0.0-20201009025420-dfb3f7c4e634/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod
h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod
h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201214210602-f9fddec55a1e/go.mod
h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod
h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod
h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod
h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod
h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -1037,6 +1075,7 @@ golang.org/x/sys
v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod
h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210816074244-15123e1e1f71/go.mod
h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211106132015-ebca88c72f68/go.mod
h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod
h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220111092808-5a964db01320/go.mod
h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod
h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod
h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -1120,6 +1159,7 @@ golang.org/x/tools
v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod
h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod
h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20201014170642-d1624618ad65/go.mod
h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
+golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod
h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod
h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.1/go.mod
h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.2/go.mod
h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
@@ -1315,6 +1355,7 @@ rsc.io/sampler v1.3.0/go.mod
h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc=
sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod
h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU=
+stathat.com/c/consistent v1.0.0/go.mod
h1:QkzMWzcbB+yQBL2AttO6sgsQS/JSTapcDISJalmCDS0=
vimagination.zapto.org/byteio v0.0.0-20200222190125-d27cba0f0b10
h1:pxt6fVJP67Hxo1qk8JalUghLlk3abYByl+3e0JYfUlE=
vimagination.zapto.org/byteio v0.0.0-20200222190125-d27cba0f0b10/go.mod
h1:fl9OF22g6MTKgvHA1hqMXe/L7+ULWofVTwbC9loGu7A=
vimagination.zapto.org/memio v0.0.0-20200222190306-588ebc67b97d
h1:Mp6WiHHuiwHaknxTdxJ8pvC9/B4pOgW1PamKGexG7Fs=
diff --git a/pkg/constant/context.go b/pkg/constant/context.go
index 5dd679c9..08db6633 100644
--- a/pkg/constant/context.go
+++ b/pkg/constant/context.go
@@ -39,4 +39,8 @@ const (
SeataVersion = "1.1.0"
TccBusinessActionContextParameter = "tccParam"
+
+ // RocketMQ message property keys for XID and BranchId propagation
+ PropertySeataXID = "SEATA_XID"
+ PropertySeataBranchId = "SEATA_BRANCH_ID"
)
diff --git a/pkg/integration/rocketmq/config.go
b/pkg/integration/rocketmq/config.go
new file mode 100644
index 00000000..e97e3b36
--- /dev/null
+++ b/pkg/integration/rocketmq/config.go
@@ -0,0 +1,57 @@
+/*
+ * 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 (
+ "time"
+
+ "github.com/apache/rocketmq-client-go/v2/producer"
+)
+
+type SeataMQProducerConfig struct {
+ NameServerAddrs []string
+ GroupName string
+ Namespace string
+ InstanceName string
+
+ RetryTimesWhenSendFailed int
+ SendMsgTimeout time.Duration
+}
+
+func NewDefaultSeataMQProducerConfig() *SeataMQProducerConfig {
+ return &SeataMQProducerConfig{
+ RetryTimesWhenSendFailed: 3,
+ SendMsgTimeout: 3 * time.Second,
+ }
+}
+
+func (c *SeataMQProducerConfig) ToRocketMQProducerOptions() []producer.Option {
+ opts := []producer.Option{
+ producer.WithNameServer(c.NameServerAddrs),
+ producer.WithGroupName(c.GroupName),
+ producer.WithRetry(c.RetryTimesWhenSendFailed),
+ producer.WithSendMsgTimeout(c.SendMsgTimeout),
+ }
+ if c.Namespace != "" {
+ opts = append(opts, producer.WithNamespace(c.Namespace))
+ }
+ if c.InstanceName != "" {
+ opts = append(opts, producer.WithInstanceName(c.InstanceName))
+ }
+ return opts
+}
diff --git a/pkg/protocol/message/other_message_test.go
b/pkg/integration/rocketmq/constants.go
similarity index 60%
copy from pkg/protocol/message/other_message_test.go
copy to pkg/integration/rocketmq/constants.go
index d4487cc4..b0bbbe7f 100644
--- a/pkg/protocol/message/other_message_test.go
+++ b/pkg/integration/rocketmq/constants.go
@@ -15,24 +15,15 @@
* limitations under the License.
*/
-package message
+package rocketmq
-import (
- "testing"
+const (
+ ResourceIDTCCRocketMQ = "tccRocketMQ"
- "github.com/stretchr/testify/assert"
+ ActionContextKeyOffsetMsgId = "offsetMsgId"
+ ActionContextKeyMsgId = "msgId"
+ ActionContextKeyQueueId = "queueId"
+ ActionContextKeyQueueOffset = "queueOffset"
+ ActionContextKeyTransactionId = "transactionId"
+ ActionContextKeyBrokerName = "brokerName"
)
-
-func TestNewMessageFuture(t *testing.T) {
- rpcMessage := RpcMessage{ID: 0}
- assert.Equal(t, int32(0), NewMessageFuture(rpcMessage).ID)
-}
-
-func TestHeartBeatMessage_ToString(t *testing.T) {
- assert.Equal(t, "services ping", HeartBeatMessagePing.ToString())
- assert.Equal(t, "services pong", HeartBeatMessagePong.ToString())
-}
-
-func TestHeartBeatMessage_GetTypeCode(t *testing.T) {
- assert.Equal(t, MessageTypeHeartbeatMsg,
HeartBeatMessage{}.GetTypeCode())
-}
diff --git a/pkg/integration/rocketmq/producer_factory.go
b/pkg/integration/rocketmq/producer_factory.go
new file mode 100644
index 00000000..77a76283
--- /dev/null
+++ b/pkg/integration/rocketmq/producer_factory.go
@@ -0,0 +1,80 @@
+/*
+ * 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 (
+ "fmt"
+ "sync"
+)
+
+var (
+ globalProducer *SeataMQProducer
+ producerMutex sync.RWMutex
+ producerInitialized bool
+ newSeataMQProducer = NewSeataMQProducer
+)
+
+func InitSeataMQProducer(cfg *SeataMQProducerConfig) error {
+ producerMutex.Lock()
+ defer producerMutex.Unlock()
+
+ // Allow re-initialization if previous attempt failed
+ if producerInitialized && globalProducer != nil {
+ return nil
+ }
+
+ producer, err := newSeataMQProducer(cfg)
+ if err != nil {
+ return fmt.Errorf("failed to create SeataMQProducer: %w", err)
+ }
+
+ if err := producer.Start(); err != nil {
+ return fmt.Errorf("failed to start SeataMQProducer: %w", err)
+ }
+
+ globalProducer = producer
+ producerInitialized = true
+
+ return nil
+}
+
+func GetSeataMQProducer() (*SeataMQProducer, error) {
+ producerMutex.RLock()
+ defer producerMutex.RUnlock()
+
+ if !producerInitialized || globalProducer == nil {
+ return nil, fmt.Errorf("SeataMQProducer not initialized, call
InitSeataMQProducer first")
+ }
+
+ return globalProducer, nil
+}
+
+func ShutdownSeataMQProducer() error {
+ producerMutex.Lock()
+ defer producerMutex.Unlock()
+
+ if !producerInitialized || globalProducer == nil {
+ return nil
+ }
+
+ err := globalProducer.Shutdown()
+ globalProducer = nil
+ producerInitialized = false
+
+ return err
+}
diff --git a/pkg/integration/rocketmq/producer_factory_test.go
b/pkg/integration/rocketmq/producer_factory_test.go
new file mode 100644
index 00000000..cffa8e8d
--- /dev/null
+++ b/pkg/integration/rocketmq/producer_factory_test.go
@@ -0,0 +1,170 @@
+/*
+ * 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 (
+ "errors"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func resetProducerFactoryForTest() {
+ globalProducer = nil
+ producerInitialized = false
+ newSeataMQProducer = NewSeataMQProducer
+}
+
+func newStubFactoryProducer() (*SeataMQProducer, *stubTransactionProducer,
*stubNormalProducer) {
+ transactionProducer := &stubTransactionProducer{}
+ normalProducer := &stubNormalProducer{}
+ return &SeataMQProducer{
+ transactionProducer: transactionProducer,
+ normalProducer: normalProducer,
+ }, transactionProducer, normalProducer
+}
+
+func TestGetSeataMQProducer_RequiresInitialization(t *testing.T) {
+ resetProducerFactoryForTest()
+ t.Cleanup(resetProducerFactoryForTest)
+
+ producer, err := GetSeataMQProducer()
+
+ require.Error(t, err)
+ assert.Nil(t, producer)
+ assert.Contains(t, err.Error(), "not initialized")
+}
+
+func TestInitSeataMQProducer_AllowsRetryAfterCreateFailure(t *testing.T) {
+ resetProducerFactoryForTest()
+ t.Cleanup(resetProducerFactoryForTest)
+
+ expectedErr := errors.New("create failed")
+ healthyProducer, transactionProducer, normalProducer :=
newStubFactoryProducer()
+ createCalls := 0
+ newSeataMQProducer = func(*SeataMQProducerConfig) (*SeataMQProducer,
error) {
+ createCalls++
+ if createCalls == 1 {
+ return nil, expectedErr
+ }
+ return healthyProducer, nil
+ }
+
+ err := InitSeataMQProducer(&SeataMQProducerConfig{})
+ require.ErrorIs(t, err, expectedErr)
+
+ got, getErr := GetSeataMQProducer()
+ require.Error(t, getErr)
+ assert.Nil(t, got)
+
+ err = InitSeataMQProducer(&SeataMQProducerConfig{})
+ require.NoError(t, err)
+
+ got, err = GetSeataMQProducer()
+ require.NoError(t, err)
+ assert.Same(t, healthyProducer, got)
+ assert.Equal(t, 2, createCalls)
+ assert.Equal(t, 1, transactionProducer.startCalls)
+ assert.Equal(t, 1, normalProducer.startCalls)
+}
+
+func TestInitSeataMQProducer_AllowsRetryAfterStartFailure(t *testing.T) {
+ resetProducerFactoryForTest()
+ t.Cleanup(resetProducerFactoryForTest)
+
+ expectedErr := errors.New("normal start failed")
+ failingProducer, failingTransactionProducer, failingNormalProducer :=
newStubFactoryProducer()
+ failingNormalProducer.startErr = expectedErr
+ healthyProducer, healthyTransactionProducer, healthyNormalProducer :=
newStubFactoryProducer()
+
+ createCalls := 0
+ newSeataMQProducer = func(*SeataMQProducerConfig) (*SeataMQProducer,
error) {
+ createCalls++
+ if createCalls == 1 {
+ return failingProducer, nil
+ }
+ return healthyProducer, nil
+ }
+
+ err := InitSeataMQProducer(&SeataMQProducerConfig{})
+ require.ErrorIs(t, err, expectedErr)
+ assert.Equal(t, 1, failingTransactionProducer.startCalls)
+ assert.Equal(t, 1, failingNormalProducer.startCalls)
+ assert.Equal(t, 1, failingTransactionProducer.shutdownCalls)
+
+ got, getErr := GetSeataMQProducer()
+ require.Error(t, getErr)
+ assert.Nil(t, got)
+
+ err = InitSeataMQProducer(&SeataMQProducerConfig{})
+ require.NoError(t, err)
+
+ got, err = GetSeataMQProducer()
+ require.NoError(t, err)
+ assert.Same(t, healthyProducer, got)
+ assert.Equal(t, 2, createCalls)
+ assert.Equal(t, 1, healthyTransactionProducer.startCalls)
+ assert.Equal(t, 1, healthyNormalProducer.startCalls)
+}
+
+func TestInitSeataMQProducer_DoesNotReinitializeAfterSuccess(t *testing.T) {
+ resetProducerFactoryForTest()
+ t.Cleanup(resetProducerFactoryForTest)
+
+ healthyProducer, transactionProducer, normalProducer :=
newStubFactoryProducer()
+ createCalls := 0
+ newSeataMQProducer = func(*SeataMQProducerConfig) (*SeataMQProducer,
error) {
+ createCalls++
+ return healthyProducer, nil
+ }
+
+ require.NoError(t, InitSeataMQProducer(&SeataMQProducerConfig{}))
+ require.NoError(t, InitSeataMQProducer(&SeataMQProducerConfig{}))
+
+ got, err := GetSeataMQProducer()
+ require.NoError(t, err)
+ assert.Same(t, healthyProducer, got)
+ assert.Equal(t, 1, createCalls)
+ assert.Equal(t, 1, transactionProducer.startCalls)
+ assert.Equal(t, 1, normalProducer.startCalls)
+}
+
+func TestShutdownSeataMQProducer_ResetsFactoryState(t *testing.T) {
+ resetProducerFactoryForTest()
+ t.Cleanup(resetProducerFactoryForTest)
+
+ healthyProducer, transactionProducer, normalProducer :=
newStubFactoryProducer()
+ newSeataMQProducer = func(*SeataMQProducerConfig) (*SeataMQProducer,
error) {
+ return healthyProducer, nil
+ }
+
+ require.NoError(t, InitSeataMQProducer(&SeataMQProducerConfig{}))
+ require.NoError(t, ShutdownSeataMQProducer())
+ require.NoError(t, ShutdownSeataMQProducer())
+
+ assert.Equal(t, 1, transactionProducer.shutdownCalls)
+ assert.Equal(t, 1, normalProducer.shutdownCalls)
+ assert.True(t, healthyProducer.closed)
+ assert.False(t, producerInitialized)
+ assert.Nil(t, globalProducer)
+
+ got, err := GetSeataMQProducer()
+ require.Error(t, err)
+ assert.Nil(t, got)
+}
diff --git a/pkg/integration/rocketmq/seata_producer.go
b/pkg/integration/rocketmq/seata_producer.go
new file mode 100644
index 00000000..3a22f8e6
--- /dev/null
+++ b/pkg/integration/rocketmq/seata_producer.go
@@ -0,0 +1,193 @@
+/*
+ * 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 (
+ "context"
+ "fmt"
+ "sync"
+
+ "github.com/apache/rocketmq-client-go/v2/primitive"
+ "github.com/apache/rocketmq-client-go/v2/producer"
+
+ "seata.apache.org/seata-go/v2/pkg/rm/tcc"
+ "seata.apache.org/seata-go/v2/pkg/tm"
+ "seata.apache.org/seata-go/v2/pkg/util/log"
+)
+
+type transactionProducerInterface interface {
+ Start() error
+ Shutdown() error
+ SendMessageInTransaction(context.Context, *primitive.Message)
(*primitive.TransactionSendResult, error)
+}
+
+type normalProducerInterface interface {
+ Start() error
+ Shutdown() error
+ SendSync(ctx context.Context, msg ...*primitive.Message)
(*primitive.SendResult, error)
+}
+
+type tccPrepareProxy interface {
+ Prepare(context.Context, interface{}) (interface{}, error)
+}
+
+type SeataMQProducer struct {
+ config *SeataMQProducerConfig
+ transactionProducer transactionProducerInterface
+ normalProducer normalProducerInterface
+ tccAction *TCCRocketMQAction
+ tccProxy tccPrepareProxy
+
+ mu sync.RWMutex
+ closed bool
+}
+
+func NewSeataMQProducer(cfg *SeataMQProducerConfig) (*SeataMQProducer, error) {
+ if cfg == nil {
+ return nil, fmt.Errorf("config cannot be nil")
+ }
+
+ if cfg.NameServerAddrs == nil || len(cfg.NameServerAddrs) == 0 {
+ return nil, fmt.Errorf("NameServerAddrs cannot be empty")
+ }
+
+ if cfg.GroupName == "" {
+ return nil, fmt.Errorf("GroupName cannot be empty")
+ }
+
+ p := &SeataMQProducer{
+ config: cfg,
+ }
+
+ p.tccAction = NewTCCRocketMQAction(p)
+
+ // NewTCCServiceProxy internally calls ParseTCCResource, so we pass the
action directly
+ var err error
+ p.tccProxy, err = tcc.NewTCCServiceProxy(p.tccAction)
+ if err != nil {
+ return nil, fmt.Errorf("create TCC proxy failed: %w", err)
+ }
+
+ listener := NewSeataTransactionListener(p)
+ opts := cfg.ToRocketMQProducerOptions()
+
+ p.transactionProducer, err = producer.NewTransactionProducer(listener,
opts...)
+ if err != nil {
+ return nil, fmt.Errorf("create transaction producer failed:
%w", err)
+ }
+
+ normalCfg := *cfg
+ normalCfg.GroupName = cfg.GroupName + "-normal"
+ normalOpts := normalCfg.ToRocketMQProducerOptions()
+ p.normalProducer, err = producer.NewDefaultProducer(normalOpts...)
+ if err != nil {
+ return nil, fmt.Errorf("create normal producer failed: %w", err)
+ }
+
+ return p, nil
+}
+
+func (p *SeataMQProducer) Start() error {
+ p.mu.Lock()
+ defer p.mu.Unlock()
+
+ if p.closed {
+ return fmt.Errorf("producer already closed")
+ }
+
+ if err := p.transactionProducer.Start(); err != nil {
+ return err
+ }
+
+ if err := p.normalProducer.Start(); err != nil {
+ p.transactionProducer.Shutdown()
+ return err
+ }
+
+ return nil
+}
+
+func (p *SeataMQProducer) Shutdown() error {
+ p.mu.Lock()
+ defer p.mu.Unlock()
+
+ if p.closed {
+ return nil
+ }
+
+ p.closed = true
+
+ var errs []error
+ if err := p.transactionProducer.Shutdown(); err != nil {
+ errs = append(errs, err)
+ }
+ if err := p.normalProducer.Shutdown(); err != nil {
+ errs = append(errs, err)
+ }
+
+ if len(errs) > 0 {
+ return fmt.Errorf("shutdown errors: %v", errs)
+ }
+ return nil
+}
+
+func (p *SeataMQProducer) Send(ctx context.Context, msg *primitive.Message)
(*primitive.SendResult, error) {
+ p.mu.RLock()
+ defer p.mu.RUnlock()
+
+ if p.closed {
+ return nil, fmt.Errorf("producer is closed")
+ }
+
+ if msg == nil {
+ return nil, fmt.Errorf("message cannot be nil")
+ }
+
+ if !tm.IsGlobalTx(ctx) {
+ return p.sendSync(ctx, msg)
+ }
+
+ _, err := p.tccProxy.Prepare(ctx, msg)
+ if err != nil {
+ log.Errorf("[SeataMQProducer] Send in global tx failed, xid=%s,
err=%v", tm.GetXID(ctx), err)
+ return nil, err
+ }
+
+ bac := tm.GetBusinessActionContext(ctx)
+ if bac == nil || bac.ActionContext == nil {
+ return nil, fmt.Errorf("BusinessActionContext action context
not found after Prepare")
+ }
+ return &primitive.SendResult{
+ Status: primitive.SendOK,
+ MsgID: getStringFromMap(bac.ActionContext,
ActionContextKeyMsgId),
+ OffsetMsgID: getStringFromMap(bac.ActionContext,
ActionContextKeyOffsetMsgId),
+ }, nil
+}
+
+func (p *SeataMQProducer) sendSync(ctx context.Context, msg
*primitive.Message) (*primitive.SendResult, error) {
+ return p.normalProducer.SendSync(ctx, msg)
+}
+
+func getStringFromMap(m map[string]interface{}, key string) string {
+ if v, ok := m[key]; ok {
+ if s, ok := v.(string); ok {
+ return s
+ }
+ }
+ return ""
+}
diff --git a/pkg/integration/rocketmq/seata_producer_test.go
b/pkg/integration/rocketmq/seata_producer_test.go
new file mode 100644
index 00000000..113e1cb6
--- /dev/null
+++ b/pkg/integration/rocketmq/seata_producer_test.go
@@ -0,0 +1,177 @@
+/*
+ * 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 (
+ "context"
+ "errors"
+ "testing"
+
+ "github.com/apache/rocketmq-client-go/v2/primitive"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ "seata.apache.org/seata-go/v2/pkg/tm"
+)
+
+type stubTCCPrepareProxy struct {
+ prepareFn func(context.Context, interface{}) (interface{}, error)
+
+ prepareCalls int
+ lastCtx context.Context
+ lastParams interface{}
+}
+
+func (s *stubTCCPrepareProxy) Prepare(ctx context.Context, params interface{})
(interface{}, error) {
+ s.prepareCalls++
+ s.lastCtx = ctx
+ s.lastParams = params
+ if s.prepareFn != nil {
+ return s.prepareFn(ctx, params)
+ }
+ return nil, nil
+}
+
+func TestSeataMQProducerSend_NonGlobalTransactionUsesNormalProducer(t
*testing.T) {
+ normalProducer := &stubNormalProducer{
+ sendResult: &primitive.SendResult{
+ Status: primitive.SendOK,
+ MsgID: "msg-1",
+ },
+ }
+ prepareProxy := &stubTCCPrepareProxy{}
+ producer := &SeataMQProducer{
+ normalProducer: normalProducer,
+ tccProxy: prepareProxy,
+ }
+ msg := primitive.NewMessage("topic-test", []byte("hello"))
+
+ result, err := producer.Send(context.Background(), msg)
+
+ require.NoError(t, err)
+ require.NotNil(t, result)
+ assert.Equal(t, primitive.SendOK, result.Status)
+ assert.Equal(t, "msg-1", result.MsgID)
+ assert.Equal(t, 1, normalProducer.sendCalls)
+ assert.Same(t, msg, normalProducer.lastMsgs[0])
+ assert.Zero(t, prepareProxy.prepareCalls)
+}
+
+func TestSeataMQProducerSend_GlobalTransactionUsesTCCProxy(t *testing.T) {
+ ctx := tm.InitSeataContext(context.Background())
+ tm.SetXID(ctx, "xid-123")
+ tm.SetBusinessActionContext(ctx, &tm.BusinessActionContext{
+ ActionContext: map[string]interface{}{},
+ })
+
+ prepareProxy := &stubTCCPrepareProxy{
+ prepareFn: func(ctx context.Context, params interface{})
(interface{}, error) {
+ bac := tm.GetBusinessActionContext(ctx)
+ bac.ActionContext[ActionContextKeyMsgId] = "msg-2"
+ bac.ActionContext[ActionContextKeyOffsetMsgId] =
"offset-2"
+ return true, nil
+ },
+ }
+ producer := &SeataMQProducer{
+ tccProxy: prepareProxy,
+ }
+ msg := primitive.NewMessage("topic-test", []byte("hello"))
+
+ result, err := producer.Send(ctx, msg)
+
+ require.NoError(t, err)
+ require.NotNil(t, result)
+ assert.Equal(t, primitive.SendOK, result.Status)
+ assert.Equal(t, "msg-2", result.MsgID)
+ assert.Equal(t, "offset-2", result.OffsetMsgID)
+ assert.Equal(t, 1, prepareProxy.prepareCalls)
+ assert.Same(t, msg, prepareProxy.lastParams)
+}
+
+func TestSeataMQProducerSend_GlobalTransactionPrepareError(t *testing.T) {
+ ctx := tm.InitSeataContext(context.Background())
+ tm.SetXID(ctx, "xid-123")
+ tm.SetBusinessActionContext(ctx, &tm.BusinessActionContext{
+ ActionContext: map[string]interface{}{},
+ })
+
+ expectedErr := errors.New("prepare failed")
+ producer := &SeataMQProducer{
+ tccProxy: &stubTCCPrepareProxy{
+ prepareFn: func(context.Context, interface{})
(interface{}, error) {
+ return nil, expectedErr
+ },
+ },
+ }
+
+ result, err := producer.Send(ctx, primitive.NewMessage("topic-test",
[]byte("hello")))
+
+ require.ErrorIs(t, err, expectedErr)
+ assert.Nil(t, result)
+}
+
+func
TestSeataMQProducerSend_GlobalTransactionRequiresActionContextAfterPrepare(t
*testing.T) {
+ ctx := tm.InitSeataContext(context.Background())
+ tm.SetXID(ctx, "xid-123")
+
+ producer := &SeataMQProducer{
+ tccProxy: &stubTCCPrepareProxy{
+ prepareFn: func(context.Context, interface{})
(interface{}, error) {
+ return true, nil
+ },
+ },
+ }
+
+ result, err := producer.Send(ctx, primitive.NewMessage("topic-test",
[]byte("hello")))
+
+ require.Error(t, err)
+ assert.Contains(t, err.Error(), "BusinessActionContext")
+ assert.Nil(t, result)
+}
+
+func TestSeataMQProducerSend_RejectsNilMessageAndClosedProducer(t *testing.T) {
+ producer := &SeataMQProducer{}
+
+ result, err := producer.Send(context.Background(), nil)
+ require.Error(t, err)
+ assert.Contains(t, err.Error(), "message cannot be nil")
+ assert.Nil(t, result)
+
+ producer.closed = true
+ result, err = producer.Send(context.Background(),
primitive.NewMessage("topic-test", []byte("hello")))
+ require.Error(t, err)
+ assert.Contains(t, err.Error(), "producer is closed")
+ assert.Nil(t, result)
+}
+
+func
TestSeataMQProducerStart_ShutsDownTransactionProducerWhenNormalProducerStartFails(t
*testing.T) {
+ expectedErr := errors.New("normal start failed")
+ transactionProducer := &stubTransactionProducer{}
+ normalProducer := &stubNormalProducer{startErr: expectedErr}
+ producer := &SeataMQProducer{
+ transactionProducer: transactionProducer,
+ normalProducer: normalProducer,
+ }
+
+ err := producer.Start()
+
+ require.ErrorIs(t, err, expectedErr)
+ assert.Equal(t, 1, transactionProducer.startCalls)
+ assert.Equal(t, 1, normalProducer.startCalls)
+ assert.Equal(t, 1, transactionProducer.shutdownCalls)
+}
diff --git a/pkg/integration/rocketmq/tcc_rocketmq_action.go
b/pkg/integration/rocketmq/tcc_rocketmq_action.go
new file mode 100644
index 00000000..a769646d
--- /dev/null
+++ b/pkg/integration/rocketmq/tcc_rocketmq_action.go
@@ -0,0 +1,104 @@
+/*
+ * 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 (
+ "context"
+ "fmt"
+
+ "github.com/apache/rocketmq-client-go/v2/primitive"
+
+ "seata.apache.org/seata-go/v2/pkg/constant"
+ "seata.apache.org/seata-go/v2/pkg/tm"
+ "seata.apache.org/seata-go/v2/pkg/util/log"
+)
+
+type TCCRocketMQAction struct {
+ producer *SeataMQProducer
+}
+
+func NewTCCRocketMQAction(producer *SeataMQProducer) *TCCRocketMQAction {
+ return &TCCRocketMQAction{
+ producer: producer,
+ }
+}
+
+func (a *TCCRocketMQAction) GetActionName() string {
+ return ResourceIDTCCRocketMQ
+}
+
+func (a *TCCRocketMQAction) Prepare(ctx context.Context, params interface{})
(bool, error) {
+ msg, ok := params.(*primitive.Message)
+ if !ok {
+ return false, fmt.Errorf("params must be *primitive.Message,
got %T", params)
+ }
+
+ bac := tm.GetBusinessActionContext(ctx)
+ if bac == nil {
+ return false, fmt.Errorf("BusinessActionContext not found in
context")
+ }
+
+ xid := tm.GetXID(ctx)
+ if xid == "" {
+ return false, fmt.Errorf("XID not found in context")
+ }
+ if bac.ActionContext == nil {
+ bac.ActionContext = make(map[string]interface{}, 6)
+ }
+
+ msg.WithProperty(constant.PropertySeataXID, xid)
+ msg.WithProperty(constant.PropertySeataBranchId, fmt.Sprintf("%d",
bac.BranchId))
+
+ result, err :=
a.producer.transactionProducer.SendMessageInTransaction(ctx, msg)
+ if err != nil {
+ log.Errorf("[TCCRocketMQ] Prepare failed, xid=%s, err=%v", xid,
err)
+ return false, err
+ }
+
+ bac.ActionContext[ActionContextKeyMsgId] = result.MsgID
+ bac.ActionContext[ActionContextKeyOffsetMsgId] = result.OffsetMsgID
+ bac.ActionContext[ActionContextKeyQueueOffset] = result.QueueOffset
+ bac.ActionContext[ActionContextKeyTransactionId] = result.TransactionID
+ if result.MessageQueue != nil {
+ bac.ActionContext[ActionContextKeyQueueId] =
result.MessageQueue.QueueId
+ bac.ActionContext[ActionContextKeyBrokerName] =
result.MessageQueue.BrokerName
+ }
+
+ log.Infof("[TCCRocketMQ] Prepare success, xid=%s, branchId=%d,
msgId=%s", xid, bac.BranchId, result.MsgID)
+
+ return true, nil
+}
+
+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)
+ 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)
+ return true, nil
+}
diff --git a/pkg/integration/rocketmq/tcc_rocketmq_action_test.go
b/pkg/integration/rocketmq/tcc_rocketmq_action_test.go
new file mode 100644
index 00000000..b3e24267
--- /dev/null
+++ b/pkg/integration/rocketmq/tcc_rocketmq_action_test.go
@@ -0,0 +1,93 @@
+/*
+ * 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 (
+ "context"
+ "testing"
+
+ "github.com/apache/rocketmq-client-go/v2/primitive"
+ "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/rm/tcc"
+ "seata.apache.org/seata-go/v2/pkg/tm"
+)
+
+func TestTCCRocketMQAction_ParseTCCResource(t *testing.T) {
+ action := &TCCRocketMQAction{}
+
+ resource, err := tcc.ParseTCCResource(action)
+ assert.NoError(t, err)
+ assert.NotNil(t, resource)
+
+ assert.Equal(t, ResourceIDTCCRocketMQ, resource.GetResourceId())
+ assert.Equal(t, ResourceIDTCCRocketMQ, action.GetActionName())
+}
+
+func TestTCCRocketMQAction_GetActionName(t *testing.T) {
+ action := &TCCRocketMQAction{}
+ assert.Equal(t, ResourceIDTCCRocketMQ, action.GetActionName())
+}
+
+func TestTCCRocketMQActionPrepare_SetsMessagePropertiesAndActionContext(t
*testing.T) {
+ transactionProducer := &stubTransactionProducer{
+ sendResult: &primitive.TransactionSendResult{
+ SendResult: &primitive.SendResult{
+ MsgID: "msg-1",
+ OffsetMsgID: "offset-1",
+ QueueOffset: 11,
+ TransactionID: "tx-1",
+ MessageQueue: &primitive.MessageQueue{
+ BrokerName: "broker-a",
+ QueueId: 3,
+ },
+ },
+ },
+ }
+ action := &TCCRocketMQAction{
+ producer: &SeataMQProducer{
+ transactionProducer: transactionProducer,
+ },
+ }
+ ctx := tm.InitSeataContext(context.Background())
+ tm.SetXID(ctx, "xid-123")
+ tm.SetBusinessActionContext(ctx, &tm.BusinessActionContext{
+ BranchId: 1001,
+ ActionContext: map[string]interface{}{},
+ })
+ msg := primitive.NewMessage("topic-test", []byte("hello"))
+
+ ok, err := action.Prepare(ctx, msg)
+
+ require.NoError(t, err)
+ assert.True(t, ok)
+ assert.Equal(t, "xid-123", msg.GetProperty(constant.PropertySeataXID))
+ assert.Equal(t, "1001", msg.GetProperty(constant.PropertySeataBranchId))
+ bac := tm.GetBusinessActionContext(ctx)
+ require.NotNil(t, bac)
+ assert.Equal(t, "msg-1", bac.ActionContext[ActionContextKeyMsgId])
+ assert.Equal(t, "offset-1",
bac.ActionContext[ActionContextKeyOffsetMsgId])
+ assert.EqualValues(t, int64(11),
bac.ActionContext[ActionContextKeyQueueOffset])
+ 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, 1, transactionProducer.sendCalls)
+ assert.Same(t, msg, transactionProducer.lastMsg)
+}
diff --git a/pkg/integration/rocketmq/test_helpers_test.go
b/pkg/integration/rocketmq/test_helpers_test.go
new file mode 100644
index 00000000..9682007d
--- /dev/null
+++ b/pkg/integration/rocketmq/test_helpers_test.go
@@ -0,0 +1,96 @@
+/*
+ * 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 (
+ "context"
+
+ "github.com/apache/rocketmq-client-go/v2/primitive"
+)
+
+type stubTransactionProducer struct {
+ startErr error
+ shutdownErr error
+ sendErr error
+ sendResult *primitive.TransactionSendResult
+
+ startCalls int
+ shutdownCalls int
+ sendCalls int
+ lastCtx context.Context
+ lastMsg *primitive.Message
+}
+
+func (s *stubTransactionProducer) Start() error {
+ s.startCalls++
+ return s.startErr
+}
+
+func (s *stubTransactionProducer) Shutdown() error {
+ s.shutdownCalls++
+ return s.shutdownErr
+}
+
+func (s *stubTransactionProducer) SendMessageInTransaction(ctx
context.Context, msg *primitive.Message) (*primitive.TransactionSendResult,
error) {
+ s.sendCalls++
+ s.lastCtx = ctx
+ s.lastMsg = msg
+ if s.sendErr != nil {
+ return nil, s.sendErr
+ }
+ if s.sendResult == nil {
+ s.sendResult = &primitive.TransactionSendResult{SendResult:
&primitive.SendResult{}}
+ }
+ return s.sendResult, nil
+}
+
+type stubNormalProducer struct {
+ startErr error
+ shutdownErr error
+ sendErr error
+ sendResult *primitive.SendResult
+
+ startCalls int
+ shutdownCalls int
+ sendCalls int
+ lastCtx context.Context
+ lastMsgs []*primitive.Message
+}
+
+func (s *stubNormalProducer) Start() error {
+ s.startCalls++
+ return s.startErr
+}
+
+func (s *stubNormalProducer) Shutdown() error {
+ s.shutdownCalls++
+ return s.shutdownErr
+}
+
+func (s *stubNormalProducer) SendSync(ctx context.Context, msg
...*primitive.Message) (*primitive.SendResult, error) {
+ s.sendCalls++
+ s.lastCtx = ctx
+ s.lastMsgs = msg
+ if s.sendErr != nil {
+ return nil, s.sendErr
+ }
+ if s.sendResult == nil {
+ s.sendResult = &primitive.SendResult{}
+ }
+ return s.sendResult, nil
+}
diff --git a/pkg/integration/rocketmq/transaction_listener.go
b/pkg/integration/rocketmq/transaction_listener.go
new file mode 100644
index 00000000..036b1482
--- /dev/null
+++ b/pkg/integration/rocketmq/transaction_listener.go
@@ -0,0 +1,116 @@
+/*
+ * 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 (
+ "fmt"
+
+ "github.com/apache/rocketmq-client-go/v2/primitive"
+
+ "seata.apache.org/seata-go/v2/pkg/constant"
+ "seata.apache.org/seata-go/v2/pkg/protocol/message"
+ "seata.apache.org/seata-go/v2/pkg/remoting/getty"
+ "seata.apache.org/seata-go/v2/pkg/util/log"
+)
+
+type SeataTransactionListener struct {
+ remotingClient globalStatusRequestSender
+}
+
+type globalStatusRequestSender interface {
+ SendSyncRequest(msg interface{}) (interface{}, error)
+}
+
+func NewSeataTransactionListener(_ *SeataMQProducer) *SeataTransactionListener
{
+ return &SeataTransactionListener{
+ remotingClient: getty.GetGettyRemotingClient(),
+ }
+}
+
+func (l *SeataTransactionListener) ExecuteLocalTransaction(msg
*primitive.Message) primitive.LocalTransactionState {
+ xid := msg.GetProperty(constant.PropertySeataXID)
+ if xid == "" {
+ return primitive.CommitMessageState
+ }
+ log.Debugf("[SeataTransactionListener] ExecuteLocalTransaction, xid=%s,
returning UnknownState", xid)
+ return primitive.UnknowState
+}
+
+func (l *SeataTransactionListener) CheckLocalTransaction(msgExt
*primitive.MessageExt) primitive.LocalTransactionState {
+ xid := msgExt.GetProperty(constant.PropertySeataXID)
+ if xid == "" {
+ log.Warnf("[SeataTransactionListener] CheckLocalTransaction:
missing XID, rollback")
+ return primitive.RollbackMessageState
+ }
+
+ branchIdStr := msgExt.GetProperty(constant.PropertySeataBranchId)
+ log.Infof("[SeataTransactionListener] CheckLocalTransaction, xid=%s,
branchId=%s", xid, branchIdStr)
+
+ globalStatus, err := l.queryGlobalStatus(xid)
+ if err != nil {
+ log.Errorf("[SeataTransactionListener] Query global status
failed, xid=%s, err=%v", xid, err)
+ return primitive.UnknowState
+ }
+
+ localTransactionState :=
mapGlobalStatusToLocalTransactionState(globalStatus)
+ switch localTransactionState {
+ case primitive.CommitMessageState:
+ log.Infof("[SeataTransactionListener] Global tx committed,
xid=%s", xid)
+ case primitive.RollbackMessageState:
+ log.Infof("[SeataTransactionListener] Global tx rollbacked,
xid=%s, status=%v", xid, globalStatus)
+ default:
+ log.Infof("[SeataTransactionListener] Global tx waiting for
final state, xid=%s, status=%v", xid, globalStatus)
+ }
+ return localTransactionState
+}
+
+func (l *SeataTransactionListener) queryGlobalStatus(xid string)
(message.GlobalStatus, error) {
+ req := message.GlobalStatusRequest{
+ AbstractGlobalEndRequest: message.AbstractGlobalEndRequest{
+ Xid: xid,
+ },
+ }
+ res, err := l.remotingClient.SendSyncRequest(req)
+ if err != nil {
+ return message.GlobalStatusUnKnown, err
+ }
+ gsResp, ok := res.(message.GlobalStatusResponse)
+ if !ok {
+ log.Errorf("[SeataTransactionListener] Invalid response type
for GetGlobalStatus, xid=%s", xid)
+ return message.GlobalStatusUnKnown, fmt.Errorf("invalid
response type: %T", res)
+ }
+ return gsResp.GlobalStatus, nil
+}
+
+func mapGlobalStatusToLocalTransactionState(globalStatus message.GlobalStatus)
primitive.LocalTransactionState {
+ // Finished only means the TC no longer manages the session, so it
cannot safely
+ // distinguish a late check on a committed transaction from a rollback
outcome.
+ switch globalStatus {
+ case message.GlobalStatusCommitted, message.GlobalStatusAsyncCommitting:
+ return primitive.CommitMessageState
+ case message.GlobalStatusRollbacked,
message.GlobalStatusTimeoutRollbacked, message.GlobalStatusRollbackFailed,
+ message.GlobalStatusTimeoutRollbackFailed,
message.GlobalStatusCommitFailed:
+ return primitive.RollbackMessageState
+ case message.GlobalStatusBegin, message.GlobalStatusCommitting,
message.GlobalStatusCommitRetrying,
+ message.GlobalStatusRollbacking,
message.GlobalStatusRollbackRetrying, message.GlobalStatusTimeoutRollbacking,
+ message.GlobalStatusTimeoutRollbackRetrying,
message.GlobalStatusFinished:
+ 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
new file mode 100644
index 00000000..9fe6248d
--- /dev/null
+++ b/pkg/integration/rocketmq/transaction_listener_test.go
@@ -0,0 +1,157 @@
+/*
+ * 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 (
+ "errors"
+ "testing"
+
+ "github.com/apache/rocketmq-client-go/v2/primitive"
+ "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/message"
+)
+
+type stubGlobalStatusRequestSender struct {
+ response interface{}
+ err error
+ lastReq interface{}
+}
+
+func (s *stubGlobalStatusRequestSender) SendSyncRequest(msg interface{})
(interface{}, error) {
+ s.lastReq = msg
+ if s.err != nil {
+ return nil, s.err
+ }
+ return s.response, nil
+}
+
+func TestSeataTransactionListenerExecuteLocalTransaction(t *testing.T) {
+ listener := NewSeataTransactionListener(nil)
+
+ msgWithoutXID := primitive.NewMessage("topic-test", []byte("hello"))
+ assert.Equal(t, primitive.CommitMessageState,
listener.ExecuteLocalTransaction(msgWithoutXID))
+
+ msgWithXID := primitive.NewMessage("topic-test", []byte("hello"))
+ msgWithXID.WithProperty(constant.PropertySeataXID, "xid-123")
+ assert.Equal(t, primitive.UnknowState,
listener.ExecuteLocalTransaction(msgWithXID))
+}
+
+func TestSeataTransactionListenerCheckLocalTransaction_MissingXIDRollsBack(t
*testing.T) {
+ listener := NewSeataTransactionListener(nil)
+
+ state := listener.CheckLocalTransaction(&primitive.MessageExt{})
+
+ assert.Equal(t, primitive.RollbackMessageState, state)
+}
+
+func TestSeataTransactionListenerCheckLocalTransaction_StatusMapping(t
*testing.T) {
+ tests := []struct {
+ name string
+ globalStatus message.GlobalStatus
+ expected primitive.LocalTransactionState
+ }{
+ {name: "committed", globalStatus:
message.GlobalStatusCommitted, expected: primitive.CommitMessageState},
+ {name: "async committing", globalStatus:
message.GlobalStatusAsyncCommitting, expected: primitive.CommitMessageState},
+ {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: "timeout rollback failed", globalStatus:
message.GlobalStatusTimeoutRollbackFailed, expected:
primitive.RollbackMessageState},
+ {name: "finished", globalStatus: message.GlobalStatusFinished,
expected: primitive.UnknowState},
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ client := &stubGlobalStatusRequestSender{
+ response: message.GlobalStatusResponse{
+ AbstractGlobalEndResponse:
message.AbstractGlobalEndResponse{
+ GlobalStatus: tt.globalStatus,
+ },
+ },
+ }
+ listener := &SeataTransactionListener{
+ remotingClient: client,
+ }
+
+ state :=
listener.CheckLocalTransaction(newTransactionMessageExt("xid-123", "1001"))
+
+ assert.Equal(t, tt.expected, state)
+ })
+ }
+}
+
+func
TestSeataTransactionListenerCheckLocalTransaction_QueryErrorReturnsUnknown(t
*testing.T) {
+ listener := &SeataTransactionListener{
+ remotingClient: &stubGlobalStatusRequestSender{
+ err: errors.New("network timeout"),
+ },
+ }
+
+ state :=
listener.CheckLocalTransaction(newTransactionMessageExt("xid-123", "1001"))
+
+ assert.Equal(t, primitive.UnknowState, state)
+}
+
+func TestSeataTransactionListenerQueryGlobalStatus(t *testing.T) {
+ client := &stubGlobalStatusRequestSender{
+ response: message.GlobalStatusResponse{
+ AbstractGlobalEndResponse:
message.AbstractGlobalEndResponse{
+ GlobalStatus: message.GlobalStatusRollbacked,
+ },
+ },
+ }
+ listener := &SeataTransactionListener{
+ remotingClient: client,
+ }
+
+ status, err := listener.queryGlobalStatus("xid-123")
+
+ require.NoError(t, err)
+ assert.Equal(t, message.GlobalStatusRollbacked, status)
+ req, ok := client.lastReq.(message.GlobalStatusRequest)
+ require.True(t, ok)
+ assert.Equal(t, "xid-123", req.Xid)
+}
+
+func TestSeataTransactionListenerQueryGlobalStatus_InvalidResponseType(t
*testing.T) {
+ listener := &SeataTransactionListener{
+ remotingClient: &stubGlobalStatusRequestSender{
+ response: "invalid-response",
+ },
+ }
+
+ status, err := listener.queryGlobalStatus("xid-123")
+
+ require.Error(t, err)
+ assert.Equal(t, message.GlobalStatusUnKnown, status)
+ assert.Contains(t, err.Error(), "invalid response type")
+}
+
+func newTransactionMessageExt(xid, branchID string) *primitive.MessageExt {
+ msg := primitive.NewMessage("topic-test", []byte("hello"))
+ if xid != "" {
+ msg.WithProperty(constant.PropertySeataXID, xid)
+ }
+ if branchID != "" {
+ msg.WithProperty(constant.PropertySeataBranchId, branchID)
+ }
+ return &primitive.MessageExt{Message: *msg}
+}
diff --git a/pkg/protocol/message/other_message.go
b/pkg/protocol/message/other_message.go
index dd2abf4b..9e4ddafa 100644
--- a/pkg/protocol/message/other_message.go
+++ b/pkg/protocol/message/other_message.go
@@ -36,7 +36,7 @@ type MessageFuture struct {
func NewMessageFuture(message RpcMessage) *MessageFuture {
return &MessageFuture{
ID: message.ID,
- Done: make(chan struct{}),
+ Done: make(chan struct{}, 1), // Buffered channel prevents lost
signals in timeout race
}
}
diff --git a/pkg/protocol/message/other_message_test.go
b/pkg/protocol/message/other_message_test.go
index d4487cc4..5625ad4c 100644
--- a/pkg/protocol/message/other_message_test.go
+++ b/pkg/protocol/message/other_message_test.go
@@ -25,7 +25,9 @@ import (
func TestNewMessageFuture(t *testing.T) {
rpcMessage := RpcMessage{ID: 0}
- assert.Equal(t, int32(0), NewMessageFuture(rpcMessage).ID)
+ messageFuture := NewMessageFuture(rpcMessage)
+ assert.Equal(t, int32(0), messageFuture.ID)
+ assert.Equal(t, 1, cap(messageFuture.Done))
}
func TestHeartBeatMessage_ToString(t *testing.T) {
diff --git a/pkg/remoting/getty/getty_remoting.go
b/pkg/remoting/getty/getty_remoting.go
index ef0b419e..7b4e1e53 100644
--- a/pkg/remoting/getty/getty_remoting.go
+++ b/pkg/remoting/getty/getty_remoting.go
@@ -128,7 +128,11 @@ func (g *GettyRemoting)
NotifyRpcMessageResponse(rpcMessage message.RpcMessage)
messageFuture.Response = rpcMessage.Body
// todo add messageFuture.Err
// messageFuture.Err = rpcMessage.Err
- messageFuture.Done <- struct{}{}
+ select {
+ case messageFuture.Done <- struct{}{}:
+ default:
+ log.Warnf("response notification dropped for msg ID: %d
because the future was already signaled", rpcMessage.ID)
+ }
// client.msgFutures.Delete(rpcMessage.RequestID)
} else {
log.Infof("msg: {} is not found in msgFutures.", rpcMessage.ID)
diff --git a/pkg/remoting/getty/getty_remoting_test.go
b/pkg/remoting/getty/getty_remoting_test.go
index 1eadc4f2..303633bb 100644
--- a/pkg/remoting/getty/getty_remoting_test.go
+++ b/pkg/remoting/getty/getty_remoting_test.go
@@ -19,6 +19,7 @@ package getty
import (
"testing"
+ "time"
"github.com/stretchr/testify/assert"
@@ -165,3 +166,48 @@ func TestGettyRemoting_RemoveMergedMessageFuture(t
*testing.T) {
})
}
}
+
+func TestGettyRemoting_NotifyRpcMessageResponseSignalsWaitingFuture(t
*testing.T) {
+ gettyRemoting := newGettyRemoting()
+ request := message.RpcMessage{ID: 1}
+ messageFuture := message.NewMessageFuture(request)
+ gettyRemoting.futures.Store(request.ID, messageFuture)
+
+ gettyRemoting.NotifyRpcMessageResponse(message.RpcMessage{
+ ID: request.ID,
+ Body: "ok",
+ })
+
+ assert.Equal(t, "ok", messageFuture.Response)
+ select {
+ case <-messageFuture.Done:
+ default:
+ t.Fatal("expected NotifyRpcMessageResponse to signal the
waiting future")
+ }
+}
+
+func
TestGettyRemoting_NotifyRpcMessageResponseDoesNotBlockWhenAlreadySignaled(t
*testing.T) {
+ gettyRemoting := newGettyRemoting()
+ request := message.RpcMessage{ID: 1}
+ messageFuture := message.NewMessageFuture(request)
+ messageFuture.Done <- struct{}{}
+ gettyRemoting.futures.Store(request.ID, messageFuture)
+
+ done := make(chan struct{})
+ go func() {
+ gettyRemoting.NotifyRpcMessageResponse(message.RpcMessage{
+ ID: request.ID,
+ Body: "late-response",
+ })
+ close(done)
+ }()
+
+ select {
+ case <-done:
+ case <-time.After(time.Second):
+ t.Fatal("NotifyRpcMessageResponse blocked when the future was
already signaled")
+ }
+
+ assert.Equal(t, "late-response", messageFuture.Response)
+ assert.Len(t, messageFuture.Done, 1)
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]