Skip to content

Commit f25b22c

Browse files
MinatoWuCode-Fight吴孝宇Copilot
authored
Fix active xa rollback failure and added error message judgment (apache#875)
* Add exception judgment apache#708 * Add exception judgment apache#708 * update const * update test * Update pkg/datasource/sql/conn_xa.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update pkg/datasource/sql/conn_xa.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: FengZhang <zfcode@qq.com> Co-authored-by: 吴孝宇 <wuxiaoyu2@xiaohongshu.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 05db834 commit f25b22c

File tree

3 files changed

+151
-3
lines changed

3 files changed

+151
-3
lines changed

pkg/datasource/sql/conn_xa.go

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,10 @@ import (
2323
"database/sql/driver"
2424
"errors"
2525
"fmt"
26+
"strings"
2627
"time"
2728

29+
"github.com/go-sql-driver/mysql"
2830
"seata.apache.org/seata-go/pkg/datasource/sql/types"
2931
"seata.apache.org/seata-go/pkg/datasource/sql/xa"
3032
"seata.apache.org/seata-go/pkg/tm"
@@ -302,9 +304,19 @@ func (c *XAConn) Rollback(ctx context.Context) error {
302304
}
303305

304306
if !c.rollBacked {
305-
if c.xaResource.End(ctx, c.xaBranchXid.String(), xa.TMFail) != nil {
306-
return c.rollbackErrorHandle()
307+
// First end the XA branch with TMFail
308+
if err := c.xaResource.End(ctx, c.xaBranchXid.String(), xa.TMFail); err != nil {
309+
// Handle XAER_RMFAIL exception - check if it's already ended
310+
//expected error: Error 1399 (XAE07): XAER_RMFAIL: The command cannot be executed when global transaction is in the IDLE state
311+
if isXAER_RMFAILAlreadyEnded(err) {
312+
// If already ended, continue with rollback
313+
log.Infof("XA branch already ended, continuing with rollback for xid: %s", c.txCtx.XID)
314+
} else {
315+
return c.rollbackErrorHandle()
316+
}
307317
}
318+
319+
// Then perform XA rollback
308320
if c.XaRollback(ctx, c.xaBranchXid) != nil {
309321
c.cleanXABranchContext()
310322
return c.rollbackErrorHandle()
@@ -313,6 +325,7 @@ func (c *XAConn) Rollback(ctx context.Context) error {
313325
c.cleanXABranchContext()
314326
return fmt.Errorf("failed to report XA branch commit-failure on xid:%s err:%w", c.txCtx.XID, err)
315327
}
328+
c.rollBacked = true
316329
}
317330
c.cleanXABranchContext()
318331
return nil
@@ -404,3 +417,19 @@ func (c *XAConn) XaRollback(ctx context.Context, xaXid XAXid) error {
404417
c.releaseIfNecessary()
405418
return err
406419
}
420+
421+
// isXAER_RMFAILAlreadyEnded checks if the XAER_RMFAIL error indicates the XA branch is already ended
422+
// expected error: Error 1399 (XAE07): XAER_RMFAIL: The command cannot be executed when global transaction is in the IDLE state
423+
func isXAER_RMFAILAlreadyEnded(err error) bool {
424+
if err == nil {
425+
return false
426+
}
427+
if mysqlErr, ok := err.(*mysql.MySQLError); ok {
428+
if mysqlErr.Number == types.ErrCodeXAER_RMFAIL_IDLE {
429+
return strings.Contains(mysqlErr.Message, "IDLE state") || strings.Contains(mysqlErr.Message, "already ended")
430+
}
431+
}
432+
// TODO: handle other DB errors
433+
434+
return false
435+
}

pkg/datasource/sql/conn_xa_test.go

Lines changed: 109 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,13 @@ import (
2222
"database/sql"
2323
"database/sql/driver"
2424
"io"
25+
"strings"
2526
"sync/atomic"
2627
"testing"
2728
"time"
2829

2930
"github.com/bluele/gcache"
31+
"github.com/go-sql-driver/mysql"
3032
"github.com/golang/mock/gomock"
3133
"github.com/google/uuid"
3234
"github.com/stretchr/testify/assert"
@@ -118,10 +120,23 @@ func (mi *mockTxHook) BeforeRollback(tx *Tx) {
118120
}
119121
}
120122

123+
// simulateExecContextError allows tests to inject driver errors for certain SQL strings.
124+
// When set, baseMockConn will call this hook for each ExecContext.
125+
var simulateExecContextError func(query string) error
126+
121127
func baseMockConn(mockConn *mock.MockTestDriverConn) {
122128
branchStatusCache = gcache.New(1024).LRU().Expiration(time.Minute * 10).Build()
123129

124-
mockConn.EXPECT().ExecContext(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes().Return(&driver.ResultNoRows, nil)
130+
mockConn.EXPECT().ExecContext(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes().DoAndReturn(
131+
func(ctx context.Context, query string, args []driver.NamedValue) (driver.Result, error) {
132+
if simulateExecContextError != nil {
133+
if err := simulateExecContextError(query); err != nil {
134+
return &driver.ResultNoRows, err
135+
}
136+
}
137+
return &driver.ResultNoRows, nil
138+
},
139+
)
125140
mockConn.EXPECT().Exec(gomock.Any(), gomock.Any()).AnyTimes().Return(&driver.ResultNoRows, nil)
126141
mockConn.EXPECT().ResetSession(gomock.Any()).AnyTimes().Return(nil)
127142
mockConn.EXPECT().Close().AnyTimes().Return(nil)
@@ -329,3 +344,96 @@ func TestXAConn_BeginTx(t *testing.T) {
329344
})
330345

331346
}
347+
348+
func TestXAConn_Rollback_XAER_RMFAIL(t *testing.T) {
349+
tests := []struct {
350+
name string
351+
err error
352+
want bool
353+
}{
354+
{
355+
name: "no error case",
356+
err: nil,
357+
want: false,
358+
},
359+
{
360+
name: "matching XAER_RMFAIL error with IDLE state",
361+
err: &mysql.MySQLError{
362+
Number: 1399,
363+
Message: "Error 1399 (XAE07): XAER_RMFAIL: The command cannot be executed when global transaction is in the IDLE state",
364+
},
365+
want: true,
366+
},
367+
{
368+
name: "matching XAER_RMFAIL error with already ended",
369+
err: &mysql.MySQLError{
370+
Number: 1399,
371+
Message: "Error 1399 (XAE07): XAER_RMFAIL: The command cannot be executed when global transaction has already ended",
372+
},
373+
want: true,
374+
},
375+
{
376+
name: "matching error code but mismatched message",
377+
err: &mysql.MySQLError{
378+
Number: 1399,
379+
Message: "Error 1399 (XAE07): XAER_RMFAIL: Other error message",
380+
},
381+
want: false,
382+
},
383+
{
384+
name: "mismatched error code but matching message",
385+
err: &mysql.MySQLError{
386+
Number: 1234,
387+
Message: "The command cannot be executed when global transaction is in the IDLE state",
388+
},
389+
want: false,
390+
},
391+
}
392+
393+
for _, tt := range tests {
394+
t.Run(tt.name, func(t *testing.T) {
395+
if got := isXAER_RMFAILAlreadyEnded(tt.err); got != tt.want {
396+
t.Errorf("isXAER_RMFAILAlreadyEnded() = %v, want %v", got, tt.want)
397+
}
398+
})
399+
}
400+
}
401+
402+
// Covers the XA rollback flow when End() returns XAER_RMFAIL (IDLE/already ended)
403+
func TestXAConn_Rollback_HandleXAERRMFAILAlreadyEnded(t *testing.T) {
404+
ctrl, db, _, ti := initXAConnTestResource(t)
405+
defer func() {
406+
simulateExecContextError = nil
407+
db.Close()
408+
ctrl.Finish()
409+
CleanTxHooks()
410+
}()
411+
412+
ctx := tm.InitSeataContext(context.Background())
413+
tm.SetXID(ctx, uuid.New().String())
414+
415+
// Ensure Tx.Rollback has a non-nil underlying target to avoid nil-deref when test triggers rollback
416+
ti.beforeRollback = func(tx *Tx) {
417+
mtx := mock.NewMockTestDriverTx(ctrl)
418+
mtx.EXPECT().Rollback().AnyTimes().Return(nil)
419+
tx.target = mtx
420+
}
421+
422+
// Inject: XA END returns XAER_RMFAIL(IDLE), normal SQL returns an error to trigger rollback
423+
simulateExecContextError = func(query string) error {
424+
upper := strings.ToUpper(query)
425+
if strings.HasPrefix(upper, "XA END") {
426+
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"}
427+
}
428+
if !strings.HasPrefix(upper, "XA ") {
429+
return io.EOF
430+
}
431+
return nil
432+
}
433+
434+
// Execute to enter XA flow; the user SQL fails, but rollback should proceed without panicking
435+
_, err := db.ExecContext(ctx, "SELECT 1")
436+
if err == nil {
437+
t.Fatalf("expected error to trigger rollback path")
438+
}
439+
}

pkg/datasource/sql/types/const.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,3 +354,14 @@ func MySQLStrToJavaType(mysqlType string) JDBCType {
354354
return JDBCTypeOther
355355
}
356356
}
357+
358+
// XA transaction related error code constants (based on MySQL/MariaDB specifications)
359+
const (
360+
// ErrCodeXAER_RMFAIL_IDLE 1399: XAER_RMFAIL - The command cannot be executed when global transaction is in the IDLE state
361+
// Typically occurs when trying to perform operations on an XA transaction that's in idle state
362+
ErrCodeXAER_RMFAIL_IDLE = 1399
363+
364+
// ErrCodeXAER_INVAL 1400: XAER_INVAL - Invalid XA transaction ID format
365+
// Triggered by malformed XID (e.g., invalid gtrid/branchid format or excessive length)
366+
ErrCodeXAER_INVAL = 1400
367+
)

0 commit comments

Comments
 (0)