mirror of https://github.com/dtm-labs/dtm.git
committed by
GitHub
21 changed files with 457 additions and 396 deletions
@ -1,341 +0,0 @@ |
|||
package dtmsvr |
|||
|
|||
import ( |
|||
"errors" |
|||
"fmt" |
|||
"strings" |
|||
"time" |
|||
|
|||
"github.com/gin-gonic/gin" |
|||
"github.com/yedf/dtm/common" |
|||
"github.com/yedf/dtm/dtmcli" |
|||
"github.com/yedf/dtm/dtmcli/dtmimp" |
|||
"github.com/yedf/dtm/dtmgrpc/dtmgimp" |
|||
"google.golang.org/grpc/codes" |
|||
"google.golang.org/grpc/status" |
|||
"gorm.io/gorm" |
|||
"gorm.io/gorm/clause" |
|||
) |
|||
|
|||
var errUniqueConflict = errors.New("unique key conflict error") |
|||
|
|||
// TransGlobal global transaction
|
|||
type TransGlobal struct { |
|||
common.ModelBase |
|||
Gid string `json:"gid"` |
|||
TransType string `json:"trans_type"` |
|||
Steps []map[string]string `json:"steps" gorm:"-"` |
|||
Payloads []string `json:"payloads" gorm:"-"` |
|||
BinPayloads [][]byte `json:"-" gorm:"-"` |
|||
Status string `json:"status"` |
|||
QueryPrepared string `json:"query_prepared"` |
|||
Protocol string `json:"protocol"` |
|||
CommitTime *time.Time |
|||
FinishTime *time.Time |
|||
RollbackTime *time.Time |
|||
Options string |
|||
CustomData string `json:"custom_data"` |
|||
NextCronInterval int64 |
|||
NextCronTime *time.Time |
|||
dtmimp.TransOptions |
|||
lastTouched time.Time // record the start time of process
|
|||
} |
|||
|
|||
// TableName TableName
|
|||
func (*TransGlobal) TableName() string { |
|||
return "dtm.trans_global" |
|||
} |
|||
|
|||
type transProcessor interface { |
|||
GenBranches() []TransBranch |
|||
ProcessOnce(db *common.DB, branches []TransBranch) error |
|||
} |
|||
|
|||
func (t *TransGlobal) touch(db *common.DB, ctype cronType) *gorm.DB { |
|||
t.lastTouched = time.Now() |
|||
updates := t.setNextCron(ctype) |
|||
return db.Model(&TransGlobal{}).Where("gid=?", t.Gid).Select(updates).Updates(t) |
|||
} |
|||
|
|||
func (t *TransGlobal) changeStatus(db *common.DB, status string) *gorm.DB { |
|||
old := t.Status |
|||
t.Status = status |
|||
updates := t.setNextCron(cronReset) |
|||
updates = append(updates, "status") |
|||
now := time.Now() |
|||
if status == dtmcli.StatusSucceed { |
|||
t.FinishTime = &now |
|||
updates = append(updates, "finish_time") |
|||
} else if status == dtmcli.StatusFailed { |
|||
t.RollbackTime = &now |
|||
updates = append(updates, "rollback_time") |
|||
} |
|||
dbr := db.Must().Model(t).Where("status=?", old).Select(updates).Updates(t) |
|||
checkAffected(dbr) |
|||
return dbr |
|||
} |
|||
|
|||
func (t *TransGlobal) isTimeout() bool { |
|||
timeout := t.TimeoutToFail |
|||
if t.TimeoutToFail == 0 && t.TransType != "saga" { |
|||
timeout = config.TimeoutToFail |
|||
} |
|||
if timeout == 0 { |
|||
return false |
|||
} |
|||
return time.Since(*t.CreateTime)+NowForwardDuration >= time.Duration(timeout)*time.Second |
|||
} |
|||
|
|||
func (t *TransGlobal) needProcess() bool { |
|||
return t.Status == dtmcli.StatusSubmitted || t.Status == dtmcli.StatusAborting || t.Status == dtmcli.StatusPrepared && t.isTimeout() |
|||
} |
|||
|
|||
// TransBranch branch transaction
|
|||
type TransBranch struct { |
|||
common.ModelBase |
|||
Gid string |
|||
URL string `json:"url"` |
|||
BinData []byte |
|||
BranchID string `json:"branch_id"` |
|||
Op string |
|||
Status string |
|||
FinishTime *time.Time |
|||
RollbackTime *time.Time |
|||
} |
|||
|
|||
// TableName TableName
|
|||
func (*TransBranch) TableName() string { |
|||
return "dtm.trans_branch_op" |
|||
} |
|||
|
|||
func (t *TransBranch) changeStatus(db *common.DB, status string) *gorm.DB { |
|||
if common.DtmConfig.UpdateBranchSync > 0 { |
|||
dbr := db.Must().Model(t).Updates(map[string]interface{}{ |
|||
"status": status, |
|||
"finish_time": time.Now(), |
|||
}) |
|||
checkAffected(dbr) |
|||
} else { // 为了性能优化,把branch的status更新异步化
|
|||
updateBranchAsyncChan <- branchStatus{id: t.ID, status: status} |
|||
} |
|||
t.Status = status |
|||
return db.DB |
|||
} |
|||
|
|||
func checkAffected(db1 *gorm.DB) { |
|||
if db1.RowsAffected == 0 { |
|||
panic(fmt.Errorf("rows affected 0, please check for abnormal trans")) |
|||
} |
|||
} |
|||
|
|||
type processorCreator func(*TransGlobal) transProcessor |
|||
|
|||
var processorFac = map[string]processorCreator{} |
|||
|
|||
func registorProcessorCreator(transType string, creator processorCreator) { |
|||
processorFac[transType] = creator |
|||
} |
|||
|
|||
func (t *TransGlobal) getProcessor() transProcessor { |
|||
return processorFac[t.TransType](t) |
|||
} |
|||
|
|||
// Process process global transaction once
|
|||
func (t *TransGlobal) Process(db *common.DB) map[string]interface{} { |
|||
r := t.process(db) |
|||
transactionMetrics(t, r["dtm_result"] == dtmcli.ResultSuccess) |
|||
return r |
|||
} |
|||
|
|||
func (t *TransGlobal) process(db *common.DB) map[string]interface{} { |
|||
if t.Options != "" { |
|||
dtmimp.MustUnmarshalString(t.Options, &t.TransOptions) |
|||
} |
|||
|
|||
if !t.WaitResult { |
|||
go t.processInner(db) |
|||
return dtmcli.MapSuccess |
|||
} |
|||
submitting := t.Status == dtmcli.StatusSubmitted |
|||
err := t.processInner(db) |
|||
if err != nil { |
|||
return map[string]interface{}{"dtm_result": dtmcli.ResultFailure, "message": err.Error()} |
|||
} |
|||
if submitting && t.Status != dtmcli.StatusSucceed { |
|||
return map[string]interface{}{"dtm_result": dtmcli.ResultFailure, "message": "trans failed by user"} |
|||
} |
|||
return dtmcli.MapSuccess |
|||
} |
|||
|
|||
func (t *TransGlobal) processInner(db *common.DB) (rerr error) { |
|||
defer handlePanic(&rerr) |
|||
defer func() { |
|||
if rerr != nil { |
|||
dtmimp.LogRedf("processInner got error: %s", rerr.Error()) |
|||
} |
|||
if TransProcessedTestChan != nil { |
|||
dtmimp.Logf("processed: %s", t.Gid) |
|||
TransProcessedTestChan <- t.Gid |
|||
dtmimp.Logf("notified: %s", t.Gid) |
|||
} |
|||
}() |
|||
dtmimp.Logf("processing: %s status: %s", t.Gid, t.Status) |
|||
branches := []TransBranch{} |
|||
db.Must().Where("gid=?", t.Gid).Order("id asc").Find(&branches) |
|||
t.lastTouched = time.Now() |
|||
rerr = t.getProcessor().ProcessOnce(db, branches) |
|||
return |
|||
} |
|||
|
|||
type cronType int |
|||
|
|||
const ( |
|||
cronBackoff cronType = iota |
|||
cronReset |
|||
cronKeep |
|||
) |
|||
|
|||
func (t *TransGlobal) setNextCron(ctype cronType) []string { |
|||
if ctype == cronBackoff { |
|||
t.NextCronInterval = t.NextCronInterval * 2 |
|||
} else if ctype == cronKeep { |
|||
// do nothing
|
|||
} else if t.RetryInterval != 0 { |
|||
t.NextCronInterval = t.RetryInterval |
|||
} else { |
|||
t.NextCronInterval = config.RetryInterval |
|||
} |
|||
|
|||
next := time.Now().Add(time.Duration(t.NextCronInterval) * time.Second) |
|||
t.NextCronTime = &next |
|||
return []string{"next_cron_interval", "next_cron_time"} |
|||
} |
|||
|
|||
func (t *TransGlobal) getURLResult(url string, branchID, op string, branchPayload []byte) (string, error) { |
|||
if t.Protocol == "grpc" { |
|||
dtmimp.PanicIf(strings.HasPrefix(url, "http"), fmt.Errorf("bad url for grpc: %s", url)) |
|||
server, method := dtmgimp.GetServerAndMethod(url) |
|||
conn := dtmgimp.MustGetGrpcConn(server, true) |
|||
ctx := dtmgimp.TransInfo2Ctx(t.Gid, t.TransType, branchID, op, "") |
|||
err := conn.Invoke(ctx, method, branchPayload, []byte{}) |
|||
if err == nil { |
|||
return dtmcli.ResultSuccess, nil |
|||
} |
|||
st, ok := status.FromError(err) |
|||
if ok && st.Code() == codes.Aborted { |
|||
if st.Message() == dtmcli.ResultOngoing { |
|||
return dtmcli.ResultOngoing, nil |
|||
} else if st.Message() == dtmcli.ResultFailure { |
|||
return dtmcli.ResultFailure, nil |
|||
} |
|||
} |
|||
return "", err |
|||
} |
|||
dtmimp.PanicIf(!strings.HasPrefix(url, "http"), fmt.Errorf("bad url for http: %s", url)) |
|||
resp, err := dtmimp.RestyClient.R().SetBody(string(branchPayload)). |
|||
SetQueryParams(map[string]string{ |
|||
"gid": t.Gid, |
|||
"trans_type": t.TransType, |
|||
"branch_id": branchID, |
|||
"op": op, |
|||
}). |
|||
SetHeader("Content-type", "application/json"). |
|||
Execute(dtmimp.If(branchPayload != nil || t.TransType == "xa", "POST", "GET").(string), url) |
|||
if err != nil { |
|||
return "", err |
|||
} |
|||
return resp.String(), nil |
|||
} |
|||
|
|||
func (t *TransGlobal) getBranchResult(branch *TransBranch) (string, error) { |
|||
body, err := t.getURLResult(branch.URL, branch.BranchID, branch.Op, branch.BinData) |
|||
if err != nil { |
|||
return "", err |
|||
} |
|||
if strings.Contains(body, dtmcli.ResultSuccess) { |
|||
return dtmcli.StatusSucceed, nil |
|||
} else if strings.HasSuffix(t.TransType, "saga") && branch.Op == dtmcli.BranchAction && strings.Contains(body, dtmcli.ResultFailure) { |
|||
return dtmcli.StatusFailed, nil |
|||
} else if strings.Contains(body, dtmcli.ResultOngoing) { |
|||
return "", dtmimp.ErrOngoing |
|||
} |
|||
return "", fmt.Errorf("http result should contains SUCCESS|FAILURE|ONGOING. grpc error should return nil|Aborted with message(FAILURE|ONGOING). \nrefer to: https://dtm.pub/summary/arch.html#http\nunkown result will be retried: %s", body) |
|||
} |
|||
|
|||
func (t *TransGlobal) execBranch(db *common.DB, branch *TransBranch) error { |
|||
status, err := t.getBranchResult(branch) |
|||
if status != "" { |
|||
branch.changeStatus(db, status) |
|||
} |
|||
branchMetrics(t, branch, status == dtmcli.StatusSucceed) |
|||
// if time pass 1500ms and NextCronInterval is not default, then reset NextCronInterval
|
|||
if err == nil && time.Since(t.lastTouched)+NowForwardDuration >= 1500*time.Millisecond || |
|||
t.NextCronInterval > config.RetryInterval && t.NextCronInterval > t.RetryInterval { |
|||
t.touch(db, cronReset) |
|||
} else if err == dtmimp.ErrOngoing { |
|||
t.touch(db, cronKeep) |
|||
} else if err != nil { |
|||
t.touch(db, cronBackoff) |
|||
} |
|||
return err |
|||
} |
|||
|
|||
func (t *TransGlobal) saveNew(db *common.DB) error { |
|||
return db.Transaction(func(db1 *gorm.DB) error { |
|||
db := &common.DB{DB: db1} |
|||
t.setNextCron(cronReset) |
|||
t.Options = dtmimp.MustMarshalString(t.TransOptions) |
|||
if t.Options == "{}" { |
|||
t.Options = "" |
|||
} |
|||
dbr := db.Must().Clauses(clause.OnConflict{ |
|||
DoNothing: true, |
|||
}).Create(t) |
|||
if dbr.RowsAffected <= 0 { // 如果这个不是新事务,返回错误
|
|||
return errUniqueConflict |
|||
} |
|||
branches := t.getProcessor().GenBranches() |
|||
if len(branches) > 0 { |
|||
checkLocalhost(branches) |
|||
db.Must().Clauses(clause.OnConflict{ |
|||
DoNothing: true, |
|||
}).Create(&branches) |
|||
} |
|||
return nil |
|||
}) |
|||
} |
|||
|
|||
// TransFromContext TransFromContext
|
|||
func TransFromContext(c *gin.Context) *TransGlobal { |
|||
b, err := c.GetRawData() |
|||
e2p(err) |
|||
m := TransGlobal{} |
|||
dtmimp.MustUnmarshal(b, &m) |
|||
dtmimp.Logf("creating trans in prepare") |
|||
// Payloads will be store in BinPayloads, Payloads is only used to Unmarshal
|
|||
for _, p := range m.Payloads { |
|||
m.BinPayloads = append(m.BinPayloads, []byte(p)) |
|||
} |
|||
for _, d := range m.Steps { |
|||
if d["data"] != "" { |
|||
m.BinPayloads = append(m.BinPayloads, []byte(d["data"])) |
|||
} |
|||
} |
|||
m.Protocol = "http" |
|||
return &m |
|||
} |
|||
|
|||
// TransFromDtmRequest TransFromContext
|
|||
func TransFromDtmRequest(c *dtmgimp.DtmRequest) *TransGlobal { |
|||
r := TransGlobal{ |
|||
Gid: c.Gid, |
|||
TransType: c.TransType, |
|||
QueryPrepared: c.QueryPrepared, |
|||
Protocol: "grpc", |
|||
BinPayloads: c.BinPayloads, |
|||
} |
|||
if c.Steps != "" { |
|||
dtmimp.MustUnmarshalString(c.Steps, &r.Steps) |
|||
} |
|||
return &r |
|||
} |
|||
@ -0,0 +1,127 @@ |
|||
package dtmsvr |
|||
|
|||
import ( |
|||
"errors" |
|||
"fmt" |
|||
"time" |
|||
|
|||
"github.com/gin-gonic/gin" |
|||
"github.com/yedf/dtm/common" |
|||
"github.com/yedf/dtm/dtmcli/dtmimp" |
|||
"github.com/yedf/dtm/dtmgrpc/dtmgimp" |
|||
"gorm.io/gorm" |
|||
) |
|||
|
|||
var errUniqueConflict = errors.New("unique key conflict error") |
|||
|
|||
// TransGlobal global transaction
|
|||
type TransGlobal struct { |
|||
common.ModelBase |
|||
Gid string `json:"gid"` |
|||
TransType string `json:"trans_type"` |
|||
Steps []map[string]string `json:"steps" gorm:"-"` |
|||
Payloads []string `json:"payloads" gorm:"-"` |
|||
BinPayloads [][]byte `json:"-" gorm:"-"` |
|||
Status string `json:"status"` |
|||
QueryPrepared string `json:"query_prepared"` |
|||
Protocol string `json:"protocol"` |
|||
CommitTime *time.Time |
|||
FinishTime *time.Time |
|||
RollbackTime *time.Time |
|||
Options string |
|||
CustomData string `json:"custom_data"` |
|||
NextCronInterval int64 |
|||
NextCronTime *time.Time |
|||
dtmimp.TransOptions |
|||
lastTouched time.Time // record the start time of process
|
|||
updateBranchSync bool |
|||
} |
|||
|
|||
// TableName TableName
|
|||
func (*TransGlobal) TableName() string { |
|||
return "dtm.trans_global" |
|||
} |
|||
|
|||
// TransBranch branch transaction
|
|||
type TransBranch struct { |
|||
common.ModelBase |
|||
Gid string |
|||
URL string `json:"url"` |
|||
BinData []byte |
|||
BranchID string `json:"branch_id"` |
|||
Op string |
|||
Status string |
|||
FinishTime *time.Time |
|||
RollbackTime *time.Time |
|||
} |
|||
|
|||
// TableName TableName
|
|||
func (*TransBranch) TableName() string { |
|||
return "dtm.trans_branch_op" |
|||
} |
|||
|
|||
type transProcessor interface { |
|||
GenBranches() []TransBranch |
|||
ProcessOnce(db *common.DB, branches []TransBranch) error |
|||
} |
|||
|
|||
type processorCreator func(*TransGlobal) transProcessor |
|||
|
|||
var processorFac = map[string]processorCreator{} |
|||
|
|||
func registorProcessorCreator(transType string, creator processorCreator) { |
|||
processorFac[transType] = creator |
|||
} |
|||
|
|||
func (t *TransGlobal) getProcessor() transProcessor { |
|||
return processorFac[t.TransType](t) |
|||
} |
|||
|
|||
type cronType int |
|||
|
|||
const ( |
|||
cronBackoff cronType = iota |
|||
cronReset |
|||
cronKeep |
|||
) |
|||
|
|||
// TransFromContext TransFromContext
|
|||
func TransFromContext(c *gin.Context) *TransGlobal { |
|||
b, err := c.GetRawData() |
|||
e2p(err) |
|||
m := TransGlobal{} |
|||
dtmimp.MustUnmarshal(b, &m) |
|||
dtmimp.Logf("creating trans in prepare") |
|||
// Payloads will be store in BinPayloads, Payloads is only used to Unmarshal
|
|||
for _, p := range m.Payloads { |
|||
m.BinPayloads = append(m.BinPayloads, []byte(p)) |
|||
} |
|||
for _, d := range m.Steps { |
|||
if d["data"] != "" { |
|||
m.BinPayloads = append(m.BinPayloads, []byte(d["data"])) |
|||
} |
|||
} |
|||
m.Protocol = "http" |
|||
return &m |
|||
} |
|||
|
|||
// TransFromDtmRequest TransFromContext
|
|||
func TransFromDtmRequest(c *dtmgimp.DtmRequest) *TransGlobal { |
|||
r := TransGlobal{ |
|||
Gid: c.Gid, |
|||
TransType: c.TransType, |
|||
QueryPrepared: c.QueryPrepared, |
|||
Protocol: "grpc", |
|||
BinPayloads: c.BinPayloads, |
|||
} |
|||
if c.Steps != "" { |
|||
dtmimp.MustUnmarshalString(c.Steps, &r.Steps) |
|||
} |
|||
return &r |
|||
} |
|||
|
|||
func checkAffected(db1 *gorm.DB) { |
|||
if db1.RowsAffected == 0 { |
|||
panic(fmt.Errorf("rows affected 0, please check for abnormal trans")) |
|||
} |
|||
} |
|||
@ -0,0 +1,83 @@ |
|||
package dtmsvr |
|||
|
|||
import ( |
|||
"time" |
|||
|
|||
"github.com/yedf/dtm/common" |
|||
"github.com/yedf/dtm/dtmcli" |
|||
"github.com/yedf/dtm/dtmcli/dtmimp" |
|||
"gorm.io/gorm" |
|||
"gorm.io/gorm/clause" |
|||
) |
|||
|
|||
// Process process global transaction once
|
|||
func (t *TransGlobal) Process(db *common.DB) map[string]interface{} { |
|||
r := t.process(db) |
|||
transactionMetrics(t, r["dtm_result"] == dtmcli.ResultSuccess) |
|||
return r |
|||
} |
|||
|
|||
func (t *TransGlobal) process(db *common.DB) map[string]interface{} { |
|||
if t.Options != "" { |
|||
dtmimp.MustUnmarshalString(t.Options, &t.TransOptions) |
|||
} |
|||
|
|||
if !t.WaitResult { |
|||
go t.processInner(db) |
|||
return dtmcli.MapSuccess |
|||
} |
|||
submitting := t.Status == dtmcli.StatusSubmitted |
|||
err := t.processInner(db) |
|||
if err != nil { |
|||
return map[string]interface{}{"dtm_result": dtmcli.ResultFailure, "message": err.Error()} |
|||
} |
|||
if submitting && t.Status != dtmcli.StatusSucceed { |
|||
return map[string]interface{}{"dtm_result": dtmcli.ResultFailure, "message": "trans failed by user"} |
|||
} |
|||
return dtmcli.MapSuccess |
|||
} |
|||
|
|||
func (t *TransGlobal) processInner(db *common.DB) (rerr error) { |
|||
defer handlePanic(&rerr) |
|||
defer func() { |
|||
if rerr != nil { |
|||
dtmimp.LogRedf("processInner got error: %s", rerr.Error()) |
|||
} |
|||
if TransProcessedTestChan != nil { |
|||
dtmimp.Logf("processed: %s", t.Gid) |
|||
TransProcessedTestChan <- t.Gid |
|||
dtmimp.Logf("notified: %s", t.Gid) |
|||
} |
|||
}() |
|||
dtmimp.Logf("processing: %s status: %s", t.Gid, t.Status) |
|||
branches := []TransBranch{} |
|||
db.Must().Where("gid=?", t.Gid).Order("id asc").Find(&branches) |
|||
t.lastTouched = time.Now() |
|||
rerr = t.getProcessor().ProcessOnce(db, branches) |
|||
return |
|||
} |
|||
|
|||
func (t *TransGlobal) saveNew(db *common.DB) error { |
|||
return db.Transaction(func(db1 *gorm.DB) error { |
|||
db := &common.DB{DB: db1} |
|||
t.setNextCron(cronReset) |
|||
t.Options = dtmimp.MustMarshalString(t.TransOptions) |
|||
if t.Options == "{}" { |
|||
t.Options = "" |
|||
} |
|||
dbr := db.Must().Clauses(clause.OnConflict{ |
|||
DoNothing: true, |
|||
}).Create(t) |
|||
if dbr.RowsAffected <= 0 { // 如果这个不是新事务,返回错误
|
|||
return errUniqueConflict |
|||
} |
|||
branches := t.getProcessor().GenBranches() |
|||
if len(branches) > 0 { |
|||
checkLocalhost(branches) |
|||
db.Must().Clauses(clause.OnConflict{ |
|||
DoNothing: true, |
|||
}).Create(&branches) |
|||
} |
|||
return nil |
|||
}) |
|||
} |
|||
@ -0,0 +1,159 @@ |
|||
package dtmsvr |
|||
|
|||
import ( |
|||
"fmt" |
|||
"strings" |
|||
"time" |
|||
|
|||
"github.com/yedf/dtm/common" |
|||
"github.com/yedf/dtm/dtmcli" |
|||
"github.com/yedf/dtm/dtmcli/dtmimp" |
|||
"github.com/yedf/dtm/dtmgrpc/dtmgimp" |
|||
"google.golang.org/grpc/codes" |
|||
"google.golang.org/grpc/status" |
|||
"gorm.io/gorm" |
|||
"gorm.io/gorm/clause" |
|||
) |
|||
|
|||
func (t *TransGlobal) touch(db *common.DB, ctype cronType) *gorm.DB { |
|||
t.lastTouched = time.Now() |
|||
updates := t.setNextCron(ctype) |
|||
return db.Model(&TransGlobal{}).Where("gid=?", t.Gid).Select(updates).Updates(t) |
|||
} |
|||
|
|||
func (t *TransGlobal) changeStatus(db *common.DB, status string) *gorm.DB { |
|||
old := t.Status |
|||
t.Status = status |
|||
updates := t.setNextCron(cronReset) |
|||
updates = append(updates, "status") |
|||
now := time.Now() |
|||
if status == dtmcli.StatusSucceed { |
|||
t.FinishTime = &now |
|||
updates = append(updates, "finish_time") |
|||
} else if status == dtmcli.StatusFailed { |
|||
t.RollbackTime = &now |
|||
updates = append(updates, "rollback_time") |
|||
} |
|||
dbr := db.Must().Model(&TransGlobal{}).Where("status=? and gid=?", old, t.Gid).Select(updates).Updates(t) |
|||
checkAffected(dbr) |
|||
return dbr |
|||
} |
|||
|
|||
func (t *TransGlobal) changeBranchStatus(db *common.DB, b *TransBranch, status string) { |
|||
if common.DtmConfig.UpdateBranchSync > 0 || t.updateBranchSync { |
|||
err := db.Transaction(func(tx *gorm.DB) error { |
|||
dbr := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Model(&TransGlobal{}).Where("gid=? and status=?", t.Gid, t.Status).Find(&[]TransGlobal{}) |
|||
checkAffected(dbr) // check TransGlobal is not modified
|
|||
dbr = tx.Model(b).Updates(map[string]interface{}{ |
|||
"status": status, |
|||
"finish_time": time.Now(), |
|||
}) |
|||
checkAffected(dbr) |
|||
return dbr.Error |
|||
}) |
|||
e2p(err) |
|||
} else { // 为了性能优化,把branch的status更新异步化
|
|||
updateBranchAsyncChan <- branchStatus{id: b.ID, status: status} |
|||
} |
|||
b.Status = status |
|||
} |
|||
|
|||
func (t *TransGlobal) isTimeout() bool { |
|||
timeout := t.TimeoutToFail |
|||
if t.TimeoutToFail == 0 && t.TransType != "saga" { |
|||
timeout = config.TimeoutToFail |
|||
} |
|||
if timeout == 0 { |
|||
return false |
|||
} |
|||
return time.Since(*t.CreateTime)+NowForwardDuration >= time.Duration(timeout)*time.Second |
|||
} |
|||
|
|||
func (t *TransGlobal) needProcess() bool { |
|||
return t.Status == dtmcli.StatusSubmitted || t.Status == dtmcli.StatusAborting || t.Status == dtmcli.StatusPrepared && t.isTimeout() |
|||
} |
|||
|
|||
func (t *TransGlobal) getURLResult(url string, branchID, op string, branchPayload []byte) (string, error) { |
|||
if t.Protocol == "grpc" { |
|||
dtmimp.PanicIf(strings.HasPrefix(url, "http"), fmt.Errorf("bad url for grpc: %s", url)) |
|||
server, method := dtmgimp.GetServerAndMethod(url) |
|||
conn := dtmgimp.MustGetGrpcConn(server, true) |
|||
ctx := dtmgimp.TransInfo2Ctx(t.Gid, t.TransType, branchID, op, "") |
|||
err := conn.Invoke(ctx, method, branchPayload, []byte{}) |
|||
if err == nil { |
|||
return dtmcli.ResultSuccess, nil |
|||
} |
|||
st, ok := status.FromError(err) |
|||
if ok && st.Code() == codes.Aborted { |
|||
if st.Message() == dtmcli.ResultOngoing { |
|||
return dtmcli.ResultOngoing, nil |
|||
} else if st.Message() == dtmcli.ResultFailure { |
|||
return dtmcli.ResultFailure, nil |
|||
} |
|||
} |
|||
return "", err |
|||
} |
|||
dtmimp.PanicIf(!strings.HasPrefix(url, "http"), fmt.Errorf("bad url for http: %s", url)) |
|||
resp, err := dtmimp.RestyClient.R().SetBody(string(branchPayload)). |
|||
SetQueryParams(map[string]string{ |
|||
"gid": t.Gid, |
|||
"trans_type": t.TransType, |
|||
"branch_id": branchID, |
|||
"op": op, |
|||
}). |
|||
SetHeader("Content-type", "application/json"). |
|||
Execute(dtmimp.If(branchPayload != nil || t.TransType == "xa", "POST", "GET").(string), url) |
|||
if err != nil { |
|||
return "", err |
|||
} |
|||
return resp.String(), nil |
|||
} |
|||
|
|||
func (t *TransGlobal) getBranchResult(branch *TransBranch) (string, error) { |
|||
body, err := t.getURLResult(branch.URL, branch.BranchID, branch.Op, branch.BinData) |
|||
if err != nil { |
|||
return "", err |
|||
} |
|||
if strings.Contains(body, dtmcli.ResultSuccess) { |
|||
return dtmcli.StatusSucceed, nil |
|||
} else if strings.HasSuffix(t.TransType, "saga") && branch.Op == dtmcli.BranchAction && strings.Contains(body, dtmcli.ResultFailure) { |
|||
return dtmcli.StatusFailed, nil |
|||
} else if strings.Contains(body, dtmcli.ResultOngoing) { |
|||
return "", dtmimp.ErrOngoing |
|||
} |
|||
return "", fmt.Errorf("http result should contains SUCCESS|FAILURE|ONGOING. grpc error should return nil|Aborted with message(FAILURE|ONGOING). \nrefer to: https://dtm.pub/summary/arch.html#http\nunkown result will be retried: %s", body) |
|||
} |
|||
|
|||
func (t *TransGlobal) execBranch(db *common.DB, branch *TransBranch) error { |
|||
status, err := t.getBranchResult(branch) |
|||
if status != "" { |
|||
t.changeBranchStatus(db, branch, status) |
|||
} |
|||
branchMetrics(t, branch, status == dtmcli.StatusSucceed) |
|||
// if time pass 1500ms and NextCronInterval is not default, then reset NextCronInterval
|
|||
if err == nil && time.Since(t.lastTouched)+NowForwardDuration >= 1500*time.Millisecond || |
|||
t.NextCronInterval > config.RetryInterval && t.NextCronInterval > t.RetryInterval { |
|||
t.touch(db, cronReset) |
|||
} else if err == dtmimp.ErrOngoing { |
|||
t.touch(db, cronKeep) |
|||
} else if err != nil { |
|||
t.touch(db, cronBackoff) |
|||
} |
|||
return err |
|||
} |
|||
|
|||
func (t *TransGlobal) setNextCron(ctype cronType) []string { |
|||
if ctype == cronBackoff { |
|||
t.NextCronInterval = t.NextCronInterval * 2 |
|||
} else if ctype == cronKeep { |
|||
// do nothing
|
|||
} else if t.RetryInterval != 0 { |
|||
t.NextCronInterval = t.RetryInterval |
|||
} else { |
|||
t.NextCronInterval = config.RetryInterval |
|||
} |
|||
|
|||
next := time.Now().Add(time.Duration(t.NextCronInterval) * time.Second) |
|||
t.NextCronTime = &next |
|||
return []string{"next_cron_interval", "next_cron_time"} |
|||
} |
|||
Loading…
Reference in new issue