diff --git a/pkg/datasource/sql/conn_xa.go b/pkg/datasource/sql/conn_xa.go index 65f42ce9..960419d5 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 other DB error + + return false +} diff --git a/pkg/datasource/sql/conn_xa_test.go b/pkg/datasource/sql/conn_xa_test.go index 3546a06f..50db2603 100644 --- a/pkg/datasource/sql/conn_xa_test.go +++ b/pkg/datasource/sql/conn_xa_test.go @@ -27,6 +27,7 @@ import ( "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" @@ -329,3 +330,57 @@ 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) + } + }) + } +} 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 +)