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 976e7b94 fix: implement XA mode commitOnXA() and LockQuery() (#1061)
976e7b94 is described below
commit 976e7b9494b39ef3bf7cde2a5946080ccb3dbd03
Author: CAICAII <[email protected]>
AuthorDate: Wed Apr 22 19:18:44 2026 +0800
fix: implement XA mode commitOnXA() and LockQuery() (#1061)
* fix: implement XA mode commitOnXA() and LockQuery()
- tx_xa.go: implement commitOnXA() to execute XA END + XA PREPARE and
report status to TC
- xa_resource_manager.go: LockQuery() delegates to rmRemoting for global
lock checking
- tx.go: add xaConn field to support XA transactions
- conn_xa.go: set xaConn reference in BeginTx
* fix: comprehensive XA mode improvements and fixes
This commit addresses PR #1061 feedback and Oracle architectural review
findings:
**Reviewer Feedback (thunguo):**
- Extract anonymous xaConn interface into named XAConnection type for
better readability
**Code Quality Improvements:**
- Add comprehensive logging to commitOnXA() following existing patterns
(log.Infof/Errorf)
- Add detailed documentation comments explaining XA two-phase commit
protocol flow
- Improve test coverage with 11 new unit tests for XATx functionality
**Critical Bug Fixes:**
1. Fix BranchReport error handling - errors are now properly checked and
logged
2. Complete XA Rollback lifecycle - now executes XA END(TMFAIL) + XA
ROLLBACK + TC reporting
3. Fix SQL error reporting - createNewTxOnExecIfNeed() now reports
PhaseoneFailed to TC
4. Fix nil pointer safety - set tx.target after real transaction is created
in XAConn.BeginTx()
5. Update prepareTime after successful XA PREPARE for correct connection
hold/timeout behavior
**Files Changed:**
- pkg/datasource/sql/tx.go: Extract XAConnection interface, add xaConn field
- pkg/datasource/sql/tx_xa.go: Implement commitOnXA() with logging and
error handling, complete Rollback() lifecycle
- pkg/datasource/sql/conn_xa.go: Fix nil pointer issue, add SQL error
reporting, update prepareTime
- pkg/datasource/sql/tx_xa_test.go: Add comprehensive unit tests (11 tests
covering all scenarios)
**Test Results:**
- All existing tests pass
- 11 new tests added and passing
- Build successful with no errors
Co-authored-by: Oracle Review <architectural-review>
* fix: tighten xa branch reporting lifecycle
* fix: align xa branch tx lifecycle
* test: tighten xa review follow-ups
---------
Co-authored-by: ThunGuo <[email protected]>
---
pkg/datasource/sql/conn_xa.go | 70 +++-
pkg/datasource/sql/conn_xa_test.go | 115 +++++-
.../sql/test_helpers_test.go} | 41 +-
pkg/datasource/sql/tx.go | 21 +-
pkg/datasource/sql/tx_xa.go | 80 +++-
pkg/datasource/sql/tx_xa_test.go | 417 +++++++++++++++++++++
pkg/datasource/sql/xa_resource_manager.go | 2 +-
pkg/datasource/sql/xa_resource_manager_test.go | 95 +++++
pkg/rm/rm_cache.go | 4 +
pkg/rm/rm_cache_test.go | 16 +
10 files changed, 807 insertions(+), 54 deletions(-)
diff --git a/pkg/datasource/sql/conn_xa.go b/pkg/datasource/sql/conn_xa.go
index 5c7d1ba5..857e885b 100644
--- a/pkg/datasource/sql/conn_xa.go
+++ b/pkg/datasource/sql/conn_xa.go
@@ -33,6 +33,8 @@ import (
var xaConnTimeout time.Duration
+var errXABranchLifecycleManaged = errors.New("xa branch lifecycle is managed
by XATx or XAConn")
+
// XAConn Database connection proxy object under XA transaction model
// Conn is assumed to be stateful.
type XAConn struct {
@@ -49,6 +51,19 @@ type XAConn struct {
isConnKept bool
}
+// xaBranchTx is a sentinel driver.Tx used to satisfy database/sql wiring while
+// the real XA branch lifecycle is driven by XATx/XAConn through XA
START/END/PREPARE.
+// Any direct Commit/Rollback on this placeholder indicates the caller
bypassed the XA flow.
+type xaBranchTx struct{}
+
+func (xaBranchTx) Commit() error {
+ return errXABranchLifecycleManaged
+}
+
+func (xaBranchTx) Rollback() error {
+ return errXABranchLifecycleManaged
+}
+
func (c *XAConn) PrepareContext(ctx context.Context, query string)
(driver.Stmt, error) {
if c.createOnceTxContext(ctx) {
defer func() {
@@ -126,11 +141,20 @@ func (c *XAConn) BeginTx(ctx context.Context, opts
driver.TxOptions) (driver.Tx,
c.txCtx.XID = tm.GetXID(ctx)
c.txCtx.TransactionMode = types.XAMode
- tx, err := c.Conn.BeginTx(ctx, opts)
+ // Keep a sentinel target in Tx so any accidental fallback to the
generic
+ // driver.Tx path fails fast instead of silently masking XA lifecycle
bugs.
+ branchTx := xaBranchTx{}
+ c.tx = branchTx
+
+ tx, err := newTx(
+ withDriverConn(c.Conn),
+ withTxCtx(c.txCtx),
+ withOriginTx(branchTx),
+ withXAConn(c),
+ )
if err != nil {
return nil, err
}
- c.tx = tx
if !c.autoCommit {
if c.xaActive {
@@ -142,6 +166,8 @@ func (c *XAConn) BeginTx(ctx context.Context, opts
driver.TxOptions) (driver.Tx,
return nil, fmt.Errorf("start xa %s transaction failure
for the tx is a wrong type", c.txCtx.XID)
}
+ baseTx.xaConn = c
+
c.branchRegisterTime = time.Now()
if err := baseTx.register(c.txCtx); err != nil {
c.cleanXABranchContext()
@@ -184,12 +210,17 @@ func (c *XAConn) createNewTxOnExecIfNeed(ctx
context.Context, f func() (types.Ex
defer func() {
recoverErr := recover()
- if err != nil || recoverErr != nil {
- log.Errorf("conn at rollback error:%v or
recoverErr:%v", err, recoverErr)
+ if recoverErr != nil {
+ log.Errorf("conn xa rollback recoverErr:%v", recoverErr)
+ if tx != nil {
+ if rollbackErr := tx.Rollback(); rollbackErr !=
nil {
+ log.Errorf("conn xa rollback error:%v",
rollbackErr)
+ }
+ return
+ }
if c.tx != nil {
- rollbackErr := c.tx.Rollback()
- if rollbackErr != nil {
- log.Errorf("conn at rollback error:%v",
rollbackErr)
+ if rollbackErr := c.Rollback(ctx); rollbackErr
!= nil {
+ log.Errorf("conn xa rollback error:%v",
rollbackErr)
}
}
}
@@ -206,20 +237,23 @@ func (c *XAConn) createNewTxOnExecIfNeed(ctx
context.Context, f func() (types.Ex
// execute SQL
ret, err := f()
if err != nil {
- // XA End & Rollback
- if rollbackErr := c.Rollback(ctx); rollbackErr != nil {
- log.Errorf("failed to rollback xa branch of :%s,
err:%v", c.txCtx.XID, rollbackErr)
+ if tx != nil {
+ if rollbackErr := tx.Rollback(); rollbackErr != nil {
+ log.Errorf("failed to rollback xa branch of
:%s, err:%v", c.txCtx.XID, rollbackErr)
+ }
+ } else {
+ if rollbackErr := c.Rollback(ctx); rollbackErr != nil {
+ log.Errorf("failed to rollback xa branch of
:%s, err:%v", c.txCtx.XID, rollbackErr)
+ }
}
return nil, err
}
if tx != nil && currentAutoCommit {
- if err = c.Commit(ctx); err != nil {
+ // Commit through XATx so phase-one reporting stays coupled to
driver.Tx lifecycle.
+ if err = tx.Commit(); err != nil {
log.Errorf("xa connection proxy commit failure xid:%s,
err:%v", c.txCtx.XID, err)
- // XA End & Rollback
- if err := c.Rollback(ctx); err != nil {
- log.Errorf("xa connection proxy rollback
failure xid:%s, err:%v", c.txCtx.XID, err)
- }
+ return nil, err
}
}
@@ -321,10 +355,6 @@ func (c *XAConn) Rollback(ctx context.Context) error {
c.cleanXABranchContext()
return c.rollbackErrorHandle()
}
- if err := c.tx.Rollback(); err != nil {
- c.cleanXABranchContext()
- return fmt.Errorf("failed to report XA branch
commit-failure on xid:%s err:%w", c.txCtx.XID, err)
- }
c.rollBacked = true
}
c.cleanXABranchContext()
@@ -356,6 +386,8 @@ func (c *XAConn) Commit(ctx context.Context) error {
if c.xaResource.XAPrepare(ctx, c.xaBranchXid.String()) != nil {
return c.commitErrorHandle(ctx)
}
+
+ c.prepareTime = time.Now()
return nil
}
diff --git a/pkg/datasource/sql/conn_xa_test.go
b/pkg/datasource/sql/conn_xa_test.go
index 6d5e26fc..46fc412c 100644
--- a/pkg/datasource/sql/conn_xa_test.go
+++ b/pkg/datasource/sql/conn_xa_test.go
@@ -38,6 +38,7 @@ import (
"seata.apache.org/seata-go/v2/pkg/datasource/sql/types"
"seata.apache.org/seata-go/v2/pkg/datasource/sql/xa"
"seata.apache.org/seata-go/v2/pkg/protocol/branch"
+ "seata.apache.org/seata-go/v2/pkg/rm"
"seata.apache.org/seata-go/v2/pkg/tm"
)
@@ -170,6 +171,31 @@ func initXAConnTestResource(t *testing.T)
(*gomock.Controller, *sql.DB, *mockSQL
return ctrl, db, mi, ti
}
+func newMockXAConn(t *testing.T, ctrl *gomock.Controller, branchID int64)
(*XAConn, *mock.MockDataSourceManager) {
+ t.Helper()
+
+ mockMgr := mock.NewMockDataSourceManager(ctrl)
+ mockMgr.SetBranchType(branch.BranchTypeXA)
+ registerResourceManagerForTest(t, mockMgr)
+ mockMgr.EXPECT().BranchRegister(gomock.Any(),
gomock.Any()).AnyTimes().Return(branchID, nil)
+
+ mockConn := mock.NewMockTestDriverConn(ctrl)
+ baseMockConn(mockConn)
+
+ return &XAConn{
+ Conn: &Conn{
+ res: &DBResource{
+ resourceID: "jdbc:mysql://test/resource",
+ dbType: types.DBTypeMySQL,
+ },
+ txCtx: types.NewTxCtx(),
+ targetConn: mockConn,
+ autoCommit: true,
+ dbType: types.DBTypeMySQL,
+ },
+ }, mockMgr
+}
+
func TestXAConn_ExecContext(t *testing.T) {
ctrl, db, mi, ti := initXAConnTestResource(t)
@@ -206,8 +232,7 @@ func TestXAConn_ExecContext(t *testing.T) {
_, err = db.ExecContext(ctx, "SELECT 1")
assert.NoError(t, err)
- // todo fix
- assert.Equal(t, int32(0), atomic.LoadInt32(&comitCnt))
+ assert.Equal(t, int32(2), atomic.LoadInt32(&comitCnt))
})
t.Run("not xid", func(t *testing.T) {
@@ -389,7 +414,7 @@ func TestXAConn_Rollback_XAER_RMFAIL(t *testing.T) {
// Covers the XA rollback flow when End() returns XAER_RMFAIL (IDLE/already
ended)
func TestXAConn_Rollback_HandleXAERRMFAILAlreadyEnded(t *testing.T) {
- ctrl, db, _, ti := initXAConnTestResource(t)
+ ctrl, db, _, _ := initXAConnTestResource(t)
defer func() {
simulateExecContextError = nil
db.Close()
@@ -400,18 +425,14 @@ func TestXAConn_Rollback_HandleXAERRMFAILAlreadyEnded(t
*testing.T) {
ctx := tm.InitSeataContext(context.Background())
tm.SetXID(ctx, uuid.New().String())
- // Ensure Tx.Rollback has a non-nil underlying target to avoid
nil-deref when test triggers rollback
- ti.beforeRollback = func(tx *Tx) {
- mtx := mock.NewMockTestDriverTx(ctrl)
- mtx.EXPECT().Rollback().AnyTimes().Return(nil)
- tx.target = mtx
- }
-
// Inject: XA END returns XAER_RMFAIL(IDLE), normal SQL returns an
error to trigger rollback
simulateExecContextError = func(query string) error {
upper := strings.ToUpper(query)
if strings.HasPrefix(upper, "XA END") {
- return &mysql.MySQLError{Number:
types.ErrCodeXAER_RMFAIL_IDLE, Message: "Error 1399 (XAE07): XAER_RMFAIL: The
command cannot be executed when global transaction is in the IDLE state"}
+ return &mysql.MySQLError{
+ Number: types.ErrCodeXAER_RMFAIL_IDLE,
+ Message: "Error 1399 (XAE07): XAER_RMFAIL: The
command cannot be executed when global transaction is in the IDLE state",
+ }
}
if !strings.HasPrefix(upper, "XA ") {
return io.EOF
@@ -425,3 +446,75 @@ func TestXAConn_Rollback_HandleXAERRMFAILAlreadyEnded(t
*testing.T) {
t.Fatalf("expected error to trigger rollback path")
}
}
+
+func TestXAConn_ExecContext_AutoCommitReportsPhaseOneDone(t *testing.T) {
+ ctrl := gomock.NewController(t)
+ defer ctrl.Finish()
+ CleanTxHooks()
+ defer CleanTxHooks()
+
+ xaConn, mockMgr := newMockXAConn(t, ctrl, 123)
+ mockMgr.EXPECT().BranchReport(gomock.Any(), gomock.Any()).DoAndReturn(
+ func(ctx context.Context, param rm.BranchReportParam) error {
+ assert.Equal(t, branch.BranchTypeXA, param.BranchType)
+ assert.Equal(t, int64(123), param.BranchId)
+ assert.EqualValues(t, branch.BranchStatusPhaseoneDone,
param.Status)
+ return nil
+ },
+ ).Times(1)
+
+ var commitCnt int32
+ RegisterTxHook(&mockTxHook{
+ beforeCommit: func(tx *Tx) error {
+ atomic.AddInt32(&commitCnt, 1)
+ return nil
+ },
+ })
+
+ ctx := tm.InitSeataContext(context.Background())
+ tm.SetXID(ctx, uuid.NewString())
+
+ _, err := xaConn.ExecContext(ctx, "SELECT 1", nil)
+ assert.NoError(t, err)
+ assert.Equal(t, int32(1), atomic.LoadInt32(&commitCnt))
+}
+
+func TestXAConn_BeginTx_DoesNotStartPhysicalTx(t *testing.T) {
+ ctrl := gomock.NewController(t)
+ defer ctrl.Finish()
+
+ xaConn, mockMgr := newMockXAConn(t, ctrl, 123)
+ mockMgr.EXPECT().BranchReport(gomock.Any(), gomock.Any()).DoAndReturn(
+ func(ctx context.Context, param rm.BranchReportParam) error {
+ assert.Equal(t, branch.BranchTypeXA, param.BranchType)
+ assert.Equal(t, int64(123), param.BranchId)
+ assert.EqualValues(t,
branch.BranchStatusPhaseoneFailed, param.Status)
+ return nil
+ },
+ ).Times(1)
+
+ ctx := tm.InitSeataContext(context.Background())
+ tm.SetXID(ctx, uuid.NewString())
+
+ tx, err := xaConn.BeginTx(ctx, driver.TxOptions{})
+ assert.NoError(t, err)
+
+ xaTx, ok := tx.(*XATx)
+ if assert.True(t, ok) {
+ _, noop := xaTx.tx.target.(xaBranchTx)
+ assert.True(t, noop)
+ }
+
+ err = tx.Rollback()
+ assert.NoError(t, err)
+}
+
+func TestXABranchTx_CommitRollbackFailFast(t *testing.T) {
+ branchTx := xaBranchTx{}
+
+ err := branchTx.Commit()
+ assert.ErrorIs(t, err, errXABranchLifecycleManaged)
+
+ err = branchTx.Rollback()
+ assert.ErrorIs(t, err, errXABranchLifecycleManaged)
+}
diff --git a/pkg/rm/rm_cache_test.go b/pkg/datasource/sql/test_helpers_test.go
similarity index 54%
copy from pkg/rm/rm_cache_test.go
copy to pkg/datasource/sql/test_helpers_test.go
index e9477b80..6e9af72f 100644
--- a/pkg/rm/rm_cache_test.go
+++ b/pkg/datasource/sql/test_helpers_test.go
@@ -15,31 +15,38 @@
* limitations under the License.
*/
-package rm
+package sql
import (
"testing"
- "github.com/golang/mock/gomock"
- "github.com/stretchr/testify/assert"
-
"seata.apache.org/seata-go/v2/pkg/protocol/branch"
+ "seata.apache.org/seata-go/v2/pkg/rm"
)
-func TestGetRmCacheInstance(t *testing.T) {
- ctl := gomock.NewController(t)
-
- mockResourceManager := NewMockResourceManager(ctl)
-
mockResourceManager.EXPECT().GetBranchType().Return(branch.BranchTypeTCC)
+func registerResourceManagerForTest(t *testing.T, resourceManager
rm.ResourceManager) {
+ t.Helper()
- tests := struct {
- name string
- want *ResourceManagerCache
- }{"test1", &ResourceManagerCache{}}
+ prevMgr, hasPrevMgr :=
currentResourceManagerForTest(resourceManager.GetBranchType())
+ rm.GetRmCacheInstance().RegisterResourceManager(resourceManager)
- t.Run(tests.name, func(t *testing.T) {
-
GetRmCacheInstance().RegisterResourceManager(mockResourceManager)
- actual :=
GetRmCacheInstance().GetResourceManager(branch.BranchTypeTCC)
- assert.Equalf(t, mockResourceManager, actual,
"GetRmCacheInstance()")
+ t.Cleanup(func() {
+ if hasPrevMgr {
+ rm.GetRmCacheInstance().RegisterResourceManager(prevMgr)
+ return
+ }
+
rm.GetRmCacheInstance().UnregisterResourceManager(resourceManager.GetBranchType())
})
}
+
+func currentResourceManagerForTest(branchType branch.BranchType) (manager
rm.ResourceManager, ok bool) {
+ defer func() {
+ if recover() != nil {
+ manager = nil
+ ok = false
+ }
+ }()
+
+ manager = rm.GetRmCacheInstance().GetResourceManager(branchType)
+ return manager, true
+}
diff --git a/pkg/datasource/sql/tx.go b/pkg/datasource/sql/tx.go
index fdf4728f..c3c74c26 100644
--- a/pkg/datasource/sql/tx.go
+++ b/pkg/datasource/sql/tx.go
@@ -59,6 +59,12 @@ type (
BeforeRollback(tx *Tx)
}
+
+ // XAConnection represents an XA-capable connection that can commit or
rollback XA transactions
+ XAConnection interface {
+ Commit(ctx context.Context) error
+ Rollback(ctx context.Context) error
+ }
)
func newTx(opts ...txOption) (driver.Tx, error) {
@@ -96,11 +102,19 @@ func withTxCtx(ctx *types.TransactionContext) txOption {
}
}
+// withXAConn
+func withXAConn(xaConn XAConnection) txOption {
+ return func(t *Tx) {
+ t.xaConn = xaConn
+ }
+}
+
// Tx
type Tx struct {
conn *Conn
tranCtx *types.TransactionContext
target driver.Tx
+ xaConn XAConnection
}
// Commit do commit action
@@ -194,9 +208,10 @@ func (tx *Tx) report(success bool) error {
}
status := getStatus(success)
request := rm.BranchReportParam{
- Xid: tx.tranCtx.XID,
- BranchId: int64(tx.tranCtx.BranchID),
- Status: status,
+ BranchType: tx.tranCtx.TransactionMode.BranchType(),
+ Xid: tx.tranCtx.XID,
+ BranchId: int64(tx.tranCtx.BranchID),
+ Status: status,
}
dataSourceManager :=
datasource.GetDataSourceManager(tx.tranCtx.TransactionMode.BranchType())
if dataSourceManager == nil {
diff --git a/pkg/datasource/sql/tx_xa.go b/pkg/datasource/sql/tx_xa.go
index d3380e57..a280eea8 100644
--- a/pkg/datasource/sql/tx_xa.go
+++ b/pkg/datasource/sql/tx_xa.go
@@ -17,6 +17,13 @@
package sql
+import (
+ "context"
+ "fmt"
+
+ "seata.apache.org/seata-go/v2/pkg/util/log"
+)
+
type XATx struct {
tx *Tx
}
@@ -32,15 +39,82 @@ func (tx *XATx) Commit() error {
return tx.commitOnXA()
}
+// Rollback executes XA END(TMFAIL), XA ROLLBACK and reports to TC
func (tx *XATx) Rollback() error {
originTx := tx.tx
- if originTx.tranCtx.OpenGlobalTransaction() &&
originTx.tranCtx.IsBranchRegistered() {
- return originTx.report(false)
+
+ if !originTx.tranCtx.OpenGlobalTransaction() {
+ return nil
+ }
+
+ if originTx.xaConn == nil {
+ return fmt.Errorf("xa transaction requires xaConn")
+ }
+
+ xid := originTx.tranCtx.XID
+ branchID := originTx.tranCtx.BranchID
+
+ log.Infof("xa branch [%d/%s] executing XA rollback", branchID, xid)
+
+ if err := originTx.xaConn.Rollback(context.Background()); err != nil {
+ log.Errorf("xa branch [%d/%s] XA END(TMFAIL) + XA ROLLBACK
failed: %v", branchID, xid, err)
+ if originTx.tranCtx.IsBranchRegistered() {
+ if reportErr := originTx.report(false); reportErr !=
nil {
+ log.Errorf("xa branch [%d/%s] failed to report
rollback failure to TC: %v", branchID, xid, reportErr)
+ }
+ }
+ return err
}
+ log.Infof("xa branch [%d/%s] XA END(TMFAIL) + XA ROLLBACK succeeded",
branchID, xid)
+
+ if originTx.tranCtx.IsBranchRegistered() {
+ if err := originTx.report(false); err != nil {
+ log.Errorf("xa branch [%d/%s] failed to report rollback
to TC: %v", branchID, xid, err)
+ return err
+ }
+ log.Infof("xa branch [%d/%s] reported rollback to TC",
branchID, xid)
+ }
+
return nil
}
-// commitOnXA commit xa and register branch transaction
+// commitOnXA executes XA END, XA PREPARE and reports to TC
func (tx *XATx) commitOnXA() error {
+ originTx := tx.tx
+
+ if !originTx.tranCtx.OpenGlobalTransaction() {
+ return nil
+ }
+
+ if originTx.xaConn == nil {
+ return fmt.Errorf("xa transaction requires xaConn")
+ }
+
+ xid := originTx.tranCtx.XID
+ branchID := originTx.tranCtx.BranchID
+
+ log.Infof("xa branch [%d/%s] executing XA END + XA PREPARE", branchID,
xid)
+
+ if err := originTx.xaConn.Commit(context.Background()); err != nil {
+ log.Errorf("xa branch [%d/%s] XA END + XA PREPARE failed: %v",
branchID, xid, err)
+ if originTx.tranCtx.IsBranchRegistered() {
+ if reportErr := originTx.report(false); reportErr !=
nil {
+ log.Errorf("xa branch [%d/%s] failed to report
phase-1 failure to TC: %v", branchID, xid, reportErr)
+ return fmt.Errorf("XA PREPARE failed: %w, and
report failed: %v", err, reportErr)
+ }
+ }
+ return err
+ }
+
+ log.Infof("xa branch [%d/%s] XA END + XA PREPARE succeeded", branchID,
xid)
+
+ if originTx.tranCtx.IsBranchRegistered() {
+ if err := originTx.report(true); err != nil {
+ log.Errorf("xa branch [%d/%s] failed to report phase-1
success to TC: %v", branchID, xid, err)
+ return fmt.Errorf("XA PREPARE succeeded but report to
TC failed: %w", err)
+ }
+ log.Infof("xa branch [%d/%s] reported phase-1 success to TC",
branchID, xid)
+ }
+
return nil
}
diff --git a/pkg/datasource/sql/tx_xa_test.go b/pkg/datasource/sql/tx_xa_test.go
new file mode 100644
index 00000000..24fa4817
--- /dev/null
+++ b/pkg/datasource/sql/tx_xa_test.go
@@ -0,0 +1,417 @@
+/*
+ * 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 sql
+
+import (
+ "context"
+ "errors"
+ "testing"
+
+ "github.com/golang/mock/gomock"
+ "github.com/stretchr/testify/assert"
+
+ "seata.apache.org/seata-go/v2/pkg/datasource/sql/mock"
+ "seata.apache.org/seata-go/v2/pkg/datasource/sql/types"
+ "seata.apache.org/seata-go/v2/pkg/protocol/branch"
+ "seata.apache.org/seata-go/v2/pkg/rm"
+)
+
+type mockXAConnection struct {
+ commitErr error
+ rollbackErr error
+ commitCalls int
+ rollbackCalls int
+}
+
+func (m *mockXAConnection) Commit(ctx context.Context) error {
+ m.commitCalls++
+ return m.commitErr
+}
+
+func (m *mockXAConnection) Rollback(ctx context.Context) error {
+ m.rollbackCalls++
+ return m.rollbackErr
+}
+
+func TestXATx_commitOnXA_NoGlobalTransaction(t *testing.T) {
+ tranCtx := types.NewTxCtx()
+ tranCtx.XID = ""
+
+ xaTx := &XATx{
+ tx: &Tx{
+ tranCtx: tranCtx,
+ },
+ }
+
+ err := xaTx.commitOnXA()
+ assert.NoError(t, err)
+}
+
+func TestXATx_commitOnXA_MissingXAConn(t *testing.T) {
+ tranCtx := types.NewTxCtx()
+ tranCtx.XID = "test-xid"
+ tranCtx.TransactionMode = types.XAMode
+
+ xaTx := &XATx{
+ tx: &Tx{
+ tranCtx: tranCtx,
+ xaConn: nil,
+ },
+ }
+
+ err := xaTx.commitOnXA()
+ assert.Error(t, err)
+ assert.Contains(t, err.Error(), "xa transaction requires xaConn")
+}
+
+func TestXATx_commitOnXA_CommitSuccess_BranchNotRegistered(t *testing.T) {
+ tranCtx := types.NewTxCtx()
+ tranCtx.XID = "test-xid"
+ tranCtx.BranchID = 0
+ tranCtx.TransactionMode = types.XAMode
+
+ mockConn := &mockXAConnection{}
+
+ xaTx := &XATx{
+ tx: &Tx{
+ tranCtx: tranCtx,
+ xaConn: mockConn,
+ },
+ }
+
+ err := xaTx.commitOnXA()
+ assert.NoError(t, err)
+ assert.Equal(t, 1, mockConn.commitCalls)
+}
+
+func TestXATx_commitOnXA_CommitFailure_BranchNotRegistered(t *testing.T) {
+ tranCtx := types.NewTxCtx()
+ tranCtx.XID = "test-xid"
+ tranCtx.BranchID = 0
+ tranCtx.TransactionMode = types.XAMode
+
+ mockConn := &mockXAConnection{
+ commitErr: errors.New("XA PREPARE failed"),
+ }
+
+ xaTx := &XATx{
+ tx: &Tx{
+ tranCtx: tranCtx,
+ xaConn: mockConn,
+ },
+ }
+
+ err := xaTx.commitOnXA()
+ assert.Error(t, err)
+ assert.Contains(t, err.Error(), "XA PREPARE failed")
+ assert.Equal(t, 1, mockConn.commitCalls)
+}
+
+func TestXATx_commitOnXA_CommitSuccess_BranchRegisteredReportsSuccess(t
*testing.T) {
+ ctrl := gomock.NewController(t)
+ defer ctrl.Finish()
+
+ mockMgr := mock.NewMockDataSourceManager(ctrl)
+ mockMgr.SetBranchType(branch.BranchTypeXA)
+ registerResourceManagerForTest(t, mockMgr)
+ mockMgr.EXPECT().BranchReport(gomock.Any(), gomock.Any()).DoAndReturn(
+ func(ctx context.Context, param rm.BranchReportParam) error {
+ assert.Equal(t, branch.BranchTypeXA, param.BranchType)
+ assert.Equal(t, int64(123), param.BranchId)
+ assert.EqualValues(t, branch.BranchStatusPhaseoneDone,
param.Status)
+ assert.Equal(t, "test-xid", param.Xid)
+ return nil
+ },
+ ).Times(1)
+
+ tranCtx := types.NewTxCtx()
+ tranCtx.XID = "test-xid"
+ tranCtx.BranchID = 123
+ tranCtx.TransactionMode = types.XAMode
+
+ mockConn := &mockXAConnection{}
+
+ xaTx := &XATx{
+ tx: &Tx{
+ tranCtx: tranCtx,
+ xaConn: mockConn,
+ },
+ }
+
+ err := xaTx.commitOnXA()
+ assert.NoError(t, err)
+ assert.Equal(t, 1, mockConn.commitCalls)
+}
+
+func TestXATx_commitOnXA_CommitFailure_BranchRegisteredReportsFailure(t
*testing.T) {
+ ctrl := gomock.NewController(t)
+ defer ctrl.Finish()
+
+ mockMgr := mock.NewMockDataSourceManager(ctrl)
+ mockMgr.SetBranchType(branch.BranchTypeXA)
+ registerResourceManagerForTest(t, mockMgr)
+ mockMgr.EXPECT().BranchReport(gomock.Any(), gomock.Any()).DoAndReturn(
+ func(ctx context.Context, param rm.BranchReportParam) error {
+ assert.Equal(t, branch.BranchTypeXA, param.BranchType)
+ assert.Equal(t, int64(123), param.BranchId)
+ assert.EqualValues(t,
branch.BranchStatusPhaseoneFailed, param.Status)
+ assert.Equal(t, "test-xid", param.Xid)
+ return nil
+ },
+ ).Times(1)
+
+ tranCtx := types.NewTxCtx()
+ tranCtx.XID = "test-xid"
+ tranCtx.BranchID = 123
+ tranCtx.TransactionMode = types.XAMode
+
+ mockConn := &mockXAConnection{
+ commitErr: errors.New("XA PREPARE failed"),
+ }
+
+ xaTx := &XATx{
+ tx: &Tx{
+ tranCtx: tranCtx,
+ xaConn: mockConn,
+ },
+ }
+
+ err := xaTx.commitOnXA()
+ assert.Error(t, err)
+ assert.Contains(t, err.Error(), "XA PREPARE failed")
+ assert.Equal(t, 1, mockConn.commitCalls)
+}
+
+func TestXATx_Rollback_NoGlobalTransaction(t *testing.T) {
+ tranCtx := types.NewTxCtx()
+ tranCtx.XID = ""
+
+ xaTx := &XATx{
+ tx: &Tx{
+ tranCtx: tranCtx,
+ },
+ }
+
+ err := xaTx.Rollback()
+ assert.NoError(t, err)
+}
+
+func TestXATx_Rollback_MissingXAConn(t *testing.T) {
+ ctrl := gomock.NewController(t)
+ defer ctrl.Finish()
+
+ mockMgr := mock.NewMockDataSourceManager(ctrl)
+ mockMgr.SetBranchType(branch.BranchTypeXA)
+ registerResourceManagerForTest(t, mockMgr)
+ mockMgr.EXPECT().BranchReport(gomock.Any(), gomock.Any()).Times(0)
+
+ tranCtx := types.NewTxCtx()
+ tranCtx.XID = "test-xid"
+ tranCtx.BranchID = 123
+ tranCtx.TransactionMode = types.XAMode
+
+ xaTx := &XATx{
+ tx: &Tx{
+ tranCtx: tranCtx,
+ },
+ }
+
+ err := xaTx.Rollback()
+ assert.Error(t, err)
+ assert.Contains(t, err.Error(), "xa transaction requires xaConn")
+}
+
+func TestXATx_Rollback_BranchNotRegistered(t *testing.T) {
+ tranCtx := types.NewTxCtx()
+ tranCtx.XID = "test-xid"
+ tranCtx.BranchID = 0
+ tranCtx.TransactionMode = types.XAMode
+
+ mockConn := &mockXAConnection{}
+
+ xaTx := &XATx{
+ tx: &Tx{
+ tranCtx: tranCtx,
+ xaConn: mockConn,
+ },
+ }
+
+ err := xaTx.Rollback()
+ assert.NoError(t, err)
+ assert.Equal(t, 1, mockConn.rollbackCalls)
+}
+
+func TestXATx_Rollback_WithXAConn(t *testing.T) {
+ tranCtx := types.NewTxCtx()
+ tranCtx.XID = "test-xid"
+ tranCtx.BranchID = 0
+ tranCtx.TransactionMode = types.XAMode
+
+ mockConn := &mockXAConnection{}
+
+ xaTx := &XATx{
+ tx: &Tx{
+ tranCtx: tranCtx,
+ xaConn: mockConn,
+ },
+ }
+
+ err := xaTx.Rollback()
+ assert.NoError(t, err)
+ assert.Equal(t, 1, mockConn.rollbackCalls)
+}
+
+func TestXATx_Rollback_XAConnError(t *testing.T) {
+ tranCtx := types.NewTxCtx()
+ tranCtx.XID = "test-xid"
+ tranCtx.BranchID = 0
+ tranCtx.TransactionMode = types.XAMode
+
+ mockConn := &mockXAConnection{
+ rollbackErr: errors.New("XA ROLLBACK failed"),
+ }
+
+ xaTx := &XATx{
+ tx: &Tx{
+ tranCtx: tranCtx,
+ xaConn: mockConn,
+ },
+ }
+
+ err := xaTx.Rollback()
+ assert.Error(t, err)
+ assert.Contains(t, err.Error(), "XA ROLLBACK failed")
+ assert.Equal(t, 1, mockConn.rollbackCalls)
+}
+
+func TestXATx_Rollback_BranchRegisteredReportsFailure(t *testing.T) {
+ ctrl := gomock.NewController(t)
+ defer ctrl.Finish()
+
+ mockMgr := mock.NewMockDataSourceManager(ctrl)
+ mockMgr.SetBranchType(branch.BranchTypeXA)
+ registerResourceManagerForTest(t, mockMgr)
+ mockMgr.EXPECT().BranchReport(gomock.Any(), gomock.Any()).DoAndReturn(
+ func(ctx context.Context, param rm.BranchReportParam) error {
+ assert.Equal(t, branch.BranchTypeXA, param.BranchType)
+ assert.Equal(t, int64(123), param.BranchId)
+ assert.EqualValues(t,
branch.BranchStatusPhaseoneFailed, param.Status)
+ assert.Equal(t, "test-xid", param.Xid)
+ return nil
+ },
+ ).Times(1)
+
+ tranCtx := types.NewTxCtx()
+ tranCtx.XID = "test-xid"
+ tranCtx.BranchID = 123
+ tranCtx.TransactionMode = types.XAMode
+
+ mockConn := &mockXAConnection{}
+
+ xaTx := &XATx{
+ tx: &Tx{
+ tranCtx: tranCtx,
+ xaConn: mockConn,
+ },
+ }
+
+ err := xaTx.Rollback()
+ assert.NoError(t, err)
+ assert.Equal(t, 1, mockConn.rollbackCalls)
+}
+
+func TestXATx_Commit_CallsCommitOnXA(t *testing.T) {
+ tranCtx := types.NewTxCtx()
+ tranCtx.XID = "test-xid"
+ tranCtx.BranchID = 0
+ tranCtx.TransactionMode = types.XAMode
+
+ mockConn := &mockXAConnection{}
+
+ xaTx := &XATx{
+ tx: &Tx{
+ tranCtx: tranCtx,
+ xaConn: mockConn,
+ },
+ }
+
+ err := xaTx.Commit()
+ assert.NoError(t, err)
+ assert.Equal(t, 1, mockConn.commitCalls)
+}
+
+func TestTx_report_NoBranchID(t *testing.T) {
+ tranCtx := types.NewTxCtx()
+ tranCtx.BranchID = 0
+
+ tx := &Tx{
+ tranCtx: tranCtx,
+ }
+
+ err := tx.report(true)
+ assert.NoError(t, err)
+}
+
+func TestXAConnection_Interface(t *testing.T) {
+ var _ XAConnection = (*mockXAConnection)(nil)
+}
+
+func TestWithXAConn(t *testing.T) {
+ mockConn := &mockXAConnection{}
+ tx := &Tx{}
+
+ opt := withXAConn(mockConn)
+ opt(tx)
+
+ assert.Equal(t, mockConn, tx.xaConn)
+}
+
+func TestXATx_Commit_BeforeCommitError(t *testing.T) {
+ tranCtx := types.NewTxCtx()
+ tranCtx.XID = "test-xid"
+ tranCtx.TransactionMode = types.XAMode
+
+ hookCalled := false
+ RegisterTxHook(&mockTxHook{
+ beforeCommit: func(tx *Tx) error {
+ hookCalled = true
+ return errors.New("hook error")
+ },
+ })
+ defer CleanTxHooks()
+
+ mockConn := &mockXAConnection{}
+
+ xaTx := &XATx{
+ tx: &Tx{
+ tranCtx: tranCtx,
+ xaConn: mockConn,
+ },
+ }
+
+ err := xaTx.Commit()
+ assert.Error(t, err)
+ assert.Contains(t, err.Error(), "hook error")
+ assert.True(t, hookCalled)
+ assert.Equal(t, 0, mockConn.commitCalls)
+}
+
+func TestGetStatus(t *testing.T) {
+ assert.EqualValues(t, branch.BranchStatusPhaseoneDone, getStatus(true))
+ assert.EqualValues(t, branch.BranchStatusPhaseoneFailed,
getStatus(false))
+}
diff --git a/pkg/datasource/sql/xa_resource_manager.go
b/pkg/datasource/sql/xa_resource_manager.go
index 0f793569..ab2446d0 100644
--- a/pkg/datasource/sql/xa_resource_manager.go
+++ b/pkg/datasource/sql/xa_resource_manager.go
@@ -197,7 +197,7 @@ func (xaManager *XAResourceManager) BranchRollback(ctx
context.Context, branchRe
}
func (xaManager *XAResourceManager) LockQuery(ctx context.Context, param
rm.LockQueryParam) (bool, error) {
- return false, nil
+ return xaManager.rmRemoting.LockQuery(param)
}
func (xaManager *XAResourceManager) BranchRegister(ctx context.Context, req
rm.BranchRegisterParam) (int64, error) {
diff --git a/pkg/datasource/sql/xa_resource_manager_test.go
b/pkg/datasource/sql/xa_resource_manager_test.go
new file mode 100644
index 00000000..e4edd0d5
--- /dev/null
+++ b/pkg/datasource/sql/xa_resource_manager_test.go
@@ -0,0 +1,95 @@
+/*
+ * 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 sql
+
+import (
+ "context"
+ "errors"
+ "reflect"
+ "testing"
+
+ "github.com/agiledragon/gomonkey/v2"
+ "github.com/stretchr/testify/assert"
+
+ "seata.apache.org/seata-go/v2/pkg/protocol/branch"
+ "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/rm"
+)
+
+func TestXAResourceManager_LockQuery(t *testing.T) {
+ tests := []struct {
+ name string
+ resp interface{}
+ respErr error
+ want bool
+ wantErr string
+ }{
+ {
+ name: "lockable",
+ resp: message.GlobalLockQueryResponse{Lockable: true},
+ want: true,
+ },
+ {
+ name: "unlockable",
+ resp: message.GlobalLockQueryResponse{Lockable: false},
+ want: false,
+ },
+ {
+ name: "remoting error",
+ respErr: errors.New("network timeout"),
+ want: false,
+ wantErr: "network timeout",
+ },
+ }
+
+ param := rm.LockQueryParam{
+ BranchType: branch.BranchTypeXA,
+ ResourceId: "jdbc:mysql://test/resource",
+ Xid: "test-xid",
+ LockKeys: "user:1",
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ patches :=
gomonkey.ApplyMethod(reflect.TypeOf(getty.GetGettyRemotingClient()),
"SendSyncRequest",
+ func(_ *getty.GettyRemotingClient, msg
interface{}) (interface{}, error) {
+ req, ok :=
msg.(message.GlobalLockQueryRequest)
+ if assert.True(t, ok) {
+ assert.Equal(t,
param.BranchType, req.BranchType)
+ assert.Equal(t,
param.ResourceId, req.ResourceId)
+ assert.Equal(t, param.Xid,
req.Xid)
+ assert.Equal(t, param.LockKeys,
req.LockKey)
+ }
+ return tt.resp, tt.respErr
+ })
+ defer patches.Reset()
+
+ xaManager := &XAResourceManager{rmRemoting:
rm.GetRMRemotingInstance()}
+
+ got, err := xaManager.LockQuery(context.Background(),
param)
+
+ assert.Equal(t, tt.want, got)
+ if tt.wantErr == "" {
+ assert.NoError(t, err)
+ } else {
+ assert.EqualError(t, err, tt.wantErr)
+ }
+ })
+ }
+}
diff --git a/pkg/rm/rm_cache.go b/pkg/rm/rm_cache.go
index 855facb3..00df6e94 100644
--- a/pkg/rm/rm_cache.go
+++ b/pkg/rm/rm_cache.go
@@ -48,6 +48,10 @@ func (d *ResourceManagerCache)
RegisterResourceManager(resourceManager ResourceM
d.resourceManagerMap.Store(resourceManager.GetBranchType(),
resourceManager)
}
+func (d *ResourceManagerCache) UnregisterResourceManager(branchType
branch.BranchType) {
+ d.resourceManagerMap.Delete(branchType)
+}
+
func (d *ResourceManagerCache) GetResourceManager(branchType
branch.BranchType) ResourceManager {
rm, ok := d.resourceManagerMap.Load(branchType)
if !ok {
diff --git a/pkg/rm/rm_cache_test.go b/pkg/rm/rm_cache_test.go
index e9477b80..80a2ba2b 100644
--- a/pkg/rm/rm_cache_test.go
+++ b/pkg/rm/rm_cache_test.go
@@ -43,3 +43,19 @@ func TestGetRmCacheInstance(t *testing.T) {
assert.Equalf(t, mockResourceManager, actual,
"GetRmCacheInstance()")
})
}
+
+func TestResourceManagerCache_UnregisterResourceManager(t *testing.T) {
+ ctl := gomock.NewController(t)
+
+ mockResourceManager := NewMockResourceManager(ctl)
+
mockResourceManager.EXPECT().GetBranchType().Return(branch.BranchTypeSAGA).AnyTimes()
+
+ cache := GetRmCacheInstance()
+ cache.RegisterResourceManager(mockResourceManager)
+
+ cache.UnregisterResourceManager(branch.BranchTypeSAGA)
+
+ assert.Panics(t, func() {
+ cache.GetResourceManager(branch.BranchTypeSAGA)
+ })
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]