This is an automated email from the ASF dual-hosted git repository.

alexstocks 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 f25b22c8 Fix active xa rollback failure and added error message 
judgment (#875)
f25b22c8 is described below

commit f25b22c8f03aa63317ae212a97345f071a24fa39
Author: Wiggins <125641755+minat...@users.noreply.github.com>
AuthorDate: Tue Sep 9 10:08:46 2025 +0800

    Fix active xa rollback failure and added error message judgment (#875)
    
    * Add exception judgment #708
    
    * Add exception judgment #708
    
    * update const
    
    * update test
    
    * Update pkg/datasource/sql/conn_xa.go
    
    Co-authored-by: Copilot <175728472+copi...@users.noreply.github.com>
    
    * Update pkg/datasource/sql/conn_xa.go
    
    Co-authored-by: Copilot <175728472+copi...@users.noreply.github.com>
    
    ---------
    
    Co-authored-by: FengZhang <zfc...@qq.com>
    Co-authored-by: 吴孝宇 <wuxiao...@xiaohongshu.com>
    Co-authored-by: Copilot <175728472+copi...@users.noreply.github.com>
---
 pkg/datasource/sql/conn_xa.go      |  33 ++++++++++-
 pkg/datasource/sql/conn_xa_test.go | 110 ++++++++++++++++++++++++++++++++++++-
 pkg/datasource/sql/types/const.go  |  11 ++++
 3 files changed, 151 insertions(+), 3 deletions(-)

diff --git a/pkg/datasource/sql/conn_xa.go b/pkg/datasource/sql/conn_xa.go
index 65f42ce9..a7751cd4 100644
--- a/pkg/datasource/sql/conn_xa.go
+++ b/pkg/datasource/sql/conn_xa.go
@@ -23,8 +23,10 @@ import (
        "database/sql/driver"
        "errors"
        "fmt"
+       "strings"
        "time"
 
+       "github.com/go-sql-driver/mysql"
        "seata.apache.org/seata-go/pkg/datasource/sql/types"
        "seata.apache.org/seata-go/pkg/datasource/sql/xa"
        "seata.apache.org/seata-go/pkg/tm"
@@ -302,9 +304,19 @@ func (c *XAConn) Rollback(ctx context.Context) error {
        }
 
        if !c.rollBacked {
-               if c.xaResource.End(ctx, c.xaBranchXid.String(), xa.TMFail) != 
nil {
-                       return c.rollbackErrorHandle()
+               // First end the XA branch with TMFail
+               if err := c.xaResource.End(ctx, c.xaBranchXid.String(), 
xa.TMFail); err != nil {
+                       // Handle XAER_RMFAIL exception - check if it's already 
ended
+                       //expected error: Error 1399 (XAE07): XAER_RMFAIL: The 
command cannot be executed when global transaction is in the  IDLE state
+                       if isXAER_RMFAILAlreadyEnded(err) {
+                               // If already ended, continue with rollback
+                               log.Infof("XA branch already ended, continuing 
with rollback for xid: %s", c.txCtx.XID)
+                       } else {
+                               return c.rollbackErrorHandle()
+                       }
                }
+
+               // Then perform XA rollback
                if c.XaRollback(ctx, c.xaBranchXid) != nil {
                        c.cleanXABranchContext()
                        return c.rollbackErrorHandle()
@@ -313,6 +325,7 @@ func (c *XAConn) Rollback(ctx context.Context) error {
                        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()
        return nil
@@ -404,3 +417,19 @@ func (c *XAConn) XaRollback(ctx context.Context, xaXid 
XAXid) error {
        c.releaseIfNecessary()
        return err
 }
+
+// isXAER_RMFAILAlreadyEnded checks if the XAER_RMFAIL error indicates the XA 
branch is already ended
+// expected error: Error 1399 (XAE07): XAER_RMFAIL: The command cannot be 
executed when global transaction is in the IDLE state
+func isXAER_RMFAILAlreadyEnded(err error) bool {
+       if err == nil {
+               return false
+       }
+       if mysqlErr, ok := err.(*mysql.MySQLError); ok {
+               if mysqlErr.Number == types.ErrCodeXAER_RMFAIL_IDLE {
+                       return strings.Contains(mysqlErr.Message, "IDLE state") 
|| strings.Contains(mysqlErr.Message, "already ended")
+               }
+       }
+       // TODO: handle other DB errors
+
+       return false
+}
diff --git a/pkg/datasource/sql/conn_xa_test.go 
b/pkg/datasource/sql/conn_xa_test.go
index 3546a06f..624bf1c2 100644
--- a/pkg/datasource/sql/conn_xa_test.go
+++ b/pkg/datasource/sql/conn_xa_test.go
@@ -22,11 +22,13 @@ import (
        "database/sql"
        "database/sql/driver"
        "io"
+       "strings"
        "sync/atomic"
        "testing"
        "time"
 
        "github.com/bluele/gcache"
+       "github.com/go-sql-driver/mysql"
        "github.com/golang/mock/gomock"
        "github.com/google/uuid"
        "github.com/stretchr/testify/assert"
@@ -118,10 +120,23 @@ func (mi *mockTxHook) BeforeRollback(tx *Tx) {
        }
 }
 
+// simulateExecContextError allows tests to inject driver errors for certain 
SQL strings.
+// When set, baseMockConn will call this hook for each ExecContext.
+var simulateExecContextError func(query string) error
+
 func baseMockConn(mockConn *mock.MockTestDriverConn) {
        branchStatusCache = gcache.New(1024).LRU().Expiration(time.Minute * 
10).Build()
 
-       mockConn.EXPECT().ExecContext(gomock.Any(), gomock.Any(), 
gomock.Any()).AnyTimes().Return(&driver.ResultNoRows, nil)
+       mockConn.EXPECT().ExecContext(gomock.Any(), gomock.Any(), 
gomock.Any()).AnyTimes().DoAndReturn(
+               func(ctx context.Context, query string, args 
[]driver.NamedValue) (driver.Result, error) {
+                       if simulateExecContextError != nil {
+                               if err := simulateExecContextError(query); err 
!= nil {
+                                       return &driver.ResultNoRows, err
+                               }
+                       }
+                       return &driver.ResultNoRows, nil
+               },
+       )
        mockConn.EXPECT().Exec(gomock.Any(), 
gomock.Any()).AnyTimes().Return(&driver.ResultNoRows, nil)
        mockConn.EXPECT().ResetSession(gomock.Any()).AnyTimes().Return(nil)
        mockConn.EXPECT().Close().AnyTimes().Return(nil)
@@ -329,3 +344,96 @@ func TestXAConn_BeginTx(t *testing.T) {
        })
 
 }
+
+func TestXAConn_Rollback_XAER_RMFAIL(t *testing.T) {
+       tests := []struct {
+               name string
+               err  error
+               want bool
+       }{
+               {
+                       name: "no error case",
+                       err:  nil,
+                       want: false,
+               },
+               {
+                       name: "matching XAER_RMFAIL error with IDLE state",
+                       err: &mysql.MySQLError{
+                               Number:  1399,
+                               Message: "Error 1399 (XAE07): XAER_RMFAIL: The 
command cannot be executed when global transaction is in the IDLE state",
+                       },
+                       want: true,
+               },
+               {
+                       name: "matching XAER_RMFAIL error with already ended",
+                       err: &mysql.MySQLError{
+                               Number:  1399,
+                               Message: "Error 1399 (XAE07): XAER_RMFAIL: The 
command cannot be executed when global transaction has already ended",
+                       },
+                       want: true,
+               },
+               {
+                       name: "matching error code but mismatched message",
+                       err: &mysql.MySQLError{
+                               Number:  1399,
+                               Message: "Error 1399 (XAE07): XAER_RMFAIL: 
Other error message",
+                       },
+                       want: false,
+               },
+               {
+                       name: "mismatched error code but matching message",
+                       err: &mysql.MySQLError{
+                               Number:  1234,
+                               Message: "The command cannot be executed when 
global transaction is in the IDLE state",
+                       },
+                       want: false,
+               },
+       }
+
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       if got := isXAER_RMFAILAlreadyEnded(tt.err); got != 
tt.want {
+                               t.Errorf("isXAER_RMFAILAlreadyEnded() = %v, 
want %v", got, tt.want)
+                       }
+               })
+       }
+}
+
+// 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)
+       defer func() {
+               simulateExecContextError = nil
+               db.Close()
+               ctrl.Finish()
+               CleanTxHooks()
+       }()
+
+       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"}
+               }
+               if !strings.HasPrefix(upper, "XA ") {
+                       return io.EOF
+               }
+               return nil
+       }
+
+       // Execute to enter XA flow; the user SQL fails, but rollback should 
proceed without panicking
+       _, err := db.ExecContext(ctx, "SELECT 1")
+       if err == nil {
+               t.Fatalf("expected error to trigger rollback path")
+       }
+}
diff --git a/pkg/datasource/sql/types/const.go 
b/pkg/datasource/sql/types/const.go
index 4d81caff..dc963998 100644
--- a/pkg/datasource/sql/types/const.go
+++ b/pkg/datasource/sql/types/const.go
@@ -354,3 +354,14 @@ func MySQLStrToJavaType(mysqlType string) JDBCType {
                return JDBCTypeOther
        }
 }
+
+// XA transaction related error code constants (based on MySQL/MariaDB 
specifications)
+const (
+       // ErrCodeXAER_RMFAIL_IDLE 1399: XAER_RMFAIL - The command cannot be 
executed when global transaction is in the IDLE state
+       // Typically occurs when trying to perform operations on an XA 
transaction that's in idle state
+       ErrCodeXAER_RMFAIL_IDLE = 1399
+
+       // ErrCodeXAER_INVAL 1400: XAER_INVAL - Invalid XA transaction ID format
+       // Triggered by malformed XID (e.g., invalid gtrid/branchid format or 
excessive length)
+       ErrCodeXAER_INVAL = 1400
+)


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscr...@seata.apache.org
For additional commands, e-mail: notifications-h...@seata.apache.org

Reply via email to