├── .github └── workflows │ └── release.yml ├── LICENSE ├── README-cn.md ├── README.md ├── barrier.go ├── barrier_mongo.go ├── barrier_redis.go ├── consts.go ├── dtmimp ├── README-cn.md ├── README.md ├── consts.go ├── db_special.go ├── db_special_test.go ├── trans_base.go ├── trans_xa_base.go ├── types.go ├── types_test.go ├── utils.go ├── utils_test.go └── vars.go ├── go.mod ├── go.sum ├── logger ├── log.go └── logger_test.go ├── msg.go ├── saga.go ├── tcc.go ├── types.go └── xa.go /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | # Sequence of patterns matched against refs/tags 4 | tags: 5 | - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10 6 | 7 | name: Create Release 8 | 9 | jobs: 10 | build: 11 | name: Create Release 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout code 15 | uses: actions/checkout@v2 16 | - name: Create Release 17 | id: create_release 18 | uses: actions/create-release@v1 19 | env: 20 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token 21 | with: 22 | tag_name: ${{ github.ref }} 23 | release_name: Release ${{ github.ref }} 24 | body: | 25 | Changes in this Release 26 | - used with dtm ${{ github.ref }} and later 27 | draft: false 28 | prerelease: false 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2021, yedf 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README-cn.md: -------------------------------------------------------------------------------- 1 | 简体中文 | [English](./README.md) 2 | 3 | ## dtmcli 4 | `dtmcli` 是分布式事务管理器[dtm](https://github.com/dtm-labs/dtm)的客户端sdk 5 | 6 | 这个库的代码与[dtm](https://github.com/dtm-labs/dtm)下的dtmcli代码保持完全同步,如果您需要线上使用,那么当前的dtmcli相关的依赖非常少,对于最终打包的镜像也会很少 7 | 8 | 具体文档和使用方式,请参考[dtm](https://github.com/dtm-labs/dtm) 9 | 10 | ### 完整示例 11 | 12 | [dtmcli-go-sample](https://github.com/dtm-labs/dtmcli-go-sample) 13 | 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Depreceated. See the new client [dtm-labs/client](https://github.com/dtm-labs/client) 2 | 3 | 已弃用。请使用新的SDK:[dtm-labs/client](https://github.com/dtm-labs/client) 4 | -------------------------------------------------------------------------------- /barrier.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021 yedf. All rights reserved. 3 | * Use of this source code is governed by a BSD-style 4 | * license that can be found in the LICENSE file. 5 | */ 6 | 7 | package dtmcli 8 | 9 | import ( 10 | "database/sql" 11 | "fmt" 12 | "net/url" 13 | 14 | "github.com/dtm-labs/dtmcli/dtmimp" 15 | "github.com/dtm-labs/dtmcli/logger" 16 | ) 17 | 18 | // BarrierBusiFunc type for busi func 19 | type BarrierBusiFunc func(tx *sql.Tx) error 20 | 21 | // BranchBarrier every branch info 22 | type BranchBarrier struct { 23 | TransType string 24 | Gid string 25 | BranchID string 26 | Op string 27 | BarrierID int 28 | DBType string // DBTypeMysql | DBTypePostgres 29 | BarrierTableName string 30 | } 31 | 32 | func (bb *BranchBarrier) String() string { 33 | return fmt.Sprintf("transInfo: %s %s %s %s", bb.TransType, bb.Gid, bb.BranchID, bb.Op) 34 | } 35 | 36 | func (bb *BranchBarrier) newBarrierID() string { 37 | bb.BarrierID++ 38 | return fmt.Sprintf("%02d", bb.BarrierID) 39 | } 40 | 41 | // BarrierFromQuery construct transaction info from request 42 | func BarrierFromQuery(qs url.Values) (*BranchBarrier, error) { 43 | return BarrierFrom(dtmimp.EscapeGet(qs, "trans_type"), dtmimp.EscapeGet(qs, "gid"), dtmimp.EscapeGet(qs, "branch_id"), dtmimp.EscapeGet(qs, "op")) 44 | } 45 | 46 | // BarrierFrom construct transaction info from request 47 | func BarrierFrom(transType, gid, branchID, op string) (*BranchBarrier, error) { 48 | ti := &BranchBarrier{ 49 | TransType: transType, 50 | Gid: gid, 51 | BranchID: branchID, 52 | Op: op, 53 | } 54 | if ti.TransType == "" || ti.Gid == "" || ti.BranchID == "" || ti.Op == "" { 55 | return nil, fmt.Errorf("invalid trans info: %v", ti) 56 | } 57 | return ti, nil 58 | } 59 | 60 | // Call see detail description in https://en.dtm.pub/practice/barrier.html 61 | // tx: local transaction connection 62 | // busiCall: busi func 63 | func (bb *BranchBarrier) Call(tx *sql.Tx, busiCall BarrierBusiFunc) (rerr error) { 64 | bid := bb.newBarrierID() 65 | defer dtmimp.DeferDo(&rerr, func() error { 66 | return tx.Commit() 67 | }, func() error { 68 | return tx.Rollback() 69 | }) 70 | originOp := map[string]string{ 71 | dtmimp.OpCancel: dtmimp.OpTry, 72 | dtmimp.OpCompensate: dtmimp.OpAction, 73 | }[bb.Op] 74 | 75 | originAffected, oerr := dtmimp.InsertBarrier(tx, bb.TransType, bb.Gid, bb.BranchID, originOp, bid, bb.Op, bb.DBType, bb.BarrierTableName) 76 | currentAffected, rerr := dtmimp.InsertBarrier(tx, bb.TransType, bb.Gid, bb.BranchID, bb.Op, bid, bb.Op, bb.DBType, bb.BarrierTableName) 77 | logger.Debugf("originAffected: %d currentAffected: %d", originAffected, currentAffected) 78 | 79 | if rerr == nil && bb.Op == dtmimp.MsgDoOp && currentAffected == 0 { // for msg's DoAndSubmit, repeated insert should be rejected. 80 | return ErrDuplicated 81 | } 82 | 83 | if rerr == nil { 84 | rerr = oerr 85 | } 86 | 87 | if (bb.Op == dtmimp.OpCancel || bb.Op == dtmimp.OpCompensate) && originAffected > 0 || // null compensate 88 | currentAffected == 0 { // repeated request or dangled request 89 | return 90 | } 91 | if rerr == nil { 92 | rerr = busiCall(tx) 93 | } 94 | return 95 | } 96 | 97 | // CallWithDB the same as Call, but with *sql.DB 98 | func (bb *BranchBarrier) CallWithDB(db *sql.DB, busiCall BarrierBusiFunc) error { 99 | tx, err := db.Begin() 100 | if err == nil { 101 | err = bb.Call(tx, busiCall) 102 | } 103 | return err 104 | } 105 | 106 | // QueryPrepared queries prepared data 107 | func (bb *BranchBarrier) QueryPrepared(db *sql.DB) error { 108 | _, err := dtmimp.InsertBarrier(db, bb.TransType, bb.Gid, dtmimp.MsgDoBranch0, dtmimp.MsgDoOp, dtmimp.MsgDoBarrier1, dtmimp.OpRollback, bb.DBType, bb.BarrierTableName) 109 | var reason string 110 | if err == nil { 111 | sql := fmt.Sprintf("select reason from %s where gid=? and branch_id=? and op=? and barrier_id=?", dtmimp.BarrierTableName) 112 | sql = dtmimp.GetDBSpecial(bb.DBType).GetPlaceHoldSQL(sql) 113 | logger.Debugf("queryrow: %s", sql, bb.Gid, dtmimp.MsgDoBranch0, dtmimp.MsgDoOp, dtmimp.MsgDoBarrier1) 114 | err = db.QueryRow(sql, bb.Gid, dtmimp.MsgDoBranch0, dtmimp.MsgDoOp, dtmimp.MsgDoBarrier1).Scan(&reason) 115 | } 116 | if reason == dtmimp.OpRollback { 117 | return ErrFailure 118 | } 119 | return err 120 | } 121 | -------------------------------------------------------------------------------- /barrier_mongo.go: -------------------------------------------------------------------------------- 1 | package dtmcli 2 | 3 | import ( 4 | "context" 5 | "strings" 6 | 7 | "github.com/dtm-labs/dtmcli/dtmimp" 8 | "github.com/dtm-labs/dtmcli/logger" 9 | "go.mongodb.org/mongo-driver/bson" 10 | "go.mongodb.org/mongo-driver/mongo" 11 | ) 12 | 13 | // MongoCall sub-trans barrier for mongo. see http://dtm.pub/practice/barrier 14 | // experimental 15 | func (bb *BranchBarrier) MongoCall(mc *mongo.Client, busiCall func(mongo.SessionContext) error) (rerr error) { 16 | bid := bb.newBarrierID() 17 | return mc.UseSession(context.Background(), func(sc mongo.SessionContext) (rerr error) { 18 | rerr = sc.StartTransaction() 19 | if rerr != nil { 20 | return nil 21 | } 22 | defer dtmimp.DeferDo(&rerr, func() error { 23 | return sc.CommitTransaction(sc) 24 | }, func() error { 25 | return sc.AbortTransaction(sc) 26 | }) 27 | originOp := map[string]string{ 28 | dtmimp.OpCancel: dtmimp.OpTry, 29 | dtmimp.OpCompensate: dtmimp.OpAction, 30 | }[bb.Op] 31 | 32 | originAffected, oerr := mongoInsertBarrier(sc, mc, bb.TransType, bb.Gid, bb.BranchID, originOp, bid, bb.Op) 33 | currentAffected, rerr := mongoInsertBarrier(sc, mc, bb.TransType, bb.Gid, bb.BranchID, bb.Op, bid, bb.Op) 34 | logger.Debugf("originAffected: %d currentAffected: %d", originAffected, currentAffected) 35 | 36 | if rerr == nil && bb.Op == dtmimp.MsgDoOp && currentAffected == 0 { // for msg's DoAndSubmit, repeated insert should be rejected. 37 | return ErrDuplicated 38 | } 39 | 40 | if rerr == nil { 41 | rerr = oerr 42 | } 43 | if (bb.Op == dtmimp.OpCancel || bb.Op == dtmimp.OpCompensate) && originAffected > 0 || // null compensate 44 | currentAffected == 0 { // repeated request or dangled request 45 | return 46 | } 47 | if rerr == nil { 48 | rerr = busiCall(sc) 49 | } 50 | return 51 | }) 52 | } 53 | 54 | // MongoQueryPrepared query prepared for redis 55 | // experimental 56 | func (bb *BranchBarrier) MongoQueryPrepared(mc *mongo.Client) error { 57 | _, err := mongoInsertBarrier(context.Background(), mc, bb.TransType, bb.Gid, dtmimp.MsgDoBranch0, dtmimp.MsgDoOp, dtmimp.MsgDoBarrier1, dtmimp.OpRollback) 58 | var result bson.M 59 | if err == nil { 60 | fs := strings.Split(dtmimp.BarrierTableName, ".") 61 | barrier := mc.Database(fs[0]).Collection(fs[1]) 62 | err = barrier.FindOne(context.Background(), bson.D{ 63 | {Key: "gid", Value: bb.Gid}, 64 | {Key: "branch_id", Value: dtmimp.MsgDoBranch0}, 65 | {Key: "op", Value: dtmimp.MsgDoOp}, 66 | {Key: "barrier_id", Value: dtmimp.MsgDoBarrier1}, 67 | }).Decode(&result) 68 | } 69 | var reason string 70 | if err == nil { 71 | reason, _ = result["reason"].(string) 72 | } 73 | if err == nil && reason == dtmimp.OpRollback { 74 | return ErrFailure 75 | } 76 | return err 77 | } 78 | 79 | func mongoInsertBarrier(sc context.Context, mc *mongo.Client, transType string, gid string, branchID string, op string, barrierID string, reason string) (int64, error) { 80 | if op == "" { 81 | return 0, nil 82 | } 83 | fs := strings.Split(dtmimp.BarrierTableName, ".") 84 | barrier := mc.Database(fs[0]).Collection(fs[1]) 85 | r := barrier.FindOne(sc, bson.D{ 86 | {Key: "gid", Value: gid}, 87 | {Key: "branch_id", Value: branchID}, 88 | {Key: "op", Value: op}, 89 | {Key: "barrier_id", Value: barrierID}, 90 | }) 91 | err := r.Err() 92 | if err == mongo.ErrNoDocuments { 93 | _, err = barrier.InsertOne(sc, 94 | bson.D{ 95 | {Key: "trans_type", Value: transType}, 96 | {Key: "gid", Value: gid}, 97 | {Key: "branch_id", Value: branchID}, 98 | {Key: "op", Value: op}, 99 | {Key: "barrier_id", Value: barrierID}, 100 | {Key: "reason", Value: reason}, 101 | }) 102 | return 1, err 103 | } 104 | return 0, err 105 | } 106 | -------------------------------------------------------------------------------- /barrier_redis.go: -------------------------------------------------------------------------------- 1 | package dtmcli 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/dtm-labs/dtmcli/dtmimp" 7 | "github.com/dtm-labs/dtmcli/logger" 8 | "github.com/go-redis/redis/v8" 9 | ) 10 | 11 | // RedisCheckAdjustAmount check the value of key is valid and >= amount. then adjust the amount 12 | func (bb *BranchBarrier) RedisCheckAdjustAmount(rd *redis.Client, key string, amount int, barrierExpire int) error { 13 | bid := bb.newBarrierID() 14 | bkey1 := fmt.Sprintf("%s-%s-%s-%s", bb.Gid, bb.BranchID, bb.Op, bid) 15 | originOp := map[string]string{ 16 | dtmimp.OpCancel: dtmimp.OpTry, 17 | dtmimp.OpCompensate: dtmimp.OpAction, 18 | }[bb.Op] 19 | bkey2 := fmt.Sprintf("%s-%s-%s-%s", bb.Gid, bb.BranchID, originOp, bid) 20 | v, err := rd.Eval(rd.Context(), ` -- RedisCheckAdjustAmount 21 | local v = redis.call('GET', KEYS[1]) 22 | local e1 = redis.call('GET', KEYS[2]) 23 | 24 | if v == false or v + ARGV[1] < 0 then 25 | return 'FAILURE' 26 | end 27 | 28 | if e1 ~= false then 29 | return 'DUPLICATE' 30 | end 31 | 32 | redis.call('SET', KEYS[2], 'op', 'EX', ARGV[3]) 33 | 34 | if ARGV[2] ~= '' then 35 | local e2 = redis.call('GET', KEYS[3]) 36 | if e2 == false then 37 | redis.call('SET', KEYS[3], 'rollback', 'EX', ARGV[3]) 38 | return 39 | end 40 | end 41 | redis.call('INCRBY', KEYS[1], ARGV[1]) 42 | `, []string{key, bkey1, bkey2}, amount, originOp, barrierExpire).Result() 43 | logger.Debugf("lua return v: %v err: %v", v, err) 44 | if err == redis.Nil { 45 | err = nil 46 | } 47 | if err == nil && bb.Op == dtmimp.MsgDoOp && v == "DUPLICATE" { // msg DoAndSubmit should be rejected when duplicate 48 | return ErrDuplicated 49 | } 50 | if err == nil && v == ResultFailure { 51 | err = ErrFailure 52 | } 53 | return err 54 | } 55 | 56 | // RedisQueryPrepared query prepared for redis 57 | func (bb *BranchBarrier) RedisQueryPrepared(rd *redis.Client, barrierExpire int) error { 58 | bkey1 := fmt.Sprintf("%s-%s-%s-%s", bb.Gid, dtmimp.MsgDoBranch0, dtmimp.MsgDoOp, dtmimp.MsgDoBarrier1) 59 | v, err := rd.Eval(rd.Context(), ` -- RedisQueryPrepared 60 | local v = redis.call('GET', KEYS[1]) 61 | if v == false then 62 | redis.call('SET', KEYS[1], 'rollback', 'EX', ARGV[1]) 63 | v = 'rollback' 64 | end 65 | if v == 'rollback' then 66 | return 'FAILURE' 67 | end 68 | `, []string{bkey1}, barrierExpire).Result() 69 | logger.Debugf("lua return v: %v err: %v", v, err) 70 | if err == redis.Nil { 71 | err = nil 72 | } 73 | if err == nil && v == ResultFailure { 74 | err = ErrFailure 75 | } 76 | return err 77 | } 78 | -------------------------------------------------------------------------------- /consts.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021 yedf. All rights reserved. 3 | * Use of this source code is governed by a BSD-style 4 | * license that can be found in the LICENSE file. 5 | */ 6 | 7 | package dtmcli 8 | 9 | import ( 10 | "github.com/dtm-labs/dtmcli/dtmimp" 11 | ) 12 | 13 | const ( 14 | // StatusPrepared status for global/branch trans status. 15 | // first step, tx preparation period 16 | StatusPrepared = "prepared" 17 | // StatusSubmitted status for global trans status. 18 | StatusSubmitted = "submitted" 19 | // StatusSucceed status for global/branch trans status. 20 | StatusSucceed = "succeed" 21 | // StatusFailed status for global/branch trans status. 22 | // NOTE: change global status to failed can stop trigger (Not recommended in production env) 23 | StatusFailed = "failed" 24 | // StatusAborting status for global trans status. 25 | StatusAborting = "aborting" 26 | 27 | // ResultSuccess for result of a trans/trans branch 28 | ResultSuccess = dtmimp.ResultSuccess 29 | // ResultFailure for result of a trans/trans branch 30 | ResultFailure = dtmimp.ResultFailure 31 | // ResultOngoing for result of a trans/trans branch 32 | ResultOngoing = dtmimp.ResultOngoing 33 | 34 | // DBTypeMysql const for driver mysql 35 | DBTypeMysql = dtmimp.DBTypeMysql 36 | // DBTypePostgres const for driver postgres 37 | DBTypePostgres = dtmimp.DBTypePostgres 38 | ) 39 | 40 | // MapSuccess HTTP result of SUCCESS 41 | var MapSuccess = dtmimp.MapSuccess 42 | 43 | // MapFailure HTTP result of FAILURE 44 | var MapFailure = dtmimp.MapFailure 45 | 46 | // ErrFailure error for returned failure 47 | var ErrFailure = dtmimp.ErrFailure 48 | 49 | // ErrOngoing error for returned ongoing 50 | var ErrOngoing = dtmimp.ErrOngoing 51 | 52 | // ErrDuplicated error of DUPLICATED for only msg 53 | // if QueryPrepared executed before call. then DoAndSubmit return this error 54 | var ErrDuplicated = dtmimp.ErrDuplicated 55 | -------------------------------------------------------------------------------- /dtmimp/README-cn.md: -------------------------------------------------------------------------------- 1 | ## 注意 2 | 此包带imp后缀,主要被dtm内部使用,相关接口可能会发生变更,请勿使用这里的接口 -------------------------------------------------------------------------------- /dtmimp/README.md: -------------------------------------------------------------------------------- 1 | ## Notice 2 | Please donot use this package, and this package should only be used in dtm internally. The interfaces are not stable, and package name has postfix "imp" -------------------------------------------------------------------------------- /dtmimp/consts.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021 yedf. All rights reserved. 3 | * Use of this source code is governed by a BSD-style 4 | * license that can be found in the LICENSE file. 5 | */ 6 | 7 | package dtmimp 8 | 9 | const ( 10 | // ResultFailure for result of a trans/trans branch 11 | // Same as HTTP status 409 and GRPC code 10 12 | ResultFailure = "FAILURE" 13 | // ResultSuccess for result of a trans/trans branch 14 | // Same as HTTP status 200 and GRPC code 0 15 | ResultSuccess = "SUCCESS" 16 | // ResultOngoing for result of a trans/trans branch 17 | // Same as HTTP status 425 and GRPC code 9 18 | ResultOngoing = "ONGOING" 19 | 20 | // OpTry branch type for TCC 21 | OpTry = "try" 22 | // OpConfirm branch type for TCC 23 | OpConfirm = "confirm" 24 | // OpCancel branch type for TCC 25 | OpCancel = "cancel" 26 | // OpAction branch type for message, SAGA, XA 27 | OpAction = "action" 28 | // OpCompensate branch type for SAGA 29 | OpCompensate = "compensate" 30 | // OpCommit branch type for XA 31 | OpCommit = "commit" 32 | // OpRollback branch type for XA 33 | OpRollback = "rollback" 34 | 35 | // DBTypeMysql const for driver mysql 36 | DBTypeMysql = "mysql" 37 | // DBTypePostgres const for driver postgres 38 | DBTypePostgres = "postgres" 39 | // DBTypeRedis const for driver redis 40 | DBTypeRedis = "redis" 41 | // Jrpc const for json-rpc 42 | Jrpc = "json-rpc" 43 | // JrpcCodeFailure const for json-rpc failure 44 | JrpcCodeFailure = -32901 45 | 46 | // JrpcCodeOngoing const for json-rpc ongoing 47 | JrpcCodeOngoing = -32902 48 | 49 | // MsgDoBranch0 const for DoAndSubmit barrier branch 50 | MsgDoBranch0 = "00" 51 | // MsgDoBarrier1 const for DoAndSubmit barrier barrierID 52 | MsgDoBarrier1 = "01" 53 | // MsgDoOp const for DoAndSubmit barrier op 54 | MsgDoOp = "msg" 55 | 56 | // XaBarrier1 const for xa barrier id 57 | XaBarrier1 = "01" 58 | 59 | // ProtocolGRPC const for protocol grpc 60 | ProtocolGRPC = "grpc" 61 | // ProtocolHTTP const for protocol http 62 | ProtocolHTTP = "http" 63 | ) 64 | -------------------------------------------------------------------------------- /dtmimp/db_special.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021 yedf. All rights reserved. 3 | * Use of this source code is governed by a BSD-style 4 | * license that can be found in the LICENSE file. 5 | */ 6 | 7 | package dtmimp 8 | 9 | import ( 10 | "fmt" 11 | "strings" 12 | ) 13 | 14 | // DBSpecial db specific operations 15 | type DBSpecial interface { 16 | GetPlaceHoldSQL(sql string) string 17 | GetInsertIgnoreTemplate(tableAndValues string, pgConstraint string) string 18 | GetXaSQL(command string, xid string) string 19 | } 20 | 21 | var dbSpecials = map[string]DBSpecial{} 22 | var currentDBType = DBTypeMysql 23 | 24 | type mysqlDBSpecial struct{} 25 | 26 | func (*mysqlDBSpecial) GetPlaceHoldSQL(sql string) string { 27 | return sql 28 | } 29 | 30 | func (*mysqlDBSpecial) GetXaSQL(command string, xid string) string { 31 | return fmt.Sprintf("xa %s '%s'", command, xid) 32 | } 33 | 34 | func (*mysqlDBSpecial) GetInsertIgnoreTemplate(tableAndValues string, pgConstraint string) string { 35 | return fmt.Sprintf("insert ignore into %s", tableAndValues) 36 | } 37 | 38 | func init() { 39 | dbSpecials[DBTypeMysql] = &mysqlDBSpecial{} 40 | } 41 | 42 | type postgresDBSpecial struct{} 43 | 44 | func (*postgresDBSpecial) GetXaSQL(command string, xid string) string { 45 | return map[string]string{ 46 | "end": "", 47 | "start": "begin", 48 | "prepare": fmt.Sprintf("prepare transaction '%s'", xid), 49 | "commit": fmt.Sprintf("commit prepared '%s'", xid), 50 | "rollback": fmt.Sprintf("rollback prepared '%s'", xid), 51 | }[command] 52 | } 53 | 54 | func (*postgresDBSpecial) GetPlaceHoldSQL(sql string) string { 55 | pos := 1 56 | parts := []string{} 57 | b := 0 58 | for i := 0; i < len(sql); i++ { 59 | if sql[i] == '?' { 60 | parts = append(parts, sql[b:i]) 61 | b = i + 1 62 | parts = append(parts, fmt.Sprintf("$%d", pos)) 63 | pos++ 64 | } 65 | } 66 | parts = append(parts, sql[b:]) 67 | return strings.Join(parts, "") 68 | } 69 | 70 | func (*postgresDBSpecial) GetInsertIgnoreTemplate(tableAndValues string, pgConstraint string) string { 71 | return fmt.Sprintf("insert into %s on conflict ON CONSTRAINT %s do nothing", tableAndValues, pgConstraint) 72 | } 73 | func init() { 74 | dbSpecials[DBTypePostgres] = &postgresDBSpecial{} 75 | } 76 | 77 | // GetDBSpecial get DBSpecial for currentDBType 78 | func GetDBSpecial(dbType string) DBSpecial { 79 | if dbType == "" { 80 | dbType = currentDBType 81 | } 82 | return dbSpecials[dbType] 83 | } 84 | 85 | // SetCurrentDBType set currentDBType 86 | func SetCurrentDBType(dbType string) { 87 | spec := dbSpecials[dbType] 88 | PanicIf(spec == nil, fmt.Errorf("unknown db type '%s'", dbType)) 89 | currentDBType = dbType 90 | } 91 | 92 | // GetCurrentDBType get currentDBType 93 | func GetCurrentDBType() string { 94 | return currentDBType 95 | } 96 | -------------------------------------------------------------------------------- /dtmimp/db_special_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021 yedf. All rights reserved. 3 | * Use of this source code is governed by a BSD-style 4 | * license that can be found in the LICENSE file. 5 | */ 6 | 7 | package dtmimp 8 | 9 | import ( 10 | "testing" 11 | 12 | "github.com/stretchr/testify/assert" 13 | ) 14 | 15 | func TestDBSpecial(t *testing.T) { 16 | old := currentDBType 17 | assert.Error(t, CatchP(func() { 18 | SetCurrentDBType("no-driver") 19 | })) 20 | SetCurrentDBType(DBTypeMysql) 21 | sp := GetDBSpecial(DBTypeMysql) 22 | 23 | assert.Equal(t, "? ?", sp.GetPlaceHoldSQL("? ?")) 24 | assert.Equal(t, "xa start 'xa1'", sp.GetXaSQL("start", "xa1")) 25 | assert.Equal(t, "insert ignore into a(f) values(?)", sp.GetInsertIgnoreTemplate("a(f) values(?)", "c")) 26 | SetCurrentDBType(DBTypePostgres) 27 | sp = GetDBSpecial(DBTypePostgres) 28 | assert.Equal(t, "$1 $2", sp.GetPlaceHoldSQL("? ?")) 29 | assert.Equal(t, "begin", sp.GetXaSQL("start", "xa1")) 30 | assert.Equal(t, "insert into a(f) values(?) on conflict ON CONSTRAINT c do nothing", sp.GetInsertIgnoreTemplate("a(f) values(?)", "c")) 31 | SetCurrentDBType(old) 32 | } 33 | -------------------------------------------------------------------------------- /dtmimp/trans_base.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021 yedf. All rights reserved. 3 | * Use of this source code is governed by a BSD-style 4 | * license that can be found in the LICENSE file. 5 | */ 6 | 7 | package dtmimp 8 | 9 | import ( 10 | "context" 11 | "errors" 12 | "fmt" 13 | "net/http" 14 | "net/url" 15 | "strings" 16 | "time" 17 | 18 | "github.com/go-resty/resty/v2" 19 | ) 20 | 21 | // BranchIDGen used to generate a sub branch id 22 | type BranchIDGen struct { 23 | BranchID string 24 | subBranchID int 25 | } 26 | 27 | // NewSubBranchID generate a sub branch id 28 | func (g *BranchIDGen) NewSubBranchID() string { 29 | if g.subBranchID >= 99 { 30 | panic(fmt.Errorf("branch id is larger than 99")) 31 | } 32 | if len(g.BranchID) >= 20 { 33 | panic(fmt.Errorf("total branch id is longer than 20")) 34 | } 35 | g.subBranchID = g.subBranchID + 1 36 | return g.CurrentSubBranchID() 37 | } 38 | 39 | // CurrentSubBranchID return current branchID 40 | func (g *BranchIDGen) CurrentSubBranchID() string { 41 | return g.BranchID + fmt.Sprintf("%02d", g.subBranchID) 42 | } 43 | 44 | // TransOptions transaction options 45 | type TransOptions struct { 46 | WaitResult bool `json:"wait_result,omitempty" gorm:"-"` 47 | TimeoutToFail int64 `json:"timeout_to_fail,omitempty" gorm:"-"` // for trans type: xa, tcc, unit: second 48 | RequestTimeout int64 `json:"request_timeout,omitempty" gorm:"-"` // for global trans resets request timeout, unit: second 49 | RetryInterval int64 `json:"retry_interval,omitempty" gorm:"-"` // for trans type: msg saga xa tcc, unit: second 50 | PassthroughHeaders []string `json:"passthrough_headers,omitempty" gorm:"-"` // for inherit the specified gin context headers 51 | BranchHeaders map[string]string `json:"branch_headers,omitempty" gorm:"-"` // custom branch headers, dtm server => service api 52 | Concurrent bool `json:"concurrent" gorm:"-"` // for trans type: saga msg 53 | } 54 | 55 | // TransBase base for all trans 56 | type TransBase struct { 57 | Gid string `json:"gid"` // NOTE: unique in storage, can customize the generation rules instead of using server-side generation, it will help with the tracking 58 | TransType string `json:"trans_type"` 59 | Dtm string `json:"-"` 60 | CustomData string `json:"custom_data,omitempty"` // nosql data persistence 61 | TransOptions 62 | Context context.Context `json:"-" gorm:"-"` 63 | 64 | Steps []map[string]string `json:"steps,omitempty"` // use in MSG/SAGA 65 | Payloads []string `json:"payloads,omitempty"` // used in MSG/SAGA 66 | BinPayloads [][]byte `json:"-"` 67 | BranchIDGen `json:"-"` // used in XA/TCC 68 | Op string `json:"-"` // used in XA/TCC 69 | 70 | QueryPrepared string `json:"query_prepared,omitempty"` // used in MSG 71 | Protocol string `json:"protocol"` 72 | } 73 | 74 | // NewTransBase new a TransBase 75 | func NewTransBase(gid string, transType string, dtm string, branchID string) *TransBase { 76 | return &TransBase{ 77 | Gid: gid, 78 | TransType: transType, 79 | BranchIDGen: BranchIDGen{BranchID: branchID}, 80 | Dtm: dtm, 81 | TransOptions: TransOptions{PassthroughHeaders: PassthroughHeaders}, 82 | Context: context.Background(), 83 | } 84 | } 85 | 86 | // WithGlobalTransRequestTimeout defines global trans request timeout 87 | func (t *TransBase) WithGlobalTransRequestTimeout(timeout int64) { 88 | t.RequestTimeout = timeout 89 | } 90 | 91 | // TransBaseFromQuery construct transaction info from request 92 | func TransBaseFromQuery(qs url.Values) *TransBase { 93 | return NewTransBase(EscapeGet(qs, "gid"), EscapeGet(qs, "trans_type"), EscapeGet(qs, "dtm"), EscapeGet(qs, "branch_id")) 94 | } 95 | 96 | // TransCallDtm TransBase call dtm 97 | func TransCallDtm(tb *TransBase, body interface{}, operation string) error { 98 | if tb.RequestTimeout != 0 { 99 | RestyClient.SetTimeout(time.Duration(tb.RequestTimeout) * time.Second) 100 | } 101 | if tb.Protocol == Jrpc { 102 | var result map[string]interface{} 103 | resp, err := RestyClient.R(). 104 | SetBody(map[string]interface{}{ 105 | "jsonrpc": "2.0", 106 | "id": "no-use", 107 | "method": operation, 108 | "params": body, 109 | }). 110 | SetResult(&result). 111 | Post(tb.Dtm) 112 | if err != nil { 113 | return err 114 | } 115 | if resp.StatusCode() != http.StatusOK || result["error"] != nil { 116 | return errors.New(resp.String()) 117 | } 118 | return nil 119 | } 120 | resp, err := RestyClient.R(). 121 | SetBody(body).Post(fmt.Sprintf("%s/%s", tb.Dtm, operation)) 122 | if err != nil { 123 | return err 124 | } 125 | if resp.StatusCode() != http.StatusOK || strings.Contains(resp.String(), ResultFailure) { 126 | return errors.New(resp.String()) 127 | } 128 | return nil 129 | } 130 | 131 | // TransRegisterBranch TransBase register a branch to dtm 132 | func TransRegisterBranch(tb *TransBase, added map[string]string, operation string) error { 133 | m := map[string]string{ 134 | "gid": tb.Gid, 135 | "trans_type": tb.TransType, 136 | } 137 | for k, v := range added { 138 | m[k] = v 139 | } 140 | return TransCallDtm(tb, m, operation) 141 | } 142 | 143 | // TransRequestBranch TransBase request branch result 144 | func TransRequestBranch(t *TransBase, method string, body interface{}, branchID string, op string, url string) (*resty.Response, error) { 145 | if url == "" { 146 | return nil, nil 147 | } 148 | query := map[string]string{ 149 | "dtm": t.Dtm, 150 | "gid": t.Gid, 151 | "branch_id": branchID, 152 | "trans_type": t.TransType, 153 | "op": op, 154 | } 155 | if t.TransType == "xa" { // xa trans will add notify_url 156 | query["phase2_url"] = url 157 | } 158 | resp, err := RestyClient.R(). 159 | SetBody(body). 160 | SetQueryParams(query). 161 | SetHeaders(t.BranchHeaders). 162 | Execute(method, url) 163 | if err == nil { 164 | err = RespAsErrorCompatible(resp) 165 | } 166 | return resp, err 167 | } 168 | -------------------------------------------------------------------------------- /dtmimp/trans_xa_base.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021 yedf. All rights reserved. 3 | * Use of this source code is governed by a BSD-style 4 | * license that can be found in the LICENSE file. 5 | */ 6 | 7 | package dtmimp 8 | 9 | import ( 10 | "database/sql" 11 | "strings" 12 | ) 13 | 14 | // XaHandlePhase2 Handle the callback of commit/rollback 15 | func XaHandlePhase2(gid string, dbConf DBConf, branchID string, op string) error { 16 | db, err := PooledDB(dbConf) 17 | if err != nil { 18 | return err 19 | } 20 | xaID := gid + "-" + branchID 21 | _, err = DBExec(dbConf.Driver, db, GetDBSpecial(dbConf.Driver).GetXaSQL(op, xaID)) 22 | if err != nil && 23 | (strings.Contains(err.Error(), "XAER_NOTA") || strings.Contains(err.Error(), "does not exist")) { // Repeat commit/rollback with the same id, report this error, ignore 24 | err = nil 25 | } 26 | if op == OpRollback && err == nil { 27 | // rollback insert a row after prepare. no-error means prepare has finished. 28 | _, err = InsertBarrier(db, "xa", gid, branchID, OpAction, XaBarrier1, op, dbConf.Driver, "") 29 | } 30 | return err 31 | } 32 | 33 | // XaHandleLocalTrans public handler of LocalTransaction via http/grpc 34 | func XaHandleLocalTrans(xa *TransBase, dbConf DBConf, cb func(*sql.DB) error) (rerr error) { 35 | xaBranch := xa.Gid + "-" + xa.BranchID 36 | db, rerr := XaDB(dbConf) 37 | if rerr != nil { 38 | return 39 | } 40 | defer func() { _ = db.Close() }() 41 | defer DeferDo(&rerr, func() error { 42 | _, err := DBExec(dbConf.Driver, db, GetDBSpecial(dbConf.Driver).GetXaSQL("prepare", xaBranch)) 43 | return err 44 | }, func() error { 45 | return nil 46 | }) 47 | _, rerr = DBExec(dbConf.Driver, db, GetDBSpecial(dbConf.Driver).GetXaSQL("start", xaBranch)) 48 | if rerr != nil { 49 | return 50 | } 51 | defer func() { 52 | _, _ = DBExec(dbConf.Driver, db, GetDBSpecial(dbConf.Driver).GetXaSQL("end", xaBranch)) 53 | }() 54 | // prepare and rollback both insert a row 55 | _, rerr = InsertBarrier(db, xa.TransType, xa.Gid, xa.BranchID, OpAction, XaBarrier1, OpAction, dbConf.Driver, "") 56 | if rerr == nil { 57 | rerr = cb(db) 58 | } 59 | return 60 | } 61 | 62 | // XaHandleGlobalTrans http/grpc GlobalTransaction shared func 63 | func XaHandleGlobalTrans(xa *TransBase, callDtm func(string) error, callBusi func() error) (rerr error) { 64 | rerr = callDtm("prepare") 65 | if rerr != nil { 66 | return 67 | } 68 | defer DeferDo(&rerr, func() error { 69 | return callDtm("submit") 70 | }, func() error { 71 | return callDtm("abort") 72 | }) 73 | rerr = callBusi() 74 | return 75 | } 76 | -------------------------------------------------------------------------------- /dtmimp/types.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021 yedf. All rights reserved. 3 | * Use of this source code is governed by a BSD-style 4 | * license that can be found in the LICENSE file. 5 | */ 6 | 7 | package dtmimp 8 | 9 | import "database/sql" 10 | 11 | // DB inteface of dtmcli db 12 | type DB interface { 13 | Exec(query string, args ...interface{}) (sql.Result, error) 14 | QueryRow(query string, args ...interface{}) *sql.Row 15 | } 16 | 17 | // DBConf defines db config 18 | type DBConf struct { 19 | Driver string `yaml:"Driver"` 20 | Host string `yaml:"Host"` 21 | Port int64 `yaml:"Port"` 22 | User string `yaml:"User"` 23 | Password string `yaml:"Password"` 24 | Db string `yaml:"Db"` 25 | } 26 | -------------------------------------------------------------------------------- /dtmimp/types_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021 yedf. All rights reserved. 3 | * Use of this source code is governed by a BSD-style 4 | * license that can be found in the LICENSE file. 5 | */ 6 | 7 | package dtmimp 8 | 9 | import ( 10 | "testing" 11 | 12 | "github.com/stretchr/testify/assert" 13 | ) 14 | 15 | func TestTypes(t *testing.T) { 16 | err := CatchP(func() { 17 | idGen := BranchIDGen{BranchID: "12345678901234567890123"} 18 | idGen.NewSubBranchID() 19 | }) 20 | assert.Error(t, err) 21 | err = CatchP(func() { 22 | idGen := BranchIDGen{subBranchID: 99} 23 | idGen.NewSubBranchID() 24 | }) 25 | assert.Error(t, err) 26 | } 27 | -------------------------------------------------------------------------------- /dtmimp/utils.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021 yedf. All rights reserved. 3 | * Use of this source code is governed by a BSD-style 4 | * license that can be found in the LICENSE file. 5 | */ 6 | 7 | package dtmimp 8 | 9 | import ( 10 | "database/sql" 11 | "encoding/json" 12 | "errors" 13 | "fmt" 14 | "net/http" 15 | "net/url" 16 | "os" 17 | "runtime" 18 | "strconv" 19 | "strings" 20 | "sync" 21 | "time" 22 | 23 | "github.com/dtm-labs/dtmcli/logger" 24 | "github.com/go-resty/resty/v2" 25 | ) 26 | 27 | // Logf an alias of Infof 28 | // Deprecated: use logger.Errorf 29 | var Logf = logger.Infof 30 | 31 | // LogRedf an alias of Errorf 32 | // Deprecated: use logger.Errorf 33 | var LogRedf = logger.Errorf 34 | 35 | // FatalIfError fatal if error is not nil 36 | // Deprecated: use logger.FatalIfError 37 | var FatalIfError = logger.FatalIfError 38 | 39 | // LogIfFatalf fatal if cond is true 40 | // Deprecated: use logger.FatalfIf 41 | var LogIfFatalf = logger.FatalfIf 42 | 43 | // AsError wrap a panic value as an error 44 | func AsError(x interface{}) error { 45 | logger.Errorf("panic wrapped to error: '%v'", x) 46 | if e, ok := x.(error); ok { 47 | return e 48 | } 49 | return fmt.Errorf("%v", x) 50 | } 51 | 52 | // P2E panic to error 53 | func P2E(perr *error) { 54 | if x := recover(); x != nil { 55 | *perr = AsError(x) 56 | } 57 | } 58 | 59 | // E2P error to panic 60 | func E2P(err error) { 61 | if err != nil { 62 | panic(err) 63 | } 64 | } 65 | 66 | // CatchP catch panic to error 67 | func CatchP(f func()) (rerr error) { 68 | defer P2E(&rerr) 69 | f() 70 | return nil 71 | } 72 | 73 | // PanicIf name is clear 74 | func PanicIf(cond bool, err error) { 75 | if cond { 76 | panic(err) 77 | } 78 | } 79 | 80 | // MustAtoi is string to int 81 | func MustAtoi(s string) int { 82 | r, err := strconv.Atoi(s) 83 | if err != nil { 84 | E2P(errors.New("convert to int error: " + s)) 85 | } 86 | return r 87 | } 88 | 89 | // OrString return the first not empty string 90 | func OrString(ss ...string) string { 91 | for _, s := range ss { 92 | if s != "" { 93 | return s 94 | } 95 | } 96 | return "" 97 | } 98 | 99 | // If ternary operator 100 | func If(condition bool, trueObj interface{}, falseObj interface{}) interface{} { 101 | if condition { 102 | return trueObj 103 | } 104 | return falseObj 105 | } 106 | 107 | // MustMarshal checked version for marshal 108 | func MustMarshal(v interface{}) []byte { 109 | b, err := json.Marshal(v) 110 | E2P(err) 111 | return b 112 | } 113 | 114 | // MustMarshalString string version of MustMarshal 115 | func MustMarshalString(v interface{}) string { 116 | return string(MustMarshal(v)) 117 | } 118 | 119 | // MustUnmarshal checked version for unmarshal 120 | func MustUnmarshal(b []byte, obj interface{}) { 121 | err := json.Unmarshal(b, obj) 122 | E2P(err) 123 | } 124 | 125 | // MustUnmarshalString string version of MustUnmarshal 126 | func MustUnmarshalString(s string, obj interface{}) { 127 | MustUnmarshal([]byte(s), obj) 128 | } 129 | 130 | // MustRemarshal marshal and unmarshal, and check error 131 | func MustRemarshal(from interface{}, to interface{}) { 132 | b, err := json.Marshal(from) 133 | E2P(err) 134 | err = json.Unmarshal(b, to) 135 | E2P(err) 136 | } 137 | 138 | // GetFuncName get current call func name 139 | func GetFuncName() string { 140 | pc, _, _, _ := runtime.Caller(1) 141 | nm := runtime.FuncForPC(pc).Name() 142 | return nm[strings.LastIndex(nm, ".")+1:] 143 | } 144 | 145 | // MayReplaceLocalhost when run in docker compose, change localhost to host.docker.internal for accessing host network 146 | func MayReplaceLocalhost(host string) string { 147 | if os.Getenv("IS_DOCKER") != "" { 148 | return strings.Replace(strings.Replace(host, 149 | "localhost", "host.docker.internal", 1), 150 | "127.0.0.1", "host.docker.internal", 1) 151 | } 152 | return host 153 | } 154 | 155 | var sqlDbs sync.Map 156 | 157 | // PooledDB get pooled sql.DB 158 | func PooledDB(conf DBConf) (*sql.DB, error) { 159 | dsn := GetDsn(conf) 160 | db, ok := sqlDbs.Load(dsn) 161 | if !ok { 162 | db2, err := StandaloneDB(conf) 163 | if err != nil { 164 | return nil, err 165 | } 166 | db = db2 167 | sqlDbs.Store(dsn, db) 168 | } 169 | return db.(*sql.DB), nil 170 | } 171 | 172 | // StandaloneDB get a standalone db instance 173 | func StandaloneDB(conf DBConf) (*sql.DB, error) { 174 | dsn := GetDsn(conf) 175 | logger.Infof("opening standalone %s: %s", conf.Driver, strings.Replace(dsn, conf.Password, "****", 1)) 176 | return sql.Open(conf.Driver, dsn) 177 | } 178 | 179 | // XaDB return a standalone db instance for xa 180 | func XaDB(conf DBConf) (*sql.DB, error) { 181 | dsn := GetDsn(conf) 182 | if conf.Driver == DBTypeMysql { 183 | dsn += "&autocommit=0" 184 | } 185 | logger.Infof("opening standalone %s: %s", conf.Driver, strings.Replace(dsn, conf.Password, "****", 1)) 186 | return sql.Open(conf.Driver, dsn) 187 | } 188 | 189 | // DBExec use raw db to exec 190 | func DBExec(dbType string, db DB, sql string, values ...interface{}) (affected int64, rerr error) { 191 | if sql == "" { 192 | return 0, nil 193 | } 194 | began := time.Now() 195 | sql = GetDBSpecial(dbType).GetPlaceHoldSQL(sql) 196 | r, rerr := db.Exec(sql, values...) 197 | used := time.Since(began) / time.Millisecond 198 | if rerr == nil { 199 | affected, rerr = r.RowsAffected() 200 | logger.Debugf("used: %d ms affected: %d for %s %v", used, affected, sql, values) 201 | } else { 202 | logger.Errorf("used: %d ms exec error: %v for %s %v", used, rerr, sql, values) 203 | } 204 | return 205 | } 206 | 207 | // GetDsn get dsn from map config 208 | func GetDsn(conf DBConf) string { 209 | host := MayReplaceLocalhost(conf.Host) 210 | driver := conf.Driver 211 | dsn := map[string]string{ 212 | "mysql": fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=true&loc=Local&interpolateParams=true", 213 | conf.User, conf.Password, host, conf.Port, conf.Db), 214 | "postgres": fmt.Sprintf("host=%s user=%s password=%s dbname='%s' port=%d sslmode=disable", 215 | host, conf.User, conf.Password, conf.Db, conf.Port), 216 | }[driver] 217 | PanicIf(dsn == "", fmt.Errorf("unknow driver: %s", driver)) 218 | return dsn 219 | } 220 | 221 | // RespAsErrorCompatible translate a resty response to error 222 | // compatible with version < v1.10 223 | func RespAsErrorCompatible(resp *resty.Response) error { 224 | code := resp.StatusCode() 225 | str := resp.String() 226 | if code == http.StatusTooEarly || strings.Contains(str, ResultOngoing) { 227 | return fmt.Errorf("%s. %w", str, ErrOngoing) 228 | } else if code == http.StatusConflict || strings.Contains(str, ResultFailure) { 229 | return fmt.Errorf("%s. %w", str, ErrFailure) 230 | } else if code != http.StatusOK { 231 | return errors.New(str) 232 | } 233 | return nil 234 | } 235 | 236 | // DeferDo a common defer do used in dtmcli/dtmgrpc 237 | func DeferDo(rerr *error, success func() error, fail func() error) { 238 | defer func() { 239 | if x := recover(); x != nil { 240 | _ = fail() 241 | panic(x) 242 | } else if *rerr != nil { 243 | _ = fail() 244 | } else { 245 | *rerr = success() 246 | } 247 | }() 248 | } 249 | 250 | // Escape solve CodeQL reported problem 251 | func Escape(input string) string { 252 | v := strings.Replace(input, "\n", "", -1) 253 | v = strings.Replace(v, "\r", "", -1) 254 | v = strings.Replace(v, ";", "", -1) 255 | // v = strings.Replace(v, "'", "", -1) 256 | return v 257 | } 258 | 259 | // EscapeGet escape get 260 | func EscapeGet(qs url.Values, key string) string { 261 | return Escape(qs.Get(key)) 262 | } 263 | 264 | // InsertBarrier insert a record to barrier 265 | func InsertBarrier(tx DB, transType string, gid string, branchID string, op string, barrierID string, reason string, dbType string, barrierTableName string) (int64, error) { 266 | if op == "" { 267 | return 0, nil 268 | } 269 | if dbType == "" { 270 | dbType = currentDBType 271 | } 272 | if barrierTableName == "" { 273 | barrierTableName = BarrierTableName 274 | } 275 | sql := GetDBSpecial(dbType).GetInsertIgnoreTemplate(barrierTableName+"(trans_type, gid, branch_id, op, barrier_id, reason) values(?,?,?,?,?,?)", "uniq_barrier") 276 | return DBExec(dbType, tx, sql, transType, gid, branchID, op, barrierID, reason) 277 | } 278 | -------------------------------------------------------------------------------- /dtmimp/utils_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021 yedf. All rights reserved. 3 | * Use of this source code is governed by a BSD-style 4 | * license that can be found in the LICENSE file. 5 | */ 6 | 7 | package dtmimp 8 | 9 | import ( 10 | "errors" 11 | "os" 12 | "strings" 13 | "testing" 14 | 15 | "github.com/stretchr/testify/assert" 16 | ) 17 | 18 | func TestEP(t *testing.T) { 19 | skipped := true 20 | err := func() (rerr error) { 21 | defer P2E(&rerr) 22 | E2P(errors.New("err1")) 23 | skipped = false 24 | return nil 25 | }() 26 | assert.Equal(t, true, skipped) 27 | assert.Equal(t, "err1", err.Error()) 28 | err = CatchP(func() { 29 | PanicIf(true, errors.New("err2")) 30 | }) 31 | assert.Equal(t, "err2", err.Error()) 32 | err = func() (rerr error) { 33 | defer P2E(&rerr) 34 | panic("raw_string") 35 | }() 36 | assert.Equal(t, "raw_string", err.Error()) 37 | } 38 | 39 | func TestTernary(t *testing.T) { 40 | assert.Equal(t, "1", OrString("", "", "1")) 41 | assert.Equal(t, "", OrString("", "", "")) 42 | assert.Equal(t, "1", If(true, "1", "2")) 43 | assert.Equal(t, "2", If(false, "1", "2")) 44 | } 45 | 46 | func TestMarshal(t *testing.T) { 47 | a := 0 48 | type e struct { 49 | A int 50 | } 51 | e1 := e{A: 10} 52 | m := map[string]int{} 53 | assert.Equal(t, "1", MustMarshalString(1)) 54 | assert.Equal(t, []byte("1"), MustMarshal(1)) 55 | MustUnmarshal([]byte("2"), &a) 56 | assert.Equal(t, 2, a) 57 | MustUnmarshalString("3", &a) 58 | assert.Equal(t, 3, a) 59 | MustRemarshal(&e1, &m) 60 | assert.Equal(t, 10, m["A"]) 61 | } 62 | 63 | func TestSome(t *testing.T) { 64 | n := MustAtoi("123") 65 | assert.Equal(t, 123, n) 66 | 67 | err := CatchP(func() { 68 | MustAtoi("abc") 69 | }) 70 | assert.Error(t, err) 71 | 72 | func1 := GetFuncName() 73 | assert.Equal(t, true, strings.HasSuffix(func1, "TestSome")) 74 | 75 | os.Setenv("IS_DOCKER", "1") 76 | s := MayReplaceLocalhost("http://localhost") 77 | assert.Equal(t, "http://host.docker.internal", s) 78 | os.Setenv("IS_DOCKER", "") 79 | s2 := MayReplaceLocalhost("http://localhost") 80 | assert.Equal(t, "http://localhost", s2) 81 | } 82 | -------------------------------------------------------------------------------- /dtmimp/vars.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021 yedf. All rights reserved. 3 | * Use of this source code is governed by a BSD-style 4 | * license that can be found in the LICENSE file. 5 | */ 6 | 7 | package dtmimp 8 | 9 | import ( 10 | "errors" 11 | 12 | "github.com/dtm-labs/dtmcli/logger" 13 | "github.com/dtm-labs/dtmdriver" 14 | "github.com/go-resty/resty/v2" 15 | ) 16 | 17 | // ErrFailure error of FAILURE 18 | var ErrFailure = errors.New("FAILURE") 19 | 20 | // ErrOngoing error of ONGOING 21 | var ErrOngoing = errors.New("ONGOING") 22 | 23 | // ErrDuplicated error of DUPLICATED for only msg 24 | // if QueryPrepared executed before call. then DoAndSubmit return this error 25 | var ErrDuplicated = errors.New("DUPLICATED") 26 | 27 | // XaSQLTimeoutMs milliseconds for Xa sql to timeout 28 | var XaSQLTimeoutMs = 15000 29 | 30 | // MapSuccess HTTP result of SUCCESS 31 | var MapSuccess = map[string]interface{}{"dtm_result": ResultSuccess} 32 | 33 | // MapFailure HTTP result of FAILURE 34 | var MapFailure = map[string]interface{}{"dtm_result": ResultFailure} 35 | 36 | // RestyClient the resty object 37 | var RestyClient = resty.New() 38 | 39 | // PassthroughHeaders will be passed to every sub-trans call 40 | var PassthroughHeaders = []string{} 41 | 42 | // BarrierTableName the table name of barrier table 43 | var BarrierTableName = "dtm_barrier.barrier" 44 | 45 | func init() { 46 | RestyClient.OnBeforeRequest(func(c *resty.Client, r *resty.Request) error { 47 | r.URL = MayReplaceLocalhost(r.URL) 48 | u, err := dtmdriver.GetHTTPDriver().ResolveURL(r.URL) 49 | logger.Debugf("requesting: %s %s %s resolved: %s", r.Method, r.URL, MustMarshalString(r.Body), u) 50 | r.URL = u 51 | return err 52 | }) 53 | RestyClient.OnAfterResponse(func(c *resty.Client, resp *resty.Response) error { 54 | r := resp.Request 55 | logger.Debugf("requested: %s %s %s", r.Method, r.URL, resp.String()) 56 | return nil 57 | }) 58 | } 59 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/dtm-labs/dtmcli 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/BurntSushi/toml v1.0.0 // indirect 7 | github.com/dtm-labs/dtmdriver v0.0.3 8 | github.com/go-redis/redis/v8 v8.11.4 9 | github.com/go-resty/resty/v2 v2.6.0 10 | github.com/natefinch/lumberjack v2.0.0+incompatible 11 | github.com/stretchr/testify v1.7.0 12 | go.mongodb.org/mongo-driver v1.8.3 13 | go.uber.org/zap v1.19.1 14 | gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect 15 | ) 16 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v1.0.0 h1:dtDWrepsVPfW9H/4y7dDgFc2MBUSeJhlaDtK13CxFlU= 2 | github.com/BurntSushi/toml v1.0.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= 3 | github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= 4 | github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= 5 | github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= 6 | github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 7 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 8 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 9 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 10 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= 11 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= 12 | github.com/dtm-labs/dtmdriver v0.0.3 h1:9iAtvXKR3lJXQ7dvS87e4xdtmqkzN+ofek+CF9AvUSY= 13 | github.com/dtm-labs/dtmdriver v0.0.3/go.mod h1:fLiEeD2BPwM9Yq96TfcP9KpbTwFsn5nTxa/PP0jmFuk= 14 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 15 | github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= 16 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 17 | github.com/go-redis/redis/v8 v8.11.4 h1:kHoYkfZP6+pe04aFTnhDH6GDROa5yJdHJVNxV3F46Tg= 18 | github.com/go-redis/redis/v8 v8.11.4/go.mod h1:2Z2wHZXdQpCDXEGzqMockDpNyYvi2l4Pxt6RJr792+w= 19 | github.com/go-resty/resty/v2 v2.6.0 h1:joIR5PNLM2EFqqESUjCMGXrWmXNHEU9CEiK813oKYS4= 20 | github.com/go-resty/resty/v2 v2.6.0/go.mod h1:PwvJS6hvaPkjtjNg9ph+VrSD92bi5Zq73w/BIH7cC3Q= 21 | github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= 22 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 23 | github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= 24 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 25 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 26 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 27 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 28 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 29 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 30 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 31 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 32 | github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= 33 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 34 | github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4= 35 | github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 36 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 37 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 38 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 39 | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 40 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 41 | github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= 42 | github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 43 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 44 | github.com/klauspost/compress v1.13.6 h1:P76CopJELS0TiO2mebmnzgWaajssP/EszplttgQxcgc= 45 | github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= 46 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 47 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 48 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 49 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 50 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 51 | github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= 52 | github.com/natefinch/lumberjack v2.0.0+incompatible h1:4QJd3OLAMgj7ph+yZTuX13Ld4UpgHp07nNdFX7mqFfM= 53 | github.com/natefinch/lumberjack v2.0.0+incompatible/go.mod h1:Wi9p2TTF5DG5oU+6YfsmYQpsTIOm0B1VNzQg9Mw6nPk= 54 | github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= 55 | github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= 56 | github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= 57 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 58 | github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= 59 | github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= 60 | github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= 61 | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= 62 | github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= 63 | github.com/onsi/gomega v1.16.0 h1:6gjqkI8iiRHMvdccRJM8rVKjCWk6ZIm6FTm3ddIe4/c= 64 | github.com/onsi/gomega v1.16.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= 65 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 66 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 67 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 68 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 69 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 70 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 71 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 72 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 73 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 74 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 75 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 76 | github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4= 77 | github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= 78 | github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= 79 | github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= 80 | github.com/xdg-go/scram v1.0.2 h1:akYIkZ28e6A96dkWNJQu3nmCzH3YfwMPQExUYDaRv7w= 81 | github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs= 82 | github.com/xdg-go/stringprep v1.0.2 h1:6iq84/ryjjeRmMJwxutI51F2GIPlP5BfTvXHeYjyhBc= 83 | github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM= 84 | github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d h1:splanxYIlg+5LfHAM6xpdFEAYOk8iySO56hMFq6uLyA= 85 | github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= 86 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 87 | github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 88 | go.mongodb.org/mongo-driver v1.8.3 h1:TDKlTkGDKm9kkJVUOAXDK5/fkqKHJVwYQSpoRfB43R4= 89 | go.mongodb.org/mongo-driver v1.8.3/go.mod h1:0sQWfOeY63QTntERDJJ/0SuKK0T1uVSgKCuAROlKEPY= 90 | go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= 91 | go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= 92 | go.uber.org/goleak v1.1.11-0.20210813005559-691160354723 h1:sHOAIxRGBp443oHZIPB+HsUGaksVCXVQENPxwTfQdH4= 93 | go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= 94 | go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= 95 | go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= 96 | go.uber.org/zap v1.19.1 h1:ue41HOKd1vGURxrmeKIgELGb3jPW9DMUDGtsinblHwI= 97 | go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI= 98 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 99 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 100 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 101 | golang.org/x/crypto v0.0.0-20201216223049-8b5274cf687f h1:aZp0e2vLN4MToVqnjNEYEtrEA8RH8U8FN1CU7JgqsPU= 102 | golang.org/x/crypto v0.0.0-20201216223049-8b5274cf687f/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= 103 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs= 104 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 105 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 106 | golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo= 107 | golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 108 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 109 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 110 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 111 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 112 | golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 113 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 114 | golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= 115 | golang.org/x/net v0.0.0-20210428140749-89ef3d95e781 h1:DzZ89McO9/gWPsQXS/FVKAlG02ZjaQ6AlZRBimEYOd0= 116 | golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= 117 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 118 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 119 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 120 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 121 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= 122 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 123 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 124 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 125 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 126 | golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 127 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 128 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 129 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 130 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 131 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 132 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 133 | golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 134 | golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 135 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 136 | golang.org/x/sys v0.0.0-20210510120138-977fb7262007 h1:gG67DSER+11cZvqIMb8S8bt0vZtiN6xWYARwirrOSfE= 137 | golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 138 | golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= 139 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 140 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 141 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 142 | golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 143 | golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= 144 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 145 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 146 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 147 | golang.org/x/tools v0.0.0-20190531172133-b3315ee88b7d/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 148 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 149 | golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 150 | golang.org/x/tools v0.1.5 h1:ouewzE6p+/VEB31YYnTbEJdi8pFqKp4P4n85vwo3DHA= 151 | golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= 152 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 153 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 154 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 155 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= 156 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 157 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 158 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 159 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 160 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 161 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 162 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 163 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 164 | google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk= 165 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 166 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 167 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 168 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 169 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 170 | gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8= 171 | gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= 172 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 173 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 174 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 175 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 176 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 177 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 178 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 179 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 180 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 181 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= 182 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 183 | -------------------------------------------------------------------------------- /logger/log.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "log" 7 | "net/url" 8 | "os" 9 | "runtime/debug" 10 | "strings" 11 | 12 | "github.com/natefinch/lumberjack" 13 | "go.uber.org/zap" 14 | "go.uber.org/zap/zapcore" 15 | ) 16 | 17 | //var logger *zap.SugaredLogger = nil 18 | 19 | var logger Logger 20 | 21 | const ( 22 | // StdErr is the default configuration for log output. 23 | StdErr = "stderr" 24 | // StdOut configuration for log output 25 | StdOut = "stdout" 26 | ) 27 | 28 | func init() { 29 | InitLog(os.Getenv("LOG_LEVEL")) 30 | } 31 | 32 | // Logger logger interface 33 | type Logger interface { 34 | Debugf(format string, args ...interface{}) 35 | Infof(format string, args ...interface{}) 36 | Warnf(format string, args ...interface{}) 37 | Errorf(format string, args ...interface{}) 38 | } 39 | 40 | // WithLogger replaces default logger 41 | func WithLogger(log Logger) { 42 | logger = log 43 | } 44 | 45 | // InitLog is an initialization for a logger 46 | // level can be: debug info warn error 47 | func InitLog(level string) { 48 | InitLog2(level, StdOut, 0, "") 49 | } 50 | 51 | // InitLog2 specify advanced log config 52 | func InitLog2(level string, outputs string, logRotationEnable int64, logRotateConfigJSON string) { 53 | outputPaths := strings.Split(outputs, ",") 54 | for i, v := range outputPaths { 55 | if logRotationEnable != 0 && v != StdErr && v != StdOut { 56 | outputPaths[i] = fmt.Sprintf("lumberjack://%s", v) 57 | } 58 | } 59 | 60 | if logRotationEnable != 0 { 61 | setupLogRotation(logRotateConfigJSON) 62 | } 63 | 64 | config := loadConfig(level) 65 | config.OutputPaths = outputPaths 66 | p, err := config.Build(zap.AddCallerSkip(1)) 67 | FatalIfError(err) 68 | logger = p.Sugar() 69 | } 70 | 71 | type lumberjackSink struct { 72 | lumberjack.Logger 73 | } 74 | 75 | func (*lumberjackSink) Sync() error { 76 | return nil 77 | } 78 | 79 | // setupLogRotation initializes log rotation for a single file path target. 80 | func setupLogRotation(logRotateConfigJSON string) { 81 | err := zap.RegisterSink("lumberjack", func(u *url.URL) (zap.Sink, error) { 82 | var conf lumberjackSink 83 | err := json.Unmarshal([]byte(logRotateConfigJSON), &conf) 84 | FatalfIf(err != nil, "bad config LogRotateConfigJSON: %v", err) 85 | conf.Filename = u.Host + u.Path 86 | return &conf, nil 87 | }) 88 | FatalIfError(err) 89 | } 90 | 91 | func loadConfig(logLevel string) zap.Config { 92 | config := zap.NewProductionConfig() 93 | err := config.Level.UnmarshalText([]byte(logLevel)) 94 | FatalIfError(err) 95 | config.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder 96 | if os.Getenv("DTM_DEBUG") != "" { 97 | config.Encoding = "console" 98 | config.EncoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder 99 | } 100 | return config 101 | } 102 | 103 | // Debugf log to level debug 104 | func Debugf(fmt string, args ...interface{}) { 105 | logger.Debugf(fmt, args...) 106 | } 107 | 108 | // Infof log to level info 109 | func Infof(fmt string, args ...interface{}) { 110 | logger.Infof(fmt, args...) 111 | } 112 | 113 | // Warnf log to level warn 114 | func Warnf(fmt string, args ...interface{}) { 115 | logger.Warnf(fmt, args...) 116 | } 117 | 118 | // Errorf log to level error 119 | func Errorf(fmt string, args ...interface{}) { 120 | logger.Errorf(fmt, args...) 121 | } 122 | 123 | // FatalfIf log to level error 124 | func FatalfIf(cond bool, fmt string, args ...interface{}) { 125 | if !cond { 126 | return 127 | } 128 | debug.PrintStack() 129 | log.Fatalf(fmt, args...) 130 | } 131 | 132 | // FatalIfError if err is not nil, then log to level fatal and call os.Exit 133 | func FatalIfError(err error) { 134 | FatalfIf(err != nil, "fatal error: %v", err) 135 | } 136 | -------------------------------------------------------------------------------- /logger/logger_test.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "go.uber.org/zap" 8 | ) 9 | 10 | func TestInitLog(t *testing.T) { 11 | os.Setenv("DTM_DEBUG", "1") 12 | InitLog("debug") 13 | Debugf("a debug msg") 14 | Infof("a info msg") 15 | Warnf("a warn msg") 16 | Errorf("a error msg") 17 | FatalfIf(false, "nothing") 18 | FatalIfError(nil) 19 | 20 | InitLog2("debug", "test.log,stderr", 0, "") 21 | Debugf("a debug msg to console and file") 22 | 23 | InitLog2("debug", "test2.log,/tmp/dtm-test1.log,/tmp/dtm-test.log,stdout,stderr", 1, 24 | "{\"maxsize\": 1, \"maxage\": 1, \"maxbackups\": 1, \"compress\": false}") 25 | Debugf("a debug msg to /tmp/dtm-test.log and test2.log and stdout and stderr") 26 | 27 | // _ = os.Remove("test.log") 28 | } 29 | 30 | func TestWithLogger(t *testing.T) { 31 | logger := zap.NewExample().Sugar() 32 | WithLogger(logger) 33 | Debugf("a debug msg") 34 | Infof("a info msg") 35 | Warnf("a warn msg") 36 | Errorf("a error msg") 37 | FatalfIf(false, "nothing") 38 | FatalIfError(nil) 39 | } 40 | -------------------------------------------------------------------------------- /msg.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021 yedf. All rights reserved. 3 | * Use of this source code is governed by a BSD-style 4 | * license that can be found in the LICENSE file. 5 | */ 6 | 7 | package dtmcli 8 | 9 | import ( 10 | "database/sql" 11 | "errors" 12 | 13 | "github.com/dtm-labs/dtmcli/dtmimp" 14 | ) 15 | 16 | // Msg reliable msg type 17 | type Msg struct { 18 | dtmimp.TransBase 19 | delay uint64 // delay call branch, unit second 20 | } 21 | 22 | // NewMsg create new msg 23 | func NewMsg(server string, gid string) *Msg { 24 | return &Msg{TransBase: *dtmimp.NewTransBase(gid, "msg", server, "")} 25 | } 26 | 27 | // Add add a new step 28 | func (s *Msg) Add(action string, postData interface{}) *Msg { 29 | s.Steps = append(s.Steps, map[string]string{"action": action}) 30 | s.Payloads = append(s.Payloads, dtmimp.MustMarshalString(postData)) 31 | return s 32 | } 33 | 34 | // SetDelay delay call branch, unit second 35 | func (s *Msg) SetDelay(delay uint64) *Msg { 36 | s.delay = delay 37 | return s 38 | } 39 | 40 | // Prepare prepare the msg, msg will later be submitted 41 | func (s *Msg) Prepare(queryPrepared string) error { 42 | s.QueryPrepared = dtmimp.OrString(queryPrepared, s.QueryPrepared) 43 | return dtmimp.TransCallDtm(&s.TransBase, s, "prepare") 44 | } 45 | 46 | // Submit submit the msg 47 | func (s *Msg) Submit() error { 48 | s.BuildCustomOptions() 49 | return dtmimp.TransCallDtm(&s.TransBase, s, "submit") 50 | } 51 | 52 | // DoAndSubmitDB short method for Do on db type. please see DoAndSubmit 53 | func (s *Msg) DoAndSubmitDB(queryPrepared string, db *sql.DB, busiCall BarrierBusiFunc) error { 54 | return s.DoAndSubmit(queryPrepared, func(bb *BranchBarrier) error { 55 | return bb.CallWithDB(db, busiCall) 56 | }) 57 | } 58 | 59 | // DoAndSubmit one method for the entire prepare->busi->submit 60 | // the error returned by busiCall will be returned 61 | // if busiCall return ErrFailure, then abort is called directly 62 | // if busiCall return not nil error other than ErrFailure, then DoAndSubmit will call queryPrepared to get the result 63 | func (s *Msg) DoAndSubmit(queryPrepared string, busiCall func(bb *BranchBarrier) error) error { 64 | bb, err := BarrierFrom(s.TransType, s.Gid, dtmimp.MsgDoBranch0, dtmimp.MsgDoOp) // a special barrier for msg QueryPrepared 65 | if err == nil { 66 | err = s.Prepare(queryPrepared) 67 | } 68 | if err == nil { 69 | errb := busiCall(bb) 70 | if errb != nil && !errors.Is(errb, ErrFailure) { 71 | // if busicall return an error other than failure, we will query the result 72 | _, err = dtmimp.TransRequestBranch(&s.TransBase, "GET", nil, bb.BranchID, bb.Op, queryPrepared) 73 | } 74 | if errors.Is(errb, ErrFailure) || errors.Is(err, ErrFailure) { 75 | _ = dtmimp.TransCallDtm(&s.TransBase, s, "abort") 76 | } else if err == nil { 77 | err = s.Submit() 78 | } 79 | if errb != nil { 80 | return errb 81 | } 82 | } 83 | return err 84 | } 85 | 86 | // BuildCustomOptions add custom options to the request context 87 | func (s *Msg) BuildCustomOptions() { 88 | if s.delay > 0 { 89 | s.CustomData = dtmimp.MustMarshalString(map[string]interface{}{"delay": s.delay}) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /saga.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021 yedf. All rights reserved. 3 | * Use of this source code is governed by a BSD-style 4 | * license that can be found in the LICENSE file. 5 | */ 6 | 7 | package dtmcli 8 | 9 | import ( 10 | "github.com/dtm-labs/dtmcli/dtmimp" 11 | ) 12 | 13 | // Saga struct of saga 14 | type Saga struct { 15 | dtmimp.TransBase 16 | orders map[int][]int 17 | } 18 | 19 | // NewSaga create a saga 20 | func NewSaga(server string, gid string) *Saga { 21 | return &Saga{TransBase: *dtmimp.NewTransBase(gid, "saga", server, ""), orders: map[int][]int{}} 22 | } 23 | 24 | // Add add a saga step 25 | func (s *Saga) Add(action string, compensate string, postData interface{}) *Saga { 26 | s.Steps = append(s.Steps, map[string]string{"action": action, "compensate": compensate}) 27 | s.Payloads = append(s.Payloads, dtmimp.MustMarshalString(postData)) 28 | return s 29 | } 30 | 31 | // AddBranchOrder specify that branch should be after preBranches. branch should is larger than all the element in preBranches 32 | func (s *Saga) AddBranchOrder(branch int, preBranches []int) *Saga { 33 | s.orders[branch] = preBranches 34 | return s 35 | } 36 | 37 | // SetConcurrent enable the concurrent exec of sub trans 38 | func (s *Saga) SetConcurrent() *Saga { 39 | s.Concurrent = true 40 | return s 41 | } 42 | 43 | // Submit submit the saga trans 44 | func (s *Saga) Submit() error { 45 | s.BuildCustomOptions() 46 | return dtmimp.TransCallDtm(&s.TransBase, s, "submit") 47 | } 48 | 49 | // BuildCustomOptions add custom options to the request context 50 | func (s *Saga) BuildCustomOptions() { 51 | if s.Concurrent { 52 | s.CustomData = dtmimp.MustMarshalString(map[string]interface{}{"orders": s.orders, "concurrent": s.Concurrent}) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /tcc.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021 yedf. All rights reserved. 3 | * Use of this source code is governed by a BSD-style 4 | * license that can be found in the LICENSE file. 5 | */ 6 | 7 | package dtmcli 8 | 9 | import ( 10 | "fmt" 11 | "net/url" 12 | 13 | "github.com/dtm-labs/dtmcli/dtmimp" 14 | "github.com/go-resty/resty/v2" 15 | ) 16 | 17 | // Tcc struct of tcc 18 | type Tcc struct { 19 | dtmimp.TransBase 20 | } 21 | 22 | // TccGlobalFunc type of global tcc call 23 | type TccGlobalFunc func(tcc *Tcc) (*resty.Response, error) 24 | 25 | // TccGlobalTransaction begin a tcc global transaction 26 | // dtm dtm server address 27 | // gid global transaction ID 28 | // tccFunc define the detail tcc busi 29 | func TccGlobalTransaction(dtm string, gid string, tccFunc TccGlobalFunc) (rerr error) { 30 | return TccGlobalTransaction2(dtm, gid, func(t *Tcc) {}, tccFunc) 31 | } 32 | 33 | // TccGlobalTransaction2 new version of TccGlobalTransaction, add custom param 34 | func TccGlobalTransaction2(dtm string, gid string, custom func(*Tcc), tccFunc TccGlobalFunc) (rerr error) { 35 | tcc := &Tcc{TransBase: *dtmimp.NewTransBase(gid, "tcc", dtm, "")} 36 | custom(tcc) 37 | rerr = dtmimp.TransCallDtm(&tcc.TransBase, tcc, "prepare") 38 | if rerr != nil { 39 | return rerr 40 | } 41 | defer dtmimp.DeferDo(&rerr, func() error { 42 | return dtmimp.TransCallDtm(&tcc.TransBase, tcc, "submit") 43 | }, func() error { 44 | return dtmimp.TransCallDtm(&tcc.TransBase, tcc, "abort") 45 | }) 46 | _, rerr = tccFunc(tcc) 47 | return 48 | } 49 | 50 | // TccFromQuery tcc from request info 51 | func TccFromQuery(qs url.Values) (*Tcc, error) { 52 | tcc := &Tcc{TransBase: *dtmimp.TransBaseFromQuery(qs)} 53 | if tcc.Dtm == "" || tcc.Gid == "" { 54 | return nil, fmt.Errorf("bad tcc info. dtm: %s, gid: %s parentID: %s", tcc.Dtm, tcc.Gid, tcc.BranchID) 55 | } 56 | return tcc, nil 57 | } 58 | 59 | // CallBranch call a tcc branch 60 | func (t *Tcc) CallBranch(body interface{}, tryURL string, confirmURL string, cancelURL string) (*resty.Response, error) { 61 | branchID := t.NewSubBranchID() 62 | err := dtmimp.TransRegisterBranch(&t.TransBase, map[string]string{ 63 | "data": dtmimp.MustMarshalString(body), 64 | "branch_id": branchID, 65 | dtmimp.OpConfirm: confirmURL, 66 | dtmimp.OpCancel: cancelURL, 67 | }, "registerBranch") 68 | if err != nil { 69 | return nil, err 70 | } 71 | return dtmimp.TransRequestBranch(&t.TransBase, "POST", body, branchID, dtmimp.OpTry, tryURL) 72 | } 73 | -------------------------------------------------------------------------------- /types.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021 yedf. All rights reserved. 3 | * Use of this source code is governed by a BSD-style 4 | * license that can be found in the LICENSE file. 5 | */ 6 | 7 | package dtmcli 8 | 9 | import ( 10 | "errors" 11 | "fmt" 12 | "net/http" 13 | 14 | "github.com/dtm-labs/dtmcli/dtmimp" 15 | "github.com/go-resty/resty/v2" 16 | ) 17 | 18 | // MustGenGid generate a new gid 19 | func MustGenGid(server string) string { 20 | res := map[string]string{} 21 | resp, err := dtmimp.RestyClient.R().SetResult(&res).Get(server + "/newGid") 22 | if err != nil || res["gid"] == "" { 23 | panic(fmt.Errorf("newGid error: %v, resp: %s", err, resp)) 24 | } 25 | return res["gid"] 26 | } 27 | 28 | // DB interface 29 | type DB = dtmimp.DB 30 | 31 | // TransOptions transaction option 32 | type TransOptions = dtmimp.TransOptions 33 | 34 | // DBConf declares db configuration 35 | type DBConf = dtmimp.DBConf 36 | 37 | // String2DtmError translate string to dtm error 38 | func String2DtmError(str string) error { 39 | return map[string]error{ 40 | ResultFailure: ErrFailure, 41 | ResultOngoing: ErrOngoing, 42 | ResultSuccess: nil, 43 | "": nil, 44 | }[str] 45 | } 46 | 47 | // SetCurrentDBType set currentDBType 48 | func SetCurrentDBType(dbType string) { 49 | dtmimp.SetCurrentDBType(dbType) 50 | } 51 | 52 | // GetCurrentDBType get currentDBType 53 | func GetCurrentDBType() string { 54 | return dtmimp.GetCurrentDBType() 55 | } 56 | 57 | // SetXaSQLTimeoutMs set XaSQLTimeoutMs 58 | func SetXaSQLTimeoutMs(ms int) { 59 | dtmimp.XaSQLTimeoutMs = ms 60 | } 61 | 62 | // GetXaSQLTimeoutMs get XaSQLTimeoutMs 63 | func GetXaSQLTimeoutMs() int { 64 | return dtmimp.XaSQLTimeoutMs 65 | } 66 | 67 | // SetBarrierTableName sets barrier table name 68 | func SetBarrierTableName(tablename string) { 69 | dtmimp.BarrierTableName = tablename 70 | } 71 | 72 | // GetRestyClient get the resty.Client for http request 73 | func GetRestyClient() *resty.Client { 74 | return dtmimp.RestyClient 75 | } 76 | 77 | // SetPassthroughHeaders experimental. 78 | // apply to http header and grpc metadata 79 | // dtm server will save these headers in trans creating request. 80 | // and then passthrough them to sub-trans 81 | func SetPassthroughHeaders(headers []string) { 82 | dtmimp.PassthroughHeaders = headers 83 | } 84 | 85 | // Result2HttpJSON return the http code and json result 86 | // if result is error, the return proper code, else return StatusOK 87 | func Result2HttpJSON(result interface{}) (code int, res interface{}) { 88 | err, _ := result.(error) 89 | if err == nil { 90 | code = http.StatusOK 91 | res = result 92 | } else { 93 | res = map[string]string{ 94 | "error": err.Error(), 95 | } 96 | if errors.Is(err, ErrFailure) { 97 | code = http.StatusConflict 98 | } else if errors.Is(err, ErrOngoing) { 99 | code = http.StatusTooEarly 100 | } else if err != nil { 101 | code = http.StatusInternalServerError 102 | } 103 | } 104 | return 105 | } 106 | -------------------------------------------------------------------------------- /xa.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021 yedf. All rights reserved. 3 | * Use of this source code is governed by a BSD-style 4 | * license that can be found in the LICENSE file. 5 | */ 6 | 7 | package dtmcli 8 | 9 | import ( 10 | "database/sql" 11 | "fmt" 12 | "net/url" 13 | 14 | "github.com/dtm-labs/dtmcli/dtmimp" 15 | "github.com/go-resty/resty/v2" 16 | ) 17 | 18 | // XaGlobalFunc type of xa global function 19 | type XaGlobalFunc func(xa *Xa) (*resty.Response, error) 20 | 21 | // XaLocalFunc type of xa local function 22 | type XaLocalFunc func(db *sql.DB, xa *Xa) error 23 | 24 | // Xa xa transaction 25 | type Xa struct { 26 | dtmimp.TransBase 27 | Phase2URL string 28 | } 29 | 30 | // XaFromQuery construct xa info from request 31 | func XaFromQuery(qs url.Values) (*Xa, error) { 32 | xa := &Xa{TransBase: *dtmimp.TransBaseFromQuery(qs)} 33 | xa.Op = dtmimp.EscapeGet(qs, "op") 34 | xa.Phase2URL = dtmimp.EscapeGet(qs, "phase2_url") 35 | if xa.Gid == "" || xa.BranchID == "" || xa.Op == "" { 36 | return nil, fmt.Errorf("bad xa info: gid: %s branchid: %s op: %s phase2_url: %s", xa.Gid, xa.BranchID, xa.Op, xa.Phase2URL) 37 | } 38 | return xa, nil 39 | } 40 | 41 | // XaLocalTransaction start a xa local transaction 42 | func XaLocalTransaction(qs url.Values, dbConf DBConf, xaFunc XaLocalFunc) error { 43 | xa, err := XaFromQuery(qs) 44 | if err != nil { 45 | return err 46 | } 47 | if xa.Op == dtmimp.OpCommit || xa.Op == dtmimp.OpRollback { 48 | return dtmimp.XaHandlePhase2(xa.Gid, dbConf, xa.BranchID, xa.Op) 49 | } 50 | return dtmimp.XaHandleLocalTrans(&xa.TransBase, dbConf, func(db *sql.DB) error { 51 | err := xaFunc(db, xa) 52 | if err != nil { 53 | return err 54 | } 55 | return dtmimp.TransRegisterBranch(&xa.TransBase, map[string]string{ 56 | "url": xa.Phase2URL, 57 | "branch_id": xa.BranchID, 58 | }, "registerBranch") 59 | }) 60 | } 61 | 62 | // XaGlobalTransaction start a xa global transaction 63 | func XaGlobalTransaction(server string, gid string, xaFunc XaGlobalFunc) error { 64 | return XaGlobalTransaction2(server, gid, func(x *Xa) {}, xaFunc) 65 | } 66 | 67 | // XaGlobalTransaction2 start a xa global transaction with xa custom function 68 | func XaGlobalTransaction2(server string, gid string, custom func(*Xa), xaFunc XaGlobalFunc) (rerr error) { 69 | xa := &Xa{TransBase: *dtmimp.NewTransBase(gid, "xa", server, "")} 70 | custom(xa) 71 | return dtmimp.XaHandleGlobalTrans(&xa.TransBase, func(action string) error { 72 | return dtmimp.TransCallDtm(&xa.TransBase, xa, action) 73 | }, func() error { 74 | _, rerr := xaFunc(xa) 75 | return rerr 76 | }) 77 | } 78 | 79 | // CallBranch call a xa branch 80 | func (x *Xa) CallBranch(body interface{}, url string) (*resty.Response, error) { 81 | branchID := x.NewSubBranchID() 82 | return dtmimp.TransRequestBranch(&x.TransBase, "POST", body, branchID, dtmimp.OpAction, url) 83 | } 84 | --------------------------------------------------------------------------------