mirror of https://github.com/dtm-labs/dtm.git
15 changed files with 577 additions and 416 deletions
@ -0,0 +1,39 @@ |
|||||
|
package common |
||||
|
|
||||
|
import ( |
||||
|
"testing" |
||||
|
|
||||
|
"github.com/go-playground/assert/v2" |
||||
|
) |
||||
|
|
||||
|
type testConfig struct { |
||||
|
Mysql map[string]string |
||||
|
} |
||||
|
|
||||
|
var config = testConfig{} |
||||
|
|
||||
|
var myinit int = func() int { |
||||
|
InitApp(&config) |
||||
|
return 0 |
||||
|
}() |
||||
|
|
||||
|
func TestDb(t *testing.T) { |
||||
|
db := DbGet(config.Mysql) |
||||
|
err := func() (rerr error) { |
||||
|
defer P2E(&rerr) |
||||
|
dbr := db.NoMust().Exec("select a") |
||||
|
assert.NotEqual(t, nil, dbr.Error) |
||||
|
db.Must().Exec("select a") |
||||
|
return nil |
||||
|
}() |
||||
|
assert.NotEqual(t, nil, err) |
||||
|
} |
||||
|
|
||||
|
func TestDbAlone(t *testing.T) { |
||||
|
db, con := DbAlone(config.Mysql) |
||||
|
dbr := db.Exec("select 1") |
||||
|
assert.Equal(t, nil, dbr.Error) |
||||
|
con.Close() |
||||
|
dbr = db.Exec("select 1") |
||||
|
assert.NotEqual(t, nil, dbr.Error) |
||||
|
} |
||||
@ -0,0 +1,95 @@ |
|||||
|
package common |
||||
|
|
||||
|
import ( |
||||
|
"errors" |
||||
|
"io" |
||||
|
"net/http" |
||||
|
"net/http/httptest" |
||||
|
"strings" |
||||
|
"testing" |
||||
|
|
||||
|
"github.com/gin-gonic/gin" |
||||
|
"github.com/go-playground/assert/v2" |
||||
|
) |
||||
|
|
||||
|
func TestEP(t *testing.T) { |
||||
|
skipped := true |
||||
|
err := func() (rerr error) { |
||||
|
defer P2E(&rerr) |
||||
|
E2P(errors.New("err1")) |
||||
|
skipped = false |
||||
|
return nil |
||||
|
}() |
||||
|
assert.Equal(t, true, skipped) |
||||
|
assert.Equal(t, "err1", err.Error()) |
||||
|
err = CatchP(func() { |
||||
|
PanicIf(true, errors.New("err2")) |
||||
|
}) |
||||
|
assert.Equal(t, "err2", err.Error()) |
||||
|
err = func() (rerr error) { |
||||
|
defer func() { |
||||
|
x := recover() |
||||
|
assert.Equal(t, 1, x) |
||||
|
}() |
||||
|
defer P2E(&rerr) |
||||
|
panic(1) |
||||
|
}() |
||||
|
} |
||||
|
|
||||
|
func TestGid(t *testing.T) { |
||||
|
id1 := GenGid() |
||||
|
id2 := GenGid() |
||||
|
assert.NotEqual(t, id1, id2) |
||||
|
} |
||||
|
|
||||
|
func TestTernary(t *testing.T) { |
||||
|
assert.Equal(t, "1", OrString("", "", "1")) |
||||
|
assert.Equal(t, "", OrString("", "", "")) |
||||
|
assert.Equal(t, "1", If(true, "1", "2")) |
||||
|
assert.Equal(t, "2", If(false, "1", "2")) |
||||
|
} |
||||
|
|
||||
|
func TestMarshal(t *testing.T) { |
||||
|
a := 0 |
||||
|
type e struct { |
||||
|
A int |
||||
|
} |
||||
|
e1 := e{A: 10} |
||||
|
m := map[string]int{} |
||||
|
assert.Equal(t, "1", MustMarshalString(1)) |
||||
|
assert.Equal(t, []byte("1"), MustMarshal(1)) |
||||
|
MustUnmarshal([]byte("2"), &a) |
||||
|
assert.Equal(t, 2, a) |
||||
|
MustUnmarshalString("3", &a) |
||||
|
assert.Equal(t, 3, a) |
||||
|
MustRemarshal(&e1, &m) |
||||
|
assert.Equal(t, 10, m["A"]) |
||||
|
} |
||||
|
|
||||
|
func TestGin(t *testing.T) { |
||||
|
app := GetGinApp() |
||||
|
app.GET("/api/sample", WrapHandler(func(c *gin.Context) (interface{}, error) { |
||||
|
return 1, nil |
||||
|
})) |
||||
|
app.GET("/api/error", WrapHandler(func(c *gin.Context) (interface{}, error) { |
||||
|
return nil, errors.New("err1") |
||||
|
})) |
||||
|
getResultString := func(api string, body io.Reader) string { |
||||
|
req, _ := http.NewRequest("GET", api, body) |
||||
|
w := httptest.NewRecorder() |
||||
|
app.ServeHTTP(w, req) |
||||
|
return string(w.Body.Bytes()) |
||||
|
} |
||||
|
assert.Equal(t, "{\"msg\":\"pong\"}", getResultString("/api/ping", nil)) |
||||
|
assert.Equal(t, "1", getResultString("/api/sample", nil)) |
||||
|
assert.Equal(t, "{\"code\":500,\"message\":\"err1\"}", getResultString("/api/error", strings.NewReader("{}"))) |
||||
|
} |
||||
|
|
||||
|
func TestResty(t *testing.T) { |
||||
|
resp, err := RestyClient.R().Get("http://baidu.com") |
||||
|
assert.Equal(t, nil, err) |
||||
|
err2 := CatchP(func() { |
||||
|
CheckRestySuccess(resp, err) |
||||
|
}) |
||||
|
assert.NotEqual(t, nil, err2) |
||||
|
} |
||||
@ -1,20 +0,0 @@ |
|||||
package dtmsvr |
|
||||
|
|
||||
import "github.com/yedf/dtm/common" |
|
||||
|
|
||||
func dbGet() *common.MyDb { |
|
||||
return common.DbGet(config.Mysql) |
|
||||
} |
|
||||
func writeTransLog(gid string, action string, status string, branch string, detail string) { |
|
||||
db := dbGet() |
|
||||
if detail == "" { |
|
||||
detail = "{}" |
|
||||
} |
|
||||
db.Must().Table("trans_log").Create(M{ |
|
||||
"gid": gid, |
|
||||
"action": action, |
|
||||
"new_status": status, |
|
||||
"branch": branch, |
|
||||
"detail": detail, |
|
||||
}) |
|
||||
} |
|
||||
@ -1,17 +0,0 @@ |
|||||
package dtmsvr |
|
||||
|
|
||||
import ( |
|
||||
"github.com/sirupsen/logrus" |
|
||||
) |
|
||||
|
|
||||
var TransProcessedTestChan chan string = nil // 用于测试时,通知处理结束
|
|
||||
|
|
||||
func WaitTransProcessed(gid string) { |
|
||||
logrus.Printf("waiting for gid %s", gid) |
|
||||
id := <-TransProcessedTestChan |
|
||||
for id != gid { |
|
||||
logrus.Errorf("-------id %s not match gid %s", id, gid) |
|
||||
id = <-TransProcessedTestChan |
|
||||
} |
|
||||
logrus.Printf("finish for gid %s", gid) |
|
||||
} |
|
||||
@ -0,0 +1,78 @@ |
|||||
|
package dtmsvr |
||||
|
|
||||
|
import ( |
||||
|
"fmt" |
||||
|
"strings" |
||||
|
|
||||
|
"github.com/yedf/dtm/common" |
||||
|
) |
||||
|
|
||||
|
type TransSagaProcessor struct { |
||||
|
*TransGlobal |
||||
|
} |
||||
|
|
||||
|
func (t *TransSagaProcessor) GenBranches() []TransBranch { |
||||
|
branches := []TransBranch{} |
||||
|
steps := []M{} |
||||
|
common.MustUnmarshalString(t.Data, &steps) |
||||
|
for _, step := range steps { |
||||
|
for _, branchType := range []string{"compensate", "action"} { |
||||
|
branches = append(branches, TransBranch{ |
||||
|
Gid: t.Gid, |
||||
|
Branch: fmt.Sprintf("%d", len(branches)+1), |
||||
|
Data: step["data"].(string), |
||||
|
Url: step[branchType].(string), |
||||
|
BranchType: branchType, |
||||
|
Status: "prepared", |
||||
|
}) |
||||
|
} |
||||
|
} |
||||
|
return branches |
||||
|
} |
||||
|
|
||||
|
func (t *TransSagaProcessor) ExecBranch(db *common.MyDb, branch *TransBranch) { |
||||
|
resp, err := common.RestyClient.R().SetBody(branch.Data).SetQueryParam("gid", branch.Gid).Post(branch.Url) |
||||
|
e2p(err) |
||||
|
body := resp.String() |
||||
|
t.touch(db) |
||||
|
if strings.Contains(body, "SUCCESS") { |
||||
|
branch.changeStatus(db, "succeed") |
||||
|
} else if branch.BranchType == "action" && strings.Contains(body, "FAIL") { |
||||
|
branch.changeStatus(db, "failed") |
||||
|
} else { |
||||
|
panic(fmt.Errorf("unknown response: %s, will be retried", body)) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func (t *TransSagaProcessor) ProcessOnce(db *common.MyDb, branches []TransBranch) { |
||||
|
t.MayQueryPrepared(db) |
||||
|
if t.Status != "committed" { |
||||
|
return |
||||
|
} |
||||
|
current := 0 // 当前正在处理的步骤
|
||||
|
for ; current < len(branches); current++ { |
||||
|
branch := &branches[current] |
||||
|
if branch.BranchType != "action" || branch.Status != "prepared" { |
||||
|
continue |
||||
|
} |
||||
|
t.ExecBranch(db, branch) |
||||
|
if branch.Status != "succeed" { |
||||
|
break |
||||
|
} |
||||
|
} |
||||
|
if current == len(branches) { // saga 事务完成
|
||||
|
t.changeStatus(db, "succeed") |
||||
|
return |
||||
|
} |
||||
|
for current = current - 1; current >= 0; current-- { |
||||
|
branch := &branches[current] |
||||
|
if branch.BranchType != "compensate" || branch.Status != "prepared" { |
||||
|
continue |
||||
|
} |
||||
|
t.ExecBranch(db, branch) |
||||
|
} |
||||
|
if current != -1 { |
||||
|
panic(fmt.Errorf("saga current not -1")) |
||||
|
} |
||||
|
t.changeStatus(db.Must(), "failed") |
||||
|
} |
||||
@ -0,0 +1,78 @@ |
|||||
|
package dtmsvr |
||||
|
|
||||
|
import ( |
||||
|
"fmt" |
||||
|
"strings" |
||||
|
|
||||
|
"github.com/yedf/dtm/common" |
||||
|
) |
||||
|
|
||||
|
type TransTccProcessor struct { |
||||
|
*TransGlobal |
||||
|
} |
||||
|
|
||||
|
func (t *TransTccProcessor) GenBranches() []TransBranch { |
||||
|
branches := []TransBranch{} |
||||
|
steps := []M{} |
||||
|
common.MustUnmarshalString(t.Data, &steps) |
||||
|
for _, step := range steps { |
||||
|
for _, branchType := range []string{"cancel", "confirm", "try"} { |
||||
|
branches = append(branches, TransBranch{ |
||||
|
Gid: t.Gid, |
||||
|
Branch: fmt.Sprintf("%d", len(branches)+1), |
||||
|
Data: step["data"].(string), |
||||
|
Url: step[branchType].(string), |
||||
|
BranchType: branchType, |
||||
|
Status: "prepared", |
||||
|
}) |
||||
|
} |
||||
|
} |
||||
|
return branches |
||||
|
} |
||||
|
|
||||
|
func (t *TransTccProcessor) ExecBranch(db *common.MyDb, branch *TransBranch) { |
||||
|
resp, err := common.RestyClient.R().SetBody(branch.Data).SetQueryParam("gid", branch.Gid).Post(branch.Url) |
||||
|
e2p(err) |
||||
|
body := resp.String() |
||||
|
t.touch(db) |
||||
|
if strings.Contains(body, "SUCCESS") { |
||||
|
branch.changeStatus(db, "succeed") |
||||
|
} else if branch.BranchType == "try" && strings.Contains(body, "FAIL") { |
||||
|
branch.changeStatus(db, "failed") |
||||
|
} else { |
||||
|
panic(fmt.Errorf("unknown response: %s, will be retried", body)) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func (t *TransTccProcessor) ProcessOnce(db *common.MyDb, branches []TransBranch) { |
||||
|
t.MayQueryPrepared(db) |
||||
|
if t.Status != "committed" { |
||||
|
return |
||||
|
} |
||||
|
current := 0 // 当前正在处理的步骤
|
||||
|
// 先处理一轮正常try状态
|
||||
|
for ; current < len(branches); current++ { |
||||
|
branch := &branches[current] |
||||
|
if branch.BranchType != "try" || branch.Status == "succeed" { |
||||
|
continue |
||||
|
} |
||||
|
if branch.BranchType == "try" && branch.Status == "prepared" { |
||||
|
t.ExecBranch(db, branch) |
||||
|
if branch.Status != "succeed" { |
||||
|
break |
||||
|
} |
||||
|
} else { |
||||
|
break |
||||
|
} |
||||
|
} |
||||
|
// 如果try全部成功,则处理confirm分支,否则处理cancel分支
|
||||
|
currentType := common.If(current == len(branches), "confirm", "cancel") |
||||
|
for current--; current >= 0; current-- { |
||||
|
branch := &branches[current] |
||||
|
if branch.BranchType != currentType || branch.Status != "prepared" { |
||||
|
continue |
||||
|
} |
||||
|
t.ExecBranch(db, branch) |
||||
|
} |
||||
|
t.changeStatus(db, common.If(currentType == "confirm", "succeed", "failed").(string)) |
||||
|
} |
||||
@ -0,0 +1,44 @@ |
|||||
|
package dtmsvr |
||||
|
|
||||
|
import ( |
||||
|
"fmt" |
||||
|
"strings" |
||||
|
|
||||
|
"github.com/yedf/dtm/common" |
||||
|
) |
||||
|
|
||||
|
type TransXaProcessor struct { |
||||
|
*TransGlobal |
||||
|
} |
||||
|
|
||||
|
func (t *TransXaProcessor) GenBranches() []TransBranch { |
||||
|
return []TransBranch{} |
||||
|
} |
||||
|
func (t *TransXaProcessor) ExecBranch(db *common.MyDb, branch *TransBranch) { |
||||
|
resp, err := common.RestyClient.R().SetBody(M{ |
||||
|
"branch": branch.Branch, |
||||
|
"action": common.If(t.Status == "prepared", "rollback", "commit"), |
||||
|
"gid": branch.Gid, |
||||
|
}).Post(branch.Url) |
||||
|
e2p(err) |
||||
|
body := resp.String() |
||||
|
t.touch(db) |
||||
|
if strings.Contains(body, "SUCCESS") { |
||||
|
branch.changeStatus(db, "succeed") |
||||
|
} else { |
||||
|
panic(fmt.Errorf("bad response: %s", body)) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func (t *TransXaProcessor) ProcessOnce(db *common.MyDb, branches []TransBranch) { |
||||
|
if t.Status == "succeed" { |
||||
|
return |
||||
|
} |
||||
|
currentType := common.If(t.Status == "committed", "commit", "rollback").(string) |
||||
|
for _, branch := range branches { |
||||
|
if branch.BranchType == currentType && branch.Status != "succeed" { |
||||
|
t.ExecBranch(db, &branch) |
||||
|
} |
||||
|
} |
||||
|
t.changeStatus(db, common.If(t.Status == "committed", "succeed", "failed").(string)) |
||||
|
} |
||||
@ -1,158 +0,0 @@ |
|||||
package dtmsvr |
|
||||
|
|
||||
import ( |
|
||||
"fmt" |
|
||||
"time" |
|
||||
|
|
||||
"github.com/gin-gonic/gin" |
|
||||
"github.com/sirupsen/logrus" |
|
||||
"github.com/yedf/dtm/common" |
|
||||
"gorm.io/gorm" |
|
||||
"gorm.io/gorm/clause" |
|
||||
) |
|
||||
|
|
||||
type M = map[string]interface{} |
|
||||
|
|
||||
var p2e = common.P2E |
|
||||
var e2p = common.E2P |
|
||||
|
|
||||
type TransGlobal struct { |
|
||||
common.ModelBase |
|
||||
Gid string `json:"gid"` |
|
||||
TransType string `json:"trans_type"` |
|
||||
Data string `json:"data"` |
|
||||
Status string `json:"status"` |
|
||||
QueryPrepared string `json:"query_prepared"` |
|
||||
CommitTime *time.Time |
|
||||
FinishTime *time.Time |
|
||||
RollbackTime *time.Time |
|
||||
} |
|
||||
|
|
||||
func (*TransGlobal) TableName() string { |
|
||||
return "trans_global" |
|
||||
} |
|
||||
|
|
||||
func (t *TransGlobal) touch(db *common.MyDb) *gorm.DB { |
|
||||
writeTransLog(t.Gid, "touch trans", "", "", "") |
|
||||
return db.Model(&TransGlobal{}).Where("gid=?", t.Gid).Update("gid", t.Gid) // 更新update_time,避免被定时任务再次
|
|
||||
} |
|
||||
|
|
||||
func (t *TransGlobal) changeStatus(db *common.MyDb, status string) *gorm.DB { |
|
||||
writeTransLog(t.Gid, "change status", status, "", "") |
|
||||
updates := M{ |
|
||||
"status": status, |
|
||||
} |
|
||||
if status == "succeed" { |
|
||||
updates["finish_time"] = time.Now() |
|
||||
} else if status == "failed" { |
|
||||
updates["rollback_time"] = time.Now() |
|
||||
} |
|
||||
dbr := db.Must().Model(t).Where("status=?", t.Status).Updates(updates) |
|
||||
checkAffected(dbr) |
|
||||
t.Status = status |
|
||||
return dbr |
|
||||
} |
|
||||
|
|
||||
type TransBranch struct { |
|
||||
common.ModelBase |
|
||||
Gid string |
|
||||
Url string |
|
||||
Data string |
|
||||
Branch string |
|
||||
BranchType string |
|
||||
Status string |
|
||||
FinishTime *time.Time |
|
||||
RollbackTime *time.Time |
|
||||
} |
|
||||
|
|
||||
func (*TransBranch) TableName() string { |
|
||||
return "trans_branch" |
|
||||
} |
|
||||
|
|
||||
func (t *TransBranch) changeStatus(db *common.MyDb, status string) *gorm.DB { |
|
||||
writeTransLog(t.Gid, "step change", status, t.Branch, "") |
|
||||
dbr := db.Must().Model(t).Where("status=?", t.Status).Updates(M{ |
|
||||
"status": status, |
|
||||
"finish_time": time.Now(), |
|
||||
}) |
|
||||
checkAffected(dbr) |
|
||||
t.Status = status |
|
||||
return dbr |
|
||||
} |
|
||||
|
|
||||
func checkAffected(db1 *gorm.DB) { |
|
||||
if db1.RowsAffected == 0 { |
|
||||
panic(fmt.Errorf("duplicate updating")) |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
func (trans *TransGlobal) getProcessor() TransProcessor { |
|
||||
if trans.TransType == "saga" { |
|
||||
return &TransSagaProcessor{TransGlobal: trans} |
|
||||
} else if trans.TransType == "tcc" { |
|
||||
return &TransTccProcessor{TransGlobal: trans} |
|
||||
} else if trans.TransType == "xa" { |
|
||||
return &TransXaProcessor{TransGlobal: trans} |
|
||||
} |
|
||||
return nil |
|
||||
} |
|
||||
|
|
||||
func (trans *TransGlobal) Process(db *common.MyDb) { |
|
||||
defer handlePanic() |
|
||||
defer func() { |
|
||||
if TransProcessedTestChan != nil { |
|
||||
TransProcessedTestChan <- trans.Gid |
|
||||
} |
|
||||
}() |
|
||||
branches := []TransBranch{} |
|
||||
db.Must().Where("gid=?", trans.Gid).Order("id asc").Find(&branches) |
|
||||
trans.getProcessor().ProcessOnce(db, branches) |
|
||||
} |
|
||||
|
|
||||
func (t *TransGlobal) SaveNew(db *common.MyDb) { |
|
||||
err := db.Transaction(func(db1 *gorm.DB) error { |
|
||||
db := &common.MyDb{DB: db1} |
|
||||
|
|
||||
writeTransLog(t.Gid, "create trans", t.Status, "", t.Data) |
|
||||
dbr := db.Must().Clauses(clause.OnConflict{ |
|
||||
DoNothing: true, |
|
||||
}).Create(t) |
|
||||
if dbr.RowsAffected == 0 && t.Status == "committed" { // 如果数据库已经存放了prepared的事务,则修改状态
|
|
||||
dbr = db.Must().Model(&TransGlobal{}).Where("gid=? and status=?", t.Gid, "prepared").Update("status", t.Status) |
|
||||
} |
|
||||
if dbr.RowsAffected == 0 { // 未保存任何数据,直接返回
|
|
||||
return nil |
|
||||
} |
|
||||
// 保存所有的分支
|
|
||||
nsteps := t.getProcessor().GenBranches() |
|
||||
if len(nsteps) > 0 { |
|
||||
writeTransLog(t.Gid, "save steps", t.Status, "", common.MustMarshalString(nsteps)) |
|
||||
db.Must().Clauses(clause.OnConflict{ |
|
||||
DoNothing: true, |
|
||||
}).Create(&nsteps) |
|
||||
} |
|
||||
return nil |
|
||||
}) |
|
||||
e2p(err) |
|
||||
} |
|
||||
|
|
||||
func TransFromContext(c *gin.Context) *TransGlobal { |
|
||||
data := M{} |
|
||||
b, err := c.GetRawData() |
|
||||
e2p(err) |
|
||||
common.MustUnmarshal(b, &data) |
|
||||
logrus.Printf("creating trans in prepare") |
|
||||
if data["steps"] != nil { |
|
||||
data["data"] = common.MustMarshalString(data["steps"]) |
|
||||
} |
|
||||
m := TransGlobal{} |
|
||||
common.MustRemarshal(data, &m) |
|
||||
return &m |
|
||||
} |
|
||||
|
|
||||
func TransFromDb(db *common.MyDb, gid string) *TransGlobal { |
|
||||
m := TransGlobal{} |
|
||||
dbr := db.Must().Model(&m).Where("gid=?", gid).First(&m) |
|
||||
e2p(dbr.Error) |
|
||||
return &m |
|
||||
} |
|
||||
@ -0,0 +1,40 @@ |
|||||
|
package dtmsvr |
||||
|
|
||||
|
import ( |
||||
|
"github.com/sirupsen/logrus" |
||||
|
"github.com/yedf/dtm/common" |
||||
|
) |
||||
|
|
||||
|
type M = map[string]interface{} |
||||
|
|
||||
|
var p2e = common.P2E |
||||
|
var e2p = common.E2P |
||||
|
|
||||
|
func dbGet() *common.MyDb { |
||||
|
return common.DbGet(config.Mysql) |
||||
|
} |
||||
|
func writeTransLog(gid string, action string, status string, branch string, detail string) { |
||||
|
db := dbGet() |
||||
|
if detail == "" { |
||||
|
detail = "{}" |
||||
|
} |
||||
|
db.Must().Table("trans_log").Create(M{ |
||||
|
"gid": gid, |
||||
|
"action": action, |
||||
|
"new_status": status, |
||||
|
"branch": branch, |
||||
|
"detail": detail, |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
var TransProcessedTestChan chan string = nil // 用于测试时,通知处理结束
|
||||
|
|
||||
|
func WaitTransProcessed(gid string) { |
||||
|
logrus.Printf("waiting for gid %s", gid) |
||||
|
id := <-TransProcessedTestChan |
||||
|
for id != gid { |
||||
|
logrus.Errorf("-------id %s not match gid %s", id, gid) |
||||
|
id = <-TransProcessedTestChan |
||||
|
} |
||||
|
logrus.Printf("finish for gid %s", gid) |
||||
|
} |
||||
Loading…
Reference in new issue