* feat : impl SagaResource #843 * feat : impl SagaResource #843 * feat : impl SagaResourceManager #843 * feat : impl SagaResourceManager BranchRollback #843 * feat : impl handler_saga #843 * feat : impl invalid_exception_test #843 * update #843 * update:elegant code #843 * abstracting tm branch registration out to rm and add test #846 * test #846 * add licensed #846 * replace panic to return #843 * update #843 * update init and handler_saga #843 --------- Co-authored-by: FengZhang <zfcode@qq.com>feature/saga
@@ -30,6 +30,7 @@ import ( | |||
"github.com/seata/seata-go/pkg/remoting/processor/client" | |||
"github.com/seata/seata-go/pkg/rm" | |||
"github.com/seata/seata-go/pkg/rm/tcc" | |||
saga "github.com/seata/seata-go/pkg/saga/rm" | |||
"github.com/seata/seata-go/pkg/tm" | |||
"github.com/seata/seata-go/pkg/util/log" | |||
) | |||
@@ -87,6 +88,7 @@ func initRmClient(cfg *Config) { | |||
client.RegisterProcessor() | |||
integration.Init() | |||
tcc.InitTCC() | |||
saga.InitSaga() | |||
at.InitAT(cfg.ClientConfig.UndoConfig, cfg.AsyncWorkerConfig) | |||
at.InitXA(cfg.ClientConfig.XaConfig) | |||
}) | |||
@@ -0,0 +1,35 @@ | |||
/* | |||
* 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 rm | |||
import ( | |||
"github.com/seata/seata-go/pkg/protocol/branch" | |||
"github.com/seata/seata-go/pkg/rm" | |||
) | |||
type RMHandlerSaga struct{} | |||
func (h *RMHandlerSaga) HandleUndoLogDeleteRequest(request interface{}) { | |||
// do nothing | |||
} | |||
func (h *RMHandlerSaga) GetResourceManager() rm.ResourceManager { | |||
return rm.GetRmCacheInstance().GetResourceManager(branch.BranchTypeSAGA) | |||
} | |||
func (h *RMHandlerSaga) GetBranchType() branch.BranchType { | |||
return branch.BranchTypeSAGA | |||
} |
@@ -0,0 +1,52 @@ | |||
/* | |||
* 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 rm | |||
import ( | |||
"fmt" | |||
"github.com/seata/seata-go/pkg/protocol/branch" | |||
) | |||
type SagaResource struct { | |||
resourceGroupId string | |||
applicationId string | |||
} | |||
func (r *SagaResource) GetResourceGroupId() string { | |||
return r.resourceGroupId | |||
} | |||
func (r *SagaResource) SetResourceGroupId(resourceGroupId string) { | |||
r.resourceGroupId = resourceGroupId | |||
} | |||
func (r *SagaResource) GetResourceId() string { | |||
return fmt.Sprintf("%s#%s", r.applicationId, r.resourceGroupId) | |||
} | |||
func (r *SagaResource) GetBranchType() branch.BranchType { | |||
return branch.BranchTypeSAGA | |||
} | |||
func (r *SagaResource) GetApplicationId() string { | |||
return r.applicationId | |||
} | |||
func (r *SagaResource) SetApplicationId(applicationId string) { | |||
r.applicationId = applicationId | |||
} |
@@ -0,0 +1,159 @@ | |||
/* | |||
* 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 rm | |||
import ( | |||
"bytes" | |||
"context" | |||
"fmt" | |||
"log" | |||
"sync" | |||
"github.com/seata/seata-go/pkg/protocol/branch" | |||
"github.com/seata/seata-go/pkg/protocol/message" | |||
"github.com/seata/seata-go/pkg/rm" | |||
"github.com/seata/seata-go/pkg/saga/statemachine/engine/exception" | |||
"github.com/seata/seata-go/pkg/saga/statemachine/statelang" | |||
seataErrors "github.com/seata/seata-go/pkg/util/errors" | |||
) | |||
var ( | |||
sagaResourceManagerInstance *SagaResourceManager | |||
once sync.Once | |||
) | |||
type SagaResourceManager struct { | |||
rmRemoting *rm.RMRemoting | |||
resourceCache sync.Map | |||
} | |||
func InitSaga() { | |||
rm.GetRmCacheInstance().RegisterResourceManager(GetSagaResourceManager()) | |||
} | |||
func GetSagaResourceManager() *SagaResourceManager { | |||
once.Do(func() { | |||
sagaResourceManagerInstance = &SagaResourceManager{ | |||
rmRemoting: rm.GetRMRemotingInstance(), | |||
resourceCache: sync.Map{}, | |||
} | |||
}) | |||
return sagaResourceManagerInstance | |||
} | |||
func (s *SagaResourceManager) RegisterResource(resource rm.Resource) error { | |||
if _, ok := resource.(*SagaResource); !ok { | |||
return fmt.Errorf("register saga resource error, SagaResource is needed, param %v", resource) | |||
} | |||
s.resourceCache.Store(resource.GetResourceId(), resource) | |||
return s.rmRemoting.RegisterResource(resource) | |||
} | |||
func (s *SagaResourceManager) GetCachedResources() *sync.Map { | |||
return &s.resourceCache | |||
} | |||
func (s *SagaResourceManager) GetBranchType() branch.BranchType { | |||
return branch.BranchTypeSAGA | |||
} | |||
func (s *SagaResourceManager) BranchCommit(ctx context.Context, resource rm.BranchResource) (branch.BranchStatus, error) { | |||
engine := GetStateMachineEngine() | |||
stMaInst, err := engine.Forward(ctx, resource.Xid, nil) | |||
if err != nil { | |||
if fie, ok := exception.IsForwardInvalidException(err); ok { | |||
log.Printf("StateMachine forward failed, xid: %s, err: %v", resource.Xid, err) | |||
if isInstanceNotExists(fie.ErrCode) { | |||
return branch.BranchStatusPhasetwoCommitted, nil | |||
} | |||
} | |||
log.Printf("StateMachine forward failed, xid: %s, err: %v", resource.Xid, err) | |||
return branch.BranchStatusPhasetwoCommitFailedRetryable, err | |||
} | |||
status := stMaInst.Status() | |||
compStatus := stMaInst.CompensationStatus() | |||
switch { | |||
case status == statelang.SU && compStatus == "": | |||
return branch.BranchStatusPhasetwoCommitted, nil | |||
case compStatus == statelang.SU: | |||
return branch.BranchStatusPhasetwoRollbacked, nil | |||
case compStatus == statelang.FA || compStatus == statelang.UN: | |||
return branch.BranchStatusPhasetwoRollbackFailedRetryable, nil | |||
case status == statelang.FA && compStatus == "": | |||
return branch.BranchStatusPhaseoneFailed, nil | |||
default: | |||
return branch.BranchStatusPhasetwoCommitFailedRetryable, nil | |||
} | |||
} | |||
func (s *SagaResourceManager) BranchRollback(ctx context.Context, resource rm.BranchResource) (branch.BranchStatus, error) { | |||
engine := GetStateMachineEngine() | |||
stMaInst, err := engine.ReloadStateMachineInstance(ctx, resource.Xid) | |||
if err != nil || stMaInst == nil { | |||
return branch.BranchStatusPhasetwoRollbacked, nil | |||
} | |||
strategy := stMaInst.StateMachine().RecoverStrategy() | |||
appData := resource.ApplicationData | |||
isTimeoutRollback := bytes.Equal(appData, []byte{byte(message.GlobalStatusTimeoutRollbacking)}) || bytes.Equal(appData, []byte{byte(message.GlobalStatusTimeoutRollbackRetrying)}) | |||
if strategy == statelang.Forward && isTimeoutRollback { | |||
log.Printf("Retry by custom recover strategy [Forward] on timeout, SAGA global[%s]", resource.Xid) | |||
return branch.BranchStatusPhasetwoCommitFailedRetryable, nil | |||
} | |||
stMaInst, err = engine.Compensate(ctx, resource.Xid, nil) | |||
if err == nil && stMaInst.CompensationStatus() == statelang.SU { | |||
return branch.BranchStatusPhasetwoRollbacked, nil | |||
} | |||
if fie, ok := exception.IsEngineExecutionException(err); ok { | |||
log.Printf("StateMachine compensate failed, xid: %s, err: %v", resource.Xid, err) | |||
if isInstanceNotExists(fie.ErrCode) { | |||
return branch.BranchStatusPhasetwoRollbacked, nil | |||
} | |||
} | |||
log.Printf("StateMachine compensate failed, xid: %s, err: %v", resource.Xid, err) | |||
return branch.BranchStatusPhasetwoRollbackFailedRetryable, err | |||
} | |||
func (s *SagaResourceManager) BranchRegister(ctx context.Context, param rm.BranchRegisterParam) (int64, error) { | |||
return s.rmRemoting.BranchRegister(param) | |||
} | |||
func (s *SagaResourceManager) BranchReport(ctx context.Context, param rm.BranchReportParam) error { | |||
return s.rmRemoting.BranchReport(param) | |||
} | |||
func (s *SagaResourceManager) LockQuery(ctx context.Context, param rm.LockQueryParam) (bool, error) { | |||
// LockQuery is not supported for Saga resources | |||
return false, fmt.Errorf("LockQuery is not supported for Saga resources") | |||
} | |||
func (s *SagaResourceManager) UnregisterResource(resource rm.Resource) error { | |||
// UnregisterResource is not supported for SagaResourceManager | |||
return fmt.Errorf("UnregisterResource is not supported for SagaResourceManager") | |||
} | |||
// isInstanceNotExists checks if the error code indicates StateMachineInstanceNotExists | |||
func isInstanceNotExists(errCode string) bool { | |||
return errCode == fmt.Sprintf("%v", seataErrors.StateMachineInstanceNotExists) | |||
} |
@@ -0,0 +1,43 @@ | |||
/* | |||
* 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 rm | |||
import ( | |||
"sync" | |||
"testing" | |||
) | |||
func TestGetSagaResourceManager_Singleton(t *testing.T) { | |||
var wg sync.WaitGroup | |||
instances := make([]*SagaResourceManager, 10) | |||
for i := 0; i < 10; i++ { | |||
wg.Add(1) | |||
go func(idx int) { | |||
defer wg.Done() | |||
instances[idx] = GetSagaResourceManager() | |||
}(i) | |||
} | |||
wg.Wait() | |||
first := instances[0] | |||
for i, inst := range instances { | |||
if inst != first { | |||
t.Errorf("Instance at index %d is not the same as the first instance", i) | |||
} | |||
} | |||
} |
@@ -0,0 +1,38 @@ | |||
/* | |||
* 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 rm | |||
import ( | |||
"github.com/seata/seata-go/pkg/saga/statemachine/engine" | |||
"sync" | |||
) | |||
var ( | |||
stateMachineEngine engine.StateMachineEngine | |||
stateMachineEngineOnce sync.Once | |||
) | |||
func GetStateMachineEngine() engine.StateMachineEngine { | |||
return stateMachineEngine | |||
} | |||
func SetStateMachineEngine(smEngine engine.StateMachineEngine) { | |||
stateMachineEngineOnce.Do(func() { | |||
stateMachineEngine = smEngine | |||
}) | |||
} |
@@ -18,6 +18,7 @@ | |||
package exception | |||
import ( | |||
perror "errors" | |||
"fmt" | |||
"github.com/seata/seata-go/pkg/util/errors" | |||
) | |||
@@ -41,6 +42,13 @@ func NewEngineExecutionException(code errors.TransactionErrorCode, msg string, p | |||
SeataError: *seataError, | |||
} | |||
} | |||
func IsEngineExecutionException(err error) (*EngineExecutionException, bool) { | |||
var fie *EngineExecutionException | |||
if perror.As(err, &fie) { | |||
return fie, true | |||
} | |||
return nil, false | |||
} | |||
func (e *EngineExecutionException) StateName() string { | |||
return e.stateName | |||
@@ -73,7 +81,3 @@ func (e *EngineExecutionException) StateInstanceId() string { | |||
func (e *EngineExecutionException) SetStateInstanceId(stateInstanceId string) { | |||
e.stateInstanceId = stateInstanceId | |||
} | |||
type ForwardInvalidException struct { | |||
EngineExecutionException | |||
} |
@@ -0,0 +1,68 @@ | |||
/* | |||
* 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 exception | |||
import ( | |||
"errors" | |||
"testing" | |||
pkgerr "github.com/seata/seata-go/pkg/util/errors" | |||
) | |||
func TestIsEngineExecutionException(t *testing.T) { | |||
cases := []struct { | |||
name string | |||
err error | |||
wantOk bool | |||
wantMsg string | |||
}{ | |||
{ | |||
name: "EngineExecutionException", | |||
err: &EngineExecutionException{SeataError: pkgerr.SeataError{Message: "engine error"}}, | |||
wantOk: true, | |||
wantMsg: "engine error", | |||
}, | |||
{ | |||
name: "Other error", | |||
err: errors.New("some other error"), | |||
wantOk: false, | |||
wantMsg: "", | |||
}, | |||
{ | |||
name: "nil error", | |||
err: nil, | |||
wantOk: false, | |||
wantMsg: "", | |||
}, | |||
} | |||
for _, c := range cases { | |||
t.Run(c.name, func(t *testing.T) { | |||
fie, ok := IsEngineExecutionException(c.err) | |||
if ok != c.wantOk { | |||
t.Errorf("expected ok=%v, got %v", c.wantOk, ok) | |||
} | |||
if ok && fie.SeataError.Message != c.wantMsg { | |||
t.Errorf("expected Message=%q, got %q", c.wantMsg, fie.SeataError.Message) | |||
} | |||
if !ok && fie != nil { | |||
t.Errorf("expected fie=nil, got %v", fie) | |||
} | |||
}) | |||
} | |||
} |
@@ -0,0 +1,32 @@ | |||
/* | |||
* 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 exception | |||
import "errors" | |||
type ForwardInvalidException struct { | |||
EngineExecutionException | |||
} | |||
func IsForwardInvalidException(err error) (*ForwardInvalidException, bool) { | |||
var fie *ForwardInvalidException | |||
if errors.As(err, &fie) { | |||
return fie, true | |||
} | |||
return nil, false | |||
} |
@@ -0,0 +1,68 @@ | |||
/* | |||
* 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 exception | |||
import ( | |||
"errors" | |||
"testing" | |||
pkgerr "github.com/seata/seata-go/pkg/util/errors" | |||
) | |||
func TestIsForwardInvalidException(t *testing.T) { | |||
cases := []struct { | |||
name string | |||
err error | |||
wantOk bool | |||
wantMsg string | |||
}{ | |||
{ | |||
name: "ForwardInvalidException", | |||
err: &ForwardInvalidException{EngineExecutionException: EngineExecutionException{SeataError: pkgerr.SeataError{Message: "forward invalid"}}}, | |||
wantOk: true, | |||
wantMsg: "forward invalid", | |||
}, | |||
{ | |||
name: "Other error", | |||
err: errors.New("some other error"), | |||
wantOk: false, | |||
wantMsg: "", | |||
}, | |||
{ | |||
name: "nil error", | |||
err: nil, | |||
wantOk: false, | |||
wantMsg: "", | |||
}, | |||
} | |||
for _, c := range cases { | |||
t.Run(c.name, func(t *testing.T) { | |||
fie, ok := IsForwardInvalidException(c.err) | |||
if ok != c.wantOk { | |||
t.Errorf("expected ok=%v, got %v", c.wantOk, ok) | |||
} | |||
if ok && fie.SeataError.Message != c.wantMsg { | |||
t.Errorf("expected Message=%q, got %q", c.wantMsg, fie.SeataError.Message) | |||
} | |||
if !ok && fie != nil { | |||
t.Errorf("expected fie=nil, got %v", fie) | |||
} | |||
}) | |||
} | |||
} |
@@ -19,13 +19,13 @@ package tm | |||
import ( | |||
"context" | |||
"github.com/seata/seata-go/pkg/rm" | |||
"time" | |||
"github.com/seata/seata-go/pkg/protocol/branch" | |||
"github.com/seata/seata-go/pkg/protocol/message" | |||
"github.com/seata/seata-go/pkg/rm" | |||
sagarm "github.com/seata/seata-go/pkg/saga/rm" | |||
"github.com/seata/seata-go/pkg/tm" | |||
"github.com/seata/seata-go/pkg/util/log" | |||
"time" | |||
) | |||
type DefaultSagaTransactionalTemplate struct { | |||
@@ -82,8 +82,7 @@ func (t *DefaultSagaTransactionalTemplate) ReportTransaction(ctx context.Context | |||
} | |||
func (t *DefaultSagaTransactionalTemplate) BranchRegister(ctx context.Context, resourceId string, clientId string, xid string, applicationData string, lockKeys string) (int64, error) { | |||
//todo Wait implement sagaResource | |||
return rm.GetRMRemotingInstance().BranchRegister(rm.BranchRegisterParam{ | |||
return sagarm.GetSagaResourceManager().BranchRegister(ctx, rm.BranchRegisterParam{ | |||
BranchType: branch.BranchTypeSAGA, | |||
ResourceId: resourceId, | |||
Xid: xid, | |||
@@ -94,8 +93,7 @@ func (t *DefaultSagaTransactionalTemplate) BranchRegister(ctx context.Context, r | |||
} | |||
func (t *DefaultSagaTransactionalTemplate) BranchReport(ctx context.Context, xid string, branchId int64, status branch.BranchStatus, applicationData string) error { | |||
//todo Wait implement sagaResource | |||
return rm.GetRMRemotingInstance().BranchReport(rm.BranchReportParam{ | |||
return sagarm.GetSagaResourceManager().BranchReport(ctx, rm.BranchReportParam{ | |||
BranchType: branch.BranchTypeSAGA, | |||
Xid: xid, | |||
BranchId: branchId, | |||