diff --git a/goimports.sh b/goimports.sh index d7f698199..e5d3ccc1b 100644 --- a/goimports.sh +++ b/goimports.sh @@ -16,4 +16,5 @@ # go get -v golang.org/x/tools/cmd/goimports -goimports -w . \ No newline at end of file +goimports -w . +go mod tidy diff --git a/pkg/common/constants.go b/pkg/common/constants.go index 5a92e0ab7..dce5836b5 100644 --- a/pkg/common/constants.go +++ b/pkg/common/constants.go @@ -18,9 +18,14 @@ package common const ( - StartTime = "action-start-time" - HostName = "host-name" - ActionContext = "actionContext" + ActionStartTime = "action-start-time" + HostName = "host-name" + ActionContext = "actionContext" + + PrepareMethod = "sys::prepare" + CommitMethod = "sys::commit" + RollbackMethod = "sys::rollback" + ActionName = "actionName" SeataXidKey = "SEATA_XID" XidKey = "TX_XID" @@ -30,4 +35,6 @@ const ( BranchTypeKey = "TX_BRANCH_TYPE" GlobalLockKey = "TX_LOCK" SeataFilterKey = "seataDubboFilter" + + TccBusinessActionContextParameter = "tccParam" ) diff --git a/pkg/rm/tcc/tcc_service.go b/pkg/rm/tcc/tcc_service.go index abe000c8e..f51cab645 100644 --- a/pkg/rm/tcc/tcc_service.go +++ b/pkg/rm/tcc/tcc_service.go @@ -20,16 +20,15 @@ package tcc import ( "context" "encoding/json" - "fmt" + "reflect" "sync" "time" - "github.com/seata/seata-go/pkg/common/types" - "github.com/pkg/errors" "github.com/seata/seata-go/pkg/common" "github.com/seata/seata-go/pkg/common/log" "github.com/seata/seata-go/pkg/common/net" + "github.com/seata/seata-go/pkg/common/types" "github.com/seata/seata-go/pkg/protocol/branch" "github.com/seata/seata-go/pkg/rm" "github.com/seata/seata-go/pkg/tm" @@ -74,52 +73,166 @@ func (t *TCCServiceProxy) Reference() string { return types.GetReference(t.TCCResource.TwoPhaseAction.GetTwoPhaseService()) } -func (t *TCCServiceProxy) Prepare(ctx context.Context, param ...interface{}) (interface{}, error) { +func (t *TCCServiceProxy) Prepare(ctx context.Context, params interface{}) (interface{}, error) { if tm.IsTransactionOpened(ctx) { - err := t.registeBranch(ctx) + err := t.registeBranch(ctx, params) if err != nil { return nil, err } } - return t.TCCResource.Prepare(ctx, param) + return t.TCCResource.Prepare(ctx, params) } -func (t *TCCServiceProxy) registeBranch(ctx context.Context) error { - // register transaction branch +// registeBranch send register branch transaction request +func (t *TCCServiceProxy) registeBranch(ctx context.Context, params interface{}) error { if !tm.IsTransactionOpened(ctx) { err := errors.New("BranchRegister error, transaction should be opened") log.Errorf(err.Error()) return err } - // todo add param - tccContext := make(map[string]interface{}, 0) - tccContext[common.StartTime] = time.Now().UnixNano() / 1e6 - tccContext[common.HostName] = net.GetLocalIp() - tccContextStr, _ := json.Marshal(map[string]interface{}{ - common.ActionContext: tccContext, + + tccContext := t.initBusinessActionContext(ctx, params) + actionContext := t.initActionContext(params) + for k, v := range actionContext { + tccContext.ActionContext[k] = v + } + + applicationData, _ := json.Marshal(map[string]interface{}{ + common.ActionContext: actionContext, }) branchId, err := rm.GetRMRemotingInstance().BranchRegister(rm.BranchRegisterParam{ BranchType: branch.BranchTypeTCC, ResourceId: t.GetActionName(), ClientId: "", Xid: tm.GetXID(ctx), - ApplicationData: string(tccContextStr), + ApplicationData: string(applicationData), LockKeys: "", }) if err != nil { - err = errors.New(fmt.Sprintf("BranchRegister error: %v", err.Error())) - log.Errorf(err.Error()) + log.Errorf("register branch transaction error %s ", err.Error()) return err } + tccContext.BranchId = branchId + tm.SetBusinessActionContext(ctx, tccContext) + return nil +} - actionContext := &tm.BusinessActionContext{ - Xid: tm.GetXID(ctx), - BranchId: branchId, - ActionName: t.GetActionName(), - //ActionContext: param, +// initActionContext init action context +func (t *TCCServiceProxy) initActionContext(params interface{}) map[string]interface{} { + actionContext := t.getActionContextParameters(params) + actionContext[common.ActionStartTime] = time.Now().UnixNano() / 1e6 + actionContext[common.PrepareMethod] = t.TCCResource.TwoPhaseAction.GetPrepareMethodName() + actionContext[common.CommitMethod] = t.TCCResource.TwoPhaseAction.GetCommitMethodName() + actionContext[common.RollbackMethod] = t.TCCResource.TwoPhaseAction.GetRollbackMethodName() + actionContext[common.ActionName] = t.TCCResource.TwoPhaseAction.GetActionName() + actionContext[common.HostName] = net.GetLocalIp() + return actionContext +} + +func (t *TCCServiceProxy) getActionContextParameters(params interface{}) map[string]interface{} { + var ( + actionContext = make(map[string]interface{}, 0) + typ reflect.Type + val reflect.Value + isStruct bool + ) + if params == nil { + return actionContext + } + if isStruct, val, typ = obtainStructValueType(params); !isStruct { + return actionContext + } + for i := 0; i < typ.NumField(); i++ { + // skip unexported anonymous filed + if typ.Field(i).PkgPath != "" { + continue + } + structField := typ.Field(i) + // skip ignored field + tagVal, hasTag := structField.Tag.Lookup(common.TccBusinessActionContextParameter) + if !hasTag || tagVal == `-` || tagVal == "" { + continue + } + actionContext[tagVal] = val.Field(i).Interface() + } + return actionContext +} + +// initBusinessActionContext init tcc context +func (t *TCCServiceProxy) initBusinessActionContext(ctx context.Context, params interface{}) *tm.BusinessActionContext { + tccContext := t.getOrCreateBusinessActionContext(params) + tccContext.Xid = tm.GetXID(ctx) + tccContext.ActionName = t.GetActionName() + // todo read from config file + tccContext.IsDelayReport = true + if tccContext.ActionContext == nil { + tccContext.ActionContext = make(map[string]interface{}, 0) + } + return tccContext +} + +// getOrCreateBusinessActionContext When the parameters of the prepare method are the following scenarios, obtain the context in the following ways: +// 1. null: create new BusinessActionContext +// 2. tm.BusinessActionContext: return it +// 3. *tm.BusinessActionContext: if nil then create new BusinessActionContext, else return it +// 4. Struct: if there is an attribute of businessactioncontext type and it is not nil, return it +// 5. else: create new BusinessActionContext +func (t *TCCServiceProxy) getOrCreateBusinessActionContext(params interface{}) *tm.BusinessActionContext { + if params == nil { + return &tm.BusinessActionContext{} + } + + switch params.(type) { + case tm.BusinessActionContext: + v := params.(tm.BusinessActionContext) + return &v + case *tm.BusinessActionContext: + v := params.(*tm.BusinessActionContext) + if v != nil { + return v + } + return &tm.BusinessActionContext{} + default: + break + } + + var ( + typ reflect.Type + val reflect.Value + isStruct bool + ) + if isStruct, val, typ = obtainStructValueType(params); !isStruct { + return &tm.BusinessActionContext{} + } + n := typ.NumField() + for i := 0; i < n; i++ { + sf := typ.Field(i) + if sf.Type == rm.TypBusinessContextInterface { + v := val.Field(i).Interface() + if v != nil { + return v.(*tm.BusinessActionContext) + } + } + if sf.Type == reflect.TypeOf(tm.BusinessActionContext{}) && val.Field(i).CanInterface() { + v := val.Field(i).Interface().(tm.BusinessActionContext) + return &v + } + } + return &tm.BusinessActionContext{} +} + +// obtainStructValueType check o is struct or pointer type +func obtainStructValueType(o interface{}) (bool, reflect.Value, reflect.Type) { + v := reflect.ValueOf(o) + t := reflect.TypeOf(o) + switch v.Kind() { + case reflect.Struct: + return true, v, t + case reflect.Ptr: + return true, v.Elem(), t.Elem() + default: + return false, v, nil } - tm.SetBusinessActionContext(ctx, actionContext) - return nil } func (t *TCCServiceProxy) GetTransactionInfo() tm.TransactionInfo { diff --git a/pkg/rm/tcc/tcc_service_test.go b/pkg/rm/tcc/tcc_service_test.go new file mode 100644 index 000000000..b526778a7 --- /dev/null +++ b/pkg/rm/tcc/tcc_service_test.go @@ -0,0 +1,225 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package tcc + +import ( + "context" + "github.com/seata/seata-go/pkg/common/log" + "os" + "reflect" + "testing" + "time" + + "github.com/agiledragon/gomonkey" + "github.com/seata/seata-go/pkg/common" + "github.com/seata/seata-go/pkg/common/net" + "github.com/seata/seata-go/pkg/rm" + "github.com/seata/seata-go/pkg/tm" + testdata2 "github.com/seata/seata-go/testdata" + "github.com/stretchr/testify/assert" +) + +var ( + testTccServiceProxy, _ = NewTCCServiceProxy(testdata2.GetTestTwoPhaseService()) + testBranchID = int64(121324345353) +) + +func InitMock() { + var ( + registerResource = func(_ *TCCServiceProxy) error { + return nil + } + prepare = func(_ *TCCServiceProxy, ctx context.Context, params interface{}) (interface{}, error) { + return nil, nil + } + branchRegister = func(_ *rm.RMRemoting, param rm.BranchRegisterParam) (int64, error) { + return testBranchID, nil + } + ) + log.Infof("run init mock") + gomonkey.ApplyMethod(reflect.TypeOf(testTccServiceProxy), "RegisterResource", registerResource) + gomonkey.ApplyMethod(reflect.TypeOf(testTccServiceProxy), "Prepare", prepare) + gomonkey.ApplyMethod(reflect.TypeOf(rm.GetRMRemotingInstance()), "BranchRegister", branchRegister) +} + +func TestMain(m *testing.M) { + InitMock() + code := m.Run() + os.Exit(code) +} + +func TestInitActionContext(t *testing.T) { + param := struct { + name string `tccParam:"name"` + Age int64 `tccParam:""` + Addr string `tccParam:"addr"` + Job string `tccParam:"-"` + Class string + Other []int8 `tccParam:"Other"` + }{ + name: "Jack", + Age: 20, + Addr: "Earth", + Job: "Dor", + Class: "1-2", + Other: []int8{1, 2, 3}, + } + + now := time.Now().UnixNano() + gomonkey.ApplyMethod(reflect.TypeOf(time.Now()), "UnixNano", func(_ time.Time) int64 { + return now + }) + result := testTccServiceProxy.initActionContext(param) + assert.Equal(t, map[string]interface{}{ + "addr": "Earth", + "Other": []int8{1, 2, 3}, + common.ActionStartTime: now / 1e6, + common.PrepareMethod: "Prepare", + common.CommitMethod: "Commit", + common.RollbackMethod: "Rollback", + common.ActionName: testdata2.ActionName, + common.HostName: net.GetLocalIp(), + }, result) +} + +func TestGetActionContextParameters(t *testing.T) { + param := struct { + name string `tccParam:"name"` + Age int64 `tccParam:""` + Addr string `tccParam:"addr"` + Job string `tccParam:"-"` + Class string + Other []int8 `tccParam:"Other"` + }{ + name: "Jack", + Age: 20, + Addr: "Earth", + Job: "Dor", + Class: "1-2", + Other: []int8{1, 2, 3}, + } + + result := testTccServiceProxy.getActionContextParameters(param) + assert.Equal(t, map[string]interface{}{ + "addr": "Earth", + "Other": []int8{1, 2, 3}, + }, result) +} + +func TestGetOrCreateBusinessActionContext(t *testing.T) { + var tests = []struct { + param interface{} + want tm.BusinessActionContext + }{ + { + param: nil, + want: tm.BusinessActionContext{}, + }, + { + param: tm.BusinessActionContext{ + ActionContext: map[string]interface{}{ + "name": "Jack", + "age": 12, + }, + }, + want: tm.BusinessActionContext{ + ActionContext: map[string]interface{}{ + "name": "Jack", + "age": 12, + }, + }, + }, { + param: &tm.BusinessActionContext{ + ActionContext: map[string]interface{}{ + "name": "Jack", + "age": 13, + }, + }, + want: tm.BusinessActionContext{ + ActionContext: map[string]interface{}{ + "name": "Jack", + "age": 13, + }, + }, + }, { + param: struct { + Context *tm.BusinessActionContext + }{ + + Context: &tm.BusinessActionContext{ + ActionContext: map[string]interface{}{ + "name": "Jack", + "age": 14, + }, + }, + }, + want: tm.BusinessActionContext{ + ActionContext: map[string]interface{}{ + "name": "Jack", + "age": 14, + }, + }, + }, + { + param: struct { + Context tm.BusinessActionContext + }{ + + Context: tm.BusinessActionContext{ + ActionContext: map[string]interface{}{ + "name": "Jack", + "age": 12, + }, + }, + }, + want: tm.BusinessActionContext{ + ActionContext: map[string]interface{}{ + "name": "Jack", + "age": 12, + }, + }, + }, + { + param: struct { + context tm.BusinessActionContext + }{ + + context: tm.BusinessActionContext{ + ActionContext: map[string]interface{}{ + "name": "Jack", + "age": 12, + }, + }, + }, + want: tm.BusinessActionContext{}, + }, + } + + for _, tt := range tests { + result := testTccServiceProxy.getOrCreateBusinessActionContext(tt.param) + assert.Equal(t, tt.want, *result) + } +} + +func TestRegisteBranch(t *testing.T) { + ctx := testdata2.GetTestContext() + err := testTccServiceProxy.registeBranch(ctx, nil) + assert.Nil(t, err) + bizContext := tm.GetBusinessActionContext(ctx) + assert.Equal(t, testBranchID, bizContext.BranchId) +} diff --git a/pkg/rm/two_phase.go b/pkg/rm/two_phase.go index f5ef1e4cf..ff1330477 100644 --- a/pkg/rm/two_phase.go +++ b/pkg/rm/two_phase.go @@ -37,11 +37,11 @@ var ( typError = reflect.Zero(reflect.TypeOf((*error)(nil)).Elem()).Type() typContext = reflect.Zero(reflect.TypeOf((*context.Context)(nil)).Elem()).Type() typBool = reflect.Zero(reflect.TypeOf((*bool)(nil)).Elem()).Type() - typBusinessContextInterface = reflect.Zero(reflect.TypeOf((*tm.BusinessActionContext)(nil))).Type() + TypBusinessContextInterface = reflect.Zero(reflect.TypeOf((*tm.BusinessActionContext)(nil))).Type() ) type TwoPhaseInterface interface { - Prepare(ctx context.Context, params ...interface{}) (bool, error) + Prepare(ctx context.Context, params interface{}) (bool, error) Commit(ctx context.Context, businessActionContext *tm.BusinessActionContext) (bool, error) Rollback(ctx context.Context, businessActionContext *tm.BusinessActionContext) (bool, error) GetActionName() string @@ -62,12 +62,20 @@ func (t *TwoPhaseAction) GetTwoPhaseService() interface{} { return t.twoPhaseService } -func (t *TwoPhaseAction) Prepare(ctx context.Context, params ...interface{}) (bool, error) { - values := make([]reflect.Value, 0, len(params)) - values = append(values, reflect.ValueOf(ctx)) - for _, param := range params { - values = append(values, reflect.ValueOf(param)) - } +func (t *TwoPhaseAction) GetPrepareMethodName() string { + return t.prepareMethodName +} + +func (t *TwoPhaseAction) GetCommitMethodName() string { + return t.commitMethodName +} + +func (t *TwoPhaseAction) GetRollbackMethodName() string { + return t.rollbackMethodName +} + +func (t *TwoPhaseAction) Prepare(ctx context.Context, params interface{}) (bool, error) { + values := []reflect.Value{reflect.ValueOf(ctx), reflect.ValueOf(params)} res := t.prepareMethod.Call(values) var ( r0 = res[0].Interface() @@ -254,7 +262,7 @@ func getCommitMethod(t reflect.StructField, f reflect.Value) (string, *reflect.V if inType := t.Type.In(0); inType != typContext { return "", nil, false } - if inType := t.Type.In(1); inType != typBusinessContextInterface { + if inType := t.Type.In(1); inType != TypBusinessContextInterface { return "", nil, false } return t.Name, &f, true @@ -285,7 +293,7 @@ func getRollbackMethod(t reflect.StructField, f reflect.Value) (string, *reflect if inType := t.Type.In(0); inType != typContext { return "", nil, false } - if inType := t.Type.In(1); inType != typBusinessContextInterface { + if inType := t.Type.In(1); inType != TypBusinessContextInterface { return "", nil, false } return t.Name, &f, true diff --git a/pkg/rm/two_phase_test.go b/pkg/rm/two_phase_test.go index 77e76320b..7617ec8b1 100644 --- a/pkg/rm/two_phase_test.go +++ b/pkg/rm/two_phase_test.go @@ -124,7 +124,7 @@ func TestParseTwoPhaseActionGetMethodName(t *testing.T) { } type TwoPhaseDemoService1 struct { - TwoPhasePrepare func(ctx context.Context, params ...interface{}) (bool, error) `seataTwoPhaseAction:"prepare" seataTwoPhaseServiceName:"TwoPhaseDemoService"` + TwoPhasePrepare func(ctx context.Context, params interface{}) (bool, error) `seataTwoPhaseAction:"prepare" seataTwoPhaseServiceName:"TwoPhaseDemoService"` TwoPhaseCommit func(ctx context.Context, businessActionContext *tm.BusinessActionContext) (bool, error) `seataTwoPhaseAction:"commit"` TwoPhaseRollback func(ctx context.Context, businessActionContext *tm.BusinessActionContext) (bool, error) `seataTwoPhaseAction:"rollback"` TwoPhaseDemoService func() string @@ -132,7 +132,7 @@ type TwoPhaseDemoService1 struct { func NewTwoPhaseDemoService1() *TwoPhaseDemoService1 { return &TwoPhaseDemoService1{ - TwoPhasePrepare: func(ctx context.Context, params ...interface{}) (bool, error) { + TwoPhasePrepare: func(ctx context.Context, params interface{}) (bool, error) { return false, fmt.Errorf("execute two phase prepare method, param %v", params) }, TwoPhaseCommit: func(ctx context.Context, businessActionContext *tm.BusinessActionContext) (bool, error) { @@ -158,7 +158,7 @@ func TestParseTwoPhaseActionExecuteMethod1(t *testing.T) { resp, err := twoPhaseService.Prepare(ctx, 11) assert.Equal(t, false, resp) - assert.Equal(t, "execute two phase prepare method, param [11]", err.Error()) + assert.Equal(t, "execute two phase prepare method, param 11", err.Error()) resp, err = twoPhaseService.Commit(ctx, &tm.BusinessActionContext{Xid: "1234"}) assert.Equal(t, false, resp) @@ -174,7 +174,7 @@ func TestParseTwoPhaseActionExecuteMethod1(t *testing.T) { type TwoPhaseDemoService2 struct { } -func (t *TwoPhaseDemoService2) Prepare(ctx context.Context, params ...interface{}) (bool, error) { +func (t *TwoPhaseDemoService2) Prepare(ctx context.Context, params interface{}) (bool, error) { return false, fmt.Errorf("execute two phase prepare method, param %v", params) } @@ -196,7 +196,7 @@ func TestParseTwoPhaseActionExecuteMethod2(t *testing.T) { assert.Nil(t, err) resp, err := twoPhaseService.Prepare(ctx, 11) assert.Equal(t, false, resp) - assert.Equal(t, "execute two phase prepare method, param [11]", err.Error()) + assert.Equal(t, "execute two phase prepare method, param 11", err.Error()) resp, err = twoPhaseService.Commit(ctx, &tm.BusinessActionContext{Xid: "1234"}) assert.Equal(t, true, resp) diff --git a/pkg/tm/context.go b/pkg/tm/context.go index 759238345..3d8053ddd 100644 --- a/pkg/tm/context.go +++ b/pkg/tm/context.go @@ -33,6 +33,8 @@ type BusinessActionContext struct { Xid string BranchId int64 ActionName string + IsDelayReport bool + IsUpdated bool ActionContext map[string]interface{} } @@ -40,7 +42,6 @@ type ContextVariable struct { TxName string Xid string XidCopy string - Status *message.GlobalStatus TxRole *GlobalTransactionRole BusinessActionContext *BusinessActionContext TxStatus *message.GlobalStatus diff --git a/sample/tcc/local/service/service.go b/sample/tcc/local/service/service.go index 0076646e6..e4cabc988 100644 --- a/sample/tcc/local/service/service.go +++ b/sample/tcc/local/service/service.go @@ -57,7 +57,7 @@ func NewTestTCCServiceBusinessProxy() *tcc.TCCServiceProxy { return tccService } -func (T TestTCCServiceBusiness) Prepare(ctx context.Context, params ...interface{}) (bool, error) { +func (T TestTCCServiceBusiness) Prepare(ctx context.Context, params interface{}) (bool, error) { log.Infof("TestTCCServiceBusiness Prepare, param %v", params) return true, nil } @@ -97,7 +97,7 @@ func NewTestTCCServiceBusiness2Proxy() *tcc.TCCServiceProxy { return tccService2 } -func (T TestTCCServiceBusiness2) Prepare(ctx context.Context, params ...interface{}) (bool, error) { +func (T TestTCCServiceBusiness2) Prepare(ctx context.Context, params interface{}) (bool, error) { log.Infof("TestTCCServiceBusiness2 Prepare, param %v", params) return true, nil } diff --git a/testdata/context.go b/testdata/context.go new file mode 100644 index 000000000..eb6dcdcdf --- /dev/null +++ b/testdata/context.go @@ -0,0 +1,27 @@ +package testdata + +import ( + "context" + + "github.com/seata/seata-go/pkg/protocol/message" + "github.com/seata/seata-go/pkg/tm" +) + +const ( + TestTxName = "TesttxName" + TestXid = "TestXid" + TestXidCopy = "TestXid" + TestTxRole = tm.LAUNCHER + TestTxStatus = message.GlobalStatusBegin +) + +func GetTestContext() context.Context { + ctx := tm.InitSeataContext(context.Background()) + tm.SetXID(ctx, TestXid) + tm.SetTxName(ctx, TestTxName) + tm.SetXID(ctx, TestXid) + tm.SetXIDCopy(ctx, TestXidCopy) + tm.SetTransactionRole(ctx, TestTxRole) + tm.SetTxStatus(ctx, TestTxStatus) + return ctx +} diff --git a/testdata/mock_tcc.go b/testdata/mock_tcc.go new file mode 100644 index 000000000..7aac1a18a --- /dev/null +++ b/testdata/mock_tcc.go @@ -0,0 +1,35 @@ +package testdata + +import ( + "context" + + "github.com/seata/seata-go/pkg/rm" + "github.com/seata/seata-go/pkg/tm" +) + +const ( + ActionName = "TestActionName" +) + +type TestTwoPhaseService struct { +} + +func (*TestTwoPhaseService) Prepare(ctx context.Context, params interface{}) (bool, error) { + return true, nil +} + +func (*TestTwoPhaseService) Commit(ctx context.Context, businessActionContext *tm.BusinessActionContext) (bool, error) { + return true, nil +} + +func (*TestTwoPhaseService) Rollback(ctx context.Context, businessActionContext *tm.BusinessActionContext) (bool, error) { + return true, nil +} + +func (*TestTwoPhaseService) GetActionName() string { + return ActionName +} + +func GetTestTwoPhaseService() rm.TwoPhaseInterface { + return &TestTwoPhaseService{} +}