Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 31 additions & 2 deletions pkg/datasource/sql/conn_xa.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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()
Expand All @@ -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
Expand Down Expand Up @@ -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
}
55 changes: 55 additions & 0 deletions pkg/datasource/sql/conn_xa_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
}
})
}
}
11 changes: 11 additions & 0 deletions pkg/datasource/sql/types/const.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
Loading