mirror of https://github.com/dtm-labs/dtm.git
39 changed files with 691 additions and 219 deletions
@ -0,0 +1,29 @@ |
|||||
|
package dtmcli |
||||
|
|
||||
|
import "fmt" |
||||
|
|
||||
|
// ConcurrentSaga struct of concurrent saga
|
||||
|
type ConcurrentSaga struct { |
||||
|
Saga |
||||
|
orders map[int][]int |
||||
|
} |
||||
|
|
||||
|
// NewConcurrentSaga create a concurrent saga
|
||||
|
func NewConcurrentSaga(server string, gid string) *ConcurrentSaga { |
||||
|
return &ConcurrentSaga{Saga: Saga{TransBase: *NewTransBase(gid, "csaga", server, "")}, orders: map[int][]int{}} |
||||
|
} |
||||
|
|
||||
|
// AddStepOrder specify that step should be after preSteps. Step is larger than all the element in preSteps
|
||||
|
func (s *ConcurrentSaga) AddStepOrder(step int, preSteps []int) *ConcurrentSaga { |
||||
|
PanicIf(step > len(s.Steps), fmt.Errorf("step value: %d is invalid. which cannot be larger than total steps: %d", step, len(s.Steps))) |
||||
|
s.orders[step] = preSteps |
||||
|
return s |
||||
|
} |
||||
|
|
||||
|
// Submit submit the saga trans
|
||||
|
func (s *ConcurrentSaga) Submit() error { |
||||
|
if len(s.orders) > 0 { |
||||
|
s.CustomData = MustMarshalString(M{"orders": s.orders}) |
||||
|
} |
||||
|
return s.callDtm(s, "submit") |
||||
|
} |
||||
@ -0,0 +1,175 @@ |
|||||
|
package dtmsvr |
||||
|
|
||||
|
import ( |
||||
|
"time" |
||||
|
|
||||
|
"github.com/yedf/dtm/common" |
||||
|
"github.com/yedf/dtm/dtmcli" |
||||
|
"gorm.io/gorm/clause" |
||||
|
) |
||||
|
|
||||
|
type transCSagaProcessor struct { |
||||
|
*TransGlobal |
||||
|
} |
||||
|
|
||||
|
func init() { |
||||
|
registorProcessorCreator("csaga", func(trans *TransGlobal) transProcessor { return &transCSagaProcessor{TransGlobal: trans} }) |
||||
|
} |
||||
|
|
||||
|
func (t *transCSagaProcessor) GenBranches() []TransBranch { |
||||
|
return genSagaBranches(t.TransGlobal) |
||||
|
} |
||||
|
|
||||
|
type cSagaCustom struct { |
||||
|
Orders map[int][]int `json:"orders"` |
||||
|
} |
||||
|
|
||||
|
func isPreconditionsSucceed(branches []TransBranch, pres []int) bool { |
||||
|
for _, pre := range pres { |
||||
|
if branches[pre].Status != dtmcli.StatusSucceed { |
||||
|
return false |
||||
|
} |
||||
|
} |
||||
|
return true |
||||
|
} |
||||
|
|
||||
|
type branchResult struct { |
||||
|
index int |
||||
|
status string |
||||
|
started bool |
||||
|
branchType string |
||||
|
} |
||||
|
|
||||
|
func (t *transCSagaProcessor) ProcessOnce(db *common.DB, branches []TransBranch) error { |
||||
|
if t.Status == dtmcli.StatusFailed || t.Status == dtmcli.StatusSucceed { |
||||
|
return nil |
||||
|
} |
||||
|
n := len(branches) |
||||
|
|
||||
|
orders := map[int][]int{} |
||||
|
if t.CustomData != "" { |
||||
|
csc := cSagaCustom{Orders: map[int][]int{}} |
||||
|
dtmcli.MustUnmarshalString(t.CustomData, &csc) |
||||
|
for k, v := range csc.Orders { // new branches is doubled, so change the order value
|
||||
|
orders[2*k+1] = []int{} |
||||
|
for j := 0; j < len(v); j++ { |
||||
|
orders[2*k+1] = append(orders[2*k+1], csc.Orders[k][j]*2+1) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
// resultStats
|
||||
|
var rsActionToStart, rsActionDone, rsActionFailed, rsActionSucceed, rsCompensateToStart, rsCompensateDone, rsCompensateSucceed int |
||||
|
branchResults := make([]branchResult, n) // save the branch result
|
||||
|
for i := 0; i < n; i++ { |
||||
|
b := branches[i] |
||||
|
if b.BranchType == dtmcli.BranchAction { |
||||
|
if b.Status == dtmcli.StatusPrepared || b.Status == dtmcli.StatusDoing { |
||||
|
rsActionToStart++ |
||||
|
} else if b.Status == dtmcli.StatusFailed { |
||||
|
rsActionFailed++ |
||||
|
} |
||||
|
} |
||||
|
branchResults[i] = branchResult{status: branches[i].Status, branchType: branches[i].BranchType} |
||||
|
} |
||||
|
stopChan := make(chan branchResult, n) |
||||
|
asyncExecBranch := func(i int) { |
||||
|
var err error |
||||
|
defer func() { |
||||
|
if x := recover(); x != nil { |
||||
|
err = dtmcli.AsError(x) |
||||
|
} |
||||
|
stopChan <- branchResult{index: i, status: branches[i].Status, branchType: branches[i].BranchType} |
||||
|
if err != nil { |
||||
|
dtmcli.LogRedf("exec branch error: %v", err) |
||||
|
} |
||||
|
}() |
||||
|
err = t.execBranch(db, &branches[i]) |
||||
|
} |
||||
|
needRollback := func(i int) bool { |
||||
|
br := &branchResults[i] |
||||
|
return !br.started && br.branchType == dtmcli.BranchCompensate && br.status != dtmcli.StatusSucceed && branchResults[i+1].branchType == dtmcli.BranchAction && branchResults[i+1].status != dtmcli.StatusPrepared |
||||
|
} |
||||
|
pickAndRun := func(branchType string) { |
||||
|
toRun := []int{} |
||||
|
for current := 0; current < n; current++ { |
||||
|
br := &branchResults[current] |
||||
|
if br.branchType == branchType && branchType == dtmcli.BranchAction { |
||||
|
if (br.status == dtmcli.StatusPrepared || br.status == dtmcli.StatusDoing) && |
||||
|
!br.started && isPreconditionsSucceed(branches, orders[current]) { |
||||
|
br.status = dtmcli.StatusDoing |
||||
|
toRun = append(toRun, current) |
||||
|
} |
||||
|
} else if br.branchType == branchType && branchType == dtmcli.BranchCompensate { |
||||
|
if needRollback(current) { |
||||
|
toRun = append(toRun, current) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
if branchType == dtmcli.BranchAction && len(toRun) > 0 { |
||||
|
updates := make([]TransBranch, len(toRun)) |
||||
|
for i, b := range toRun { |
||||
|
updates[i].ID = branches[b].ID |
||||
|
branches[b].Status = dtmcli.StatusDoing |
||||
|
updates[i].Status = dtmcli.StatusDoing |
||||
|
} |
||||
|
dbGet().Must().Clauses(clause.OnConflict{ |
||||
|
OnConstraint: "trans_branch_pkey", |
||||
|
DoUpdates: clause.AssignmentColumns([]string{"status"}), |
||||
|
}).Create(updates) |
||||
|
} else if branchType == dtmcli.BranchCompensate { |
||||
|
rsCompensateToStart = len(toRun) |
||||
|
} |
||||
|
for _, b := range toRun { |
||||
|
branchResults[b].started = true |
||||
|
go asyncExecBranch(b) |
||||
|
} |
||||
|
} |
||||
|
processorTimeout := func() bool { |
||||
|
return time.Since(t.processStarted)+NowForwardDuration > time.Duration(t.getRetryInterval()-3)*time.Second |
||||
|
} |
||||
|
waitOnceForDone := func() { |
||||
|
select { |
||||
|
case r := <-stopChan: |
||||
|
br := &branchResults[r.index] |
||||
|
br.status = r.status |
||||
|
if r.branchType == dtmcli.BranchAction { |
||||
|
rsActionDone++ |
||||
|
if r.status == dtmcli.StatusFailed { |
||||
|
rsActionFailed++ |
||||
|
} else if r.status == dtmcli.StatusSucceed { |
||||
|
rsActionSucceed++ |
||||
|
} |
||||
|
} else { |
||||
|
rsCompensateDone++ |
||||
|
if r.status == dtmcli.StatusSucceed { |
||||
|
rsCompensateSucceed++ |
||||
|
} |
||||
|
} |
||||
|
dtmcli.Logf("branch done: %v", r) |
||||
|
case <-time.After(time.Duration(time.Second * 3)): |
||||
|
dtmcli.Logf("wait once for done") |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
for t.Status == dtmcli.StatusSubmitted && !t.isTimeout() && rsActionFailed == 0 && rsActionDone != rsActionToStart && !processorTimeout() { |
||||
|
pickAndRun(dtmcli.BranchAction) |
||||
|
waitOnceForDone() |
||||
|
} |
||||
|
if t.Status == dtmcli.StatusSubmitted && rsActionFailed == 0 && rsActionToStart == rsActionSucceed { |
||||
|
t.changeStatus(db, dtmcli.StatusSucceed) |
||||
|
return nil |
||||
|
} |
||||
|
if t.Status == dtmcli.StatusSubmitted && (rsActionFailed > 0 || t.isTimeout()) { |
||||
|
t.changeStatus(db, dtmcli.StatusAborting) |
||||
|
} |
||||
|
if t.Status == dtmcli.StatusAborting { |
||||
|
pickAndRun(dtmcli.BranchCompensate) |
||||
|
for rsCompensateDone != rsCompensateToStart && !processorTimeout() { |
||||
|
waitOnceForDone() |
||||
|
} |
||||
|
} |
||||
|
if (t.Status == dtmcli.StatusSubmitted || t.Status == dtmcli.StatusAborting) && rsActionFailed > 0 && rsCompensateToStart == rsCompensateSucceed { |
||||
|
t.changeStatus(db, dtmcli.StatusFailed) |
||||
|
} |
||||
|
return nil |
||||
|
} |
||||
@ -0,0 +1,53 @@ |
|||||
|
package test |
||||
|
|
||||
|
import ( |
||||
|
"fmt" |
||||
|
"testing" |
||||
|
|
||||
|
"github.com/stretchr/testify/assert" |
||||
|
"github.com/yedf/dtm/common" |
||||
|
"github.com/yedf/dtm/dtmcli" |
||||
|
"github.com/yedf/dtm/examples" |
||||
|
) |
||||
|
|
||||
|
func TestSqlDB(t *testing.T) { |
||||
|
asserts := assert.New(t) |
||||
|
db := common.DbGet(config.DB) |
||||
|
barrier := &dtmcli.BranchBarrier{ |
||||
|
TransType: "saga", |
||||
|
Gid: "gid2", |
||||
|
BranchID: "branch_id2", |
||||
|
BranchType: dtmcli.BranchAction, |
||||
|
} |
||||
|
db.Must().Exec("insert into dtm_barrier.barrier(trans_type, gid, branch_id, branch_type, reason) values('saga', 'gid1', 'branch_id1', 'action', 'saga')") |
||||
|
tx, err := db.ToSQLDB().Begin() |
||||
|
asserts.Nil(err) |
||||
|
err = barrier.Call(tx, func(db dtmcli.DB) error { |
||||
|
dtmcli.Logf("rollback gid2") |
||||
|
return fmt.Errorf("gid2 error") |
||||
|
}) |
||||
|
asserts.Error(err, fmt.Errorf("gid2 error")) |
||||
|
dbr := db.Model(&BarrierModel{}).Where("gid=?", "gid1").Find(&[]BarrierModel{}) |
||||
|
asserts.Equal(dbr.RowsAffected, int64(1)) |
||||
|
dbr = db.Model(&BarrierModel{}).Where("gid=?", "gid2").Find(&[]BarrierModel{}) |
||||
|
asserts.Equal(dbr.RowsAffected, int64(0)) |
||||
|
barrier.BarrierID = 0 |
||||
|
tx2, err := db.ToSQLDB().Begin() |
||||
|
asserts.Nil(err) |
||||
|
err = barrier.Call(tx2, func(db dtmcli.DB) error { |
||||
|
dtmcli.Logf("submit gid2") |
||||
|
return nil |
||||
|
}) |
||||
|
asserts.Nil(err) |
||||
|
dbr = db.Model(&BarrierModel{}).Where("gid=?", "gid2").Find(&[]BarrierModel{}) |
||||
|
asserts.Equal(dbr.RowsAffected, int64(1)) |
||||
|
} |
||||
|
|
||||
|
func TestHttp(t *testing.T) { |
||||
|
resp, err := dtmcli.RestyClient.R().SetQueryParam("panic_string", "1").Post(examples.Busi + "/TestPanic") |
||||
|
assert.Nil(t, err) |
||||
|
assert.Contains(t, resp.String(), "panic_string") |
||||
|
resp, err = dtmcli.RestyClient.R().SetQueryParam("panic_error", "1").Post(examples.Busi + "/TestPanic") |
||||
|
assert.Nil(t, err) |
||||
|
assert.Contains(t, resp.String(), "panic_error") |
||||
|
} |
||||
@ -0,0 +1,72 @@ |
|||||
|
package test |
||||
|
|
||||
|
import ( |
||||
|
"testing" |
||||
|
|
||||
|
"github.com/stretchr/testify/assert" |
||||
|
"github.com/yedf/dtm/dtmcli" |
||||
|
"github.com/yedf/dtm/examples" |
||||
|
) |
||||
|
|
||||
|
func TestCSaga(t *testing.T) { |
||||
|
csagaNormal(t) |
||||
|
csagaRollback(t) |
||||
|
csagaRollback2(t) |
||||
|
csagaCommittedOngoing(t) |
||||
|
} |
||||
|
|
||||
|
func genCSaga(gid string, outFailed bool, inFailed bool) *dtmcli.ConcurrentSaga { |
||||
|
dtmcli.Logf("beginning a concurrent saga test ---------------- %s", gid) |
||||
|
csaga := dtmcli.NewConcurrentSaga(examples.DtmServer, gid) |
||||
|
req := examples.GenTransReq(30, outFailed, inFailed) |
||||
|
csaga.Add(examples.Busi+"/TransOut", examples.Busi+"/TransOutRevert", &req) |
||||
|
csaga.Add(examples.Busi+"/TransIn", examples.Busi+"/TransInRevert", &req) |
||||
|
return csaga |
||||
|
} |
||||
|
|
||||
|
func csagaNormal(t *testing.T) { |
||||
|
csaga := genCSaga("gid-noraml-csaga", false, false) |
||||
|
csaga.Submit() |
||||
|
WaitTransProcessed(csaga.Gid) |
||||
|
assert.Equal(t, []string{dtmcli.StatusPrepared, dtmcli.StatusSucceed, dtmcli.StatusPrepared, dtmcli.StatusSucceed}, getBranchesStatus(csaga.Gid)) |
||||
|
assert.Equal(t, dtmcli.StatusSucceed, getTransStatus(csaga.Gid)) |
||||
|
} |
||||
|
|
||||
|
func csagaRollback(t *testing.T) { |
||||
|
csaga := genCSaga("gid-rollback-csaga", true, false) |
||||
|
examples.MainSwitch.TransOutRevertResult.SetOnce(dtmcli.ResultOngoing) |
||||
|
err := csaga.Submit() |
||||
|
assert.Nil(t, err) |
||||
|
WaitTransProcessed(csaga.Gid) |
||||
|
assert.Equal(t, dtmcli.StatusAborting, getTransStatus(csaga.Gid)) |
||||
|
CronTransOnce() |
||||
|
assert.Equal(t, dtmcli.StatusFailed, getTransStatus(csaga.Gid)) |
||||
|
assert.Equal(t, []string{dtmcli.StatusSucceed, dtmcli.StatusFailed, dtmcli.StatusSucceed, dtmcli.StatusSucceed}, getBranchesStatus(csaga.Gid)) |
||||
|
err = csaga.Submit() |
||||
|
assert.Error(t, err) |
||||
|
} |
||||
|
|
||||
|
func csagaRollback2(t *testing.T) { |
||||
|
csaga := genCSaga("gid-rollback-csaga2", true, false) |
||||
|
csaga.AddStepOrder(1, []int{0}) |
||||
|
err := csaga.Submit() |
||||
|
assert.Nil(t, err) |
||||
|
WaitTransProcessed(csaga.Gid) |
||||
|
assert.Equal(t, dtmcli.StatusFailed, getTransStatus(csaga.Gid)) |
||||
|
assert.Equal(t, []string{dtmcli.StatusSucceed, dtmcli.StatusFailed, dtmcli.StatusPrepared, dtmcli.StatusPrepared}, getBranchesStatus(csaga.Gid)) |
||||
|
err = csaga.Submit() |
||||
|
assert.Error(t, err) |
||||
|
} |
||||
|
|
||||
|
func csagaCommittedOngoing(t *testing.T) { |
||||
|
csaga := genCSaga("gid-committed-ongoing-csaga", false, false) |
||||
|
examples.MainSwitch.TransOutResult.SetOnce(dtmcli.ResultOngoing) |
||||
|
csaga.Submit() |
||||
|
WaitTransProcessed(csaga.Gid) |
||||
|
assert.Equal(t, []string{dtmcli.StatusPrepared, dtmcli.StatusDoing, dtmcli.StatusPrepared, dtmcli.StatusSucceed}, getBranchesStatus(csaga.Gid)) |
||||
|
assert.Equal(t, dtmcli.StatusSubmitted, getTransStatus(csaga.Gid)) |
||||
|
|
||||
|
CronTransOnce() |
||||
|
assert.Equal(t, []string{dtmcli.StatusPrepared, dtmcli.StatusSucceed, dtmcli.StatusPrepared, dtmcli.StatusSucceed}, getBranchesStatus(csaga.Gid)) |
||||
|
assert.Equal(t, dtmcli.StatusSucceed, getTransStatus(csaga.Gid)) |
||||
|
} |
||||
Loading…
Reference in new issue