Repository: apache/incubator-seata-go

Issue:
[seata-java] 事务提交返回TimeoutRollbacking未抛异常
- [x] I have searched the [issues](https://github.com/seata/seata/issues) of this repository and believe that this is not a duplicate.

### Ⅰ. Issue Description
TM向TC发起事务提交时，TC返回了状态`TimeoutRollbacking`, 但没有抛出异常，导致TM认为事务提交成功，实际TC推动各个RM进行了回滚。

refer to: https://github.com/apache/incubator-seata-go/issues/698

### Ⅱ. Describe what happened

None


### Ⅲ. Describe what you expected to happen
事务提交返回TimeoutRollbacking未抛异常

### Ⅳ. How to reproduce it (as minimally and precisely as possible)

在事务中执行业务逻辑代码超出分布式事务的超时时长，就会发生

### Ⅴ. Anything else we need to know?
参考代码
- `io.seata.tm.api.TransactionalTemplate#execute`
- `io.seata.tm.api.DefaultGlobalTransaction#commit`

### Ⅵ. Environment:

- JDK version : 1.8
- Seata version:  1.4.2
- OS :  CentOS
- Others:

Hints from issue comments:
@Road2Melon: pls assign to me.



Solution patch:
diff --git a/pkg/tm/context.go b/pkg/tm/context.go
index aede33d..635f129 100644
--- a/pkg/tm/context.go
+++ b/pkg/tm/context.go
@@ -19,9 +19,9 @@ package tm
 
 import (
 	"context"
-
 	"seata.apache.org/seata-go/pkg/protocol/message"
 	"seata.apache.org/seata-go/pkg/rm/tcc/fence/enum"
+	"time"
 )
 
 type ContextParam string
@@ -56,6 +56,13 @@ type ContextVariable struct {
 	BusinessActionContext *BusinessActionContext
 	// GlobalTransaction Represent seata ctx is a global transaction
 	GlobalTransaction
+	// use to calculate the timeout
+	timeInfo TimeInfo
+}
+
+type TimeInfo struct {
+	createTime time.Duration
+	timeout    time.Duration
 }
 
 func InitSeataContext(ctx context.Context) context.Context {
@@ -211,3 +218,18 @@ func IsFenceTxBegin(ctx context.Context) bool {
 
 	return false
 }
+
+func GetTimeInfo(ctx context.Context) (ti *TimeInfo) {
+	variable := ctx.Value(seataContextVariable)
+	if variable != nil {
+		ti = &variable.(*ContextVariable).timeInfo
+	}
+	return
+}
+
+func SetTimeInfo(ctx context.Context, ti TimeInfo) {
+	variable := ctx.Value(seataContextVariable)
+	if variable != nil {
+		variable.(*ContextVariable).timeInfo = ti
+	}
+}
diff --git a/pkg/tm/global_transaction.go b/pkg/tm/global_transaction.go
index 3984bdd..8846e79 100644
--- a/pkg/tm/global_transaction.go
+++ b/pkg/tm/global_transaction.go
@@ -71,6 +71,14 @@ func (g *GlobalTransactionManager) Begin(ctx context.Context, timeout time.Durat
 
 // Commit the global transaction.
 func (g *GlobalTransactionManager) Commit(ctx context.Context, gtr *GlobalTransaction) error {
+	if isTimeout(ctx) {
+		log.Infof("Rollback: tm detected timeout in global gtr %s", gtr.Xid)
+		if err := GetGlobalTransactionManager().Rollback(ctx, gtr); err != nil {
+			log.Errorf("Rollback transaction failed, error: %v in global gtr % s", err, gtr.Xid)
+			return err
+		}
+		return nil
+	}
 	if gtr.TxRole != Launcher {
 		log.Infof("Ignore Commit(): just involved in global gtr %s", gtr.Xid)
 		return nil
@@ -151,3 +159,11 @@ func (g *GlobalTransactionManager) Rollback(ctx context.Context, gtr *GlobalTran
 
 	return nil
 }
+
+func isTimeout(ctx context.Context) bool {
+	ti := GetTimeInfo(ctx)
+	if ti == nil || ti.createTime == 0 || ti.timeout == 0 {
+		return false
+	}
+	return time.Since(time.Unix(int64(ti.createTime), 0)) > ti.timeout
+}
diff --git a/pkg/tm/transaction_executor.go b/pkg/tm/transaction_executor.go
index 6f37621..a9f42b5 100644
--- a/pkg/tm/transaction_executor.go
+++ b/pkg/tm/transaction_executor.go
@@ -97,6 +97,10 @@ func WithGlobalTx(ctx context.Context, gc *GtxConfig, business CallbackWithCtx)
 // construct a new context object and set the xid.
 // the advantage of this is that the suspend and resume operations of xid need not to be considered.
 func begin(ctx context.Context, gc *GtxConfig) error {
+	// record time
+	ti := TimeInfo{createTime: time.Duration(time.Now().Unix()), timeout: gc.Timeout}
+	SetTimeInfo(ctx, ti)
+
 	switch pg := gc.Propagation; pg {
 	case NotSupported:
 		// If transaction is existing, suspend it



Test patch:
diff --git a/pkg/tm/transaction_executor_test.go b/pkg/tm/transaction_executor_test.go
index 8fe541c..dafcea9 100644
--- a/pkg/tm/transaction_executor_test.go
+++ b/pkg/tm/transaction_executor_test.go
@@ -19,6 +19,7 @@ package tm
 
 import (
 	"context"
+	"fmt"
 	"reflect"
 	"testing"
 	"time"
@@ -378,16 +379,21 @@ func TestWithGlobalTx(t *testing.T) {
 	}
 
 	type testCase struct {
-		GtxConfig             *GtxConfig
-		occurError            bool
-		errMessage            string
-		callbackErr           bool
-		mockBeginFunc         interface{}
-		mockBeginTarget       interface{}
-		mockSecondPhaseFunc   interface{}
-		mockSecondPhaseTarget interface{}
-		secondErr             bool
-		callback              CallbackWithCtx
+		GtxConfig              *GtxConfig
+		occurError             bool
+		timeoutErr             bool
+		errMessage             string
+		callbackErr            bool
+		mockBeginFunc          interface{}
+		mockBeginTarget        interface{}
+		mockSecondPhaseFunc    interface{}
+		mockSecondPhaseTarget  interface{}
+		mockRollbackTargetName string
+		mockRollbackFunc       interface{}
+		mockTimeoutTarget      interface{}
+		mockTimeoutFunc        interface{}
+		secondErr              bool
+		callback               CallbackWithCtx
 	}
 
 	gts := []testCase{
@@ -464,18 +470,74 @@ func TestWithGlobalTx(t *testing.T) {
 			},
 			secondErr: true,
 		},
+
+		// case tm detected a timeout and executed rollback successfully.
+		{
+			GtxConfig: &GtxConfig{
+				Name:    "MockGtxConfig",
+				Timeout: time.Second * 30,
+			},
+			timeoutErr:      false,
+			mockBeginTarget: begin,
+			mockBeginFunc: func(ctx context.Context, gc *GtxConfig) error {
+				SetXID(ctx, "123456")
+				SetTxRole(ctx, Launcher)
+				return nil
+			},
+			callback:          callbackNil,
+			mockTimeoutTarget: isTimeout,
+			mockTimeoutFunc: func(ctx context.Context) bool {
+				return true
+			},
+			mockRollbackTargetName: "Rollback",
+			mockRollbackFunc: func(_ *GlobalTransactionManager, ctx context.Context, gtr *GlobalTransaction) error {
+				return nil
+			},
+		},
+		// tm detected a timeout but rollback threw an exception.
+		{
+			GtxConfig: &GtxConfig{
+				Name:    "MockGtxConfig",
+				Timeout: time.Second * 30,
+			},
+			timeoutErr:      true,
+			errMessage:      "tm detected a timeout but rollback threw an exception",
+			mockBeginTarget: begin,
+			mockBeginFunc: func(ctx context.Context, gc *GtxConfig) error {
+				SetXID(ctx, "123456")
+				SetTxRole(ctx, Launcher)
+				return nil
+			},
+			callback:          callbackNil,
+			mockTimeoutTarget: isTimeout,
+			mockTimeoutFunc: func(ctx context.Context) bool {
+				return true
+			},
+			mockRollbackTargetName: "Rollback",
+			mockRollbackFunc: func(_ *GlobalTransactionManager, ctx context.Context, gtr *GlobalTransaction) error {
+				return fmt.Errorf("tm detected a timeout but rollback threw an exception")
+			},
+		},
 	}
 
 	for i, v := range gts {
 		t.Logf("Case %v: %+v", i, v)
 		var beginStub *gomonkey.Patches
 		var secondStub *gomonkey.Patches
+		var timeoutStub *gomonkey.Patches
+		var rollbackStub *gomonkey.Patches
 		if v.mockBeginTarget != nil {
 			beginStub = gomonkey.ApplyFunc(v.mockBeginTarget, v.mockBeginFunc)
 		}
 		if v.mockSecondPhaseTarget != nil {
 			secondStub = gomonkey.ApplyFunc(v.mockSecondPhaseTarget, v.mockSecondPhaseFunc)
 		}
+		if v.mockTimeoutTarget != nil {
+			timeoutStub = gomonkey.ApplyFunc(v.mockTimeoutTarget, v.mockTimeoutFunc)
+		}
+		if v.mockRollbackTargetName != "" {
+			rollbackStub = gomonkey.ApplyMethod(reflect.TypeOf(GetGlobalTransactionManager()), v.mockRollbackTargetName, v.mockRollbackFunc)
+		}
 
 		ctx := context.Background()
 		err := WithGlobalTx(ctx, v.GtxConfig, v.callback)
@@ -492,6 +554,10 @@ func TestWithGlobalTx(t *testing.T) {
 			assert.NotNil(t, err)
 		}
 
+		if v.timeoutErr {
+			assert.Regexp(t, v.errMessage, err.Error())
+		}
+
 		if v.mockBeginTarget != nil {
 			beginStub.Reset()
 		}
@@ -499,5 +565,14 @@ func TestWithGlobalTx(t *testing.T) {
 		if v.mockSecondPhaseTarget != nil {
 			secondStub.Reset()
 		}
+
+		if v.mockTimeoutTarget != nil {
+			timeoutStub.Reset()
+		}
+
+		if v.mockRollbackTargetName != "" {
+			rollbackStub.Reset()
+		}
+
 	}
 }