├── dtmcli ├── dtmimp │ ├── README-cn.md │ ├── README.md │ ├── types_test.go │ ├── types.go │ ├── db_special_test.go │ ├── vars.go │ ├── consts.go │ ├── utils_test.go │ ├── trans_xa_base.go │ └── db_special.go ├── trans_test.go ├── types_test.go ├── logger │ ├── logger_test.go │ └── log.go ├── saga.go ├── consts.go ├── types.go ├── tcc.go ├── barrier_redis.go ├── xa.go ├── msg.go ├── barrier_mongo.go └── barrier.go ├── dtmgrpc ├── dtmgimp │ ├── README-cn.md │ ├── README.md │ ├── grpc_clients.go │ └── types.go ├── type_test.go ├── barrier.go ├── dtmgpb │ └── dtmgimp.proto ├── saga.go ├── type.go ├── tcc.go ├── msg.go └── xa.go ├── sqls ├── busi.mongo.js ├── dtmcli.barrier.mongo.js ├── busi.mysql.sql ├── dtmcli.barrier.postgres.sql ├── dtmcli.barrier.mysql.sql ├── busi.postgres.sql ├── dtmsvr.storage.postgres.sql ├── dtmsvr.storage.mysql.sql └── dtmsvr.storage.tdsql.sql ├── helper ├── bench │ ├── test-boltdb.sh │ ├── setup-redis6.sh │ ├── prepare.sh │ ├── test-flash-sales.sh │ ├── setup.sh │ ├── test-redis.sh │ ├── Makefile │ ├── main.go │ └── test-mysql.sh ├── golint.sh ├── compose.postgres.yml ├── compose.mysql.yml ├── Makefile ├── .goreleaser.yml ├── Dockerfile-release ├── compose.cloud.yml ├── test-cover.sh ├── compose.store.yml └── sync-dtmcli.sh ├── test ├── saga_cover_test.go ├── busi │ ├── startup.go │ ├── base_jrpc.go │ ├── busi.proto │ └── quick_start.go ├── tcc_jrpc_test.go ├── saga_compatible_test.go ├── msg_delay_test.go ├── common_test.go ├── dtmsvr_test.go ├── saga_grpc_barrier_test.go ├── tcc_cover_test.go ├── saga_barrier_mongo_test.go ├── tcc_grpc_cover_test.go ├── xa_cover_test.go ├── saga_barrier_redis_test.go ├── saga_barrier_test.go ├── main_test.go ├── msg_options_test.go ├── base_test.go ├── msg_grpc_test.go ├── xa_grpc_test.go ├── msg_test.go ├── tcc_old_test.go ├── msg_grpc_barrier_test.go ├── saga_concurrent_test.go ├── msg_grpc_barrier_redis_test.go └── types.go ├── charts ├── templates │ ├── configmap.yaml │ ├── tests │ │ └── test-connection.yaml │ ├── service.yaml │ ├── hpa.yaml │ ├── NOTES.txt │ ├── _helpers.tpl │ ├── ingress.yaml │ └── deployment.yaml ├── .helmignore ├── Chart.yaml ├── values.yaml └── README.md ├── .gitignore ├── qs └── main.go ├── .golangci.yml ├── dtmsvr ├── storage │ ├── registry │ │ ├── factory.go │ │ └── registry.go │ ├── store.go │ └── trans.go ├── utils_test.go ├── trans_type_xa.go ├── utils.go ├── trans_type_tcc.go ├── api_grpc.go ├── cron.go ├── config │ ├── config_utils.go │ └── config_test.go ├── trans_process.go ├── trans_type_msg.go ├── api.go └── trans_class.go ├── dtmutil ├── consts.go └── utils_test.go ├── go.mod ├── .github └── workflows │ ├── tests.yml │ ├── release.yml │ └── codeql-analysis.yml ├── LICENSE ├── main.go └── conf.sample.yml /dtmcli/dtmimp/README-cn.md: -------------------------------------------------------------------------------- 1 | ## 注意 2 | 此包带imp后缀,主要被dtm内部使用,相关接口可能会发生变更,请勿使用这里的接口 -------------------------------------------------------------------------------- /dtmgrpc/dtmgimp/README-cn.md: -------------------------------------------------------------------------------- 1 | ## 注意 2 | 此包带imp后缀,主要被dtm内部使用,相关接口可能会发生变更,请勿使用这里的接口 -------------------------------------------------------------------------------- /sqls/busi.mongo.js: -------------------------------------------------------------------------------- 1 | use busi 2 | db.busi.insert({user_id: 1, balance: 10000}) 3 | db.busi.insert({user_id: 2, balance: 10000}) 4 | -------------------------------------------------------------------------------- /helper/bench/test-boltdb.sh: -------------------------------------------------------------------------------- 1 | # !/bin/bash 2 | 3 | set -x 4 | 5 | ab -n 50000 -c 10 "http://127.0.0.1:8083/api/busi_bench/benchEmptyUrl" 6 | -------------------------------------------------------------------------------- /dtmcli/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" -------------------------------------------------------------------------------- /dtmgrpc/dtmgimp/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" -------------------------------------------------------------------------------- /helper/bench/setup-redis6.sh: -------------------------------------------------------------------------------- 1 | # !/bin/bash 2 | apt update 3 | apt install -y software-properties-common 4 | add-apt-repository -y ppa:redislabs/redis 5 | apt install -y redis redis-tools 6 | 7 | -------------------------------------------------------------------------------- /helper/golint.sh: -------------------------------------------------------------------------------- 1 | set -x 2 | 3 | curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b $(go env GOPATH)/bin v1.41.0 4 | $(go env GOPATH)/bin/golangci-lint run 5 | -------------------------------------------------------------------------------- /test/saga_cover_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/dtm-labs/dtm/dtmcli" 7 | ) 8 | 9 | func TestSagaCover(t *testing.T) { 10 | dtmcli.SetPassthroughHeaders([]string{}) 11 | } 12 | -------------------------------------------------------------------------------- /sqls/dtmcli.barrier.mongo.js: -------------------------------------------------------------------------------- 1 | use dtm_barrier 2 | db.barrier.drop() 3 | db.barrier.createIndex({gid:1, branch_id:1, op: 1, barrier_id: 1}, {unique: true}) 4 | //db.barrier.insert({gid:"123", branch_id:"01", op:"action", barrier_id:"01", reason:"action"}); 5 | -------------------------------------------------------------------------------- /charts/templates/configmap.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: {{ include "dtm.fullname" . }}-conf 5 | labels: 6 | {{- include "dtm.labels" . | nindent 4 }} 7 | data: 8 | config.yaml: |- 9 | {{- .Values.configuration | nindent 4 }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | conf.yml 2 | *.out 3 | *.log 4 | */**/main 5 | main 6 | dist 7 | .idea/** 8 | .vscode 9 | default.etcd 10 | */**/*.bolt 11 | bench/bench 12 | helper/bench/bench 13 | helper/qs/qs 14 | # Output file of unit test coverage 15 | coverage.* 16 | profile.* 17 | test.sh 18 | dtm 19 | dtm-* 20 | dtm.* 21 | -------------------------------------------------------------------------------- /qs/main.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 main 8 | 9 | import ( 10 | "github.com/dtm-labs/dtm/test/busi" 11 | ) 12 | 13 | func main() { 14 | busi.QsMain() 15 | } 16 | -------------------------------------------------------------------------------- /helper/compose.postgres.yml: -------------------------------------------------------------------------------- 1 | version: '3.3' 2 | services: 3 | postgres: 4 | image: 'postgres:13' 5 | command: postgres --max_prepared_transactions=1000 6 | volumes: 7 | - /etc/localtime:/etc/localtime:ro 8 | - /etc/timezone:/etc/timezone:ro 9 | environment: 10 | POSTGRES_PASSWORD: mysecretpassword 11 | 12 | ports: 13 | - '5432:5432' 14 | -------------------------------------------------------------------------------- /helper/bench/prepare.sh: -------------------------------------------------------------------------------- 1 | # !/bin/bash 2 | apt update 3 | apt install -y git 4 | git clone https://github.com/dtm-labs/dtm.git && cd dtm && git checkout alpha && cd bench && make 5 | 6 | 7 | echo 'all prepared. you shoud run following commands to test in different terminal' 8 | echo 9 | echo 'cd dtf && go run helper/bench/main.go redis|boltdb|db' 10 | echo 'cd dtf && ./helper/bench/test-redis|boltdb|mysql.sh' 11 | -------------------------------------------------------------------------------- /helper/compose.mysql.yml: -------------------------------------------------------------------------------- 1 | version: '3.3' 2 | services: 3 | mysql: 4 | image: 'mysql:5.7' 5 | volumes: 6 | - /etc/localtime:/etc/localtime:ro 7 | - /etc/timezone:/etc/timezone:ro 8 | environment: 9 | MYSQL_ALLOW_EMPTY_PASSWORD: 1 10 | command: 11 | [ 12 | '--character-set-server=utf8mb4', 13 | '--collation-server=utf8mb4_unicode_ci', 14 | ] 15 | ports: 16 | - '3306:3306' 17 | -------------------------------------------------------------------------------- /charts/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *.orig 18 | *~ 19 | # Various IDEs 20 | .project 21 | .idea/ 22 | *.tmproj 23 | .vscode/ 24 | -------------------------------------------------------------------------------- /helper/bench/test-flash-sales.sh: -------------------------------------------------------------------------------- 1 | # !/bin/bash 2 | 3 | set -x 4 | 5 | export LOG_LEVEL=fatal 6 | export STORE_DRIVER=redis 7 | export STORE_HOST=localhost 8 | export STORE_PORT=6379 9 | export BUSI_REDIS=localhost:6379 10 | ./bench redis & 11 | echo 'sleeping 3s for dtm bench to run up.' && sleep 3 12 | curl "http://127.0.0.1:8083/api/busi_bench/benchFlashSalesReset" 13 | ab -n 300000 -c 20 "http://127.0.0.1:8083/api/busi_bench/benchFlashSales" 14 | pkill bench 15 | -------------------------------------------------------------------------------- /charts/templates/tests/test-connection.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | name: "{{ include "dtm.fullname" . }}-test-connection" 5 | labels: 6 | {{- include "dtm.labels" . | nindent 4 }} 7 | annotations: 8 | "helm.sh/hook": test 9 | spec: 10 | containers: 11 | - name: wget 12 | image: busybox 13 | command: ['wget'] 14 | args: ['{{ include "dtm.fullname" . }}:{{ .Values.service.port }}'] 15 | restartPolicy: Never 16 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | run: 2 | deadline: 5m 3 | skip-dirs: 4 | # - test 5 | # - bench 6 | 7 | linter-settings: 8 | goconst: 9 | min-len: 2 10 | min-occurrences: 2 11 | 12 | linters: 13 | enable: 14 | - revive 15 | - goconst 16 | - gofmt 17 | - goimports 18 | - misspell 19 | - unparam 20 | 21 | issues: 22 | exclude-use-default: false 23 | exclude-rules: 24 | - path: _test.go 25 | linters: 26 | - errcheck 27 | - revive 28 | -------------------------------------------------------------------------------- /helper/Makefile: -------------------------------------------------------------------------------- 1 | # dev env https://www.dtm.pub/other/develop.html 2 | all: fmt lint test_redis 3 | .PHONY: all 4 | 5 | fmt: 6 | @gofmt -s -w ./ 7 | 8 | lint: 9 | @golangci-lint run 10 | 11 | .PHONY: test 12 | test: 13 | @go test ./... 14 | 15 | test_redis: 16 | TEST_STORE=redis go test ./... 17 | 18 | test_all: 19 | TEST_STORE=redis go test ./... 20 | TEST_STORE=boltdb go test ./... 21 | TEST_STORE=mysql go test ./... 22 | TEST_STORE=postgres go test ./... 23 | 24 | cover_test: 25 | ./helper/test-cover.sh 26 | 27 | -------------------------------------------------------------------------------- /helper/.goreleaser.yml: -------------------------------------------------------------------------------- 1 | # .goreleaser.yml 2 | project_name: dtm 3 | builds: 4 | - id: dtm_amd64 5 | env: [CGO_ENABLED=0] 6 | goos: 7 | - linux 8 | - windows 9 | - darwin 10 | goarch: 11 | - amd64 12 | dir: . 13 | main: main.go 14 | ldflags: 15 | - -s -w -X main.Version={{.Version}} 16 | - id: dtm_arm64 17 | env: [ CGO_ENABLED=0 ] 18 | goos: 19 | - darwin 20 | goarch: 21 | - arm64 22 | dir: . 23 | main: main.go 24 | ldflags: 25 | - -s -w -X main.Version={{.Version}} 26 | -------------------------------------------------------------------------------- /charts/templates/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ include "dtm.fullname" . }} 5 | labels: 6 | {{- include "dtm.labels" . | nindent 4 }} 7 | spec: 8 | type: {{ .Values.service.type }} 9 | ports: 10 | - port: {{ .Values.service.ports.http }} 11 | targetPort: http 12 | protocol: TCP 13 | name: http 14 | - port: {{ .Values.service.ports.grpc }} 15 | targetPort: grpc 16 | protocol: TCP 17 | name: grpc 18 | selector: 19 | {{- include "dtm.selectorLabels" . | nindent 4 }} 20 | -------------------------------------------------------------------------------- /helper/Dockerfile-release: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | FROM --platform=$TARGETPLATFORM golang:1.16-alpine as builder 3 | ARG TARGETARCH 4 | ARG TARGETOS 5 | ARG RELEASE_VERSION 6 | WORKDIR /app/dtm 7 | # RUN go env -w GOPROXY=https://mirrors.aliyun.com/goproxy/,direct 8 | EXPOSE 8080 9 | COPY . . 10 | RUN CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -ldflags="-s -w -X main.Version=$RELEASE_VERSION" 11 | 12 | FROM --platform=$TARGETPLATFORM alpine 13 | COPY --from=builder /app/dtm/dtm /app/dtm/ 14 | WORKDIR /app/dtm 15 | ENTRYPOINT ["/app/dtm/dtm"] 16 | -------------------------------------------------------------------------------- /dtmsvr/storage/registry/factory.go: -------------------------------------------------------------------------------- 1 | package registry 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/dtm-labs/dtm/dtmsvr/storage" 7 | ) 8 | 9 | // SingletonFactory is the factory to build store in SINGLETON pattern. 10 | type SingletonFactory struct { 11 | once sync.Once 12 | 13 | store storage.Store 14 | 15 | creatorFunction func() storage.Store 16 | } 17 | 18 | // GetStorage implement the StorageFactory.GetStorage 19 | func (f *SingletonFactory) GetStorage() storage.Store { 20 | f.once.Do(func() { 21 | f.store = f.creatorFunction() 22 | }) 23 | 24 | return f.store 25 | } 26 | -------------------------------------------------------------------------------- /dtmgrpc/type_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 dtmgrpc 8 | 9 | import ( 10 | "context" 11 | "testing" 12 | 13 | "github.com/stretchr/testify/assert" 14 | ) 15 | 16 | func TestType(t *testing.T) { 17 | _, err := BarrierFromGrpc(context.Background()) 18 | assert.Error(t, err) 19 | 20 | _, err = TccFromGrpc(context.Background()) 21 | assert.Error(t, err) 22 | 23 | err = UseDriver("default") 24 | assert.Nil(t, err) 25 | } 26 | -------------------------------------------------------------------------------- /dtmcli/trans_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 dtmcli 8 | 9 | import ( 10 | "net/url" 11 | "testing" 12 | 13 | "github.com/stretchr/testify/assert" 14 | ) 15 | 16 | func TestQuery(t *testing.T) { 17 | qs, err := url.ParseQuery("a=b") 18 | assert.Nil(t, err) 19 | _, err = XaFromQuery(qs) 20 | assert.Error(t, err) 21 | _, err = TccFromQuery(qs) 22 | assert.Error(t, err) 23 | _, err = BarrierFromQuery(qs) 24 | assert.Error(t, err) 25 | } 26 | -------------------------------------------------------------------------------- /dtmgrpc/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 dtmgrpc 8 | 9 | import ( 10 | "context" 11 | 12 | "github.com/dtm-labs/dtm/dtmcli" 13 | "github.com/dtm-labs/dtm/dtmgrpc/dtmgimp" 14 | ) 15 | 16 | // BarrierFromGrpc generate a Barrier from grpc context 17 | func BarrierFromGrpc(ctx context.Context) (*dtmcli.BranchBarrier, error) { 18 | tb := dtmgimp.TransBaseFromGrpc(ctx) 19 | return dtmcli.BarrierFrom(tb.TransType, tb.Gid, tb.BranchID, tb.Op) 20 | } 21 | -------------------------------------------------------------------------------- /dtmcli/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 | -------------------------------------------------------------------------------- /dtmutil/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 dtmutil 8 | 9 | const ( 10 | // DefaultHTTPServer default url for http server. used by test and examples 11 | DefaultHTTPServer = "http://localhost:36789/api/dtmsvr" 12 | // DefaultJrpcServer default url for http json-rpc server. used by test and examples 13 | DefaultJrpcServer = "http://localhost:36789/api/json-rpc" 14 | // DefaultGrpcServer default url for grpc server. used by test and examples 15 | DefaultGrpcServer = "localhost:36790" 16 | ) 17 | -------------------------------------------------------------------------------- /helper/bench/setup.sh: -------------------------------------------------------------------------------- 1 | # !/bin/bash 2 | 3 | # install all commands needed 4 | 5 | apt update 6 | apt install -y sysbench apache2-utils mysql-client-core-8.0 redis redis-tools 7 | 8 | # install docker and docker-compose 9 | curl -fsSL https://get.docker.com -o get-docker.sh 10 | sh get-docker.sh 11 | curl -L "https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose 12 | chmod +x /usr/local/bin/docker-compose 13 | 14 | # install go 15 | wget https://golang.org/dl/go1.17.1.linux-amd64.tar.gz 16 | rm -rf /usr/local/go && tar -C /usr/local -xzf go1.17.1.linux-amd64.tar.gz && cp -f /usr/local/go/bin/go /usr/local/bin/go 17 | -------------------------------------------------------------------------------- /dtmcli/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 | } 25 | -------------------------------------------------------------------------------- /sqls/busi.mysql.sql: -------------------------------------------------------------------------------- 1 | CREATE DATABASE if not exists dtm_busi 2 | /*!40100 DEFAULT CHARACTER SET utf8mb4 */ 3 | ; 4 | drop table if exists dtm_busi.user_account; 5 | create table if not exists dtm_busi.user_account( 6 | id int(11) PRIMARY KEY AUTO_INCREMENT, 7 | user_id int(11) UNIQUE, 8 | balance DECIMAL(10, 2) not null default '0', 9 | trading_balance DECIMAL(10, 2) not null default '0', 10 | create_time datetime DEFAULT now(), 11 | update_time datetime DEFAULT now(), 12 | key(create_time), 13 | key(update_time) 14 | ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4; 15 | insert into dtm_busi.user_account (user_id, balance) 16 | values (1, 10000), 17 | (2, 10000) on DUPLICATE KEY 18 | UPDATE balance = 19 | values (balance); -------------------------------------------------------------------------------- /sqls/dtmcli.barrier.postgres.sql: -------------------------------------------------------------------------------- 1 | create schema if not exists dtm_barrier; 2 | drop table if exists dtm_barrier.barrier; 3 | CREATE SEQUENCE if not EXISTS dtm_barrier.barrier_seq; 4 | create table if not exists dtm_barrier.barrier( 5 | id bigint NOT NULL DEFAULT NEXTVAL ('dtm_barrier.barrier_seq'), 6 | trans_type varchar(45) default '', 7 | gid varchar(128) default '', 8 | branch_id varchar(128) default '', 9 | op varchar(45) default '', 10 | barrier_id varchar(45) default '', 11 | reason varchar(45) default '', 12 | create_time timestamp(0) with time zone DEFAULT NULL, 13 | update_time timestamp(0) with time zone DEFAULT NULL, 14 | PRIMARY KEY(id), 15 | CONSTRAINT uniq_barrier unique(gid, branch_id, op, barrier_id) 16 | ); -------------------------------------------------------------------------------- /helper/compose.cloud.yml: -------------------------------------------------------------------------------- 1 | version: '3.3' 2 | services: 3 | api: 4 | build: .. 5 | volumes: 6 | - /etc/localtime:/etc/localtime:ro 7 | - /etc/timezone:/etc/timezone:ro 8 | - ..:/app/dtm 9 | extra_hosts: 10 | - 'host.docker.internal:host-gateway' 11 | environment: 12 | IS_DOCKER: 1 13 | ports: 14 | - '9080:8080' 15 | mysql: 16 | image: 'mysql:5.7' 17 | volumes: 18 | - /etc/localtime:/etc/localtime:ro 19 | - /etc/timezone:/etc/timezone:ro 20 | environment: 21 | MYSQL_ALLOW_EMPTY_PASSWORD: 1 22 | command: 23 | [ 24 | '--character-set-server=utf8mb4', 25 | '--collation-server=utf8mb4_unicode_ci', 26 | ] 27 | ports: 28 | - '3306:3306' 29 | -------------------------------------------------------------------------------- /sqls/dtmcli.barrier.mysql.sql: -------------------------------------------------------------------------------- 1 | create database if not exists dtm_barrier 2 | /*!40100 DEFAULT CHARACTER SET utf8mb4 */ 3 | ; 4 | drop table if exists dtm_barrier.barrier; 5 | create table if not exists dtm_barrier.barrier( 6 | id bigint(22) PRIMARY KEY AUTO_INCREMENT, 7 | trans_type varchar(45) default '', 8 | gid varchar(128) default '', 9 | branch_id varchar(128) default '', 10 | op varchar(45) default '', 11 | barrier_id varchar(45) default '', 12 | reason varchar(45) default '' comment 'the branch type who insert this record', 13 | create_time datetime DEFAULT now(), 14 | update_time datetime DEFAULT now(), 15 | key(create_time), 16 | key(update_time), 17 | UNIQUE key(gid, branch_id, op, barrier_id) 18 | ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4; -------------------------------------------------------------------------------- /dtmcli/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 dtmcli 8 | 9 | import ( 10 | "net/url" 11 | "testing" 12 | 13 | "github.com/dtm-labs/dtm/dtmcli/dtmimp" 14 | "github.com/stretchr/testify/assert" 15 | ) 16 | 17 | func TestTypes(t *testing.T) { 18 | err := dtmimp.CatchP(func() { 19 | MustGenGid("http://localhost:36789/api/no") 20 | }) 21 | assert.Error(t, err) 22 | assert.Error(t, err) 23 | _, err = BarrierFromQuery(url.Values{}) 24 | assert.Error(t, err) 25 | 26 | } 27 | 28 | func TestXaSqlTimeout(t *testing.T) { 29 | old := GetXaSQLTimeoutMs() 30 | SetXaSQLTimeoutMs(old) 31 | SetBarrierTableName(dtmimp.BarrierTableName) // just cover this func 32 | } 33 | -------------------------------------------------------------------------------- /dtmsvr/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 dtmsvr 8 | 9 | import ( 10 | "testing" 11 | 12 | "github.com/stretchr/testify/assert" 13 | ) 14 | 15 | func TestUtils(t *testing.T) { 16 | CronExpiredTrans(1) 17 | sleepCronTime() 18 | } 19 | 20 | func TestSetNextCron(t *testing.T) { 21 | conf.RetryInterval = 10 22 | tg := TransGlobal{} 23 | tg.NextCronInterval = conf.RetryInterval 24 | tg.RetryInterval = 15 25 | assert.Equal(t, int64(15), tg.getNextCronInterval(cronReset)) 26 | tg.RetryInterval = 0 27 | assert.Equal(t, conf.RetryInterval, tg.getNextCronInterval(cronReset)) 28 | assert.Equal(t, conf.RetryInterval*2, tg.getNextCronInterval(cronBackoff)) 29 | tg.TimeoutToFail = 3 30 | assert.Equal(t, int64(3), tg.getNextCronInterval(cronReset)) 31 | } 32 | -------------------------------------------------------------------------------- /helper/test-cover.sh: -------------------------------------------------------------------------------- 1 | set -x 2 | echo "" > coverage.txt 3 | for store in redis mysql boltdb; do 4 | for d in $(go list ./... | grep -v vendor | grep -v test); do 5 | TEST_STORE=$store go test -covermode count -coverprofile=profile.out -coverpkg=github.com/dtm-labs/dtm/dtmcli,github.com/dtm-labs/dtm/dtmcli/dtmimp,github.com/dtm-labs/dtm/dtmcli/logger,github.com/dtm-labs/dtm/dtmgrpc,github.com/dtm-labs/dtm/dtmgrpc/dtmgimp,github.com/dtm-labs/dtm/dtmsvr,github.com/dtm-labs/dtm/dtmsvr/config,github.com/dtm-labs/dtm/dtmsvr/storage,github.com/dtm-labs/dtm/dtmsvr/storage/boltdb,github.com/dtm-labs/dtm/dtmsvr/storage/redis,github.com/dtm-labs/dtm/dtmsvr/storage/registry,github.com/dtm-labs/dtm/dtmsvr/storage/sql,github.com/dtm-labs/dtm/dtmutil -gcflags=-l $d || exit 1 6 | if [ -f profile.out ]; then 7 | cat profile.out >> coverage.txt 8 | echo > profile.out 9 | fi 10 | done 11 | done 12 | 13 | curl -s https://codecov.io/bash | bash 14 | -------------------------------------------------------------------------------- /sqls/busi.postgres.sql: -------------------------------------------------------------------------------- 1 | CREATE SCHEMA if not exists dtm_busi 2 | /* SQLINES DEMO *** RACTER SET utf8mb4 */ 3 | ; 4 | drop table if exists dtm_busi.user_account; 5 | -- SQLINES LICENSE FOR EVALUATION USE ONLY 6 | create sequence if not exists dtm_busi.user_account_seq; 7 | create table if not exists dtm_busi.user_account( 8 | id int PRIMARY KEY DEFAULT NEXTVAL ('dtm_busi.user_account_seq'), 9 | user_id int UNIQUE, 10 | balance DECIMAL(10, 2) not null default '0', 11 | trading_balance DECIMAL(10, 2) not null default '0', 12 | create_time timestamp(0) with time zone DEFAULT now(), 13 | update_time timestamp(0) with time zone DEFAULT now() 14 | ); 15 | -- SQLINES LICENSE FOR EVALUATION USE ONLY 16 | create index if not exists create_idx on dtm_busi.user_account(create_time); 17 | -- SQLINES LICENSE FOR EVALUATION USE ONLY 18 | create index if not exists update_idx on dtm_busi.user_account(update_time); 19 | TRUNCATE dtm_busi.user_account; 20 | insert into dtm_busi.user_account (user_id, balance) 21 | values (1, 10000), 22 | (2, 10000); -------------------------------------------------------------------------------- /charts/templates/hpa.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.autoscaling.enabled }} 2 | apiVersion: autoscaling/v2beta1 3 | kind: HorizontalPodAutoscaler 4 | metadata: 5 | name: {{ include "dtm.fullname" . }} 6 | labels: 7 | {{- include "dtm.labels" . | nindent 4 }} 8 | spec: 9 | scaleTargetRef: 10 | apiVersion: apps/v1 11 | kind: Deployment 12 | name: {{ include "dtm.fullname" . }} 13 | minReplicas: {{ .Values.autoscaling.minReplicas }} 14 | maxReplicas: {{ .Values.autoscaling.maxReplicas }} 15 | metrics: 16 | {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} 17 | - type: Resource 18 | resource: 19 | name: cpu 20 | targetAverageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} 21 | {{- end }} 22 | {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} 23 | - type: Resource 24 | resource: 25 | name: memory 26 | targetAverageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} 27 | {{- end }} 28 | {{- end }} 29 | -------------------------------------------------------------------------------- /dtmcli/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 | -------------------------------------------------------------------------------- /helper/bench/test-redis.sh: -------------------------------------------------------------------------------- 1 | # !/bin/bash 2 | 3 | set -x 4 | 5 | export LOG_LEVEL=warn 6 | export STORE_DRIVER=redis 7 | export STORE_HOST=localhost 8 | export STORE_PORT=6379 9 | cd .. && bench/bench redis & 10 | echo 'sleeping 3s for dtm bench to run up.' && sleep 3 11 | ab -n 1000000 -c 10 "http://127.0.0.1:8083/api/busi_bench/benchEmptyUrl" 12 | pkill bench 13 | 14 | redis-benchmark -n 300000 SET 'abcdefg' 'ddddddd' 15 | 16 | redis-benchmark -n 300000 EVAL "redis.call('SET', 'abcdedf', 'ddddddd')" 0 17 | 18 | redis-benchmark -n 300000 EVAL "redis.call('SET', KEYS[1], ARGV[1])" 1 'aaaaaaaaa' 'bbbbbbbbbb' 19 | 20 | redis-benchmark -n 3000000 -P 50 SET 'abcdefg' 'ddddddd' 21 | 22 | redis-benchmark -n 300000 EVAL "for k=1, 10 do; redis.call('SET', KEYS[1], ARGV[1]);end" 1 'aaaaaaaaa' 'bbbbbbbbbb' 23 | 24 | redis-benchmark -n 300000 -P 50 EVAL "redis.call('SET', KEYS[1], ARGV[1])" 1 'aaaaaaaaa' 'bbbbbbbbbb' 25 | 26 | redis-benchmark -n 300000 EVAL "for k=1,10 do;local c = cjson.decode(ARGV[1]);end" 1 'aaaaaaaaa' '{"aaaaa":"bbbbb","b":1,"t":"2012-01-01 14:00:00"}' 27 | 28 | -------------------------------------------------------------------------------- /test/busi/startup.go: -------------------------------------------------------------------------------- 1 | package busi 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/dtm-labs/dtm/dtmcli/dtmimp" 8 | "github.com/dtm-labs/dtm/dtmutil" 9 | "github.com/gin-gonic/gin" 10 | ) 11 | 12 | // Startup startup the busi's grpc and http service 13 | func Startup() *gin.Engine { 14 | GrpcStartup() 15 | return BaseAppStartup() 16 | } 17 | 18 | // PopulateDB populate example mysql data 19 | func PopulateDB(skipDrop bool) { 20 | resetXaData() 21 | file := fmt.Sprintf("%s/busi.%s.sql", dtmutil.GetSQLDir(), BusiConf.Driver) 22 | dtmutil.RunSQLScript(BusiConf, file, skipDrop) 23 | file = fmt.Sprintf("%s/dtmcli.barrier.%s.sql", dtmutil.GetSQLDir(), BusiConf.Driver) 24 | dtmutil.RunSQLScript(BusiConf, file, skipDrop) 25 | file = fmt.Sprintf("%s/dtmsvr.storage.%s.sql", dtmutil.GetSQLDir(), BusiConf.Driver) 26 | dtmutil.RunSQLScript(BusiConf, file, skipDrop) 27 | _, err := RedisGet().FlushAll(context.Background()).Result() // redis barrier need clear 28 | dtmimp.E2P(err) 29 | SetRedisBothAccount(10000, 10000) 30 | SetupMongoBarrierAndBusi() 31 | } 32 | -------------------------------------------------------------------------------- /test/tcc_jrpc_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/dtm-labs/dtm/dtmcli" 7 | "github.com/dtm-labs/dtm/dtmcli/dtmimp" 8 | "github.com/dtm-labs/dtm/dtmutil" 9 | "github.com/dtm-labs/dtm/test/busi" 10 | "github.com/go-resty/resty/v2" 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | func TestTccJrpcNormal(t *testing.T) { 15 | req := busi.GenTransReq(30, false, false) 16 | gid := dtmimp.GetFuncName() 17 | err := dtmcli.TccGlobalTransaction2(dtmutil.DefaultJrpcServer, gid, func(tcc *dtmcli.Tcc) { 18 | tcc.Protocol = dtmimp.Jrpc 19 | }, func(tcc *dtmcli.Tcc) (*resty.Response, error) { 20 | _, err := tcc.CallBranch(req, Busi+"/TransOut", Busi+"/TransOutConfirm", Busi+"/TransOutRevert") 21 | assert.Nil(t, err) 22 | return tcc.CallBranch(req, Busi+"/TransIn", Busi+"/TransInConfirm", Busi+"/TransInRevert") 23 | }) 24 | assert.Nil(t, err) 25 | waitTransProcessed(gid) 26 | assert.Equal(t, StatusSucceed, getTransStatus(gid)) 27 | assert.Equal(t, []string{StatusPrepared, StatusSucceed, StatusPrepared, StatusSucceed}, getBranchesStatus(gid)) 28 | } 29 | -------------------------------------------------------------------------------- /test/busi/base_jrpc.go: -------------------------------------------------------------------------------- 1 | package busi 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/dtm-labs/dtm/dtmcli/dtmimp" 7 | "github.com/dtm-labs/dtm/dtmcli/logger" 8 | "github.com/dtm-labs/dtm/dtmutil" 9 | "github.com/gin-gonic/gin" 10 | ) 11 | 12 | // BusiJrpcURL url prefix for busi 13 | var BusiJrpcURL = fmt.Sprintf("http://localhost:%d/api/json-rpc?method=", BusiPort) 14 | 15 | func addJrpcRoute(app *gin.Engine) { 16 | app.POST("/api/json-rpc", dtmutil.WrapHandler2(func(c *gin.Context) interface{} { 17 | var data map[string]interface{} 18 | err := c.BindJSON(&data) 19 | dtmimp.E2P(err) 20 | logger.Debugf("method is: %s", data["method"]) 21 | var rerr map[string]interface{} 22 | r := MainSwitch.JrpcResult.Fetch() 23 | if r != "" { 24 | rerr = map[string]interface{}{ 25 | "code": map[string]int{ 26 | "FAILURE": dtmimp.JrpcCodeFailure, 27 | "ONGOING": dtmimp.JrpcCodeOngoing, 28 | "OTHER": -23977, 29 | }, 30 | } 31 | } 32 | return map[string]interface{}{ 33 | "jsonrpc": "2.0", 34 | "error": rerr, 35 | "id": data["id"], 36 | } 37 | })) 38 | } 39 | -------------------------------------------------------------------------------- /helper/bench/Makefile: -------------------------------------------------------------------------------- 1 | # All targets. 2 | default: bench 3 | 4 | # configure these paths according to you system 5 | bench: /usr/local/bin/go /etc/redis/redis.conf /usr/local/bin/docker-compose main.go 6 | rm -f ../conf.sample.yml 7 | go build -o bench 8 | 9 | go: /usr/local/bin/go 10 | 11 | redis: /etc/redis/redis.conf 12 | 13 | mysql: /usr/local/bin/docker-compose 14 | 15 | /usr/local/bin/go: 16 | wget https://golang.org/dl/go1.17.1.linux-amd64.tar.gz 17 | rm -rf /usr/local/go && tar -C /usr/local -xzf go1.17.1.linux-amd64.tar.gz && cp -f /usr/local/go/bin/go /usr/local/bin/go && rm go1.* 18 | 19 | /etc/redis/redis.conf: 20 | apt update 21 | apt install -y redis redis-tools 22 | 23 | /usr/local/bin/docker-compose: 24 | apt update 25 | apt install -y sysbench apache2-utils mysql-client-core-8.0 26 | curl -fsSL https://get.docker.com | sh 27 | curl -L "https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose 28 | chmod +x /usr/local/bin/docker-compose 29 | cd .. && docker-compose -f helper/compose.mysql.yml up -d && cd bench 30 | -------------------------------------------------------------------------------- /dtmcli/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() 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() 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 | -------------------------------------------------------------------------------- /helper/compose.store.yml: -------------------------------------------------------------------------------- 1 | version: '3.3' 2 | services: 3 | mysql: 4 | image: 'mysql:5.7' 5 | volumes: 6 | - /etc/localtime:/etc/localtime:ro 7 | - /etc/timezone:/etc/timezone:ro 8 | environment: 9 | MYSQL_ALLOW_EMPTY_PASSWORD: 1 10 | command: 11 | [ 12 | '--character-set-server=utf8mb4', 13 | '--collation-server=utf8mb4_unicode_ci', 14 | ] 15 | ports: 16 | - '3306:3306' 17 | postgres: 18 | image: 'postgres:13' 19 | command: postgres --max_prepared_transactions=1000 20 | volumes: 21 | - /etc/localtime:/etc/localtime:ro 22 | - /etc/timezone:/etc/timezone:ro 23 | environment: 24 | POSTGRES_PASSWORD: mysecretpassword 25 | 26 | ports: 27 | - '5432:5432' 28 | redis: 29 | image: 'redis' 30 | volumes: 31 | - /etc/localtime:/etc/localtime:ro 32 | - /etc/timezone:/etc/timezone:ro 33 | ports: 34 | - '6379:6379' 35 | mongo: 36 | image: yedf/mongo-rs 37 | volumes: 38 | - /etc/localtime:/etc/localtime:ro 39 | - /etc/timezone:/etc/timezone:ro 40 | ports: 41 | - '27017:27017' 42 | -------------------------------------------------------------------------------- /charts/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: dtm 3 | description: A Helm chart for Kubernetes 4 | 5 | # A chart can be either an 'application' or a 'library' chart. 6 | # 7 | # Application charts are a collection of templates that can be packaged into versioned archives 8 | # to be deployed. 9 | # 10 | # Library charts provide useful utilities or functions for the chart developer. They're included as 11 | # a dependency of application charts to inject those utilities and functions into the rendering 12 | # pipeline. Library charts do not define any templates and therefore cannot be deployed. 13 | type: application 14 | 15 | # This is the chart version. This version number should be incremented each time you make changes 16 | # to the chart and its templates, including the app version. 17 | # Versions are expected to follow Semantic Versioning (https://semver.org/) 18 | version: 0.1.0 19 | 20 | # This is the version number of the application being deployed. This version number should be 21 | # incremented each time you make changes to the application. Versions are not expected to 22 | # follow Semantic Versioning. They should reflect the version the application is using. 23 | # It is recommended to use it with quotes. 24 | appVersion: "1.12.2" 25 | -------------------------------------------------------------------------------- /test/saga_compatible_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 test 8 | 9 | import ( 10 | "fmt" 11 | "testing" 12 | 13 | "github.com/dtm-labs/dtm/dtmcli/dtmimp" 14 | "github.com/dtm-labs/dtm/dtmutil" 15 | "github.com/dtm-labs/dtm/test/busi" 16 | "github.com/stretchr/testify/assert" 17 | ) 18 | 19 | func TestSagaCompatibleNormal(t *testing.T) { // compatible with old http, which put payload in steps.data 20 | gid := dtmimp.GetFuncName() 21 | body := fmt.Sprintf(`{"gid":"%s","trans_type":"saga","steps":[{"action":"%s/TransOut","compensate":"%s/TransOutRevert","data":"{\"amount\":30,\"transInResult\":\"SUCCESS\",\"transOutResult\":\"SUCCESS\"}"},{"action":"%s/TransIn","compensate":"%s/TransInRevert","data":"{\"amount\":30,\"transInResult\":\"SUCCESS\",\"transOutResult\":\"SUCCESS\"}"}]}`, 22 | gid, busi.Busi, busi.Busi, busi.Busi, busi.Busi) 23 | dtmimp.RestyClient.R().SetBody(body).Post(fmt.Sprintf("%s/submit", dtmutil.DefaultHTTPServer)) 24 | waitTransProcessed(gid) 25 | assert.Equal(t, []string{StatusPrepared, StatusSucceed, StatusPrepared, StatusSucceed}, getBranchesStatus(gid)) 26 | assert.Equal(t, StatusSucceed, getTransStatus(gid)) 27 | } 28 | -------------------------------------------------------------------------------- /test/msg_delay_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/dtm-labs/dtm/dtmcli" 7 | "github.com/dtm-labs/dtm/dtmcli/dtmimp" 8 | "github.com/dtm-labs/dtm/dtmsvr" 9 | "github.com/dtm-labs/dtm/dtmutil" 10 | "github.com/dtm-labs/dtm/test/busi" 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | func genMsgDelay(gid string) *dtmcli.Msg { 15 | req := busi.GenTransReq(30, false, false) 16 | msg := dtmcli.NewMsg(dtmutil.DefaultHTTPServer, gid). 17 | Add(busi.Busi+"/TransOut", &req). 18 | Add(busi.Busi+"/TransIn", &req).SetDelay(10) 19 | msg.QueryPrepared = busi.Busi + "/QueryPrepared" 20 | return msg 21 | } 22 | 23 | func TestMsgDelayNormal(t *testing.T) { 24 | gid := dtmimp.GetFuncName() 25 | msg := genMsgDelay(gid) 26 | submitForwardCron(0, func() { 27 | msg.Submit() 28 | waitTransProcessed(msg.Gid) 29 | }) 30 | 31 | dtmsvr.NowForwardDuration = 0 32 | assert.Equal(t, []string{StatusPrepared, StatusPrepared}, getBranchesStatus(msg.Gid)) 33 | assert.Equal(t, StatusSubmitted, getTransStatus(msg.Gid)) 34 | cronTransOnceForwardCron(t, "", 0) 35 | cronTransOnceForwardCron(t, "", 8) 36 | cronTransOnceForwardCron(t, gid, 12) 37 | assert.Equal(t, []string{StatusSucceed, StatusSucceed}, getBranchesStatus(msg.Gid)) 38 | assert.Equal(t, StatusSucceed, getTransStatus(msg.Gid)) 39 | } 40 | -------------------------------------------------------------------------------- /test/common_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/dtm-labs/dtm/dtmcli" 7 | "github.com/dtm-labs/dtm/dtmcli/dtmimp" 8 | "github.com/dtm-labs/dtm/dtmgrpc" 9 | "github.com/dtm-labs/dtm/dtmsvr/storage/sql" 10 | "github.com/dtm-labs/dtm/dtmutil" 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | func TestGeneralDB(t *testing.T) { 15 | if conf.Store.IsDB() { 16 | testSql(t) 17 | testDbAlone(t) 18 | } 19 | } 20 | 21 | func testSql(t *testing.T) { 22 | conf := conf.Store.GetDBConf() 23 | conf.Host = "127.0.0.1" // use a new host to trigger SetDBConn called 24 | db := dtmutil.DbGet(conf, sql.SetDBConn) 25 | err := func() (rerr error) { 26 | defer dtmimp.P2E(&rerr) 27 | db.Must().Exec("select a") 28 | return nil 29 | }() 30 | assert.NotEqual(t, nil, err) 31 | } 32 | 33 | func testDbAlone(t *testing.T) { 34 | db, err := dtmimp.StandaloneDB(conf.Store.GetDBConf()) 35 | assert.Nil(t, err) 36 | _, err = dtmimp.DBExec(db, "select 1") 37 | assert.Equal(t, nil, err) 38 | _, err = dtmimp.DBExec(db, "") 39 | assert.Equal(t, nil, err) 40 | db.Close() 41 | _, err = dtmimp.DBExec(db, "select 1") 42 | assert.NotEqual(t, nil, err) 43 | } 44 | 45 | func TestMustGenGid(t *testing.T) { 46 | dtmgrpc.MustGenGid(dtmutil.DefaultGrpcServer) 47 | dtmcli.MustGenGid(dtmutil.DefaultHTTPServer) 48 | } 49 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/dtm-labs/dtm 2 | 3 | go 1.16 4 | 5 | require ( 6 | bou.ke/monkey v1.0.2 7 | github.com/BurntSushi/toml v0.4.1 // indirect 8 | github.com/dtm-labs/dtmdriver v0.0.1 9 | github.com/dtm-labs/dtmdriver-gozero v0.0.2 10 | github.com/dtm-labs/dtmdriver-kratos v0.0.4 11 | github.com/dtm-labs/dtmdriver-polaris v0.0.4 12 | github.com/dtm-labs/dtmdriver-protocol1 v0.0.1 13 | github.com/gin-gonic/gin v1.7.7 14 | github.com/go-redis/redis/v8 v8.11.4 15 | github.com/go-resty/resty/v2 v2.7.0 16 | github.com/go-sql-driver/mysql v1.6.0 17 | github.com/lib/pq v1.10.4 18 | github.com/lithammer/shortuuid v2.0.3+incompatible 19 | github.com/lithammer/shortuuid/v3 v3.0.7 20 | github.com/natefinch/lumberjack v2.0.0+incompatible 21 | github.com/onsi/gomega v1.16.0 22 | github.com/prometheus/client_golang v1.11.0 23 | github.com/stretchr/testify v1.7.0 24 | go.etcd.io/bbolt v1.3.6 25 | go.mongodb.org/mongo-driver v1.8.3 26 | go.uber.org/automaxprocs v1.4.1-0.20210525221652-0180b04c18a7 27 | go.uber.org/zap v1.21.0 28 | golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa // indirect 29 | google.golang.org/grpc v1.44.0 30 | google.golang.org/protobuf v1.27.1 31 | gopkg.in/yaml.v2 v2.4.0 32 | gorm.io/driver/mysql v1.0.3 33 | gorm.io/driver/postgres v1.2.1 34 | gorm.io/gorm v1.22.2 35 | // gotest.tools v2.2.0+incompatible 36 | ) 37 | -------------------------------------------------------------------------------- /dtmsvr/trans_type_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 dtmsvr 8 | 9 | import ( 10 | "github.com/dtm-labs/dtm/dtmcli" 11 | "github.com/dtm-labs/dtm/dtmcli/dtmimp" 12 | ) 13 | 14 | type transXaProcessor struct { 15 | *TransGlobal 16 | } 17 | 18 | func init() { 19 | registorProcessorCreator("xa", func(trans *TransGlobal) transProcessor { return &transXaProcessor{TransGlobal: trans} }) 20 | } 21 | 22 | func (t *transXaProcessor) GenBranches() []TransBranch { 23 | return []TransBranch{} 24 | } 25 | 26 | func (t *transXaProcessor) ProcessOnce(branches []TransBranch) error { 27 | if !t.needProcess() { 28 | return nil 29 | } 30 | if t.Status == dtmcli.StatusPrepared && t.isTimeout() { 31 | t.changeStatus(dtmcli.StatusAborting) 32 | } 33 | currentType := dtmimp.If(t.Status == dtmcli.StatusSubmitted, dtmimp.OpCommit, dtmimp.OpRollback).(string) 34 | for i, branch := range branches { 35 | if branch.Op == currentType && branch.Status != dtmcli.StatusSucceed { 36 | err := t.execBranch(&branch, i) 37 | if err != nil { 38 | return err 39 | } 40 | } 41 | } 42 | t.changeStatus(dtmimp.If(t.Status == dtmcli.StatusSubmitted, dtmcli.StatusSucceed, dtmcli.StatusFailed).(string)) 43 | return nil 44 | } 45 | -------------------------------------------------------------------------------- /helper/bench/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/dtm-labs/dtm/dtmcli" 8 | "github.com/dtm-labs/dtm/dtmcli/logger" 9 | "github.com/dtm-labs/dtm/dtmsvr" 10 | "github.com/dtm-labs/dtm/dtmsvr/config" 11 | "github.com/dtm-labs/dtm/dtmsvr/storage/registry" 12 | "github.com/dtm-labs/dtm/helper/bench/svr" 13 | "github.com/dtm-labs/dtm/test/busi" 14 | ) 15 | 16 | var usage = `bench is a bench test server for dtmf 17 | usage: 18 | redis prepare for redis bench test 19 | db prepare for mysql|postgres bench test 20 | boltdb prepare for boltdb bench test 21 | ` 22 | 23 | func hintAndExit() { 24 | fmt.Print(usage) 25 | os.Exit(0) 26 | } 27 | 28 | var conf = &config.Config 29 | 30 | func main() { 31 | if len(os.Args) <= 1 { 32 | hintAndExit() 33 | } 34 | logger.Infof("starting bench server") 35 | config.MustLoadConfig("") 36 | logger.InitLog(conf.LogLevel) 37 | registry.WaitStoreUp() 38 | dtmsvr.PopulateDB(false) 39 | if os.Args[1] == "db" { 40 | if busi.BusiConf.Driver == "mysql" { 41 | dtmcli.SetCurrentDBType(busi.BusiConf.Driver) 42 | svr.PrepareBenchDB() 43 | } 44 | busi.PopulateDB(false) 45 | } else if os.Args[1] == "redis" || os.Args[1] == "boltdb" { 46 | 47 | } else { 48 | hintAndExit() 49 | } 50 | dtmsvr.StartSvr() 51 | go dtmsvr.CronExpiredTrans(-1) 52 | svr.StartSvr() 53 | select {} 54 | } 55 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | on: 3 | push: 4 | branches-ignore: 5 | - 'tmp-*' 6 | pull_request: 7 | branches-ignore: 8 | - 'tmp-*' 9 | 10 | jobs: 11 | tests: 12 | name: CI 13 | runs-on: ubuntu-latest 14 | services: 15 | mysql: 16 | image: 'mysql:5.7' 17 | env: 18 | MYSQL_ALLOW_EMPTY_PASSWORD: 1 19 | volumes: 20 | - /etc/localtime:/etc/localtime:ro 21 | - /etc/timezone:/etc/timezone:ro 22 | ports: 23 | - 3306:3306 24 | redis: 25 | image: 'redis' 26 | volumes: 27 | - /etc/localtime:/etc/localtime:ro 28 | - /etc/timezone:/etc/timezone:ro 29 | ports: 30 | - 6379:6379 31 | mongo: 32 | image: 'yedf/mongo-rs' 33 | volumes: 34 | - /etc/localtime:/etc/localtime:ro 35 | - /etc/timezone:/etc/timezone:ro 36 | ports: 37 | - 27017:27017 38 | steps: 39 | - name: Set up Go 1.16 40 | uses: actions/setup-go@v2 41 | with: 42 | go-version: '1.16' 43 | 44 | - name: Check out code 45 | uses: actions/checkout@v2 46 | 47 | - name: Install dependencies 48 | run: | 49 | go mod download 50 | 51 | - name: Run CI lint 52 | run: sh helper/golint.sh 53 | 54 | - name: Run test cover 55 | run: sh helper/test-cover.sh 56 | -------------------------------------------------------------------------------- /dtmgrpc/dtmgpb/dtmgimp.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | option go_package = "./dtmgpb"; 4 | import "google/protobuf/empty.proto"; 5 | 6 | package dtmgimp; 7 | 8 | // The dtm service definition. 9 | service Dtm { 10 | rpc NewGid(google.protobuf.Empty) returns (DtmGidReply) {} 11 | rpc Submit(DtmRequest) returns (google.protobuf.Empty) {} 12 | rpc Prepare(DtmRequest) returns (google.protobuf.Empty) {} 13 | rpc Abort(DtmRequest) returns (google.protobuf.Empty) {} 14 | rpc RegisterBranch(DtmBranchRequest) returns (google.protobuf.Empty) {} 15 | } 16 | 17 | message DtmTransOptions { 18 | bool WaitResult = 1; 19 | int64 TimeoutToFail = 2; 20 | int64 RetryInterval = 3; 21 | repeated string PassthroughHeaders = 4; 22 | map BranchHeaders = 5; 23 | int64 RequestTimeout = 6; 24 | } 25 | 26 | // DtmRequest request sent to dtm server 27 | message DtmRequest { 28 | string Gid = 1; 29 | string TransType = 2; 30 | DtmTransOptions TransOptions = 3; 31 | string CustomedData = 4; 32 | repeated bytes BinPayloads = 5; // for MSG/SAGA branch payloads 33 | string QueryPrepared = 6; // for MSG 34 | string Steps = 7; 35 | } 36 | 37 | message DtmGidReply { 38 | string Gid = 1; 39 | } 40 | 41 | message DtmBranchRequest { 42 | string Gid = 1; 43 | string TransType = 2; 44 | string BranchID = 3; 45 | string Op = 4; 46 | map Data = 5; 47 | bytes BusiPayload = 6; 48 | } 49 | 50 | -------------------------------------------------------------------------------- /dtmsvr/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 dtmsvr 8 | 9 | import ( 10 | "fmt" 11 | "time" 12 | 13 | "github.com/dtm-labs/dtm/dtmcli/dtmimp" 14 | "github.com/dtm-labs/dtm/dtmsvr/config" 15 | "github.com/dtm-labs/dtm/dtmsvr/storage" 16 | "github.com/dtm-labs/dtm/dtmsvr/storage/registry" 17 | "github.com/lithammer/shortuuid/v3" 18 | ) 19 | 20 | type branchStatus struct { 21 | id uint64 22 | gid string 23 | status string 24 | finishTime *time.Time 25 | } 26 | 27 | var e2p = dtmimp.E2P 28 | 29 | var conf = &config.Config 30 | 31 | // GetStore returns storage.Store 32 | func GetStore() storage.Store { 33 | return registry.GetStore() 34 | } 35 | 36 | // TransProcessedTestChan only for test usage. when transaction processed once, write gid to this chan 37 | var TransProcessedTestChan chan string 38 | 39 | // GenGid generate gid, use uuid 40 | func GenGid() string { 41 | return shortuuid.New() 42 | } 43 | 44 | // GetTransGlobal construct trans from db 45 | func GetTransGlobal(gid string) *TransGlobal { 46 | trans := GetStore().FindTransGlobalStore(gid) 47 | //nolint:staticcheck 48 | dtmimp.PanicIf(trans == nil, fmt.Errorf("no TransGlobal with gid: %s found", gid)) 49 | //nolint:staticcheck 50 | return &TransGlobal{TransGlobalStore: *trans} 51 | } 52 | -------------------------------------------------------------------------------- /dtmsvr/storage/registry/registry.go: -------------------------------------------------------------------------------- 1 | package registry 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/dtm-labs/dtm/dtmsvr/config" 7 | "github.com/dtm-labs/dtm/dtmsvr/storage" 8 | "github.com/dtm-labs/dtm/dtmsvr/storage/boltdb" 9 | "github.com/dtm-labs/dtm/dtmsvr/storage/redis" 10 | "github.com/dtm-labs/dtm/dtmsvr/storage/sql" 11 | ) 12 | 13 | var conf = &config.Config 14 | 15 | // StorageFactory is factory to get storage instance. 16 | type StorageFactory interface { 17 | // GetStorage will return the Storage instance. 18 | GetStorage() storage.Store 19 | } 20 | 21 | var sqlFac = &SingletonFactory{ 22 | creatorFunction: func() storage.Store { 23 | return &sql.Store{} 24 | }, 25 | } 26 | 27 | var storeFactorys = map[string]StorageFactory{ 28 | "boltdb": &SingletonFactory{ 29 | creatorFunction: func() storage.Store { 30 | return boltdb.NewStore(conf.Store.DataExpire, conf.RetryInterval) 31 | }, 32 | }, 33 | "redis": &SingletonFactory{ 34 | creatorFunction: func() storage.Store { 35 | return &redis.Store{} 36 | }, 37 | }, 38 | "mysql": sqlFac, 39 | "postgres": sqlFac, 40 | } 41 | 42 | // GetStore returns storage.Store 43 | func GetStore() storage.Store { 44 | return storeFactorys[conf.Store.Driver].GetStorage() 45 | } 46 | 47 | // WaitStoreUp wait for db to go up 48 | func WaitStoreUp() { 49 | for err := GetStore().Ping(); err != nil; err = GetStore().Ping() { 50 | time.Sleep(3 * time.Second) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /helper/sync-dtmcli.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | set -x 3 | ver=$1 4 | if [ x$ver == x ]; then 5 | echo please specify you version like vx.x.x; 6 | exit 1; 7 | fi 8 | 9 | if [ ${ver:0:1} != v ]; then 10 | echo please specify you version like vx.x.x; 11 | exit 1; 12 | fi 13 | 14 | cd ../dtmcli 15 | cp -rf ../dtm/dtmcli/* ./ 16 | rm -f *_test.go logger/*.log 17 | sed -i '' -e 's/dtm-labs\/dtm\//dtm-labs\//g' *.go */**.go 18 | go mod tidy 19 | go build || exit 1 20 | git add . 21 | git commit -m"update from dtm to version $ver" 22 | git push 23 | git tag $ver 24 | git push --tags 25 | 26 | cd ../dtmcli-go-sample 27 | go get -u github.com/dtm-labs/dtmcli@$ver 28 | go mod tidy 29 | go build || exit 1 30 | git add . 31 | git commit -m"update from dtm to version $ver" 32 | git push 33 | 34 | 35 | cd ../dtmgrpc 36 | rm -rf *.go dtmgimp 37 | cp -r ../dtm/dtmgrpc/* ./ 38 | go get github.com/dtm-labs/dtmcli@$ver 39 | sed -i '' -e 's/dtm-labs\/dtm\//dtm-labs\//g' *.go */**.go 40 | rm -rf *_test.go 41 | go mod tidy 42 | go build || exit 1 43 | git add . 44 | git commit -m"update from dtm to version $ver" 45 | git push 46 | git tag $ver 47 | git push --tags 48 | 49 | cd ../dtmgrpc-go-sample 50 | go get github.com/dtm-labs/dtmcli@$ver 51 | go get github.com/dtm-labs/dtmgrpc@$ver 52 | protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative busi/*.proto || exit 1 53 | go build || exit 1 54 | git add . 55 | git commit -m"update from dtm to version $ver" 56 | git push -------------------------------------------------------------------------------- /dtmsvr/storage/store.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 storage 8 | 9 | import ( 10 | "errors" 11 | "time" 12 | ) 13 | 14 | // ErrNotFound defines the query item is not found in storage implement. 15 | var ErrNotFound = errors.New("storage: NotFound") 16 | 17 | // ErrUniqueConflict defines the item is conflict with unique key in storage implement. 18 | var ErrUniqueConflict = errors.New("storage: UniqueKeyConflict") 19 | 20 | // Store defines storage relevant interface 21 | type Store interface { 22 | Ping() error 23 | PopulateData(skipDrop bool) 24 | FindTransGlobalStore(gid string) *TransGlobalStore 25 | ScanTransGlobalStores(position *string, limit int64) []TransGlobalStore 26 | FindBranches(gid string) []TransBranchStore 27 | UpdateBranches(branches []TransBranchStore, updates []string) (int, error) 28 | LockGlobalSaveBranches(gid string, status string, branches []TransBranchStore, branchStart int) 29 | MaySaveNewTrans(global *TransGlobalStore, branches []TransBranchStore) error 30 | ChangeGlobalStatus(global *TransGlobalStore, newStatus string, updates []string, finished bool) 31 | TouchCronTime(global *TransGlobalStore, nextCronInterval int64, nextCronTime *time.Time) 32 | LockOneGlobalTrans(expireIn time.Duration) *TransGlobalStore 33 | ResetCronTime(timeout time.Duration, limit int64) (succeedCount int64, hasRemaining bool, err error) 34 | } 35 | -------------------------------------------------------------------------------- /dtmsvr/trans_type_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 dtmsvr 8 | 9 | import ( 10 | "github.com/dtm-labs/dtm/dtmcli" 11 | "github.com/dtm-labs/dtm/dtmcli/dtmimp" 12 | "github.com/dtm-labs/dtm/dtmcli/logger" 13 | ) 14 | 15 | type transTccProcessor struct { 16 | *TransGlobal 17 | } 18 | 19 | func init() { 20 | registorProcessorCreator("tcc", func(trans *TransGlobal) transProcessor { return &transTccProcessor{TransGlobal: trans} }) 21 | } 22 | 23 | func (t *transTccProcessor) GenBranches() []TransBranch { 24 | return []TransBranch{} 25 | } 26 | 27 | func (t *transTccProcessor) ProcessOnce(branches []TransBranch) error { 28 | if !t.needProcess() { 29 | return nil 30 | } 31 | if t.Status == dtmcli.StatusPrepared && t.isTimeout() { 32 | t.changeStatus(dtmcli.StatusAborting) 33 | } 34 | op := dtmimp.If(t.Status == dtmcli.StatusSubmitted, dtmimp.OpConfirm, dtmimp.OpCancel).(string) 35 | for current := len(branches) - 1; current >= 0; current-- { 36 | if branches[current].Op == op && branches[current].Status == dtmcli.StatusPrepared { 37 | logger.Debugf("branch info: current: %d ID: %d", current, branches[current].ID) 38 | err := t.execBranch(&branches[current], current) 39 | if err != nil { 40 | return err 41 | } 42 | } 43 | } 44 | t.changeStatus(dtmimp.If(t.Status == dtmcli.StatusSubmitted, dtmcli.StatusSucceed, dtmcli.StatusFailed).(string)) 45 | return nil 46 | } 47 | -------------------------------------------------------------------------------- /test/dtmsvr_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 test 8 | 9 | import ( 10 | "testing" 11 | "time" 12 | 13 | "github.com/dtm-labs/dtm/dtmcli/dtmimp" 14 | "github.com/dtm-labs/dtm/dtmsvr" 15 | "github.com/dtm-labs/dtm/dtmsvr/config" 16 | "github.com/dtm-labs/dtm/dtmutil" 17 | "github.com/dtm-labs/dtm/test/busi" 18 | "github.com/stretchr/testify/assert" 19 | ) 20 | 21 | var DtmServer = dtmutil.DefaultHTTPServer 22 | var DtmGrpcServer = dtmutil.DefaultGrpcServer 23 | var Busi = busi.Busi 24 | 25 | func getTransStatus(gid string) string { 26 | return dtmsvr.GetTransGlobal(gid).Status 27 | } 28 | 29 | func getBranchesStatus(gid string) []string { 30 | branches := dtmsvr.GetStore().FindBranches(gid) 31 | status := []string{} 32 | for _, branch := range branches { 33 | status = append(status, branch.Status) 34 | } 35 | return status 36 | } 37 | 38 | func TestUpdateBranchAsync(t *testing.T) { 39 | if conf.Store.Driver != config.Mysql { 40 | return 41 | } 42 | conf.UpdateBranchSync = 0 43 | saga := genSaga1(dtmimp.GetFuncName(), false, false) 44 | saga.WaitResult = true 45 | err := saga.Submit() 46 | assert.Nil(t, err) 47 | waitTransProcessed(saga.Gid) 48 | time.Sleep(dtmsvr.UpdateBranchAsyncInterval) 49 | assert.Equal(t, []string{StatusPrepared, StatusSucceed}, getBranchesStatus(saga.Gid)) 50 | assert.Equal(t, StatusSucceed, getTransStatus(saga.Gid)) 51 | conf.UpdateBranchSync = 1 52 | } 53 | -------------------------------------------------------------------------------- /dtmgrpc/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 dtmgrpc 8 | 9 | import ( 10 | "github.com/dtm-labs/dtm/dtmcli" 11 | "github.com/dtm-labs/dtm/dtmgrpc/dtmgimp" 12 | "google.golang.org/protobuf/proto" 13 | ) 14 | 15 | // SagaGrpc struct of saga 16 | type SagaGrpc struct { 17 | dtmcli.Saga 18 | } 19 | 20 | // NewSagaGrpc create a saga 21 | func NewSagaGrpc(server string, gid string) *SagaGrpc { 22 | return &SagaGrpc{Saga: *dtmcli.NewSaga(server, gid)} 23 | } 24 | 25 | // Add add a saga step 26 | func (s *SagaGrpc) Add(action string, compensate string, payload proto.Message) *SagaGrpc { 27 | s.Steps = append(s.Steps, map[string]string{"action": action, "compensate": compensate}) 28 | s.BinPayloads = append(s.BinPayloads, dtmgimp.MustProtoMarshal(payload)) 29 | return s 30 | } 31 | 32 | // AddBranchOrder specify that branch should be after preBranches. branch should is larger than all the element in preBranches 33 | func (s *SagaGrpc) AddBranchOrder(branch int, preBranches []int) *SagaGrpc { 34 | s.Saga.AddBranchOrder(branch, preBranches) 35 | return s 36 | } 37 | 38 | // EnableConcurrent enable the concurrent exec of sub trans 39 | func (s *SagaGrpc) EnableConcurrent() *SagaGrpc { 40 | s.Saga.SetConcurrent() 41 | return s 42 | } 43 | 44 | // Submit submit the saga trans 45 | func (s *SagaGrpc) Submit() error { 46 | s.Saga.BuildCustomOptions() 47 | return dtmgimp.DtmGrpcCall(&s.Saga.TransBase, "Submit") 48 | } 49 | -------------------------------------------------------------------------------- /dtmutil/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 dtmutil 8 | 9 | import ( 10 | "errors" 11 | "fmt" 12 | "io" 13 | "net/http" 14 | "net/http/httptest" 15 | "strings" 16 | "testing" 17 | 18 | "github.com/gin-gonic/gin" 19 | "github.com/stretchr/testify/assert" 20 | ) 21 | 22 | func TestGin(t *testing.T) { 23 | app := GetGinApp() 24 | app.GET("/api/sample", WrapHandler2(func(c *gin.Context) interface{} { 25 | return 1 26 | })) 27 | app.GET("/api/error", WrapHandler2(func(c *gin.Context) interface{} { 28 | return errors.New("err1") 29 | })) 30 | getResultString := func(api string, body io.Reader) string { 31 | req, _ := http.NewRequest("GET", api, body) 32 | w := httptest.NewRecorder() 33 | app.ServeHTTP(w, req) 34 | return w.Body.String() 35 | } 36 | assert.Equal(t, "{\"msg\":\"pong\"}", getResultString("/api/ping", nil)) 37 | assert.Equal(t, "1", getResultString("/api/sample", nil)) 38 | assert.Equal(t, "{\"message\":\"err1\"}", getResultString("/api/error", strings.NewReader("{}"))) 39 | } 40 | 41 | func TestFuncs(t *testing.T) { 42 | wd := MustGetwd() 43 | assert.NotEqual(t, "", wd) 44 | 45 | dir1 := GetSQLDir() 46 | assert.Equal(t, true, strings.HasSuffix(dir1, "/sqls")) 47 | 48 | } 49 | 50 | func TestRecoverPanic(t *testing.T) { 51 | err := func() (rerr error) { 52 | defer RecoverPanic(&rerr) 53 | panic(fmt.Errorf("an error")) 54 | }() 55 | assert.Equal(t, "an error", err.Error()) 56 | } 57 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /test/saga_grpc_barrier_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 test 8 | 9 | import ( 10 | "testing" 11 | 12 | "github.com/dtm-labs/dtm/dtmcli/dtmimp" 13 | "github.com/dtm-labs/dtm/dtmgrpc" 14 | "github.com/dtm-labs/dtm/dtmutil" 15 | "github.com/dtm-labs/dtm/test/busi" 16 | "github.com/stretchr/testify/assert" 17 | ) 18 | 19 | func TestSagaGrpcBarrierNormal(t *testing.T) { 20 | saga := genSagaGrpcBarrier(dtmimp.GetFuncName(), false, false) 21 | err := saga.Submit() 22 | assert.Nil(t, err) 23 | waitTransProcessed(saga.Gid) 24 | assert.Equal(t, StatusSucceed, getTransStatus(saga.Gid)) 25 | assert.Equal(t, []string{StatusPrepared, StatusSucceed, StatusPrepared, StatusSucceed}, getBranchesStatus(saga.Gid)) 26 | } 27 | 28 | func TestSagaGrpcBarrierRollback(t *testing.T) { 29 | saga := genSagaGrpcBarrier(dtmimp.GetFuncName(), false, true) 30 | err := saga.Submit() 31 | assert.Nil(t, err) 32 | waitTransProcessed(saga.Gid) 33 | assert.Equal(t, StatusFailed, getTransStatus(saga.Gid)) 34 | assert.Equal(t, []string{StatusSucceed, StatusSucceed, StatusSucceed, StatusFailed}, getBranchesStatus(saga.Gid)) 35 | } 36 | 37 | func genSagaGrpcBarrier(gid string, outFailed bool, inFailed bool) *dtmgrpc.SagaGrpc { 38 | saga := dtmgrpc.NewSagaGrpc(dtmutil.DefaultGrpcServer, gid) 39 | req := busi.GenBusiReq(30, outFailed, inFailed) 40 | saga.Add(busi.BusiGrpc+"/busi.Busi/TransOutBSaga", busi.BusiGrpc+"/busi.Busi/TransOutRevertBSaga", req) 41 | saga.Add(busi.BusiGrpc+"/busi.Busi/TransInBSaga", busi.BusiGrpc+"/busi.Busi/TransInRevertBSaga", req) 42 | return saga 43 | } 44 | -------------------------------------------------------------------------------- /charts/values.yaml: -------------------------------------------------------------------------------- 1 | # Default values for dtm. 2 | # This is a YAML-formatted file. 3 | # Declare variables to be passed into your templates. 4 | 5 | # DTM configuration. Specify content for config.yaml 6 | # ref: https://github.com/dtm-labs/dtm/blob/main/conf.sample.yml 7 | configuration: |- 8 | Store: # specify which engine to store trans status 9 | Driver: 'boltdb' # default store engine 10 | 11 | # replicaCount Number of dtm replicas to deploy 12 | replicaCount: 1 13 | 14 | # dtm image version 15 | image: 16 | repository: yedf/dtm 17 | tag: "1.12.2" 18 | pullPolicy: IfNotPresent 19 | 20 | imagePullSecrets: [] 21 | nameOverride: "" 22 | fullnameOverride: "" 23 | 24 | podSecurityContext: {} 25 | # fsGroup: 2000 26 | 27 | securityContext: {} 28 | # capabilities: 29 | # drop: 30 | # - ALL 31 | # readOnlyRootFilesystem: true 32 | # runAsNonRoot: true 33 | # runAsUser: 1000 34 | 35 | resources: 36 | requests: 37 | cpu: 200m 38 | memory: 200Mi 39 | 40 | nodeSelector: {} 41 | 42 | tolerations: [] 43 | 44 | affinity: {} 45 | 46 | service: 47 | type: ClusterIP 48 | ports: 49 | http: 36789 50 | grpc: 36790 51 | 52 | autoscaling: 53 | enabled: false 54 | minReplicas: 1 55 | maxReplicas: 10 56 | targetCPUUtilizationPercentage: 80 57 | targetMemoryUtilizationPercentage: 80 58 | 59 | ingress: 60 | enabled: false 61 | className: "nginx" 62 | annotations: 63 | {} 64 | # kubernetes.io/ingress.class: nginx 65 | # kubernetes.io/tls-acme: "true" 66 | hosts: 67 | - host: your-domain.com 68 | paths: 69 | - path: / 70 | pathType: Prefix 71 | tls: [] 72 | # - secretName: chart-example-tls 73 | # hosts: 74 | # - your-domain.com 75 | -------------------------------------------------------------------------------- /test/tcc_cover_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/dtm-labs/dtm/dtmcli" 7 | "github.com/dtm-labs/dtm/dtmcli/dtmimp" 8 | "github.com/dtm-labs/dtm/dtmutil" 9 | "github.com/dtm-labs/dtm/test/busi" 10 | "github.com/go-resty/resty/v2" 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | func TestTccCoverNotConnected(t *testing.T) { 15 | gid := dtmimp.GetFuncName() 16 | err := dtmcli.TccGlobalTransaction("localhost:01", gid, func(tcc *dtmcli.Tcc) (*resty.Response, error) { 17 | return nil, nil 18 | }) 19 | assert.Error(t, err) 20 | } 21 | 22 | func TestTccCoverPanic(t *testing.T) { 23 | gid := dtmimp.GetFuncName() 24 | err := dtmimp.CatchP(func() { 25 | _ = dtmcli.TccGlobalTransaction(dtmutil.DefaultHTTPServer, gid, func(tcc *dtmcli.Tcc) (*resty.Response, error) { 26 | panic("user panic") 27 | }) 28 | assert.FailNow(t, "not executed") 29 | }) 30 | assert.Contains(t, err.Error(), "user panic") 31 | waitTransProcessed(gid) 32 | } 33 | 34 | func TestTccNested(t *testing.T) { 35 | req := busi.GenTransReq(30, false, false) 36 | gid := dtmimp.GetFuncName() 37 | err := dtmcli.TccGlobalTransaction(dtmutil.DefaultHTTPServer, gid, func(tcc *dtmcli.Tcc) (*resty.Response, error) { 38 | _, err := tcc.CallBranch(req, Busi+"/TransOut", Busi+"/TransOutConfirm", Busi+"/TransOutRevert") 39 | assert.Nil(t, err) 40 | return tcc.CallBranch(req, Busi+"/TransInTccNested", Busi+"/TransInConfirm", Busi+"/TransInRevert") 41 | }) 42 | assert.Nil(t, err) 43 | waitTransProcessed(gid) 44 | assert.Equal(t, StatusSucceed, getTransStatus(gid)) 45 | assert.Equal(t, []string{StatusPrepared, StatusSucceed, StatusPrepared, StatusSucceed, StatusPrepared, StatusSucceed}, getBranchesStatus(gid)) 46 | } 47 | -------------------------------------------------------------------------------- /test/saga_barrier_mongo_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 test 8 | 9 | import ( 10 | "testing" 11 | 12 | "github.com/dtm-labs/dtm/dtmcli" 13 | "github.com/dtm-labs/dtm/dtmcli/dtmimp" 14 | "github.com/dtm-labs/dtm/test/busi" 15 | "github.com/stretchr/testify/assert" 16 | ) 17 | 18 | func TestSagaBarrierMongoNormal(t *testing.T) { 19 | before := getBeforeBalances("mongo") 20 | saga := genSagaBarrierMongo(dtmimp.GetFuncName(), false) 21 | err := saga.Submit() 22 | assert.Nil(t, err) 23 | waitTransProcessed(saga.Gid) 24 | assert.Equal(t, []string{StatusPrepared, StatusSucceed, StatusPrepared, StatusSucceed}, getBranchesStatus(saga.Gid)) 25 | assert.Equal(t, StatusSucceed, getTransStatus(saga.Gid)) 26 | assertNotSameBalance(t, before, "mongo") 27 | } 28 | 29 | func TestSagaBarrierMongoRollback(t *testing.T) { 30 | before := getBeforeBalances("mongo") 31 | saga := genSagaBarrierMongo(dtmimp.GetFuncName(), true) 32 | err := saga.Submit() 33 | assert.Nil(t, err) 34 | waitTransProcessed(saga.Gid) 35 | assert.Equal(t, StatusFailed, getTransStatus(saga.Gid)) 36 | assert.Equal(t, []string{StatusSucceed, StatusSucceed, StatusSucceed, StatusFailed}, getBranchesStatus(saga.Gid)) 37 | assertSameBalance(t, before, "mongo") 38 | } 39 | 40 | func genSagaBarrierMongo(gid string, transInFailed bool) *dtmcli.Saga { 41 | req := busi.GenTransReq(30, false, transInFailed) 42 | req.Store = "mongo" 43 | return dtmcli.NewSaga(DtmServer, gid). 44 | Add(Busi+"/SagaMongoTransOut", Busi+"/SagaMongoTransOutCom", req). 45 | Add(Busi+"/SagaMongoTransIn", Busi+"/SagaMongoTransInCom", req) 46 | } 47 | -------------------------------------------------------------------------------- /dtmcli/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/dtm/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 | -------------------------------------------------------------------------------- /dtmsvr/api_grpc.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 dtmsvr 8 | 9 | import ( 10 | "context" 11 | 12 | "github.com/dtm-labs/dtm/dtmcli" 13 | "github.com/dtm-labs/dtm/dtmgrpc" 14 | pb "github.com/dtm-labs/dtm/dtmgrpc/dtmgpb" 15 | "google.golang.org/protobuf/types/known/emptypb" 16 | ) 17 | 18 | // dtmServer is used to implement dtmgimp.DtmServer. 19 | type dtmServer struct { 20 | pb.UnimplementedDtmServer 21 | } 22 | 23 | func (s *dtmServer) NewGid(ctx context.Context, in *emptypb.Empty) (*pb.DtmGidReply, error) { 24 | return &pb.DtmGidReply{Gid: GenGid()}, nil 25 | } 26 | 27 | func (s *dtmServer) Submit(ctx context.Context, in *pb.DtmRequest) (*emptypb.Empty, error) { 28 | r := svcSubmit(TransFromDtmRequest(ctx, in)) 29 | return &emptypb.Empty{}, dtmgrpc.DtmError2GrpcError(r) 30 | } 31 | 32 | func (s *dtmServer) Prepare(ctx context.Context, in *pb.DtmRequest) (*emptypb.Empty, error) { 33 | r := svcPrepare(TransFromDtmRequest(ctx, in)) 34 | return &emptypb.Empty{}, dtmgrpc.DtmError2GrpcError(r) 35 | } 36 | 37 | func (s *dtmServer) Abort(ctx context.Context, in *pb.DtmRequest) (*emptypb.Empty, error) { 38 | r := svcAbort(TransFromDtmRequest(ctx, in)) 39 | return &emptypb.Empty{}, dtmgrpc.DtmError2GrpcError(r) 40 | } 41 | 42 | func (s *dtmServer) RegisterBranch(ctx context.Context, in *pb.DtmBranchRequest) (*emptypb.Empty, error) { 43 | r := svcRegisterBranch(in.TransType, &TransBranch{ 44 | Gid: in.Gid, 45 | BranchID: in.BranchID, 46 | Status: dtmcli.StatusPrepared, 47 | BinData: in.BusiPayload, 48 | }, in.Data) 49 | return &emptypb.Empty{}, dtmgrpc.DtmError2GrpcError(r) 50 | } 51 | -------------------------------------------------------------------------------- /test/tcc_grpc_cover_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/dtm-labs/dtm/dtmcli/dtmimp" 7 | "github.com/dtm-labs/dtm/dtmgrpc" 8 | "github.com/dtm-labs/dtm/dtmutil" 9 | "github.com/dtm-labs/dtm/test/busi" 10 | "github.com/stretchr/testify/assert" 11 | "google.golang.org/protobuf/types/known/emptypb" 12 | ) 13 | 14 | func TestTccGrpcCoverNotConnected(t *testing.T) { 15 | gid := dtmimp.GetFuncName() 16 | err := dtmgrpc.TccGlobalTransaction("localhost:01", gid, func(tcc *dtmgrpc.TccGrpc) error { 17 | return nil 18 | }) 19 | assert.Error(t, err) 20 | } 21 | 22 | func TestTccGrpcCoverPanic(t *testing.T) { 23 | gid := dtmimp.GetFuncName() 24 | err := dtmimp.CatchP(func() { 25 | _ = dtmgrpc.TccGlobalTransaction(dtmutil.DefaultGrpcServer, gid, func(tcc *dtmgrpc.TccGrpc) error { 26 | panic("user panic") 27 | }) 28 | assert.FailNow(t, "not executed") 29 | }) 30 | assert.Contains(t, err.Error(), "user panic") 31 | waitTransProcessed(gid) 32 | } 33 | 34 | func TestTccGrpcCoverCallBranch(t *testing.T) { 35 | req := busi.GenBusiReq(30, false, false) 36 | gid := dtmimp.GetFuncName() 37 | err := dtmgrpc.TccGlobalTransaction(dtmutil.DefaultGrpcServer, gid, func(tcc *dtmgrpc.TccGrpc) error { 38 | 39 | r := &emptypb.Empty{} 40 | err := tcc.CallBranch(req, "not_exists://abc", busi.BusiGrpc+"/busi.Busi/TransOutConfirm", busi.BusiGrpc+"/busi.Busi/TransOutRevert", r) 41 | assert.Error(t, err) 42 | 43 | tcc.Dtm = "localhost:01" 44 | err = tcc.CallBranch(req, busi.BusiGrpc+"/busi.Busi/TransOut", busi.BusiGrpc+"/busi.Busi/TransOutConfirm", busi.BusiGrpc+"/busi.Busi/TransOutRevert", r) 45 | assert.Error(t, err) 46 | 47 | return err 48 | }) 49 | assert.Error(t, err) 50 | cronTransOnceForwardNow(t, gid, 300) 51 | } 52 | -------------------------------------------------------------------------------- /test/xa_cover_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/dtm-labs/dtm/dtmcli" 7 | "github.com/dtm-labs/dtm/dtmcli/dtmimp" 8 | "github.com/dtm-labs/dtm/test/busi" 9 | "github.com/go-resty/resty/v2" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestXaCoverDBError(t *testing.T) { 14 | oldDriver := busi.BusiConf.Driver 15 | gid := dtmimp.GetFuncName() 16 | err := dtmcli.XaGlobalTransaction(DtmServer, gid, func(xa *dtmcli.Xa) (*resty.Response, error) { 17 | req := busi.GenTransReq(30, false, false) 18 | _, err := xa.CallBranch(req, busi.Busi+"/TransOutXa") 19 | assert.Nil(t, err) 20 | busi.BusiConf.Driver = "no-driver" 21 | _, err = xa.CallBranch(req, busi.Busi+"/TransInXa") 22 | assert.Error(t, err) 23 | return nil, err 24 | }) 25 | assert.Error(t, err) 26 | waitTransProcessed(gid) 27 | busi.BusiConf.Driver = oldDriver 28 | cronTransOnceForwardNow(t, gid, 500) // rollback succeeded here 29 | assert.Equal(t, StatusFailed, getTransStatus(gid)) 30 | assert.Equal(t, []string{StatusSucceed, StatusPrepared}, getBranchesStatus(gid)) 31 | } 32 | 33 | func TestXaCoverDTMError(t *testing.T) { 34 | gid := dtmimp.GetFuncName() 35 | err := dtmcli.XaGlobalTransaction("localhost:01", gid, func(xa *dtmcli.Xa) (*resty.Response, error) { 36 | return nil, nil 37 | }) 38 | assert.Error(t, err) 39 | } 40 | 41 | func TestXaCoverGidError(t *testing.T) { 42 | gid := dtmimp.GetFuncName() + "-' '" 43 | err := dtmcli.XaGlobalTransaction(DtmServer, gid, func(xa *dtmcli.Xa) (*resty.Response, error) { 44 | req := busi.GenTransReq(30, false, false) 45 | _, err := xa.CallBranch(req, busi.Busi+"/TransOutXa") 46 | assert.Error(t, err) 47 | return nil, err 48 | }) 49 | assert.Error(t, err) 50 | waitTransProcessed(gid) 51 | } 52 | -------------------------------------------------------------------------------- /test/saga_barrier_redis_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 test 8 | 9 | import ( 10 | "testing" 11 | 12 | "github.com/dtm-labs/dtm/dtmcli" 13 | "github.com/dtm-labs/dtm/dtmcli/dtmimp" 14 | "github.com/dtm-labs/dtm/test/busi" 15 | "github.com/stretchr/testify/assert" 16 | ) 17 | 18 | func TestSagaBarrierRedisNormal(t *testing.T) { 19 | busi.SetRedisBothAccount(100, 100) 20 | before := getBeforeBalances("redis") 21 | saga := genSagaBarrierRedis(dtmimp.GetFuncName()) 22 | err := saga.Submit() 23 | assert.Nil(t, err) 24 | waitTransProcessed(saga.Gid) 25 | assert.Equal(t, []string{StatusPrepared, StatusSucceed, StatusPrepared, StatusSucceed}, getBranchesStatus(saga.Gid)) 26 | assert.Equal(t, StatusSucceed, getTransStatus(saga.Gid)) 27 | assertNotSameBalance(t, before, "redis") 28 | } 29 | 30 | func TestSagaBarrierRedisRollback(t *testing.T) { 31 | busi.SetRedisBothAccount(20, 20) 32 | before := getBeforeBalances("redis") 33 | saga := genSagaBarrierRedis(dtmimp.GetFuncName()) 34 | err := saga.Submit() 35 | assert.Nil(t, err) 36 | waitTransProcessed(saga.Gid) 37 | assert.Equal(t, StatusFailed, getTransStatus(saga.Gid)) 38 | assert.Equal(t, []string{StatusSucceed, StatusSucceed, StatusSucceed, StatusFailed}, getBranchesStatus(saga.Gid)) 39 | assertSameBalance(t, before, "redis") 40 | } 41 | 42 | func genSagaBarrierRedis(gid string) *dtmcli.Saga { 43 | req := busi.GenTransReq(30, false, false) 44 | req.Store = "redis" 45 | return dtmcli.NewSaga(DtmServer, gid). 46 | Add(Busi+"/SagaRedisTransIn", Busi+"/SagaRedisTransInCom", req). 47 | Add(Busi+"/SagaRedisTransOut", Busi+"/SagaRedisTransOutCom", req) 48 | } 49 | -------------------------------------------------------------------------------- /dtmcli/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/dtm/dtmcli/logger" 13 | "github.com/go-resty/resty/v2" 14 | ) 15 | 16 | // ErrFailure error of FAILURE 17 | var ErrFailure = errors.New("FAILURE") 18 | 19 | // ErrOngoing error of ONGOING 20 | var ErrOngoing = errors.New("ONGOING") 21 | 22 | // ErrDuplicated error of DUPLICATED for only msg 23 | // if QueryPrepared executed before call. then DoAndSubmit return this error 24 | var ErrDuplicated = errors.New("DUPLICATED") 25 | 26 | // XaSQLTimeoutMs milliseconds for Xa sql to timeout 27 | var XaSQLTimeoutMs = 15000 28 | 29 | // MapSuccess HTTP result of SUCCESS 30 | var MapSuccess = map[string]interface{}{"dtm_result": ResultSuccess} 31 | 32 | // MapFailure HTTP result of FAILURE 33 | var MapFailure = map[string]interface{}{"dtm_result": ResultFailure} 34 | 35 | // RestyClient the resty object 36 | var RestyClient = resty.New() 37 | 38 | // PassthroughHeaders will be passed to every sub-trans call 39 | var PassthroughHeaders = []string{} 40 | 41 | // BarrierTableName the table name of barrier table 42 | var BarrierTableName = "dtm_barrier.barrier" 43 | 44 | func init() { 45 | RestyClient.OnBeforeRequest(func(c *resty.Client, r *resty.Request) error { 46 | r.URL = MayReplaceLocalhost(r.URL) 47 | logger.Debugf("requesting: %s %s %s", r.Method, r.URL, MustMarshalString(r.Body)) 48 | return nil 49 | }) 50 | RestyClient.OnAfterResponse(func(c *resty.Client, resp *resty.Response) error { 51 | r := resp.Request 52 | logger.Debugf("requested: %s %s %s", r.Method, r.URL, resp.String()) 53 | return nil 54 | }) 55 | } 56 | -------------------------------------------------------------------------------- /charts/templates/NOTES.txt: -------------------------------------------------------------------------------- 1 | 1. Get the application URL by running these commands: 2 | {{- if .Values.ingress.enabled }} 3 | {{- range $host := .Values.ingress.hosts }} 4 | {{- range .paths }} 5 | http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} 6 | {{- end }} 7 | {{- end }} 8 | {{- else if contains "NodePort" .Values.service.type }} 9 | export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "dtm.fullname" . }}) 10 | export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") 11 | echo http://$NODE_IP:$NODE_PORT 12 | {{- else if contains "LoadBalancer" .Values.service.type }} 13 | NOTE: It may take a few minutes for the LoadBalancer IP to be available. 14 | You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "dtm.fullname" . }}' 15 | export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "dtm.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") 16 | echo http://$SERVICE_IP:{{ .Values.service.port }} 17 | {{- else if contains "ClusterIP" .Values.service.type }} 18 | export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "dtm.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") 19 | export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") 20 | echo "Visit http://127.0.0.1:8080 to use your application" 21 | kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT 22 | {{- end }} 23 | -------------------------------------------------------------------------------- /helper/bench/test-mysql.sh: -------------------------------------------------------------------------------- 1 | # !/bin/bash 2 | 3 | set -x 4 | 5 | cd /usr/share/sysbench/ 6 | echo 'create database sbtest;' > mysql -h 127.0.0.1 -uroot 7 | 8 | sysbench oltp_write_only.lua --time=60 --mysql-host=127.0.0.1 --mysql-port=3306 --mysql-user=root --mysql-password= --mysql-db=sbtest --table-size=1000000 --tables=10 --threads=10 --events=999999999 --report-interval=10 prepare 9 | 10 | sysbench oltp_write_only.lua --time=60 --mysql-host=127.0.0.1 --mysql-port=3306 --mysql-user=root --mysql-password= --mysql-db=sbtest --table-size=1000000 --tables=10 --threads=10 --events=999999999 --report-interval=10 run 11 | 12 | export TIME=10 13 | export CONCURRENT=20 14 | curl "http://127.0.0.1:8083/api/busi_bench/reloadData?m=dtm_tx&sqls=0" && ab -t $TIME -c $CONCURRENT "http://127.0.0.1:8083/api/busi_bench/bench" 15 | curl "http://127.0.0.1:8083/api/busi_bench/reloadData?m=dtm_tx&sqls=5" && ab -t $TIME -c $CONCURRENT "http://127.0.0.1:8083/api/busi_bench/bench" 16 | curl "http://127.0.0.1:8083/api/busi_bench/reloadData?m=dtm_barrier&sqls=5" && ab -t $TIME -c $CONCURRENT "http://127.0.0.1:8083/api/busi_bench/bench" 17 | curl "http://127.0.0.1:8083/api/busi_bench/reloadData?m=raw_tx&sqls=5" && ab -t $TIME -c $CONCURRENT "http://127.0.0.1:8083/api/busi_bench/bench" 18 | curl "http://127.0.0.1:8083/api/busi_bench/reloadData?m=dtm_tx&sqls=1" && ab -t $TIME -c $CONCURRENT "http://127.0.0.1:8083/api/busi_bench/bench" 19 | curl "http://127.0.0.1:8083/api/busi_bench/reloadData?m=dtm_barrier&sqls=1" && ab -t $TIME -c $CONCURRENT "http://127.0.0.1:8083/api/busi_bench/bench" 20 | curl "http://127.0.0.1:8083/api/busi_bench/reloadData?m=raw_tx&sqls=1" && ab -t $TIME -c $CONCURRENT "http://127.0.0.1:8083/api/busi_bench/bench" 21 | curl "http://127.0.0.1:8083/api/busi_bench/reloadData?m=raw_empty" && ab -t $TIME -c $CONCURRENT "http://127.0.0.1:8083/api/busi_bench/bench" 22 | 23 | -------------------------------------------------------------------------------- /dtmcli/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 | -------------------------------------------------------------------------------- /dtmcli/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/dtm/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 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | tags: 5 | - 'v*.*.*' 6 | 7 | jobs: 8 | release: 9 | name: Release on GitHub 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Check out code 13 | uses: actions/checkout@v2 14 | 15 | - name: Validates GO releaser config 16 | uses: docker://goreleaser/goreleaser:latest 17 | with: 18 | args: check 19 | 20 | - name: Create release on GitHub 21 | uses: docker://goreleaser/goreleaser:latest 22 | with: 23 | args: release -f helper/.goreleaser.yml --rm-dist 24 | env: 25 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 26 | 27 | - name: Docker meta 28 | id: meta 29 | uses: docker/metadata-action@v3 30 | with: 31 | images: | 32 | yedf/dtm 33 | tags: | 34 | type=semver,pattern={{version}} 35 | type=semver,pattern={{major}}.{{minor}} 36 | type=semver,pattern={{major}} 37 | type=sha 38 | 39 | - name: Set up QEMU 40 | uses: docker/setup-qemu-action@v1 41 | 42 | - name: Set up Docker Buildx 43 | uses: docker/setup-buildx-action@v1 44 | 45 | - name: Login to DockerHub 46 | uses: docker/login-action@v1 47 | with: 48 | username: ${{ secrets.DOCKERHUB_USERNAME }} 49 | password: ${{ secrets.DOCKERHUB_TOKEN }} 50 | 51 | - name: Get Release Version 52 | run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV 53 | 54 | - name: Build and push 55 | uses: docker/build-push-action@v2 56 | with: 57 | context: . 58 | file: ./helper/Dockerfile-release 59 | push: true 60 | platforms: linux/amd64,linux/arm64 61 | tags: ${{ steps.meta.outputs.tags }} 62 | labels: ${{ steps.meta.outputs.labels }} 63 | build-args: | 64 | RELEASE_VERSION=${{ env.RELEASE_VERSION }} -------------------------------------------------------------------------------- /charts/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* 2 | Expand the name of the chart. 3 | */}} 4 | {{- define "dtm.name" -}} 5 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} 6 | {{- end }} 7 | 8 | {{/* 9 | Create a default fully qualified app name. 10 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 11 | If release name contains chart name it will be used as a full name. 12 | */}} 13 | {{- define "dtm.fullname" -}} 14 | {{- if .Values.fullnameOverride }} 15 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} 16 | {{- else }} 17 | {{- $name := default .Chart.Name .Values.nameOverride }} 18 | {{- if contains $name .Release.Name }} 19 | {{- .Release.Name | trunc 63 | trimSuffix "-" }} 20 | {{- else }} 21 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} 22 | {{- end }} 23 | {{- end }} 24 | {{- end }} 25 | 26 | {{/* 27 | Create chart name and version as used by the chart label. 28 | */}} 29 | {{- define "dtm.chart" -}} 30 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} 31 | {{- end }} 32 | 33 | {{/* 34 | Common labels 35 | */}} 36 | {{- define "dtm.labels" -}} 37 | helm.sh/chart: {{ include "dtm.chart" . }} 38 | {{ include "dtm.selectorLabels" . }} 39 | {{- if .Chart.AppVersion }} 40 | app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} 41 | {{- end }} 42 | app.kubernetes.io/managed-by: {{ .Release.Service }} 43 | {{- end }} 44 | 45 | {{/* 46 | Selector labels 47 | */}} 48 | {{- define "dtm.selectorLabels" -}} 49 | app.kubernetes.io/name: {{ include "dtm.name" . }} 50 | app.kubernetes.io/instance: {{ .Release.Name }} 51 | {{- end }} 52 | 53 | {{/* 54 | Create the name of the service account to use 55 | */}} 56 | {{- define "dtm.serviceAccountName" -}} 57 | {{- if .Values.serviceAccount.create }} 58 | {{- default (include "dtm.fullname" .) .Values.serviceAccount.name }} 59 | {{- else }} 60 | {{- default "default" .Values.serviceAccount.name }} 61 | {{- end }} 62 | {{- end }} 63 | -------------------------------------------------------------------------------- /sqls/dtmsvr.storage.postgres.sql: -------------------------------------------------------------------------------- 1 | CREATE SCHEMA if not EXISTS dtm 2 | /* SQLINES DEMO *** RACTER SET utf8mb4 */ 3 | ; 4 | drop table IF EXISTS dtm.trans_global; 5 | -- SQLINES LICENSE FOR EVALUATION USE ONLY 6 | CREATE SEQUENCE if not EXISTS dtm.trans_global_seq; 7 | CREATE TABLE if not EXISTS dtm.trans_global ( 8 | id bigint NOT NULL DEFAULT NEXTVAL ('dtm.trans_global_seq'), 9 | gid varchar(128) NOT NULL, 10 | trans_type varchar(45) not null, 11 | status varchar(45) NOT NULL, 12 | query_prepared varchar(128) NOT NULL, 13 | protocol varchar(45) not null, 14 | create_time timestamp(0) with time zone DEFAULT NULL, 15 | update_time timestamp(0) with time zone DEFAULT NULL, 16 | finish_time timestamp(0) with time zone DEFAULT NULL, 17 | rollback_time timestamp(0) with time zone DEFAULT NULL, 18 | options varchar(1024) DEFAULT '', 19 | custom_data varchar(256) DEFAULT '', 20 | next_cron_interval int default null, 21 | next_cron_time timestamp(0) with time zone default null, 22 | owner varchar(128) not null default '', 23 | ext_data text, 24 | PRIMARY KEY (id), 25 | CONSTRAINT gid UNIQUE (gid) 26 | ); 27 | create index if not EXISTS owner on dtm.trans_global(owner); 28 | create index if not EXISTS status_next_cron_time on dtm.trans_global (status, next_cron_time); 29 | drop table IF EXISTS dtm.trans_branch_op; 30 | -- SQLINES LICENSE FOR EVALUATION USE ONLY 31 | CREATE SEQUENCE if not EXISTS dtm.trans_branch_op_seq; 32 | CREATE TABLE IF NOT EXISTS dtm.trans_branch_op ( 33 | id bigint NOT NULL DEFAULT NEXTVAL ('dtm.trans_branch_op_seq'), 34 | gid varchar(128) NOT NULL, 35 | url varchar(128) NOT NULL, 36 | data TEXT, 37 | bin_data bytea, 38 | branch_id VARCHAR(128) NOT NULL, 39 | op varchar(45) NOT NULL, 40 | status varchar(45) NOT NULL, 41 | finish_time timestamp(0) with time zone DEFAULT NULL, 42 | rollback_time timestamp(0) with time zone DEFAULT NULL, 43 | create_time timestamp(0) with time zone DEFAULT NULL, 44 | update_time timestamp(0) with time zone DEFAULT NULL, 45 | PRIMARY KEY (id), 46 | CONSTRAINT gid_branch_uniq UNIQUE (gid, branch_id, op) 47 | ); -------------------------------------------------------------------------------- /test/saga_barrier_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 test 8 | 9 | import ( 10 | "testing" 11 | 12 | "github.com/dtm-labs/dtm/dtmcli" 13 | "github.com/dtm-labs/dtm/dtmcli/dtmimp" 14 | "github.com/dtm-labs/dtm/test/busi" 15 | "github.com/stretchr/testify/assert" 16 | ) 17 | 18 | func TestSagaBarrierNormal(t *testing.T) { 19 | saga := genSagaBarrier(dtmimp.GetFuncName(), false, false) 20 | err := saga.Submit() 21 | assert.Nil(t, err) 22 | waitTransProcessed(saga.Gid) 23 | assert.Equal(t, []string{StatusPrepared, StatusSucceed, StatusPrepared, StatusSucceed}, getBranchesStatus(saga.Gid)) 24 | assert.Equal(t, StatusSucceed, getTransStatus(saga.Gid)) 25 | } 26 | 27 | func TestSagaBarrierRollback(t *testing.T) { 28 | saga := genSagaBarrier(dtmimp.GetFuncName(), false, true) 29 | err := saga.Submit() 30 | assert.Nil(t, err) 31 | waitTransProcessed(saga.Gid) 32 | assert.Equal(t, StatusFailed, getTransStatus(saga.Gid)) 33 | assert.Equal(t, []string{StatusSucceed, StatusSucceed, StatusSucceed, StatusFailed}, getBranchesStatus(saga.Gid)) 34 | } 35 | 36 | func genSagaBarrier(gid string, outFailed, inFailed bool) *dtmcli.Saga { 37 | req := busi.GenTransReq(30, outFailed, inFailed) 38 | return dtmcli.NewSaga(DtmServer, gid). 39 | Add(Busi+"/SagaBTransOut", Busi+"/SagaBTransOutCom", req). 40 | Add(Busi+"/SagaBTransIn", Busi+"/SagaBTransInCom", req) 41 | } 42 | 43 | func TestSagaBarrier2Normal(t *testing.T) { 44 | req := busi.GenTransReq(30, false, false) 45 | gid := dtmimp.GetFuncName() 46 | saga := dtmcli.NewSaga(DtmServer, gid). 47 | Add(Busi+"/SagaBTransOut", Busi+"/SagaBTransOutCom", req). 48 | Add(Busi+"/SagaB2TransIn", Busi+"/SagaB2TransInCom", req) 49 | err := saga.Submit() 50 | assert.Nil(t, err) 51 | waitTransProcessed(saga.Gid) 52 | assert.Equal(t, []string{StatusPrepared, StatusSucceed, StatusPrepared, StatusSucceed}, getBranchesStatus(saga.Gid)) 53 | assert.Equal(t, StatusSucceed, getTransStatus(saga.Gid)) 54 | } 55 | -------------------------------------------------------------------------------- /dtmcli/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 | -------------------------------------------------------------------------------- /test/main_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 test 8 | 9 | import ( 10 | "os" 11 | "testing" 12 | "time" 13 | 14 | "github.com/dtm-labs/dtm/dtmcli" 15 | "github.com/dtm-labs/dtm/dtmcli/logger" 16 | "github.com/dtm-labs/dtm/dtmgrpc" 17 | "github.com/dtm-labs/dtm/dtmsvr" 18 | "github.com/dtm-labs/dtm/dtmsvr/config" 19 | "github.com/dtm-labs/dtm/dtmsvr/storage/registry" 20 | "github.com/dtm-labs/dtm/test/busi" 21 | "github.com/go-resty/resty/v2" 22 | ) 23 | 24 | func exitIf(code int) { 25 | if code != 0 { 26 | os.Exit(code) 27 | } 28 | } 29 | 30 | func TestMain(m *testing.M) { 31 | config.MustLoadConfig("") 32 | logger.InitLog("debug") 33 | dtmcli.SetCurrentDBType(busi.BusiConf.Driver) 34 | dtmsvr.TransProcessedTestChan = make(chan string, 1) 35 | dtmsvr.NowForwardDuration = 0 * time.Second 36 | dtmsvr.CronForwardDuration = 180 * time.Second 37 | conf.UpdateBranchSync = 1 38 | 39 | dtmgrpc.AddUnaryInterceptor(busi.SetGrpcHeaderForHeadersYes) 40 | dtmcli.GetRestyClient().OnBeforeRequest(busi.SetHTTPHeaderForHeadersYes) 41 | dtmcli.GetRestyClient().OnAfterResponse(func(c *resty.Client, resp *resty.Response) error { return nil }) 42 | 43 | tenv := os.Getenv("TEST_STORE") 44 | if tenv == "boltdb" { 45 | conf.Store.Driver = "boltdb" 46 | } else if tenv == "mysql" { 47 | conf.Store.Driver = "mysql" 48 | conf.Store.Host = "localhost" 49 | conf.Store.Port = 3306 50 | conf.Store.User = "root" 51 | conf.Store.Password = "" 52 | } else { 53 | conf.Store.Driver = "redis" 54 | conf.Store.Host = "localhost" 55 | conf.Store.User = "" 56 | conf.Store.Password = "" 57 | conf.Store.Port = 6379 58 | } 59 | registry.WaitStoreUp() 60 | 61 | dtmsvr.PopulateDB(false) 62 | go dtmsvr.StartSvr() 63 | 64 | busi.PopulateDB(false) 65 | _ = busi.Startup() 66 | r := m.Run() 67 | exitIf(r) 68 | close(dtmsvr.TransProcessedTestChan) 69 | gid, more := <-dtmsvr.TransProcessedTestChan 70 | logger.FatalfIf(more, "extra gid: %s in test chan", gid) 71 | os.Exit(0) 72 | } 73 | -------------------------------------------------------------------------------- /test/msg_options_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 test 8 | 9 | import ( 10 | "testing" 11 | 12 | "github.com/dtm-labs/dtm/dtmcli" 13 | "github.com/dtm-labs/dtm/dtmcli/dtmimp" 14 | "github.com/dtm-labs/dtm/test/busi" 15 | "github.com/stretchr/testify/assert" 16 | ) 17 | 18 | func TestMsgOptionsTimeout(t *testing.T) { 19 | gid := dtmimp.GetFuncName() 20 | msg := genMsg(gid) 21 | msg.Prepare("") 22 | cronTransOnce(t, gid) 23 | assert.Equal(t, StatusPrepared, getTransStatus(msg.Gid)) 24 | cronTransOnceForwardNow(t, gid, 60) 25 | assert.Equal(t, StatusSucceed, getTransStatus(msg.Gid)) 26 | } 27 | 28 | func TestMsgOptionsTimeoutCustom(t *testing.T) { 29 | gid := dtmimp.GetFuncName() 30 | msg := genMsg(gid) 31 | msg.TimeoutToFail = 120 32 | msg.Prepare("") 33 | cronTransOnce(t, gid) 34 | assert.Equal(t, StatusPrepared, getTransStatus(msg.Gid)) 35 | cronTransOnceForwardNow(t, gid, 60) 36 | assert.Equal(t, StatusPrepared, getTransStatus(msg.Gid)) 37 | cronTransOnceForwardNow(t, gid, 180) 38 | assert.Equal(t, StatusSucceed, getTransStatus(msg.Gid)) 39 | } 40 | 41 | func TestMsgOptionsTimeoutFailed(t *testing.T) { 42 | gid := dtmimp.GetFuncName() 43 | msg := genMsg(gid) 44 | msg.TimeoutToFail = 120 45 | msg.Prepare("") 46 | cronTransOnce(t, gid) 47 | assert.Equal(t, StatusPrepared, getTransStatus(msg.Gid)) 48 | cronTransOnceForwardNow(t, gid, 60) 49 | assert.Equal(t, StatusPrepared, getTransStatus(msg.Gid)) 50 | busi.MainSwitch.QueryPreparedResult.SetOnce(dtmcli.ResultFailure) 51 | cronTransOnceForwardNow(t, gid, 180) 52 | assert.Equal(t, StatusFailed, getTransStatus(msg.Gid)) 53 | } 54 | 55 | func TestMsgConcurrent(t *testing.T) { 56 | msg := genMsg(dtmimp.GetFuncName()) 57 | msg.Concurrent = true 58 | msg.Submit() 59 | assert.Equal(t, StatusSubmitted, getTransStatus(msg.Gid)) 60 | waitTransProcessed(msg.Gid) 61 | assert.Equal(t, []string{StatusSucceed, StatusSucceed}, getBranchesStatus(msg.Gid)) 62 | assert.Equal(t, StatusSucceed, getTransStatus(msg.Gid)) 63 | } 64 | -------------------------------------------------------------------------------- /dtmgrpc/type.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 dtmgrpc 8 | 9 | import ( 10 | context "context" 11 | 12 | "github.com/dtm-labs/dtm/dtmcli" 13 | "github.com/dtm-labs/dtm/dtmcli/dtmimp" 14 | "github.com/dtm-labs/dtm/dtmgrpc/dtmgimp" 15 | "github.com/dtm-labs/dtmdriver" 16 | grpc "google.golang.org/grpc" 17 | "google.golang.org/grpc/codes" 18 | "google.golang.org/grpc/status" 19 | emptypb "google.golang.org/protobuf/types/known/emptypb" 20 | ) 21 | 22 | // DtmError2GrpcError translate dtm error to grpc error 23 | func DtmError2GrpcError(res interface{}) error { 24 | e, ok := res.(error) 25 | if ok && e == dtmimp.ErrFailure { 26 | return status.New(codes.Aborted, dtmcli.ResultFailure).Err() 27 | } else if ok && e == dtmimp.ErrOngoing { 28 | return status.New(codes.FailedPrecondition, dtmcli.ResultOngoing).Err() 29 | } 30 | return e 31 | } 32 | 33 | // GrpcError2DtmError translate grpc error to dtm error 34 | func GrpcError2DtmError(err error) error { 35 | st, ok := status.FromError(err) 36 | if ok && st.Code() == codes.Aborted { 37 | // version lower then v1.10, will specify Ongoing in code Aborted 38 | if st.Message() == dtmcli.ResultOngoing { 39 | return dtmcli.ErrOngoing 40 | } 41 | return dtmcli.ErrFailure 42 | } else if ok && st.Code() == codes.FailedPrecondition { 43 | return dtmcli.ErrOngoing 44 | } 45 | return err 46 | } 47 | 48 | // MustGenGid must gen a gid from grpcServer 49 | func MustGenGid(grpcServer string) string { 50 | dc := dtmgimp.MustGetDtmClient(grpcServer) 51 | r, err := dc.NewGid(context.Background(), &emptypb.Empty{}) 52 | dtmimp.E2P(err) 53 | return r.Gid 54 | } 55 | 56 | // UseDriver use the specified driver to handle grpc urls 57 | func UseDriver(driverName string) error { 58 | return dtmdriver.Use(driverName) 59 | } 60 | 61 | // AddUnaryInterceptor adds grpc.UnaryClientInterceptor 62 | func AddUnaryInterceptor(interceptor grpc.UnaryClientInterceptor) { 63 | dtmgimp.ClientInterceptors = append(dtmgimp.ClientInterceptors, interceptor) 64 | } 65 | -------------------------------------------------------------------------------- /test/busi/busi.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package busi; 4 | import "google/protobuf/empty.proto"; 5 | 6 | option go_package = "./busi"; 7 | 8 | // DtmRequest request sent to dtm server 9 | message BusiReq { 10 | int64 Amount = 1; 11 | string TransOutResult = 2; 12 | string TransInResult = 3; 13 | } 14 | 15 | message BusiReply { 16 | string message = 1; 17 | } 18 | // The dtm service definition. 19 | service Busi { 20 | rpc TransIn(BusiReq) returns (google.protobuf.Empty) {} 21 | rpc TransOut(BusiReq) returns (google.protobuf.Empty) {} 22 | rpc TransInRevert(BusiReq) returns (google.protobuf.Empty) {} 23 | rpc TransOutRevert(BusiReq) returns (google.protobuf.Empty) {} 24 | rpc TransInConfirm(BusiReq) returns (google.protobuf.Empty) {} 25 | rpc TransOutConfirm(BusiReq) returns (google.protobuf.Empty) {} 26 | rpc XaNotify(google.protobuf.Empty) returns (google.protobuf.Empty) {} 27 | 28 | rpc TransInXa(BusiReq) returns (google.protobuf.Empty) {} 29 | rpc TransOutXa(BusiReq) returns (google.protobuf.Empty) {} 30 | rpc TransInTcc(BusiReq) returns (google.protobuf.Empty) {} 31 | rpc TransOutTcc(BusiReq) returns (google.protobuf.Empty) {} 32 | rpc TransInTccNested(BusiReq) returns (google.protobuf.Empty) {} 33 | 34 | rpc TransInBSaga(BusiReq) returns (google.protobuf.Empty) {} 35 | rpc TransOutBSaga(BusiReq) returns (google.protobuf.Empty) {} 36 | rpc TransInRevertBSaga(BusiReq) returns (google.protobuf.Empty) {} 37 | rpc TransOutRevertBSaga(BusiReq) returns (google.protobuf.Empty) {} 38 | rpc TransOutHeaderYes(BusiReq) returns (google.protobuf.Empty) {} 39 | rpc TransOutHeaderNo(BusiReq) returns (google.protobuf.Empty) {} 40 | 41 | rpc TransInRedis(BusiReq) returns (google.protobuf.Empty) {} 42 | rpc TransOutRedis(BusiReq) returns (google.protobuf.Empty) {} 43 | rpc TransInRevertRedis(BusiReq) returns (google.protobuf.Empty) {} 44 | rpc TransOutRevertRedis(BusiReq) returns (google.protobuf.Empty) {} 45 | 46 | rpc QueryPrepared(BusiReq) returns (BusiReply) {} 47 | rpc QueryPreparedB(BusiReq) returns (google.protobuf.Empty) {} 48 | rpc QueryPreparedRedis(BusiReq) returns (google.protobuf.Empty) {} 49 | } 50 | 51 | -------------------------------------------------------------------------------- /main.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 main 8 | 9 | import ( 10 | "flag" 11 | "fmt" 12 | "os" 13 | "path/filepath" 14 | 15 | "go.uber.org/automaxprocs/maxprocs" 16 | 17 | "github.com/dtm-labs/dtm/dtmcli/logger" 18 | "github.com/dtm-labs/dtm/dtmsvr" 19 | "github.com/dtm-labs/dtm/dtmsvr/config" 20 | "github.com/dtm-labs/dtm/dtmsvr/storage/registry" 21 | 22 | // load the microserver driver 23 | _ "github.com/dtm-labs/dtmdriver-gozero" 24 | _ "github.com/dtm-labs/dtmdriver-kratos" 25 | _ "github.com/dtm-labs/dtmdriver-polaris" 26 | _ "github.com/dtm-labs/dtmdriver-protocol1" 27 | ) 28 | 29 | // Version declares version info 30 | var Version string 31 | 32 | func version() { 33 | if Version == "" { 34 | Version = "0.0.0-dev" 35 | } 36 | fmt.Printf("dtm version: %s\n", Version) 37 | } 38 | 39 | func usage() { 40 | cmd := filepath.Base(os.Args[0]) 41 | s := "Usage: %s [options]\n\n" 42 | fmt.Fprintf(os.Stderr, s, cmd) 43 | flag.PrintDefaults() 44 | } 45 | 46 | var isVersion = flag.Bool("v", false, "Show the version of dtm.") 47 | var isDebug = flag.Bool("d", false, "Set log level to debug.") 48 | var isHelp = flag.Bool("h", false, "Show the help information about dtm.") 49 | var isReset = flag.Bool("r", false, "Reset dtm server data.") 50 | var confFile = flag.String("c", "", "Path to the server configuration file.") 51 | 52 | func main() { 53 | flag.Parse() 54 | if flag.NArg() > 0 || *isHelp { 55 | usage() 56 | return 57 | } else if *isVersion { 58 | version() 59 | return 60 | } 61 | logger.Infof("dtm version is: %s", Version) 62 | config.MustLoadConfig(*confFile) 63 | conf := &config.Config 64 | if *isDebug { 65 | conf.LogLevel = "debug" 66 | } 67 | logger.InitLog2(conf.LogLevel, conf.Log.Outputs, conf.Log.RotationEnable, conf.Log.RotationConfigJSON) 68 | if *isReset { 69 | dtmsvr.PopulateDB(false) 70 | } 71 | _, _ = maxprocs.Set(maxprocs.Logger(logger.Infof)) 72 | registry.WaitStoreUp() 73 | dtmsvr.StartSvr() // start dtmsvr api 74 | go dtmsvr.CronExpiredTrans(-1) // start dtmsvr cron job 75 | select {} 76 | } 77 | -------------------------------------------------------------------------------- /dtmcli/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 | "fmt" 11 | 12 | "github.com/dtm-labs/dtm/dtmcli/dtmimp" 13 | "github.com/go-resty/resty/v2" 14 | ) 15 | 16 | // MustGenGid generate a new gid 17 | func MustGenGid(server string) string { 18 | res := map[string]string{} 19 | resp, err := dtmimp.RestyClient.R().SetResult(&res).Get(server + "/newGid") 20 | if err != nil || res["gid"] == "" { 21 | panic(fmt.Errorf("newGid error: %v, resp: %s", err, resp)) 22 | } 23 | return res["gid"] 24 | } 25 | 26 | // DB interface 27 | type DB = dtmimp.DB 28 | 29 | // TransOptions transaction option 30 | type TransOptions = dtmimp.TransOptions 31 | 32 | // DBConf declares db configuration 33 | type DBConf = dtmimp.DBConf 34 | 35 | // String2DtmError translate string to dtm error 36 | func String2DtmError(str string) error { 37 | return map[string]error{ 38 | ResultFailure: ErrFailure, 39 | ResultOngoing: ErrOngoing, 40 | ResultSuccess: nil, 41 | "": nil, 42 | }[str] 43 | } 44 | 45 | // SetCurrentDBType set currentDBType 46 | func SetCurrentDBType(dbType string) { 47 | dtmimp.SetCurrentDBType(dbType) 48 | } 49 | 50 | // GetCurrentDBType get currentDBType 51 | func GetCurrentDBType() string { 52 | return dtmimp.GetCurrentDBType() 53 | } 54 | 55 | // SetXaSQLTimeoutMs set XaSQLTimeoutMs 56 | func SetXaSQLTimeoutMs(ms int) { 57 | dtmimp.XaSQLTimeoutMs = ms 58 | } 59 | 60 | // GetXaSQLTimeoutMs get XaSQLTimeoutMs 61 | func GetXaSQLTimeoutMs() int { 62 | return dtmimp.XaSQLTimeoutMs 63 | } 64 | 65 | // SetBarrierTableName sets barrier table name 66 | func SetBarrierTableName(tablename string) { 67 | dtmimp.BarrierTableName = tablename 68 | } 69 | 70 | // GetRestyClient get the resty.Client for http request 71 | func GetRestyClient() *resty.Client { 72 | return dtmimp.RestyClient 73 | } 74 | 75 | // SetPassthroughHeaders experimental. 76 | // apply to http header and grpc metadata 77 | // dtm server will save these headers in trans creating request. 78 | // and then passthrough them to sub-trans 79 | func SetPassthroughHeaders(headers []string) { 80 | dtmimp.PassthroughHeaders = headers 81 | } 82 | -------------------------------------------------------------------------------- /dtmsvr/cron.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 dtmsvr 8 | 9 | import ( 10 | "errors" 11 | "fmt" 12 | "math/rand" 13 | "runtime/debug" 14 | "time" 15 | 16 | "github.com/dtm-labs/dtm/dtmcli" 17 | "github.com/dtm-labs/dtm/dtmcli/dtmimp" 18 | "github.com/dtm-labs/dtm/dtmcli/logger" 19 | ) 20 | 21 | // NowForwardDuration will be set in test, trans may be timeout 22 | var NowForwardDuration = time.Duration(0) 23 | 24 | // CronForwardDuration will be set in test. cron will fetch trans which expire in CronForwardDuration 25 | var CronForwardDuration = time.Duration(0) 26 | 27 | // CronTransOnce cron expired trans. use expireIn as expire time 28 | func CronTransOnce() (gid string) { 29 | defer handlePanic(nil) 30 | trans := lockOneTrans(CronForwardDuration) 31 | if trans == nil { 32 | return 33 | } 34 | gid = trans.Gid 35 | trans.WaitResult = true 36 | branches := GetStore().FindBranches(gid) 37 | err := trans.Process(branches) 38 | dtmimp.PanicIf(err != nil && !errors.Is(err, dtmcli.ErrFailure), err) 39 | return 40 | } 41 | 42 | // CronExpiredTrans cron expired trans, num == -1 indicate for ever 43 | func CronExpiredTrans(num int) { 44 | for i := 0; i < num || num == -1; i++ { 45 | gid := CronTransOnce() 46 | if gid == "" && num != 1 { 47 | sleepCronTime() 48 | } 49 | } 50 | } 51 | 52 | func lockOneTrans(expireIn time.Duration) *TransGlobal { 53 | global := GetStore().LockOneGlobalTrans(expireIn) 54 | if global == nil { 55 | return nil 56 | } 57 | logger.Infof("cron job return a trans: %s", global.String()) 58 | return &TransGlobal{TransGlobalStore: *global} 59 | } 60 | 61 | func handlePanic(perr *error) { 62 | if err := recover(); err != nil { 63 | logger.Errorf("----recovered panic %v\n%s", err, string(debug.Stack())) 64 | if perr != nil { 65 | *perr = fmt.Errorf("dtm panic: %v", err) 66 | } 67 | } 68 | } 69 | 70 | func sleepCronTime() { 71 | normal := time.Duration((float64(conf.TransCronInterval) - rand.Float64()) * float64(time.Second)) 72 | interval := dtmimp.If(CronForwardDuration > 0, 1*time.Millisecond, normal).(time.Duration) 73 | logger.Debugf("sleeping for %v milli", interval/time.Microsecond) 74 | time.Sleep(interval) 75 | } 76 | -------------------------------------------------------------------------------- /charts/templates/ingress.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.ingress.enabled -}} 2 | {{- $fullName := include "dtm.fullname" . -}} 3 | {{- $svcPort := .Values.service.ports.http -}} 4 | {{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }} 5 | {{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }} 6 | {{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}} 7 | {{- end }} 8 | {{- end }} 9 | {{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}} 10 | apiVersion: networking.k8s.io/v1 11 | {{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} 12 | apiVersion: networking.k8s.io/v1beta1 13 | {{- else -}} 14 | apiVersion: extensions/v1beta1 15 | {{- end }} 16 | kind: Ingress 17 | metadata: 18 | name: {{ $fullName }} 19 | labels: 20 | {{- include "dtm.labels" . | nindent 4 }} 21 | {{- with .Values.ingress.annotations }} 22 | annotations: 23 | {{- toYaml . | nindent 4 }} 24 | {{- end }} 25 | spec: 26 | {{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} 27 | ingressClassName: {{ .Values.ingress.className }} 28 | {{- end }} 29 | {{- if .Values.ingress.tls }} 30 | tls: 31 | {{- range .Values.ingress.tls }} 32 | - hosts: 33 | {{- range .hosts }} 34 | - {{ . | quote }} 35 | {{- end }} 36 | secretName: {{ .secretName }} 37 | {{- end }} 38 | {{- end }} 39 | rules: 40 | {{- range .Values.ingress.hosts }} 41 | - host: {{ .host | quote }} 42 | http: 43 | paths: 44 | {{- range .paths }} 45 | - path: {{ .path }} 46 | {{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }} 47 | pathType: {{ .pathType }} 48 | {{- end }} 49 | backend: 50 | {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }} 51 | service: 52 | name: {{ $fullName }} 53 | port: 54 | number: {{ $svcPort }} 55 | {{- else }} 56 | serviceName: {{ $fullName }} 57 | servicePort: {{ $svcPort }} 58 | {{- end }} 59 | {{- end }} 60 | {{- end }} 61 | {{- end }} 62 | -------------------------------------------------------------------------------- /test/busi/quick_start.go: -------------------------------------------------------------------------------- 1 | package busi 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "time" 7 | 8 | "github.com/dtm-labs/dtm/dtmcli" 9 | "github.com/gin-gonic/gin" 10 | ) 11 | 12 | // busi address 13 | const qsBusiAPI = "/api/busi_start" 14 | const qsBusiPort = 8082 15 | 16 | var qsBusi = fmt.Sprintf("http://localhost:%d%s", qsBusiPort, qsBusiAPI) 17 | 18 | // QsMain will be call from dtm/qs 19 | func QsMain() { 20 | QsStartSvr() 21 | QsFireRequest() 22 | select {} 23 | } 24 | 25 | // QsStartSvr quick start: start server 26 | func QsStartSvr() { 27 | app := gin.New() 28 | qsAddRoute(app) 29 | log.Printf("quick start examples listening at %d", qsBusiPort) 30 | go func() { 31 | _ = app.Run(fmt.Sprintf(":%d", qsBusiPort)) 32 | }() 33 | time.Sleep(100 * time.Millisecond) 34 | } 35 | 36 | func qsAddRoute(app *gin.Engine) { 37 | app.POST(qsBusiAPI+"/TransIn", func(c *gin.Context) { 38 | log.Printf("TransIn") 39 | // c.JSON(200, "") 40 | c.JSON(409, "") // Status 409 for Failure. Won't be retried 41 | }) 42 | app.POST(qsBusiAPI+"/TransInCompensate", func(c *gin.Context) { 43 | log.Printf("TransInCompensate") 44 | c.JSON(200, "") 45 | }) 46 | app.POST(qsBusiAPI+"/TransOut", func(c *gin.Context) { 47 | log.Printf("TransOut") 48 | c.JSON(200, "") 49 | }) 50 | app.POST(qsBusiAPI+"/TransOutCompensate", func(c *gin.Context) { 51 | log.Printf("TransOutCompensate") 52 | c.JSON(200, "") 53 | }) 54 | } 55 | 56 | const dtmServer = "http://localhost:36789/api/dtmsvr" 57 | 58 | // QsFireRequest quick start: fire request 59 | func QsFireRequest() string { 60 | req := &gin.H{"amount": 30} // load of micro-service 61 | // DtmServer is the url of dtm 62 | saga := dtmcli.NewSaga(dtmServer, dtmcli.MustGenGid(dtmServer)). 63 | // add a TransOut subtraction,forward operation with url: qsBusi+"/TransOut", reverse compensation operation with url: qsBusi+"/TransOutCompensate" 64 | Add(qsBusi+"/TransOut", qsBusi+"/TransOutCompensate", req). 65 | // add a TransIn subtraction, forward operation with url: qsBusi+"/TransIn", reverse compensation operation with url: qsBusi+"/TransInCompensate" 66 | Add(qsBusi+"/TransIn", qsBusi+"/TransInCompensate", req) 67 | // submit the created saga transaction,dtm ensures all subtractions either complete or get revoked 68 | err := saga.Submit() 69 | 70 | if err != nil { 71 | panic(err) 72 | } 73 | return saga.Gid 74 | } 75 | -------------------------------------------------------------------------------- /charts/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: {{ include "dtm.fullname" . }} 5 | labels: 6 | {{- include "dtm.labels" . | nindent 4 }} 7 | spec: 8 | {{- if not .Values.autoscaling.enabled }} 9 | replicas: {{ .Values.replicaCount }} 10 | {{- end }} 11 | selector: 12 | matchLabels: 13 | {{- include "dtm.selectorLabels" . | nindent 6 }} 14 | template: 15 | metadata: 16 | labels: 17 | {{- include "dtm.selectorLabels" . | nindent 8 }} 18 | spec: 19 | {{- with .Values.imagePullSecrets }} 20 | imagePullSecrets: 21 | {{- toYaml . | nindent 8 }} 22 | {{- end }} 23 | securityContext: 24 | {{- toYaml .Values.podSecurityContext | nindent 8 }} 25 | containers: 26 | - name: {{ .Chart.Name }} 27 | securityContext: 28 | {{- toYaml .Values.securityContext | nindent 12 }} 29 | image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" 30 | imagePullPolicy: {{ .Values.image.pullPolicy }} 31 | args: 32 | - "-c=/app/dtm/configs/config.yaml" 33 | volumeMounts: 34 | - mountPath: /app/dtm/configs 35 | name: config 36 | ports: 37 | - containerPort: 36789 38 | protocol: TCP 39 | name: http 40 | - containerPort: 36790 41 | protocol: TCP 42 | name: grpc 43 | livenessProbe: 44 | httpGet: 45 | path: /api/ping 46 | port: 36789 47 | scheme: HTTP 48 | readinessProbe: 49 | httpGet: 50 | path: /api/ping 51 | port: 36789 52 | scheme: HTTP 53 | resources: 54 | {{- toYaml .Values.resources | nindent 12 }} 55 | {{- with .Values.nodeSelector }} 56 | nodeSelector: 57 | {{- toYaml . | nindent 8 }} 58 | {{- end }} 59 | {{- with .Values.affinity }} 60 | affinity: 61 | {{- toYaml . | nindent 8 }} 62 | {{- end }} 63 | {{- with .Values.tolerations }} 64 | tolerations: 65 | {{- toYaml . | nindent 8 }} 66 | {{- end }} 67 | volumes: 68 | - name: config 69 | configMap: 70 | name: {{ include "dtm.fullname" . }}-conf 71 | -------------------------------------------------------------------------------- /dtmgrpc/dtmgimp/grpc_clients.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 dtmgimp 8 | 9 | import ( 10 | "fmt" 11 | "sync" 12 | 13 | "github.com/dtm-labs/dtm/dtmcli/dtmimp" 14 | "github.com/dtm-labs/dtm/dtmcli/logger" 15 | "github.com/dtm-labs/dtm/dtmgrpc/dtmgpb" 16 | grpc "google.golang.org/grpc" 17 | "google.golang.org/grpc/credentials/insecure" 18 | ) 19 | 20 | type rawCodec struct{} 21 | 22 | func (cb rawCodec) Marshal(v interface{}) ([]byte, error) { 23 | return v.([]byte), nil 24 | } 25 | 26 | func (cb rawCodec) Unmarshal(data []byte, v interface{}) error { 27 | ba, ok := v.(*[]byte) 28 | dtmimp.PanicIf(!ok, fmt.Errorf("please pass in *[]byte")) 29 | *ba = append(*ba, data...) 30 | 31 | return nil 32 | } 33 | 34 | func (cb rawCodec) Name() string { return "dtm_raw" } 35 | 36 | var normalClients, rawClients sync.Map 37 | 38 | // ClientInterceptors declares grpc.UnaryClientInterceptors slice 39 | var ClientInterceptors = []grpc.UnaryClientInterceptor{} 40 | 41 | // MustGetDtmClient 1 42 | func MustGetDtmClient(grpcServer string) dtmgpb.DtmClient { 43 | return dtmgpb.NewDtmClient(MustGetGrpcConn(grpcServer, false)) 44 | } 45 | 46 | // GetGrpcConn 1 47 | func GetGrpcConn(grpcServer string, isRaw bool) (conn *grpc.ClientConn, rerr error) { 48 | clients := &normalClients 49 | if isRaw { 50 | clients = &rawClients 51 | } 52 | grpcServer = dtmimp.MayReplaceLocalhost(grpcServer) 53 | v, ok := clients.Load(grpcServer) 54 | if !ok { 55 | opts := grpc.WithDefaultCallOptions() 56 | if isRaw { 57 | opts = grpc.WithDefaultCallOptions(grpc.ForceCodec(rawCodec{})) 58 | } 59 | logger.Debugf("grpc client connecting %s", grpcServer) 60 | interceptors := append(ClientInterceptors, GrpcClientLog) 61 | inOpt := grpc.WithChainUnaryInterceptor(interceptors...) 62 | conn, rerr := grpc.Dial(grpcServer, inOpt, grpc.WithTransportCredentials(insecure.NewCredentials()), opts) 63 | if rerr == nil { 64 | clients.Store(grpcServer, conn) 65 | v = conn 66 | logger.Debugf("grpc client inited for %s", grpcServer) 67 | } 68 | } 69 | return v.(*grpc.ClientConn), rerr 70 | } 71 | 72 | // MustGetGrpcConn 1 73 | func MustGetGrpcConn(grpcServer string, isRaw bool) *grpc.ClientConn { 74 | conn, err := GetGrpcConn(grpcServer, isRaw) 75 | dtmimp.E2P(err) 76 | return conn 77 | } 78 | -------------------------------------------------------------------------------- /dtmcli/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(db, GetDBSpecial().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) 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 := StandaloneDB(dbConf) 37 | if rerr != nil { 38 | return 39 | } 40 | defer func() { _ = db.Close() }() 41 | defer DeferDo(&rerr, func() error { 42 | _, err := DBExec(db, GetDBSpecial().GetXaSQL("prepare", xaBranch)) 43 | return err 44 | }, func() error { 45 | return nil 46 | }) 47 | _, rerr = DBExec(db, GetDBSpecial().GetXaSQL("start", xaBranch)) 48 | if rerr != nil { 49 | return 50 | } 51 | defer func() { 52 | _, _ = DBExec(db, GetDBSpecial().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) 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 | -------------------------------------------------------------------------------- /dtmcli/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/dtm/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 | -------------------------------------------------------------------------------- /test/base_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 test 8 | 9 | import ( 10 | "database/sql" 11 | "fmt" 12 | "testing" 13 | 14 | "github.com/dtm-labs/dtm/dtmcli" 15 | "github.com/dtm-labs/dtm/dtmcli/dtmimp" 16 | "github.com/dtm-labs/dtm/dtmcli/logger" 17 | "github.com/dtm-labs/dtm/dtmutil" 18 | "github.com/dtm-labs/dtm/test/busi" 19 | "github.com/stretchr/testify/assert" 20 | ) 21 | 22 | // BarrierModel barrier model for gorm 23 | type BarrierModel struct { 24 | dtmutil.ModelBase 25 | dtmcli.BranchBarrier 26 | } 27 | 28 | // TableName gorm table name 29 | func (BarrierModel) TableName() string { return "dtm_barrier.barrier" } 30 | 31 | func TestBaseSqlDB(t *testing.T) { 32 | asserts := assert.New(t) 33 | db := dtmutil.DbGet(busi.BusiConf) 34 | barrier := &dtmcli.BranchBarrier{ 35 | TransType: "saga", 36 | Gid: "gid2", 37 | BranchID: "branch_id2", 38 | Op: dtmimp.OpAction, 39 | BarrierID: 1, 40 | } 41 | db.Must().Exec("insert into dtm_barrier.barrier(trans_type, gid, branch_id, op, barrier_id, reason) values('saga', 'gid1', 'branch_id1', 'action', '01', 'saga')") 42 | tx, err := db.ToSQLDB().Begin() 43 | asserts.Nil(err) 44 | err = barrier.Call(tx, func(tx *sql.Tx) error { 45 | logger.Debugf("rollback gid2") 46 | return fmt.Errorf("gid2 error") 47 | }) 48 | asserts.Error(err, fmt.Errorf("gid2 error")) 49 | dbr := db.Model(&BarrierModel{}).Where("gid=?", "gid1").Find(&[]BarrierModel{}) 50 | asserts.Equal(dbr.RowsAffected, int64(1)) 51 | dbr = db.Model(&BarrierModel{}).Where("gid=?", "gid2").Find(&[]BarrierModel{}) 52 | asserts.Equal(dbr.RowsAffected, int64(0)) 53 | barrier.BarrierID = 0 54 | err = barrier.CallWithDB(db.ToSQLDB(), func(tx *sql.Tx) error { 55 | logger.Debugf("submit gid2") 56 | return nil 57 | }) 58 | asserts.Nil(err) 59 | dbr = db.Model(&BarrierModel{}).Where("gid=?", "gid2").Find(&[]BarrierModel{}) 60 | asserts.Equal(dbr.RowsAffected, int64(1)) 61 | } 62 | 63 | func TestBaseHttp(t *testing.T) { 64 | resp, err := dtmimp.RestyClient.R().SetQueryParam("panic_string", "1").Post(busi.Busi + "/TestPanic") 65 | assert.Nil(t, err) 66 | assert.Contains(t, resp.String(), "panic_string") 67 | resp, err = dtmimp.RestyClient.R().SetQueryParam("panic_error", "1").Post(busi.Busi + "/TestPanic") 68 | assert.Nil(t, err) 69 | assert.Contains(t, resp.String(), "panic_error") 70 | } 71 | -------------------------------------------------------------------------------- /dtmcli/barrier_redis.go: -------------------------------------------------------------------------------- 1 | package dtmcli 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/dtm-labs/dtm/dtmcli/dtmimp" 7 | "github.com/dtm-labs/dtm/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 | -------------------------------------------------------------------------------- /sqls/dtmsvr.storage.mysql.sql: -------------------------------------------------------------------------------- 1 | CREATE DATABASE IF NOT EXISTS dtm 2 | /*!40100 DEFAULT CHARACTER SET utf8mb4 */ 3 | ; 4 | drop table IF EXISTS dtm.trans_global; 5 | CREATE TABLE if not EXISTS dtm.trans_global ( 6 | `id` bigint(22) NOT NULL AUTO_INCREMENT, 7 | `gid` varchar(128) NOT NULL COMMENT 'global transaction id', 8 | `trans_type` varchar(45) not null COMMENT 'transaction type: saga | xa | tcc | msg', 9 | `status` varchar(12) NOT NULL COMMENT 'tranaction status: prepared | submitted | aborting | finished | rollbacked', 10 | `query_prepared` varchar(128) NOT NULL COMMENT 'url to check for 2-phase message', 11 | `protocol` varchar(45) not null comment 'protocol: http | grpc | json-rpc', 12 | `create_time` datetime DEFAULT NULL, 13 | `update_time` datetime DEFAULT NULL, 14 | `finish_time` datetime DEFAULT NULL, 15 | `rollback_time` datetime DEFAULT NULL, 16 | `options` varchar(1024) DEFAULT 'options for transaction like: TimeoutToFail, RequestTimeout', 17 | `custom_data` varchar(256) DEFAULT '' COMMENT 'custom data for transaction', 18 | `next_cron_interval` int(11) default null comment 'next cron interval. for use of cron job', 19 | `next_cron_time` datetime default null comment 'next time to process this trans. for use of cron job', 20 | `owner` varchar(128) not null default '' comment 'who is locking this trans', 21 | `ext_data` TEXT comment 'extended data for this trans', 22 | PRIMARY KEY (`id`), 23 | UNIQUE KEY `gid` (`gid`), 24 | key `owner`(`owner`), 25 | key `status_next_cron_time` (`status`, `next_cron_time`) comment 'cron job will use this index to query trans' 26 | ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4; 27 | drop table IF EXISTS dtm.trans_branch_op; 28 | CREATE TABLE IF NOT EXISTS dtm.trans_branch_op ( 29 | `id` bigint(22) NOT NULL AUTO_INCREMENT, 30 | `gid` varchar(128) NOT NULL COMMENT 'global transaction id', 31 | `url` varchar(128) NOT NULL COMMENT 'the url of this op', 32 | `data` TEXT COMMENT 'request body, depreceated', 33 | `bin_data` BLOB COMMENT 'request body', 34 | `branch_id` VARCHAR(128) NOT NULL COMMENT 'transaction branch ID', 35 | `op` varchar(45) NOT NULL COMMENT 'transaction operation type like: action | compensate | try | confirm | cancel', 36 | `status` varchar(45) NOT NULL COMMENT 'transaction op status: prepared | succeed | failed', 37 | `finish_time` datetime DEFAULT NULL, 38 | `rollback_time` datetime DEFAULT NULL, 39 | `create_time` datetime DEFAULT NULL, 40 | `update_time` datetime DEFAULT NULL, 41 | PRIMARY KEY (`id`), 42 | UNIQUE KEY `gid_uniq` (`gid`, `branch_id`, `op`) 43 | ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4; 44 | -------------------------------------------------------------------------------- /dtmgrpc/dtmgimp/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 dtmgimp 8 | 9 | import ( 10 | "context" 11 | "fmt" 12 | "time" 13 | 14 | "github.com/dtm-labs/dtm/dtmcli/dtmimp" 15 | "github.com/dtm-labs/dtm/dtmcli/logger" 16 | "github.com/dtm-labs/dtmdriver" 17 | "google.golang.org/grpc" 18 | "google.golang.org/grpc/metadata" 19 | "google.golang.org/protobuf/proto" 20 | ) 21 | 22 | // GrpcServerLog middleware to print server-side grpc log 23 | func GrpcServerLog(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { 24 | began := time.Now() 25 | logger.Debugf("grpc server handling: %s %s", info.FullMethod, dtmimp.MustMarshalString(req)) 26 | LogDtmCtx(ctx) 27 | m, err := handler(ctx, req) 28 | res := fmt.Sprintf("%2dms %v %s %s %s", 29 | time.Since(began).Milliseconds(), err, info.FullMethod, dtmimp.MustMarshalString(m), dtmimp.MustMarshalString(req)) 30 | if err != nil { 31 | logger.Errorf("%s", res) 32 | } else { 33 | logger.Infof("%s", res) 34 | } 35 | return m, err 36 | } 37 | 38 | // GrpcClientLog middleware to print client-side grpc log 39 | func GrpcClientLog(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error { 40 | logger.Debugf("grpc client calling: %s%s %v", cc.Target(), method, dtmimp.MustMarshalString(req)) 41 | LogDtmCtx(ctx) 42 | err := invoker(ctx, method, req, reply, cc, opts...) 43 | res := fmt.Sprintf("grpc client called: %s%s %s result: %s err: %v", 44 | cc.Target(), method, dtmimp.MustMarshalString(req), dtmimp.MustMarshalString(reply), err) 45 | if err != nil { 46 | logger.Errorf("%s", res) 47 | } else { 48 | logger.Debugf("%s", res) 49 | } 50 | return err 51 | } 52 | 53 | // InvokeBranch invoke a url for trans 54 | func InvokeBranch(t *dtmimp.TransBase, isRaw bool, msg proto.Message, url string, reply interface{}, branchID string, op string) error { 55 | server, method, err := dtmdriver.GetDriver().ParseServerMethod(url) 56 | if err != nil { 57 | return err 58 | } 59 | ctx := TransInfo2Ctx(t.Gid, t.TransType, branchID, op, t.Dtm) 60 | ctx = metadata.AppendToOutgoingContext(ctx, Map2Kvs(t.BranchHeaders)...) 61 | if t.TransType == "xa" { // xa branch need additional phase2_url 62 | ctx = metadata.AppendToOutgoingContext(ctx, Map2Kvs(map[string]string{dtmpre + "phase2_url": url})...) 63 | } 64 | return MustGetGrpcConn(server, isRaw).Invoke(ctx, method, msg, reply) 65 | } 66 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ main ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ main ] 20 | schedule: 21 | - cron: '25 19 * * 2' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'go' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://git.io/codeql-language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v2 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v1 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 52 | 53 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 54 | # If this step fails, then you should remove it and run the build manually (see below) 55 | - name: Autobuild 56 | uses: github/codeql-action/autobuild@v1 57 | 58 | # ℹ️ Command-line programs to run using the OS shell. 59 | # 📚 https://git.io/JvXDl 60 | 61 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 62 | # and modify them (or add more) to build your code if your project 63 | # uses a compiled language 64 | 65 | #- run: | 66 | # make bootstrap 67 | # make release 68 | 69 | - name: Perform CodeQL Analysis 70 | uses: github/codeql-action/analyze@v1 71 | -------------------------------------------------------------------------------- /test/msg_grpc_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 test 8 | 9 | import ( 10 | "fmt" 11 | "testing" 12 | 13 | "github.com/dtm-labs/dtm/dtmcli" 14 | "github.com/dtm-labs/dtm/dtmcli/dtmimp" 15 | "github.com/dtm-labs/dtm/dtmgrpc" 16 | "github.com/dtm-labs/dtm/dtmutil" 17 | "github.com/dtm-labs/dtm/test/busi" 18 | "github.com/stretchr/testify/assert" 19 | ) 20 | 21 | func TestMsgGrpcNormal(t *testing.T) { 22 | msg := genGrpcMsg(dtmimp.GetFuncName()) 23 | err := msg.Submit() 24 | assert.Nil(t, err) 25 | waitTransProcessed(msg.Gid) 26 | assert.Equal(t, StatusSucceed, getTransStatus(msg.Gid)) 27 | assert.Equal(t, []string{StatusSucceed, StatusSucceed}, getBranchesStatus(msg.Gid)) 28 | } 29 | 30 | func TestMsgGrpcTimeoutSuccess(t *testing.T) { 31 | gid := dtmimp.GetFuncName() 32 | msg := genGrpcMsg(gid) 33 | err := msg.Prepare("") 34 | assert.Nil(t, err) 35 | busi.MainSwitch.QueryPreparedResult.SetOnce(dtmcli.ResultOngoing) 36 | cronTransOnceForwardNow(t, gid, 180) 37 | assert.Equal(t, StatusPrepared, getTransStatus(msg.Gid)) 38 | busi.MainSwitch.TransOutResult.SetOnce(dtmcli.ResultOngoing) 39 | cronTransOnceForwardNow(t, gid, 180) 40 | assert.Equal(t, StatusSubmitted, getTransStatus(msg.Gid)) 41 | assert.Equal(t, []string{StatusPrepared, StatusPrepared}, getBranchesStatus(msg.Gid)) 42 | cronTransOnce(t, gid) 43 | assert.Equal(t, StatusSucceed, getTransStatus(msg.Gid)) 44 | assert.Equal(t, []string{StatusSucceed, StatusSucceed}, getBranchesStatus(msg.Gid)) 45 | } 46 | 47 | func TestMsgGrpcTimeoutFailed(t *testing.T) { 48 | gid := dtmimp.GetFuncName() 49 | msg := genGrpcMsg(gid) 50 | msg.Prepare("") 51 | assert.Equal(t, StatusPrepared, getTransStatus(msg.Gid)) 52 | busi.MainSwitch.QueryPreparedResult.SetOnce(dtmcli.ResultOngoing) 53 | cronTransOnceForwardNow(t, gid, 180) 54 | assert.Equal(t, StatusPrepared, getTransStatus(msg.Gid)) 55 | busi.MainSwitch.QueryPreparedResult.SetOnce(dtmcli.ResultFailure) 56 | cronTransOnceForwardNow(t, gid, 180) 57 | assert.Equal(t, StatusFailed, getTransStatus(msg.Gid)) 58 | assert.Equal(t, []string{StatusPrepared, StatusPrepared}, getBranchesStatus(msg.Gid)) 59 | } 60 | 61 | func genGrpcMsg(gid string) *dtmgrpc.MsgGrpc { 62 | req := &busi.BusiReq{Amount: 30} 63 | msg := dtmgrpc.NewMsgGrpc(dtmutil.DefaultGrpcServer, gid). 64 | Add(busi.BusiGrpc+"/busi.Busi/TransOut", req). 65 | Add(busi.BusiGrpc+"/busi.Busi/TransIn", req) 66 | msg.QueryPrepared = fmt.Sprintf("%s/busi.Busi/QueryPrepared", busi.BusiGrpc) 67 | return msg 68 | } 69 | -------------------------------------------------------------------------------- /test/xa_grpc_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 test 8 | 9 | import ( 10 | "context" 11 | "fmt" 12 | "testing" 13 | 14 | "github.com/dtm-labs/dtm/dtmcli/dtmimp" 15 | "github.com/dtm-labs/dtm/dtmgrpc" 16 | "github.com/dtm-labs/dtm/test/busi" 17 | "github.com/stretchr/testify/assert" 18 | "google.golang.org/protobuf/types/known/emptypb" 19 | ) 20 | 21 | func TestXaGrpcNormal(t *testing.T) { 22 | gid := dtmimp.GetFuncName() 23 | err := dtmgrpc.XaGlobalTransaction(DtmGrpcServer, gid, func(xa *dtmgrpc.XaGrpc) error { 24 | req := busi.GenBusiReq(30, false, false) 25 | r := &emptypb.Empty{} 26 | err := xa.CallBranch(req, busi.BusiGrpc+"/busi.Busi/TransOutXa", r) 27 | if err != nil { 28 | return err 29 | } 30 | return xa.CallBranch(req, busi.BusiGrpc+"/busi.Busi/TransInXa", r) 31 | }) 32 | assert.Equal(t, nil, err) 33 | waitTransProcessed(gid) 34 | assert.Equal(t, StatusSucceed, getTransStatus(gid)) 35 | assert.Equal(t, []string{StatusPrepared, StatusSucceed, StatusPrepared, StatusSucceed}, getBranchesStatus(gid)) 36 | } 37 | 38 | func TestXaGrpcRollback(t *testing.T) { 39 | gid := dtmimp.GetFuncName() 40 | err := dtmgrpc.XaGlobalTransaction(DtmGrpcServer, gid, func(xa *dtmgrpc.XaGrpc) error { 41 | req := busi.GenBusiReq(30, false, true) 42 | r := &emptypb.Empty{} 43 | err := xa.CallBranch(req, busi.BusiGrpc+"/busi.Busi/TransOutXa", r) 44 | if err != nil { 45 | return err 46 | } 47 | return xa.CallBranch(req, busi.BusiGrpc+"/busi.Busi/TransInXa", r) 48 | }) 49 | assert.Error(t, err) 50 | waitTransProcessed(gid) 51 | assert.Equal(t, []string{StatusSucceed, StatusPrepared}, getBranchesStatus(gid)) 52 | assert.Equal(t, StatusFailed, getTransStatus(gid)) 53 | } 54 | 55 | func TestXaGrpcType(t *testing.T) { 56 | gid := dtmimp.GetFuncName() 57 | _, err := dtmgrpc.XaGrpcFromRequest(context.Background()) 58 | assert.Error(t, err) 59 | 60 | err = dtmgrpc.XaLocalTransaction(context.Background(), busi.BusiConf, nil) 61 | assert.Error(t, err) 62 | 63 | err = dtmimp.CatchP(func() { 64 | dtmgrpc.XaGlobalTransaction(DtmGrpcServer, gid, func(xa *dtmgrpc.XaGrpc) error { panic(fmt.Errorf("hello")) }) 65 | }) 66 | assert.Error(t, err) 67 | waitTransProcessed(gid) 68 | } 69 | 70 | func TestXaGrpcLocalError(t *testing.T) { 71 | gid := dtmimp.GetFuncName() 72 | err := dtmgrpc.XaGlobalTransaction(DtmGrpcServer, gid, func(xa *dtmgrpc.XaGrpc) error { 73 | return fmt.Errorf("an error") 74 | }) 75 | assert.Error(t, err, fmt.Errorf("an error")) 76 | waitTransProcessed(gid) 77 | } 78 | -------------------------------------------------------------------------------- /dtmsvr/config/config_utils.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | "reflect" 8 | "regexp" 9 | "strings" 10 | 11 | "github.com/dtm-labs/dtm/dtmcli/dtmimp" 12 | ) 13 | 14 | func loadFromEnv(prefix string, conf interface{}) { 15 | rv := reflect.ValueOf(conf) 16 | dtmimp.PanicIf(rv.Kind() != reflect.Ptr || rv.IsNil(), 17 | fmt.Errorf("should be a valid pointer, but %s found", reflect.TypeOf(conf).Name())) 18 | loadFromEnvInner(prefix, rv.Elem(), "") 19 | } 20 | 21 | func loadFromEnvInner(prefix string, conf reflect.Value, defaultValue string) { 22 | kind := conf.Kind() 23 | switch kind { 24 | case reflect.Struct: 25 | t := conf.Type() 26 | for i := 0; i < t.NumField(); i++ { 27 | tag := t.Field(i).Tag 28 | loadFromEnvInner(prefix+"_"+tag.Get("yaml"), conf.Field(i), tag.Get("default")) 29 | } 30 | case reflect.String: 31 | str := os.Getenv(toUnderscoreUpper(prefix)) 32 | if str == "" { 33 | str = defaultValue 34 | } 35 | conf.Set(reflect.ValueOf(str)) 36 | case reflect.Int64: 37 | str := os.Getenv(toUnderscoreUpper(prefix)) 38 | if str == "" { 39 | str = defaultValue 40 | } 41 | if str == "" { 42 | str = "0" 43 | } 44 | conf.Set(reflect.ValueOf(int64(dtmimp.MustAtoi(str)))) 45 | default: 46 | panic(fmt.Errorf("unsupported type: %s", conf.Type().Name())) 47 | } 48 | } 49 | 50 | func toUnderscoreUpper(key string) string { 51 | key = strings.Trim(key, "_") 52 | matchLastCap := regexp.MustCompile("([A-Z])([A-Z][a-z])") 53 | s2 := matchLastCap.ReplaceAllString(key, "${1}_${2}") 54 | 55 | matchFirstCap := regexp.MustCompile("([a-z])([A-Z]+)") 56 | s2 = matchFirstCap.ReplaceAllString(s2, "${1}_${2}") 57 | // logger.Infof("loading from env: %s", strings.ToUpper(s2)) 58 | return strings.ToUpper(s2) 59 | } 60 | 61 | func checkConfig(conf *configType) error { 62 | if conf.RetryInterval < 10 { 63 | return errors.New("RetryInterval should not be less than 10") 64 | } 65 | if conf.TimeoutToFail < conf.RetryInterval { 66 | return errors.New("TimeoutToFail should not be less than RetryInterval") 67 | } 68 | switch conf.Store.Driver { 69 | case BoltDb: 70 | return nil 71 | case Mysql, Postgres: 72 | if conf.Store.Host == "" { 73 | return errors.New("Db host not valid ") 74 | } 75 | if conf.Store.Port == 0 { 76 | return errors.New("Db port not valid ") 77 | } 78 | if conf.Store.User == "" { 79 | return errors.New("Db user not valid ") 80 | } 81 | case Redis: 82 | if conf.Store.Host == "" { 83 | return errors.New("Redis host not valid") 84 | } 85 | if conf.Store.Port == 0 { 86 | return errors.New("Redis port not valid") 87 | } 88 | } 89 | return nil 90 | } 91 | -------------------------------------------------------------------------------- /dtmsvr/config/config_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "errors" 5 | "os" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestLoadFromEnv(t *testing.T) { 12 | assert.Equal(t, "MICRO_SERVICE_DRIVER", toUnderscoreUpper("MicroService_Driver")) 13 | 14 | ms := MicroService{} 15 | os.Setenv("T_DRIVER", "d1") 16 | loadFromEnv("T", &ms) 17 | assert.Equal(t, "d1", ms.Driver) 18 | } 19 | 20 | func TestLoadConfig(t *testing.T) { 21 | MustLoadConfig("../../conf.sample.yml") 22 | } 23 | func TestCheckConfig(t *testing.T) { 24 | conf := Config 25 | conf.RetryInterval = 1 26 | retryIntervalErr := checkConfig(&conf) 27 | retryIntervalExpect := errors.New("RetryInterval should not be less than 10") 28 | assert.Equal(t, retryIntervalErr, retryIntervalExpect) 29 | 30 | conf.RetryInterval = 10 31 | conf.TimeoutToFail = 5 32 | timeoutToFailErr := checkConfig(&conf) 33 | timeoutToFailExpect := errors.New("TimeoutToFail should not be less than RetryInterval") 34 | assert.Equal(t, timeoutToFailErr, timeoutToFailExpect) 35 | 36 | conf.TimeoutToFail = 20 37 | driverErr := checkConfig(&conf) 38 | assert.Equal(t, driverErr, nil) 39 | 40 | conf.Store = Store{Driver: Mysql} 41 | hostErr := checkConfig(&conf) 42 | hostExpect := errors.New("Db host not valid ") 43 | assert.Equal(t, hostErr, hostExpect) 44 | 45 | conf.Store = Store{Driver: Mysql, Host: "127.0.0.1"} 46 | portErr := checkConfig(&conf) 47 | portExpect := errors.New("Db port not valid ") 48 | assert.Equal(t, portErr, portExpect) 49 | 50 | conf.Store = Store{Driver: Mysql, Host: "127.0.0.1", Port: 8686} 51 | userErr := checkConfig(&conf) 52 | userExpect := errors.New("Db user not valid ") 53 | assert.Equal(t, userErr, userExpect) 54 | 55 | conf.Store = Store{Driver: Redis, Host: "", Port: 8686} 56 | assert.Equal(t, errors.New("Redis host not valid"), checkConfig(&conf)) 57 | 58 | conf.Store = Store{Driver: Redis, Host: "127.0.0.1", Port: 0} 59 | assert.Equal(t, errors.New("Redis port not valid"), checkConfig(&conf)) 60 | 61 | } 62 | 63 | func TestConfig(t *testing.T) { 64 | testConfigStringField(&Config.Store.Driver, "", t) 65 | testConfigStringField(&Config.Store.User, "", t) 66 | testConfigIntField(&Config.RetryInterval, 9, t) 67 | testConfigIntField(&Config.TimeoutToFail, 9, t) 68 | } 69 | 70 | func testConfigStringField(fd *string, val string, t *testing.T) { 71 | old := *fd 72 | *fd = val 73 | str := checkConfig(&Config) 74 | assert.NotEqual(t, "", str) 75 | *fd = old 76 | } 77 | 78 | func testConfigIntField(fd *int64, val int64, t *testing.T) { 79 | old := *fd 80 | *fd = val 81 | str := checkConfig(&Config) 82 | assert.NotEqual(t, "", str) 83 | *fd = old 84 | } 85 | -------------------------------------------------------------------------------- /dtmgrpc/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 dtmgrpc 8 | 9 | import ( 10 | context "context" 11 | "fmt" 12 | 13 | "github.com/dtm-labs/dtm/dtmcli/dtmimp" 14 | "github.com/dtm-labs/dtm/dtmgrpc/dtmgimp" 15 | "github.com/dtm-labs/dtm/dtmgrpc/dtmgpb" 16 | "google.golang.org/protobuf/proto" 17 | ) 18 | 19 | // TccGrpc struct of tcc 20 | type TccGrpc struct { 21 | dtmimp.TransBase 22 | } 23 | 24 | // TccGlobalFunc type of global tcc call 25 | type TccGlobalFunc func(tcc *TccGrpc) error 26 | 27 | // TccGlobalTransaction begin a tcc global transaction 28 | // dtm dtm server url 29 | // gid global transaction id 30 | // tccFunc tcc busi func, define the transaction logic 31 | func TccGlobalTransaction(dtm string, gid string, tccFunc TccGlobalFunc) (rerr error) { 32 | return TccGlobalTransaction2(dtm, gid, func(tg *TccGrpc) {}, tccFunc) 33 | } 34 | 35 | // TccGlobalTransaction2 new version of TccGlobalTransaction 36 | func TccGlobalTransaction2(dtm string, gid string, custom func(*TccGrpc), tccFunc TccGlobalFunc) (rerr error) { 37 | tcc := &TccGrpc{TransBase: *dtmimp.NewTransBase(gid, "tcc", dtm, "")} 38 | custom(tcc) 39 | rerr = dtmgimp.DtmGrpcCall(&tcc.TransBase, "Prepare") 40 | if rerr != nil { 41 | return rerr 42 | } 43 | defer dtmimp.DeferDo(&rerr, func() error { 44 | return dtmgimp.DtmGrpcCall(&tcc.TransBase, "Submit") 45 | }, func() error { 46 | return dtmgimp.DtmGrpcCall(&tcc.TransBase, "Abort") 47 | }) 48 | return tccFunc(tcc) 49 | } 50 | 51 | // TccFromGrpc tcc from request info 52 | func TccFromGrpc(ctx context.Context) (*TccGrpc, error) { 53 | tcc := &TccGrpc{ 54 | TransBase: *dtmgimp.TransBaseFromGrpc(ctx), 55 | } 56 | if tcc.Dtm == "" || tcc.Gid == "" { 57 | return nil, fmt.Errorf("bad tcc info. dtm: %s, gid: %s branchid: %s", tcc.Dtm, tcc.Gid, tcc.BranchID) 58 | } 59 | return tcc, nil 60 | } 61 | 62 | // CallBranch call a tcc branch 63 | func (t *TccGrpc) CallBranch(busiMsg proto.Message, tryURL string, confirmURL string, cancelURL string, reply interface{}) error { 64 | branchID := t.NewSubBranchID() 65 | bd, err := proto.Marshal(busiMsg) 66 | if err == nil { 67 | _, err = dtmgimp.MustGetDtmClient(t.Dtm).RegisterBranch(context.Background(), &dtmgpb.DtmBranchRequest{ 68 | Gid: t.Gid, 69 | TransType: t.TransType, 70 | BranchID: branchID, 71 | BusiPayload: bd, 72 | Data: map[string]string{"confirm": confirmURL, "cancel": cancelURL}, 73 | }) 74 | } 75 | if err != nil { 76 | return err 77 | } 78 | return dtmgimp.InvokeBranch(&t.TransBase, false, busiMsg, tryURL, reply, branchID, "try") 79 | } 80 | -------------------------------------------------------------------------------- /sqls/dtmsvr.storage.tdsql.sql: -------------------------------------------------------------------------------- 1 | CREATE DATABASE IF NOT EXISTS dtm 2 | /*!40100 DEFAULT CHARACTER SET utf8mb4 */ 3 | ; 4 | drop table IF EXISTS dtm.trans_global; 5 | CREATE TABLE if not EXISTS dtm.trans_global ( 6 | `id` bigint(22) NOT NULL AUTO_INCREMENT, 7 | `gid` varchar(128) NOT NULL COMMENT 'global transaction id', 8 | `trans_type` varchar(45) not null COMMENT 'transaction type: saga | xa | tcc | msg', 9 | `status` varchar(12) NOT NULL COMMENT 'tranaction status: prepared | submitted | aborting | finished | rollbacked', 10 | `query_prepared` varchar(128) NOT NULL COMMENT 'url to check for 2-phase message', 11 | `protocol` varchar(45) not null comment 'protocol: http | grpc | json-rpc', 12 | `create_time` datetime DEFAULT NULL, 13 | `update_time` datetime DEFAULT NULL, 14 | `finish_time` datetime DEFAULT NULL, 15 | `rollback_time` datetime DEFAULT NULL, 16 | `options` varchar(1024) DEFAULT 'options for transaction like: TimeoutToFail, RequestTimeout', 17 | `custom_data` varchar(256) DEFAULT '' COMMENT 'custom data for transaction', 18 | `next_cron_interval` int(11) default null comment 'next cron interval. for use of cron job', 19 | `next_cron_time` datetime default null comment 'next time to process this trans. for use of cron job', 20 | `owner` varchar(128) not null default '' comment 'who is locking this trans', 21 | `ext_data` TEXT comment 'extended data for this trans', 22 | PRIMARY KEY (`id`,`gid`), 23 | UNIQUE KEY `id` (`id`,`gid`), 24 | UNIQUE KEY `gid` (`gid`), 25 | key `owner`(`owner`), 26 | key `status_next_cron_time` (`status`, `next_cron_time`) comment 'cron job will use this index to query trans' 27 | ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 shardkey=gid; 28 | drop table IF EXISTS dtm.trans_branch_op; 29 | CREATE TABLE IF NOT EXISTS dtm.trans_branch_op ( 30 | `id` bigint(22) NOT NULL AUTO_INCREMENT, 31 | `gid` varchar(128) NOT NULL COMMENT 'global transaction id', 32 | `url` varchar(128) NOT NULL COMMENT 'the url of this op', 33 | `data` TEXT COMMENT 'request body, depreceated', 34 | `bin_data` BLOB COMMENT 'request body', 35 | `branch_id` VARCHAR(128) NOT NULL COMMENT 'transaction branch ID', 36 | `op` varchar(45) NOT NULL COMMENT 'transaction operation type like: action | compensate | try | confirm | cancel', 37 | `status` varchar(45) NOT NULL COMMENT 'transaction op status: prepared | succeed | failed', 38 | `finish_time` datetime DEFAULT NULL, 39 | `rollback_time` datetime DEFAULT NULL, 40 | `create_time` datetime DEFAULT NULL, 41 | `update_time` datetime DEFAULT NULL, 42 | PRIMARY KEY (`id`,`gid`), 43 | UNIQUE KEY `id` (`id`,`gid`), 44 | UNIQUE KEY `gid_uniq` (`gid`, `branch_id`, `op`) 45 | ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 shardkey=gid; 46 | -------------------------------------------------------------------------------- /dtmcli/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() DBSpecial { 79 | return dbSpecials[currentDBType] 80 | } 81 | 82 | // SetCurrentDBType set currentDBType 83 | func SetCurrentDBType(dbType string) { 84 | spec := dbSpecials[dbType] 85 | PanicIf(spec == nil, fmt.Errorf("unknown db type '%s'", dbType)) 86 | currentDBType = dbType 87 | } 88 | 89 | // GetCurrentDBType get currentDBType 90 | func GetCurrentDBType() string { 91 | return currentDBType 92 | } 93 | -------------------------------------------------------------------------------- /test/msg_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 test 8 | 9 | import ( 10 | "testing" 11 | 12 | "github.com/dtm-labs/dtm/dtmcli" 13 | "github.com/dtm-labs/dtm/dtmcli/dtmimp" 14 | "github.com/dtm-labs/dtm/dtmutil" 15 | "github.com/dtm-labs/dtm/test/busi" 16 | "github.com/stretchr/testify/assert" 17 | ) 18 | 19 | func TestMsgNormal(t *testing.T) { 20 | msg := genMsg(dtmimp.GetFuncName()) 21 | msg.Submit() 22 | assert.Equal(t, StatusSubmitted, getTransStatus(msg.Gid)) 23 | waitTransProcessed(msg.Gid) 24 | assert.Equal(t, []string{StatusSucceed, StatusSucceed}, getBranchesStatus(msg.Gid)) 25 | assert.Equal(t, StatusSucceed, getTransStatus(msg.Gid)) 26 | } 27 | 28 | func TestMsgTimeoutSuccess(t *testing.T) { 29 | gid := dtmimp.GetFuncName() 30 | msg := genMsg(gid) 31 | msg.Prepare("") 32 | assert.Equal(t, StatusPrepared, getTransStatus(msg.Gid)) 33 | busi.MainSwitch.QueryPreparedResult.SetOnce(dtmcli.ResultOngoing) 34 | cronTransOnceForwardNow(t, gid, 180) 35 | assert.Equal(t, StatusPrepared, getTransStatus(msg.Gid)) 36 | busi.MainSwitch.TransInResult.SetOnce(dtmcli.ResultOngoing) 37 | cronTransOnceForwardNow(t, gid, 180) 38 | assert.Equal(t, StatusSubmitted, getTransStatus(msg.Gid)) 39 | cronTransOnce(t, gid) 40 | assert.Equal(t, []string{StatusSucceed, StatusSucceed}, getBranchesStatus(msg.Gid)) 41 | assert.Equal(t, StatusSucceed, getTransStatus(msg.Gid)) 42 | } 43 | 44 | func TestMsgTimeoutFailed(t *testing.T) { 45 | gid := dtmimp.GetFuncName() 46 | msg := genMsg(gid) 47 | msg.Prepare("") 48 | assert.Equal(t, StatusPrepared, getTransStatus(msg.Gid)) 49 | busi.MainSwitch.QueryPreparedResult.SetOnce(dtmcli.ResultOngoing) 50 | cronTransOnceForwardNow(t, gid, 360) 51 | assert.Equal(t, StatusPrepared, getTransStatus(msg.Gid)) 52 | busi.MainSwitch.QueryPreparedResult.SetOnce(dtmcli.ResultFailure) 53 | cronTransOnceForwardNow(t, gid, 180) 54 | assert.Equal(t, []string{StatusPrepared, StatusPrepared}, getBranchesStatus(msg.Gid)) 55 | assert.Equal(t, StatusFailed, getTransStatus(msg.Gid)) 56 | } 57 | 58 | func TestMsgAbnormal(t *testing.T) { 59 | msg := genMsg(dtmimp.GetFuncName()) 60 | msg.Prepare("") 61 | err := msg.Prepare("") 62 | assert.Nil(t, err) 63 | err = msg.Submit() 64 | assert.Nil(t, err) 65 | waitTransProcessed(msg.Gid) 66 | err = msg.Prepare("") 67 | assert.Error(t, err) 68 | } 69 | 70 | func genMsg(gid string) *dtmcli.Msg { 71 | req := busi.GenTransReq(30, false, false) 72 | msg := dtmcli.NewMsg(dtmutil.DefaultHTTPServer, gid). 73 | Add(busi.Busi+"/TransOut", &req). 74 | Add(busi.Busi+"/TransIn", &req) 75 | msg.QueryPrepared = busi.Busi + "/QueryPrepared" 76 | return msg 77 | } 78 | -------------------------------------------------------------------------------- /conf.sample.yml: -------------------------------------------------------------------------------- 1 | ##################################################################### 2 | ### dtm can be run without any config. 3 | ### all config in this file is optional. the default value is as specified in each line 4 | ### all configs can be specified from env. for example: 5 | ### MicroService.EndPoint => MICRO_SERVICE_END_POINT 6 | ##################################################################### 7 | 8 | # Store: # specify which engine to store trans status 9 | # Driver: 'boltdb' # default store engine 10 | 11 | # Driver: 'redis' 12 | # Host: 'localhost' 13 | # User: '' 14 | # Password: '' 15 | # Port: 6379 16 | 17 | # Driver: 'mysql' 18 | # Host: 'localhost' 19 | # User: 'root' 20 | # Password: '' 21 | # Port: 3306 22 | 23 | # Driver: 'postgres' 24 | # Host: 'localhost' 25 | # User: 'postgres' 26 | # Password: 'mysecretpassword' 27 | # Port: '5432' 28 | 29 | ### following config is for only Driver postgres/mysql 30 | # MaxOpenConns: 500 31 | # MaxIdleConns: 500 32 | # ConnMaxLifeTime 5 # default value is 5 (minutes) 33 | # TransGlobalTable: 'dtm.trans_global' 34 | # TransBranchOpTable: 'dtm.trans_branch_op' 35 | 36 | ### flollowing config is only for some Driver 37 | # DataExpire: 604800 # Trans data will expire in 7 days. only for redis/boltdb. 38 | # SuccessDataExpire: 86400 # successful Trans data will expire in 1 days. only for redis. 39 | # RedisPrefix: '{a}' # default value is '{a}'. Redis storage prefix. store data to only one slot in cluster 40 | 41 | # MicroService: 42 | # Driver: 'dtm-driver-gozero' # name of the driver to handle register/discover 43 | # Target: 'etcd://localhost:2379/dtmservice' # register dtm server to this url 44 | # EndPoint: 'localhost:36790' 45 | 46 | ### the unit of following configurations is second 47 | # TransCronInterval: 3 # the interval to poll unfinished global transaction for every dtm process 48 | # TimeoutToFail: 35 # timeout for XA, TCC to fail. saga's timeout default to infinite, which can be overwritten in saga options 49 | # RetryInterval: 10 # the subtrans branch will be retried after this interval 50 | # RequestTimeout: 3 # the timeout of HTTP/gRPC request in dtm 51 | 52 | # LogLevel: 'info' # default: info. can be debug|info|warn|error 53 | # Log: 54 | # Outputs: 'stderr' # default: stderr, split by ",", you can append files to Outputs if need. example:'stderr,/tmp/test.log' 55 | # RotationEnable: 0 # default: 0 56 | # RotationConfigJSON: '{}' # example: '{"maxsize": 100, "maxage": 0, "maxbackups": 0, "localtime": false, "compress": false}' 57 | 58 | # HttpPort: 36789 59 | # GrpcPort: 36790 60 | # JsonRpcPort: 36791 61 | 62 | ### advanced options 63 | # UpdateBranchAsyncGoroutineNum: 1 # num of async goroutine to update branch status 64 | -------------------------------------------------------------------------------- /test/tcc_old_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/dtm-labs/dtm/dtmcli" 7 | "github.com/dtm-labs/dtm/dtmcli/dtmimp" 8 | "github.com/dtm-labs/dtm/dtmutil" 9 | "github.com/dtm-labs/dtm/test/busi" 10 | "github.com/go-resty/resty/v2" 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | func TestTccOldNormal(t *testing.T) { 15 | req := busi.GenTransReq(30, false, false) 16 | gid := dtmimp.GetFuncName() 17 | err := dtmcli.TccGlobalTransaction(dtmutil.DefaultHTTPServer, gid, func(tcc *dtmcli.Tcc) (*resty.Response, error) { 18 | _, err := tcc.CallBranch(req, Busi+"/TransOutOld", Busi+"/TransOutConfirmOld", Busi+"/TransOutRevertOld") 19 | assert.Nil(t, err) 20 | return tcc.CallBranch(req, Busi+"/TransInOld", Busi+"/TransInConfirmOld", Busi+"/TransInRevertOld") 21 | }) 22 | assert.Nil(t, err) 23 | waitTransProcessed(gid) 24 | assert.Equal(t, StatusSucceed, getTransStatus(gid)) 25 | assert.Equal(t, []string{StatusPrepared, StatusSucceed, StatusPrepared, StatusSucceed}, getBranchesStatus(gid)) 26 | } 27 | 28 | func TestTccOldRollback(t *testing.T) { 29 | gid := dtmimp.GetFuncName() 30 | req := busi.GenTransReq(30, false, true) 31 | err := dtmcli.TccGlobalTransaction(dtmutil.DefaultHTTPServer, gid, func(tcc *dtmcli.Tcc) (*resty.Response, error) { 32 | _, rerr := tcc.CallBranch(req, Busi+"/TransOutOld", Busi+"/TransOutConfirmOld", Busi+"/TransOutRevertOld") 33 | assert.Nil(t, rerr) 34 | busi.MainSwitch.TransOutRevertResult.SetOnce(dtmcli.ResultOngoing) 35 | return tcc.CallBranch(req, Busi+"/TransInOld", Busi+"/TransInConfirmOld", Busi+"/TransInRevertOld") 36 | }) 37 | assert.Error(t, err) 38 | waitTransProcessed(gid) 39 | assert.Equal(t, StatusAborting, getTransStatus(gid)) 40 | cronTransOnce(t, gid) 41 | assert.Equal(t, StatusFailed, getTransStatus(gid)) 42 | assert.Equal(t, []string{StatusSucceed, StatusPrepared, StatusSucceed, StatusPrepared}, getBranchesStatus(gid)) 43 | } 44 | 45 | func TestTccOldTimeout(t *testing.T) { 46 | req := busi.GenTransReq(30, false, false) 47 | gid := dtmimp.GetFuncName() 48 | timeoutChan := make(chan int, 1) 49 | 50 | err := dtmcli.TccGlobalTransaction(dtmutil.DefaultHTTPServer, gid, func(tcc *dtmcli.Tcc) (*resty.Response, error) { 51 | _, err := tcc.CallBranch(req, Busi+"/TransOutOld", Busi+"/TransOutConfirmOld", Busi+"/TransOutRevertOld") 52 | assert.Nil(t, err) 53 | go func() { 54 | cronTransOnceForwardNow(t, gid, 300) 55 | timeoutChan <- 0 56 | }() 57 | <-timeoutChan 58 | _, err = tcc.CallBranch(req, Busi+"/TransInOld", Busi+"/TransInConfirmOld", Busi+"/TransInRevertOld") 59 | assert.Error(t, err) 60 | return nil, err 61 | }) 62 | assert.Error(t, err) 63 | assert.Equal(t, StatusFailed, getTransStatus(gid)) 64 | assert.Equal(t, []string{StatusSucceed, StatusPrepared}, getBranchesStatus(gid)) 65 | } 66 | -------------------------------------------------------------------------------- /dtmcli/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/dtm/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 | -------------------------------------------------------------------------------- /dtmsvr/trans_process.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 dtmsvr 8 | 9 | import ( 10 | "fmt" 11 | "time" 12 | 13 | "github.com/dtm-labs/dtm/dtmcli" 14 | "github.com/dtm-labs/dtm/dtmcli/dtmimp" 15 | "github.com/dtm-labs/dtm/dtmcli/logger" 16 | "github.com/dtm-labs/dtm/dtmutil" 17 | ) 18 | 19 | // Process process global transaction once 20 | func (t *TransGlobal) Process(branches []TransBranch) error { 21 | r := t.process(branches) 22 | transactionMetrics(t, r == nil) 23 | return r 24 | } 25 | 26 | func (t *TransGlobal) process(branches []TransBranch) error { 27 | if t.Options != "" { 28 | dtmimp.MustUnmarshalString(t.Options, &t.TransOptions) 29 | } 30 | if t.ExtData != "" { 31 | dtmimp.MustUnmarshalString(t.ExtData, &t.Ext) 32 | } 33 | 34 | if !t.WaitResult { 35 | go func() { 36 | err := t.processInner(branches) 37 | if err != nil { 38 | logger.Errorf("processInner err: %v", err) 39 | } 40 | }() 41 | return nil 42 | } 43 | submitting := t.Status == dtmcli.StatusSubmitted 44 | err := t.processInner(branches) 45 | if err != nil { 46 | return err 47 | } 48 | if submitting && t.Status != dtmcli.StatusSucceed { 49 | return fmt.Errorf("wait result not return success: %w", dtmcli.ErrFailure) 50 | } 51 | return nil 52 | } 53 | 54 | func (t *TransGlobal) processInner(branches []TransBranch) (rerr error) { 55 | defer handlePanic(&rerr) 56 | defer func() { 57 | if rerr != nil && rerr != dtmcli.ErrOngoing { 58 | logger.Errorf("processInner got error: %s", rerr.Error()) 59 | } 60 | if TransProcessedTestChan != nil { 61 | logger.Debugf("processed: %s", t.Gid) 62 | TransProcessedTestChan <- t.Gid 63 | logger.Debugf("notified: %s", t.Gid) 64 | } 65 | }() 66 | logger.Debugf("processing: %s status: %s", t.Gid, t.Status) 67 | t.lastTouched = time.Now() 68 | rerr = t.getProcessor().ProcessOnce(branches) 69 | return 70 | } 71 | 72 | func (t *TransGlobal) saveNew() ([]TransBranch, error) { 73 | t.NextCronInterval = t.getNextCronInterval(cronReset) 74 | t.NextCronTime = dtmutil.GetNextTime(t.NextCronInterval) 75 | t.ExtData = dtmimp.MustMarshalString(t.Ext) 76 | if t.ExtData == "{}" { 77 | t.ExtData = "" 78 | } 79 | t.Options = dtmimp.MustMarshalString(t.TransOptions) 80 | if t.Options == "{}" { 81 | t.Options = "" 82 | } 83 | now := time.Now() 84 | t.CreateTime = &now 85 | t.UpdateTime = &now 86 | branches := t.getProcessor().GenBranches() 87 | for i := range branches { 88 | branches[i].CreateTime = &now 89 | branches[i].UpdateTime = &now 90 | } 91 | err := GetStore().MaySaveNewTrans(&t.TransGlobalStore, branches) 92 | logger.Infof("MaySaveNewTrans result: %v, global: %v branches: %v", 93 | err, t.TransGlobalStore.String(), dtmimp.MustMarshalString(branches)) 94 | return branches, err 95 | } 96 | -------------------------------------------------------------------------------- /dtmsvr/storage/trans.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 storage 8 | 9 | import ( 10 | "time" 11 | 12 | "github.com/dtm-labs/dtm/dtmcli" 13 | "github.com/dtm-labs/dtm/dtmcli/dtmimp" 14 | "github.com/dtm-labs/dtm/dtmsvr/config" 15 | "github.com/dtm-labs/dtm/dtmutil" 16 | ) 17 | 18 | // TransGlobalExt defines Header info 19 | type TransGlobalExt struct { 20 | Headers map[string]string `json:"headers,omitempty" gorm:"-"` 21 | } 22 | 23 | // TransGlobalStore defines GlobalStore storage info 24 | type TransGlobalStore struct { 25 | dtmutil.ModelBase 26 | Gid string `json:"gid,omitempty"` 27 | TransType string `json:"trans_type,omitempty"` 28 | Steps []map[string]string `json:"steps,omitempty" gorm:"-"` 29 | Payloads []string `json:"payloads,omitempty" gorm:"-"` 30 | BinPayloads [][]byte `json:"-" gorm:"-"` 31 | Status string `json:"status,omitempty"` 32 | QueryPrepared string `json:"query_prepared,omitempty"` 33 | Protocol string `json:"protocol,omitempty"` 34 | FinishTime *time.Time `json:"finish_time,omitempty"` 35 | RollbackTime *time.Time `json:"rollback_time,omitempty"` 36 | Options string `json:"options,omitempty"` 37 | CustomData string `json:"custom_data,omitempty"` 38 | NextCronInterval int64 `json:"next_cron_interval,omitempty"` 39 | NextCronTime *time.Time `json:"next_cron_time,omitempty"` 40 | Owner string `json:"owner,omitempty"` 41 | Ext TransGlobalExt `json:"-" gorm:"-"` 42 | ExtData string `json:"ext_data,omitempty"` // storage of ext. a db field to store many values. like Options 43 | dtmcli.TransOptions 44 | } 45 | 46 | // TableName TableName 47 | func (g *TransGlobalStore) TableName() string { 48 | return config.Config.Store.TransGlobalTable 49 | } 50 | 51 | func (g *TransGlobalStore) String() string { 52 | return dtmimp.MustMarshalString(g) 53 | } 54 | 55 | // TransBranchStore branch transaction 56 | type TransBranchStore struct { 57 | dtmutil.ModelBase 58 | Gid string `json:"gid,omitempty"` 59 | URL string `json:"url,omitempty"` 60 | BinData []byte 61 | BranchID string `json:"branch_id,omitempty"` 62 | Op string `json:"op,omitempty"` 63 | Status string `json:"status,omitempty"` 64 | FinishTime *time.Time `json:"finish_time,omitempty"` 65 | RollbackTime *time.Time `json:"rollback_time,omitempty"` 66 | } 67 | 68 | // TableName TableName 69 | func (b *TransBranchStore) TableName() string { 70 | return config.Config.Store.TransBranchOpTable 71 | } 72 | 73 | func (b *TransBranchStore) String() string { 74 | return dtmimp.MustMarshalString(*b) 75 | } 76 | -------------------------------------------------------------------------------- /dtmgrpc/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 dtmgrpc 8 | 9 | import ( 10 | "database/sql" 11 | "errors" 12 | 13 | "github.com/dtm-labs/dtm/dtmcli" 14 | "github.com/dtm-labs/dtm/dtmcli/dtmimp" 15 | "github.com/dtm-labs/dtm/dtmgrpc/dtmgimp" 16 | "google.golang.org/protobuf/proto" 17 | ) 18 | 19 | // MsgGrpc reliable msg type 20 | type MsgGrpc struct { 21 | dtmcli.Msg 22 | } 23 | 24 | // NewMsgGrpc create new msg 25 | func NewMsgGrpc(server string, gid string) *MsgGrpc { 26 | return &MsgGrpc{Msg: *dtmcli.NewMsg(server, gid)} 27 | } 28 | 29 | // Add add a new step 30 | func (s *MsgGrpc) Add(action string, msg proto.Message) *MsgGrpc { 31 | s.Steps = append(s.Steps, map[string]string{"action": action}) 32 | s.BinPayloads = append(s.BinPayloads, dtmgimp.MustProtoMarshal(msg)) 33 | return s 34 | } 35 | 36 | // SetDelay delay call branch, unit second 37 | func (s *MsgGrpc) SetDelay(delay uint64) *MsgGrpc { 38 | s.Msg.SetDelay(delay) 39 | return s 40 | } 41 | 42 | // Prepare prepare the msg, msg will later be submitted 43 | func (s *MsgGrpc) Prepare(queryPrepared string) error { 44 | s.QueryPrepared = dtmimp.OrString(queryPrepared, s.QueryPrepared) 45 | return dtmgimp.DtmGrpcCall(&s.TransBase, "Prepare") 46 | } 47 | 48 | // Submit submit the msg 49 | func (s *MsgGrpc) Submit() error { 50 | s.Msg.BuildCustomOptions() 51 | return dtmgimp.DtmGrpcCall(&s.TransBase, "Submit") 52 | } 53 | 54 | // DoAndSubmitDB short method for Do on db type. please see DoAndSubmit 55 | func (s *MsgGrpc) DoAndSubmitDB(queryPrepared string, db *sql.DB, busiCall dtmcli.BarrierBusiFunc) error { 56 | return s.DoAndSubmit(queryPrepared, func(bb *dtmcli.BranchBarrier) error { 57 | return bb.CallWithDB(db, busiCall) 58 | }) 59 | } 60 | 61 | // DoAndSubmit one method for the entire prepare->busi->submit 62 | // the error returned by busiCall will be returned 63 | // if busiCall return ErrFailure, then abort is called directly 64 | // if busiCall return not nil error other than ErrFailure, then DoAndSubmit will call queryPrepared to get the result 65 | func (s *MsgGrpc) DoAndSubmit(queryPrepared string, busiCall func(bb *dtmcli.BranchBarrier) error) error { 66 | bb, err := dtmcli.BarrierFrom(s.TransType, s.Gid, dtmimp.MsgDoBranch0, dtmimp.MsgDoOp) // a special barrier for msg QueryPrepared 67 | if err == nil { 68 | err = s.Prepare(queryPrepared) 69 | } 70 | if err == nil { 71 | errb := busiCall(bb) 72 | if errb != nil && !errors.Is(errb, dtmcli.ErrFailure) { 73 | err = dtmgimp.InvokeBranch(&s.TransBase, true, nil, queryPrepared, &[]byte{}, bb.BranchID, bb.Op) 74 | err = GrpcError2DtmError(err) 75 | } 76 | if errors.Is(errb, dtmcli.ErrFailure) || errors.Is(err, dtmcli.ErrFailure) { 77 | _ = dtmgimp.DtmGrpcCall(&s.TransBase, "Abort") 78 | } else if err == nil { 79 | err = s.Submit() 80 | } 81 | if errb != nil { 82 | return errb 83 | } 84 | } 85 | return err 86 | } 87 | -------------------------------------------------------------------------------- /charts/README.md: -------------------------------------------------------------------------------- 1 | # DTM charts 2 | 3 | ## Usage 4 | 5 | Install the dtm chart: 6 | 7 | ```bash 8 | helm install --create-namespace -n dtm-system dtm ./charts 9 | ``` 10 | 11 | Upgrade the dtm chart: 12 | 13 | ```bash 14 | helm upgrade -n dtm-system dtm ./charts 15 | ``` 16 | 17 | Uninstall the chart: 18 | 19 | ```bash 20 | helm delete -n dtm-system dtm 21 | ``` 22 | 23 | ## Parameters 24 | 25 | ### Configuration parameters 26 | 27 | | Key | Description | Value | 28 | |-----------------|---------------------------------------------------------------------------------------------------------------------------------------|-------| 29 | | `configuration` | DTM configuration. Specify content for `config.yaml`, ref: [sample config](https://github.com/dtm-labs/dtm/blob/main/conf.sample.yml) | `""` | 30 | 31 | 32 | 33 | ### Autoscaling Parameters 34 | 35 | | Name | Description | Value | 36 | |-------------------------------------------------|-------------------------------------------|---------| 37 | | `autoscaling.enabled` | Enable Horizontal POD autoscaling for DTM | `false` | 38 | | `autoscaling.minReplicas` | Minimum number of DTM replicas | `1` | 39 | | `autoscaling.maxReplicas` | Maximum number of DTM replicas | `10` | 40 | | `autoscaling.targetCPUUtilizationPercentage` | Target CPU utilization percentage | `80` | 41 | | `autoscaling.targetMemoryUtilizationPercentage` | Target Memory utilization percentage | `80` | 42 | 43 | ### Ingress parameters 44 | 45 | | Key | Description | Value | 46 | |--------------------------------|----------------------------------------------------------------------------------|---------------------| 47 | | `ingress.enabled` | Enable ingress record generation for DTM | `false` | 48 | | `ingress.className` | IngressClass that will be be used to implement the Ingress (Kubernetes 1.18+) | `"nginx"` | 49 | | `ingress.annotations` | To enable certificate auto generation, place here your cert-manager annotations. | `{}` | 50 | | `ingress.hosts.host` | Default host for the ingress record. | `"your-domain.com"` | 51 | | `ingress.hosts.paths.path` | Default path for the ingress record | `"/"` | 52 | | `ingress.hosts.paths.pathType` | Ingress path type | `"Prefix"` | 53 | | `ingress.tls` | Enable TLS configuration for the host defined at ingress.hostname parameter | `[]` | 54 | -------------------------------------------------------------------------------- /test/msg_grpc_barrier_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "database/sql" 5 | "errors" 6 | "reflect" 7 | "testing" 8 | 9 | "bou.ke/monkey" 10 | "github.com/dtm-labs/dtm/dtmcli/dtmimp" 11 | "github.com/dtm-labs/dtm/dtmcli/logger" 12 | "github.com/dtm-labs/dtm/dtmgrpc" 13 | "github.com/dtm-labs/dtm/test/busi" 14 | "github.com/stretchr/testify/assert" 15 | ) 16 | 17 | func TestMsgGrpcPrepareAndSubmit(t *testing.T) { 18 | before := getBeforeBalances("mysql") 19 | gid := dtmimp.GetFuncName() 20 | req := busi.GenBusiReq(30, false, false) 21 | msg := dtmgrpc.NewMsgGrpc(DtmGrpcServer, gid). 22 | Add(busi.BusiGrpc+"/busi.Busi/TransInBSaga", req) 23 | err := msg.DoAndSubmitDB(busi.BusiGrpc+"/busi.Busi/QueryPreparedB", dbGet().ToSQLDB(), func(tx *sql.Tx) error { 24 | return busi.SagaAdjustBalance(tx, busi.TransOutUID, -int(req.Amount), "SUCCESS") 25 | }) 26 | assert.Nil(t, err) 27 | waitTransProcessed(msg.Gid) 28 | assert.Equal(t, []string{StatusSucceed}, getBranchesStatus(msg.Gid)) 29 | assert.Equal(t, StatusSucceed, getTransStatus(msg.Gid)) 30 | assertNotSameBalance(t, before, "mysql") 31 | } 32 | 33 | func TestMsgGrpcPrepareAndSubmitCommitAfterFailed(t *testing.T) { 34 | if conf.Store.IsDB() { // cannot patch tx.Commit, because Prepare also do Commit 35 | return 36 | } 37 | before := getBeforeBalances("mysql") 38 | gid := dtmimp.GetFuncName() 39 | req := busi.GenBusiReq(30, false, false) 40 | msg := dtmgrpc.NewMsgGrpc(DtmGrpcServer, gid). 41 | Add(busi.BusiGrpc+"/busi.Busi/TransInBSaga", req) 42 | var guard *monkey.PatchGuard 43 | err := msg.DoAndSubmitDB(busi.BusiGrpc+"/busi.Busi/QueryPreparedB", dbGet().ToSQLDB(), func(tx *sql.Tx) error { 44 | err := busi.SagaAdjustBalance(tx, busi.TransOutUID, -int(req.Amount), "SUCCESS") 45 | guard = monkey.PatchInstanceMethod(reflect.TypeOf(tx), "Commit", func(tx *sql.Tx) error { 46 | guard.Unpatch() 47 | _ = tx.Commit() 48 | return errors.New("test error for patch") 49 | }) 50 | return err 51 | }) 52 | assert.Error(t, err) 53 | waitTransProcessed(gid) 54 | assertNotSameBalance(t, before, "mysql") 55 | } 56 | 57 | func TestMsgGrpcPrepareAndSubmitCommitFailed(t *testing.T) { 58 | if conf.Store.IsDB() { // cannot patch tx.Commit, because Prepare also do Commit 59 | return 60 | } 61 | before := getBeforeBalances("mysql") 62 | gid := dtmimp.GetFuncName() 63 | req := busi.GenBusiReq(30, false, false) 64 | msg := dtmgrpc.NewMsgGrpc(DtmGrpcServer, gid). 65 | Add(busi.Busi+"/SagaBTransIn", req) 66 | var g *monkey.PatchGuard 67 | err := msg.DoAndSubmitDB(busi.BusiGrpc+"/busi.Busi/QueryPreparedB", dbGet().ToSQLDB(), func(tx *sql.Tx) error { 68 | g = monkey.PatchInstanceMethod(reflect.TypeOf(tx), "Commit", func(tx *sql.Tx) error { 69 | logger.Debugf("tx.Commit rollback and return error in test") 70 | _ = tx.Rollback() 71 | return errors.New("test error for patch") 72 | }) 73 | return busi.SagaAdjustBalance(tx, busi.TransOutUID, -int(req.Amount), "SUCCESS") 74 | }) 75 | g.Unpatch() 76 | assert.Error(t, err) 77 | assertSameBalance(t, before, "mysql") 78 | } 79 | -------------------------------------------------------------------------------- /dtmsvr/trans_type_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 dtmsvr 8 | 9 | import ( 10 | "errors" 11 | "fmt" 12 | 13 | "github.com/dtm-labs/dtm/dtmcli" 14 | "github.com/dtm-labs/dtm/dtmcli/dtmimp" 15 | "github.com/dtm-labs/dtm/dtmcli/logger" 16 | ) 17 | 18 | type transMsgProcessor struct { 19 | *TransGlobal 20 | } 21 | 22 | func init() { 23 | registorProcessorCreator("msg", func(trans *TransGlobal) transProcessor { return &transMsgProcessor{TransGlobal: trans} }) 24 | } 25 | 26 | func (t *transMsgProcessor) GenBranches() []TransBranch { 27 | branches := []TransBranch{} 28 | for i, step := range t.Steps { 29 | b := &TransBranch{ 30 | Gid: t.Gid, 31 | BranchID: fmt.Sprintf("%02d", i+1), 32 | BinData: t.BinPayloads[i], 33 | URL: step[dtmimp.OpAction], 34 | Op: dtmimp.OpAction, 35 | Status: dtmcli.StatusPrepared, 36 | } 37 | branches = append(branches, *b) 38 | } 39 | return branches 40 | } 41 | 42 | type cMsgCustom struct { 43 | Delay uint64 //delay call branch, unit second 44 | } 45 | 46 | func (t *TransGlobal) mayQueryPrepared() { 47 | if !t.needProcess() || t.Status == dtmcli.StatusSubmitted { 48 | return 49 | } 50 | err := t.getURLResult(t.QueryPrepared, "00", "msg", nil) 51 | if err == nil { 52 | t.changeStatus(dtmcli.StatusSubmitted) 53 | } else if errors.Is(err, dtmcli.ErrFailure) { 54 | t.changeStatus(dtmcli.StatusFailed) 55 | } else if errors.Is(err, dtmcli.ErrOngoing) { 56 | t.touchCronTime(cronReset, 0) 57 | } else { 58 | logger.Errorf("getting result failed for %s. error: %v", t.QueryPrepared, err) 59 | t.touchCronTime(cronBackoff, 0) 60 | } 61 | } 62 | 63 | func (t *transMsgProcessor) ProcessOnce(branches []TransBranch) error { 64 | t.mayQueryPrepared() 65 | if !t.needProcess() || t.Status == dtmcli.StatusPrepared { 66 | return nil 67 | } 68 | cmc := cMsgCustom{Delay: 0} 69 | if t.CustomData != "" { 70 | dtmimp.MustUnmarshalString(t.CustomData, &cmc) 71 | } 72 | 73 | if cmc.Delay > 0 && t.needDelay(cmc.Delay) { 74 | t.touchCronTime(cronKeep, cmc.Delay) 75 | return nil 76 | } 77 | var started int 78 | resultsChan := make(chan error, len(branches)) 79 | var err error 80 | for i := range branches { 81 | b := &branches[i] 82 | if b.Op != dtmimp.OpAction || b.Status != dtmcli.StatusPrepared { 83 | continue 84 | } 85 | if t.Concurrent { 86 | started++ 87 | go func(pos int) { 88 | resultsChan <- t.execBranch(b, pos) 89 | }(i) 90 | } else { 91 | err = t.execBranch(b, i) 92 | if err != nil { 93 | break 94 | } 95 | } 96 | } 97 | for i := 0; i < started && err == nil; i++ { 98 | err = <-resultsChan 99 | } 100 | if err == dtmcli.ErrOngoing { 101 | return nil 102 | } else if err != nil { 103 | return err 104 | } 105 | t.changeStatus(dtmcli.StatusSucceed) 106 | return nil 107 | } 108 | -------------------------------------------------------------------------------- /test/saga_concurrent_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 test 8 | 9 | import ( 10 | "testing" 11 | 12 | "github.com/dtm-labs/dtm/dtmcli" 13 | "github.com/dtm-labs/dtm/dtmcli/dtmimp" 14 | "github.com/dtm-labs/dtm/test/busi" 15 | "github.com/stretchr/testify/assert" 16 | ) 17 | 18 | func genSagaCon(gid string, outFailed bool, inFailed bool) *dtmcli.Saga { 19 | return genSaga(gid, outFailed, inFailed).SetConcurrent() 20 | } 21 | 22 | func TestSagaConNormal(t *testing.T) { 23 | sagaCon := genSagaCon(dtmimp.GetFuncName(), false, false) 24 | sagaCon.Submit() 25 | waitTransProcessed(sagaCon.Gid) 26 | assert.Equal(t, []string{StatusPrepared, StatusSucceed, StatusPrepared, StatusSucceed}, getBranchesStatus(sagaCon.Gid)) 27 | assert.Equal(t, StatusSucceed, getTransStatus(sagaCon.Gid)) 28 | } 29 | 30 | func TestSagaConRollbackNormal(t *testing.T) { 31 | gid := dtmimp.GetFuncName() 32 | sagaCon := genSagaCon(gid, true, false) 33 | busi.MainSwitch.TransOutRevertResult.SetOnce(dtmcli.ResultOngoing) 34 | err := sagaCon.Submit() 35 | assert.Nil(t, err) 36 | waitTransProcessed(sagaCon.Gid) 37 | assert.Equal(t, StatusAborting, getTransStatus(sagaCon.Gid)) 38 | cronTransOnce(t, gid) 39 | assert.Equal(t, StatusFailed, getTransStatus(sagaCon.Gid)) 40 | // TODO should fix this 41 | // assert.Equal(t, []string{StatusSucceed, StatusFailed, StatusSucceed, StatusSucceed}, getBranchesStatus(sagaCon.Gid)) 42 | } 43 | 44 | func TestSagaConRollbackOrder(t *testing.T) { 45 | sagaCon := genSagaCon(dtmimp.GetFuncName(), true, false) 46 | sagaCon.AddBranchOrder(1, []int{0}) 47 | err := sagaCon.Submit() 48 | assert.Nil(t, err) 49 | waitTransProcessed(sagaCon.Gid) 50 | assert.Equal(t, StatusFailed, getTransStatus(sagaCon.Gid)) 51 | assert.Equal(t, []string{StatusSucceed, StatusFailed, StatusPrepared, StatusPrepared}, getBranchesStatus(sagaCon.Gid)) 52 | } 53 | 54 | func TestSagaConRollbackOrder2(t *testing.T) { 55 | sagaCon := genSagaCon(dtmimp.GetFuncName(), false, true) 56 | sagaCon.AddBranchOrder(1, []int{0}) 57 | err := sagaCon.Submit() 58 | assert.Nil(t, err) 59 | waitTransProcessed(sagaCon.Gid) 60 | assert.Equal(t, StatusFailed, getTransStatus(sagaCon.Gid)) 61 | assert.Equal(t, []string{StatusSucceed, StatusSucceed, StatusSucceed, StatusFailed}, getBranchesStatus(sagaCon.Gid)) 62 | } 63 | func TestSagaConCommittedOngoing(t *testing.T) { 64 | gid := dtmimp.GetFuncName() 65 | sagaCon := genSagaCon(gid, false, false) 66 | busi.MainSwitch.TransOutResult.SetOnce(dtmcli.ResultOngoing) 67 | sagaCon.Submit() 68 | waitTransProcessed(sagaCon.Gid) 69 | assert.Equal(t, []string{StatusPrepared, StatusPrepared, StatusPrepared, StatusSucceed}, getBranchesStatus(sagaCon.Gid)) 70 | assert.Equal(t, StatusSubmitted, getTransStatus(sagaCon.Gid)) 71 | 72 | cronTransOnce(t, gid) 73 | assert.Equal(t, []string{StatusPrepared, StatusSucceed, StatusPrepared, StatusSucceed}, getBranchesStatus(sagaCon.Gid)) 74 | assert.Equal(t, StatusSucceed, getTransStatus(sagaCon.Gid)) 75 | } 76 | -------------------------------------------------------------------------------- /dtmcli/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/dtm/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 | -------------------------------------------------------------------------------- /test/msg_grpc_barrier_redis_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "github.com/dtm-labs/dtm/dtmcli" 8 | "github.com/dtm-labs/dtm/dtmcli/dtmimp" 9 | "github.com/dtm-labs/dtm/dtmgrpc" 10 | "github.com/dtm-labs/dtm/test/busi" 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | func TestMsgGrpcRedisDo(t *testing.T) { 15 | before := getBeforeBalances("redis") 16 | gid := dtmimp.GetFuncName() 17 | req := busi.GenBusiReq(30, false, false) 18 | msg := dtmgrpc.NewMsgGrpc(DtmGrpcServer, gid). 19 | Add(busi.BusiGrpc+"/busi.Busi/TransInRedis", req) 20 | err := msg.DoAndSubmit(busi.BusiGrpc+"/busi.Busi/QueryPreparedRedis", func(bb *dtmcli.BranchBarrier) error { 21 | return bb.RedisCheckAdjustAmount(busi.RedisGet(), busi.GetRedisAccountKey(busi.TransOutUID), -30, 86400) 22 | }) 23 | assert.Nil(t, err) 24 | waitTransProcessed(msg.Gid) 25 | assert.Equal(t, []string{StatusSucceed}, getBranchesStatus(msg.Gid)) 26 | assert.Equal(t, StatusSucceed, getTransStatus(msg.Gid)) 27 | assertNotSameBalance(t, before, "redis") 28 | } 29 | 30 | func TestMsgGrpcRedisDoBusiFailed(t *testing.T) { 31 | before := getBeforeBalances("redis") 32 | gid := dtmimp.GetFuncName() 33 | req := busi.GenBusiReq(30, false, false) 34 | msg := dtmgrpc.NewMsgGrpc(DtmGrpcServer, gid). 35 | Add(busi.BusiGrpc+"/busi.Busi/TransInRedis", req) 36 | err := msg.DoAndSubmit(busi.BusiGrpc+"/busi.Busi/QueryPreparedRedis", func(bb *dtmcli.BranchBarrier) error { 37 | return errors.New("an error") 38 | }) 39 | assert.Error(t, err) 40 | assertSameBalance(t, before, "redis") 41 | } 42 | 43 | func TestMsgGrpcRedisDoPrepareFailed(t *testing.T) { 44 | before := getBeforeBalances("redis") 45 | gid := dtmimp.GetFuncName() 46 | req := busi.GenBusiReq(30, false, false) 47 | msg := dtmgrpc.NewMsgGrpc(DtmGrpcServer+"not-exists", gid). 48 | Add(busi.BusiGrpc+"/busi.Busi/TransInRedis", req) 49 | err := msg.DoAndSubmit(busi.BusiGrpc+"/busi.Busi/QueryPreparedRedis", func(bb *dtmcli.BranchBarrier) error { 50 | return bb.RedisCheckAdjustAmount(busi.RedisGet(), busi.GetRedisAccountKey(busi.TransOutUID), -30, 86400) 51 | }) 52 | assert.Error(t, err) 53 | assertSameBalance(t, before, "redis") 54 | } 55 | 56 | func TestMsgGrpcRedisDoCommitFailed(t *testing.T) { 57 | before := getBeforeBalances("redis") 58 | gid := dtmimp.GetFuncName() 59 | req := busi.GenBusiReq(30, false, false) 60 | msg := dtmgrpc.NewMsgGrpc(DtmGrpcServer, gid). 61 | Add(busi.BusiGrpc+"/busi.Busi/TransInRedis", req) 62 | err := msg.DoAndSubmit(busi.BusiGrpc+"/busi.Busi/QueryPreparedRedis", func(bb *dtmcli.BranchBarrier) error { 63 | return errors.New("after commit error") 64 | }) 65 | assert.Error(t, err) 66 | assertSameBalance(t, before, "redis") 67 | } 68 | 69 | func TestMsgGrpcRedisDoCommitAfterFailed(t *testing.T) { 70 | before := getBeforeBalances("redis") 71 | gid := dtmimp.GetFuncName() 72 | req := busi.GenBusiReq(30, false, false) 73 | msg := dtmgrpc.NewMsgGrpc(DtmGrpcServer, gid). 74 | Add(busi.BusiGrpc+"/busi.Busi/TransInRedis", req) 75 | err := msg.DoAndSubmit(busi.BusiGrpc+"/busi.Busi/QueryPreparedRedis", func(bb *dtmcli.BranchBarrier) error { 76 | err := bb.RedisCheckAdjustAmount(busi.RedisGet(), busi.GetRedisAccountKey(busi.TransOutUID), -30, 86400) 77 | dtmimp.E2P(err) 78 | return errors.New("an error") 79 | }) 80 | assert.Error(t, err) 81 | waitTransProcessed(gid) 82 | assertNotSameBalance(t, before, "redis") 83 | } 84 | -------------------------------------------------------------------------------- /dtmsvr/api.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 dtmsvr 8 | 9 | import ( 10 | "fmt" 11 | 12 | "github.com/dtm-labs/dtm/dtmcli" 13 | "github.com/dtm-labs/dtm/dtmcli/dtmimp" 14 | "github.com/dtm-labs/dtm/dtmcli/logger" 15 | "github.com/dtm-labs/dtm/dtmsvr/storage" 16 | ) 17 | 18 | func svcSubmit(t *TransGlobal) interface{} { 19 | t.Status = dtmcli.StatusSubmitted 20 | branches, err := t.saveNew() 21 | 22 | if err == storage.ErrUniqueConflict { 23 | dbt := GetTransGlobal(t.Gid) 24 | if dbt.Status == dtmcli.StatusPrepared { 25 | dbt.changeStatus(t.Status) 26 | branches = GetStore().FindBranches(t.Gid) 27 | } else if dbt.Status != dtmcli.StatusSubmitted { 28 | return fmt.Errorf("current status '%s', cannot sumbmit. %w", dbt.Status, dtmcli.ErrFailure) 29 | } 30 | } 31 | return t.Process(branches) 32 | } 33 | 34 | func svcPrepare(t *TransGlobal) interface{} { 35 | t.Status = dtmcli.StatusPrepared 36 | _, err := t.saveNew() 37 | if err == storage.ErrUniqueConflict { 38 | dbt := GetTransGlobal(t.Gid) 39 | if dbt.Status != dtmcli.StatusPrepared { 40 | return fmt.Errorf("current status '%s', cannot prepare. %w", dbt.Status, dtmcli.ErrFailure) 41 | } 42 | return nil 43 | } 44 | return err 45 | } 46 | 47 | func svcAbort(t *TransGlobal) interface{} { 48 | dbt := GetTransGlobal(t.Gid) 49 | if dbt.TransType == "msg" && dbt.Status == dtmcli.StatusPrepared { 50 | dbt.changeStatus(dtmcli.StatusFailed) 51 | return nil 52 | } 53 | if t.TransType != "xa" && t.TransType != "tcc" || dbt.Status != dtmcli.StatusPrepared && dbt.Status != dtmcli.StatusAborting { 54 | return fmt.Errorf("trans type: '%s' current status '%s', cannot abort. %w", dbt.TransType, dbt.Status, dtmcli.ErrFailure) 55 | } 56 | dbt.changeStatus(dtmcli.StatusAborting) 57 | branches := GetStore().FindBranches(t.Gid) 58 | return dbt.Process(branches) 59 | } 60 | 61 | func svcForceStop(t *TransGlobal) interface{} { 62 | dbt := GetTransGlobal(t.Gid) 63 | if dbt.Status == dtmcli.StatusSucceed || dbt.Status == dtmcli.StatusFailed { 64 | return fmt.Errorf("global transaction force stop error. status: %s. error: %w", dbt.Status, dtmcli.ErrFailure) 65 | } 66 | dbt.changeStatus(dtmcli.StatusFailed) 67 | return nil 68 | } 69 | 70 | func svcRegisterBranch(transType string, branch *TransBranch, data map[string]string) error { 71 | branches := []TransBranch{*branch, *branch} 72 | if transType == "tcc" { 73 | for i, b := range []string{dtmimp.OpCancel, dtmimp.OpConfirm} { 74 | branches[i].Op = b 75 | branches[i].URL = data[b] 76 | } 77 | } else if transType == "xa" { 78 | branches[0].Op = dtmimp.OpRollback 79 | branches[0].URL = data["url"] 80 | branches[1].Op = dtmimp.OpCommit 81 | branches[1].URL = data["url"] 82 | } else { 83 | return fmt.Errorf("unknow trans type: %s", transType) 84 | } 85 | 86 | err := dtmimp.CatchP(func() { 87 | GetStore().LockGlobalSaveBranches(branch.Gid, dtmcli.StatusPrepared, branches, -1) 88 | }) 89 | if err == storage.ErrNotFound { 90 | msg := fmt.Sprintf("no trans with gid: %s status: %s found", branch.Gid, dtmcli.StatusPrepared) 91 | logger.Errorf(msg) 92 | return fmt.Errorf("message: %s %w", msg, dtmcli.ErrFailure) 93 | } 94 | logger.Infof("LockGlobalSaveBranches result: %v: gid: %s old status: %s branches: %s", 95 | err, branch.Gid, dtmcli.StatusPrepared, dtmimp.MustMarshalString(branches)) 96 | return err 97 | } 98 | -------------------------------------------------------------------------------- /dtmgrpc/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 dtmgrpc 8 | 9 | import ( 10 | "context" 11 | "database/sql" 12 | "fmt" 13 | 14 | "github.com/dtm-labs/dtm/dtmcli" 15 | "github.com/dtm-labs/dtm/dtmcli/dtmimp" 16 | "github.com/dtm-labs/dtm/dtmgrpc/dtmgimp" 17 | "github.com/dtm-labs/dtm/dtmgrpc/dtmgpb" 18 | grpc "google.golang.org/grpc" 19 | "google.golang.org/protobuf/proto" 20 | emptypb "google.golang.org/protobuf/types/known/emptypb" 21 | ) 22 | 23 | // XaGrpcGlobalFunc type of xa global function 24 | type XaGrpcGlobalFunc func(xa *XaGrpc) error 25 | 26 | // XaGrpcLocalFunc type of xa local function 27 | type XaGrpcLocalFunc func(db *sql.DB, xa *XaGrpc) error 28 | 29 | // XaGrpc xa transaction 30 | type XaGrpc struct { 31 | dtmimp.TransBase 32 | Phase2URL string 33 | } 34 | 35 | // XaGrpcFromRequest construct xa info from request 36 | func XaGrpcFromRequest(ctx context.Context) (*XaGrpc, error) { 37 | xa := &XaGrpc{ 38 | TransBase: *dtmgimp.TransBaseFromGrpc(ctx), 39 | } 40 | xa.Phase2URL = dtmgimp.GetDtmMetaFromContext(ctx, "phase2_url") 41 | if xa.Gid == "" || xa.BranchID == "" || xa.Op == "" { 42 | return nil, fmt.Errorf("bad xa info: gid: %s branchid: %s op: %s phase2_url: %s", xa.Gid, xa.BranchID, xa.Op, xa.Phase2URL) 43 | } 44 | return xa, nil 45 | } 46 | 47 | // XaLocalTransaction start a xa local transaction 48 | func XaLocalTransaction(ctx context.Context, dbConf dtmcli.DBConf, xaFunc XaGrpcLocalFunc) error { 49 | xa, err := XaGrpcFromRequest(ctx) 50 | if err != nil { 51 | return err 52 | } 53 | if xa.Op == dtmimp.OpCommit || xa.Op == dtmimp.OpRollback { 54 | return dtmimp.XaHandlePhase2(xa.Gid, dbConf, xa.BranchID, xa.Op) 55 | } 56 | return dtmimp.XaHandleLocalTrans(&xa.TransBase, dbConf, func(db *sql.DB) error { 57 | err := xaFunc(db, xa) 58 | if err != nil { 59 | return err 60 | } 61 | _, err = dtmgimp.MustGetDtmClient(xa.Dtm).RegisterBranch(context.Background(), &dtmgpb.DtmBranchRequest{ 62 | Gid: xa.Gid, 63 | BranchID: xa.BranchID, 64 | TransType: xa.TransType, 65 | BusiPayload: nil, 66 | Data: map[string]string{"url": xa.Phase2URL}, 67 | }) 68 | return err 69 | }) 70 | } 71 | 72 | // XaGlobalTransaction start a xa global transaction 73 | func XaGlobalTransaction(server string, gid string, xaFunc XaGrpcGlobalFunc) error { 74 | return XaGlobalTransaction2(server, gid, func(xg *XaGrpc) {}, xaFunc) 75 | } 76 | 77 | // XaGlobalTransaction2 new version of XaGlobalTransaction. support custom 78 | func XaGlobalTransaction2(server string, gid string, custom func(*XaGrpc), xaFunc XaGrpcGlobalFunc) error { 79 | xa := &XaGrpc{TransBase: *dtmimp.NewTransBase(gid, "xa", server, "")} 80 | custom(xa) 81 | dc := dtmgimp.MustGetDtmClient(xa.Dtm) 82 | req := &dtmgpb.DtmRequest{ 83 | Gid: gid, 84 | TransType: xa.TransType, 85 | } 86 | return dtmimp.XaHandleGlobalTrans(&xa.TransBase, func(action string) error { 87 | f := map[string]func(context.Context, *dtmgpb.DtmRequest, ...grpc.CallOption) (*emptypb.Empty, error){ 88 | "prepare": dc.Prepare, 89 | "submit": dc.Submit, 90 | "abort": dc.Abort, 91 | }[action] 92 | _, err := f(context.Background(), req) 93 | return err 94 | }, func() error { 95 | return xaFunc(xa) 96 | }) 97 | } 98 | 99 | // CallBranch call a xa branch 100 | func (x *XaGrpc) CallBranch(msg proto.Message, url string, reply interface{}) error { 101 | return dtmgimp.InvokeBranch(&x.TransBase, false, msg, url, reply, x.NewSubBranchID(), "action") 102 | } 103 | -------------------------------------------------------------------------------- /test/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 test 8 | 9 | import ( 10 | "testing" 11 | "time" 12 | 13 | "github.com/dtm-labs/dtm/dtmcli" 14 | "github.com/dtm-labs/dtm/dtmcli/dtmimp" 15 | "github.com/dtm-labs/dtm/dtmcli/logger" 16 | "github.com/dtm-labs/dtm/dtmsvr" 17 | "github.com/dtm-labs/dtm/dtmsvr/config" 18 | "github.com/dtm-labs/dtm/dtmutil" 19 | "github.com/dtm-labs/dtm/test/busi" 20 | "github.com/stretchr/testify/assert" 21 | ) 22 | 23 | var conf = &config.Config 24 | 25 | func dbGet() *dtmutil.DB { 26 | return dtmutil.DbGet(busi.BusiConf) 27 | } 28 | 29 | // waitTransProcessed only for test usage. wait for transaction processed once 30 | func waitTransProcessed(gid string) { 31 | logger.Debugf("waiting for gid %s", gid) 32 | select { 33 | case id := <-dtmsvr.TransProcessedTestChan: 34 | logger.FatalfIf(id != gid, "------- expecting: %s but %s found", gid, id) 35 | logger.Debugf("finish for gid %s", gid) 36 | case <-time.After(time.Duration(time.Second * 4)): 37 | logger.FatalfIf(true, "Wait Trans timeout") 38 | } 39 | } 40 | 41 | func cronTransOnce(t *testing.T, gid string) { 42 | gid2 := dtmsvr.CronTransOnce() 43 | assert.Equal(t, gid, gid2) 44 | if dtmsvr.TransProcessedTestChan != nil && gid != "" { 45 | waitTransProcessed(gid) 46 | } 47 | } 48 | 49 | var e2p = dtmimp.E2P 50 | 51 | // TransGlobal alias 52 | type TransGlobal = dtmsvr.TransGlobal 53 | 54 | // TransBranch alias 55 | type TransBranch = dtmsvr.TransBranch 56 | 57 | func cronTransOnceForwardNow(t *testing.T, gid string, seconds int) { 58 | old := dtmsvr.NowForwardDuration 59 | dtmsvr.NowForwardDuration = time.Duration(seconds) * time.Second 60 | cronTransOnce(t, gid) 61 | dtmsvr.NowForwardDuration = old 62 | } 63 | 64 | func cronTransOnceForwardCron(t *testing.T, gid string, seconds int) { 65 | old := dtmsvr.CronForwardDuration 66 | dtmsvr.CronForwardDuration = time.Duration(seconds) * time.Second 67 | cronTransOnce(t, gid) 68 | dtmsvr.CronForwardDuration = old 69 | } 70 | 71 | func submitForwardCron(seconds int, fn func()) { 72 | old := dtmsvr.CronForwardDuration 73 | dtmsvr.CronForwardDuration = time.Duration(seconds) * time.Second 74 | fn() 75 | dtmsvr.CronForwardDuration = old 76 | } 77 | 78 | const ( 79 | // StatusPrepared status for global/branch trans status. 80 | StatusPrepared = dtmcli.StatusPrepared 81 | // StatusSubmitted status for global trans status. 82 | StatusSubmitted = dtmcli.StatusSubmitted 83 | // StatusSucceed status for global/branch trans status. 84 | StatusSucceed = dtmcli.StatusSucceed 85 | // StatusFailed status for global/branch trans status. 86 | StatusFailed = dtmcli.StatusFailed 87 | // StatusAborting status for global trans status. 88 | StatusAborting = dtmcli.StatusAborting 89 | ) 90 | 91 | func getBeforeBalances(store string) []int { 92 | b1 := busi.GetBalanceByUID(busi.TransOutUID, store) 93 | b2 := busi.GetBalanceByUID(busi.TransInUID, store) 94 | return []int{b1, b2} 95 | } 96 | 97 | func assertSameBalance(t *testing.T, before []int, store string) { 98 | b1 := busi.GetBalanceByUID(busi.TransOutUID, store) 99 | b2 := busi.GetBalanceByUID(busi.TransInUID, store) 100 | assert.Equal(t, before[0], b1) 101 | assert.Equal(t, before[1], b2) 102 | } 103 | 104 | func assertNotSameBalance(t *testing.T, before []int, store string) { 105 | b1 := busi.GetBalanceByUID(busi.TransOutUID, store) 106 | b2 := busi.GetBalanceByUID(busi.TransInUID, store) 107 | assert.NotEqual(t, before[0], b1) 108 | assert.Equal(t, before[0]+before[1], b1+b2) 109 | } 110 | -------------------------------------------------------------------------------- /dtmcli/logger/log.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "log" 7 | "net/url" 8 | "os" 9 | "strings" 10 | 11 | "github.com/natefinch/lumberjack" 12 | "go.uber.org/zap" 13 | "go.uber.org/zap/zapcore" 14 | ) 15 | 16 | //var logger *zap.SugaredLogger = nil 17 | 18 | var logger Logger 19 | 20 | const ( 21 | // StdErr is the default configuration for log output. 22 | StdErr = "stderr" 23 | // StdOut configuration for log output 24 | StdOut = "stdout" 25 | ) 26 | 27 | func init() { 28 | InitLog(os.Getenv("LOG_LEVEL")) 29 | } 30 | 31 | // Logger logger interface 32 | type Logger interface { 33 | Debugf(format string, args ...interface{}) 34 | Infof(format string, args ...interface{}) 35 | Warnf(format string, args ...interface{}) 36 | Errorf(format string, args ...interface{}) 37 | } 38 | 39 | // WithLogger replaces default logger 40 | func WithLogger(log Logger) { 41 | logger = log 42 | } 43 | 44 | // InitLog is an initialization for a logger 45 | // level can be: debug info warn error 46 | func InitLog(level string) { 47 | InitLog2(level, StdOut, 0, "") 48 | } 49 | 50 | // InitLog2 specify advanced log config 51 | func InitLog2(level string, outputs string, logRotationEnable int64, logRotateConfigJSON string) { 52 | outputPaths := strings.Split(outputs, ",") 53 | for i, v := range outputPaths { 54 | if logRotationEnable != 0 && v != StdErr && v != StdOut { 55 | outputPaths[i] = fmt.Sprintf("lumberjack://%s", v) 56 | } 57 | } 58 | 59 | if logRotationEnable != 0 { 60 | setupLogRotation(logRotateConfigJSON) 61 | } 62 | 63 | config := loadConfig(level) 64 | config.OutputPaths = outputPaths 65 | p, err := config.Build(zap.AddCallerSkip(1)) 66 | FatalIfError(err) 67 | logger = p.Sugar() 68 | } 69 | 70 | type lumberjackSink struct { 71 | lumberjack.Logger 72 | } 73 | 74 | func (*lumberjackSink) Sync() error { 75 | return nil 76 | } 77 | 78 | // setupLogRotation initializes log rotation for a single file path target. 79 | func setupLogRotation(logRotateConfigJSON string) { 80 | err := zap.RegisterSink("lumberjack", func(u *url.URL) (zap.Sink, error) { 81 | var conf lumberjackSink 82 | err := json.Unmarshal([]byte(logRotateConfigJSON), &conf) 83 | FatalfIf(err != nil, "bad config LogRotateConfigJSON: %v", err) 84 | conf.Filename = u.Host + u.Path 85 | return &conf, nil 86 | }) 87 | FatalIfError(err) 88 | } 89 | 90 | func loadConfig(logLevel string) zap.Config { 91 | config := zap.NewProductionConfig() 92 | err := config.Level.UnmarshalText([]byte(logLevel)) 93 | FatalIfError(err) 94 | config.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder 95 | if os.Getenv("DTM_DEBUG") != "" { 96 | config.Encoding = "console" 97 | config.EncoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder 98 | } 99 | return config 100 | } 101 | 102 | // Debugf log to level debug 103 | func Debugf(fmt string, args ...interface{}) { 104 | logger.Debugf(fmt, args...) 105 | } 106 | 107 | // Infof log to level info 108 | func Infof(fmt string, args ...interface{}) { 109 | logger.Infof(fmt, args...) 110 | } 111 | 112 | // Warnf log to level warn 113 | func Warnf(fmt string, args ...interface{}) { 114 | logger.Warnf(fmt, args...) 115 | } 116 | 117 | // Errorf log to level error 118 | func Errorf(fmt string, args ...interface{}) { 119 | logger.Errorf(fmt, args...) 120 | } 121 | 122 | // FatalfIf log to level error 123 | func FatalfIf(cond bool, fmt string, args ...interface{}) { 124 | if !cond { 125 | return 126 | } 127 | log.Fatalf(fmt, args...) 128 | } 129 | 130 | // FatalIfError if err is not nil, then log to level fatal and call os.Exit 131 | func FatalIfError(err error) { 132 | FatalfIf(err != nil, "fatal error: %v", err) 133 | } 134 | -------------------------------------------------------------------------------- /dtmcli/barrier_mongo.go: -------------------------------------------------------------------------------- 1 | package dtmcli 2 | 3 | import ( 4 | "context" 5 | "strings" 6 | 7 | "github.com/dtm-labs/dtm/dtmcli/dtmimp" 8 | "github.com/dtm-labs/dtm/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 | -------------------------------------------------------------------------------- /dtmsvr/trans_class.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 dtmsvr 8 | 9 | import ( 10 | "context" 11 | "time" 12 | 13 | "github.com/dtm-labs/dtm/dtmcli" 14 | "github.com/dtm-labs/dtm/dtmcli/dtmimp" 15 | "github.com/dtm-labs/dtm/dtmcli/logger" 16 | "github.com/dtm-labs/dtm/dtmgrpc/dtmgimp" 17 | "github.com/dtm-labs/dtm/dtmgrpc/dtmgpb" 18 | "github.com/dtm-labs/dtm/dtmsvr/storage" 19 | "github.com/gin-gonic/gin" 20 | ) 21 | 22 | // TransGlobal global transaction 23 | type TransGlobal struct { 24 | storage.TransGlobalStore 25 | lastTouched time.Time // record the start time of process 26 | updateBranchSync bool 27 | } 28 | 29 | func (t *TransGlobal) setupPayloads() { 30 | // Payloads will be store in BinPayloads, Payloads is only used to Unmarshal 31 | for _, p := range t.Payloads { 32 | t.BinPayloads = append(t.BinPayloads, []byte(p)) 33 | } 34 | for _, d := range t.Steps { 35 | if d["data"] != "" { 36 | t.BinPayloads = append(t.BinPayloads, []byte(d["data"])) 37 | } 38 | } 39 | if t.Protocol == "" { 40 | t.Protocol = "http" 41 | } 42 | 43 | } 44 | 45 | // TransBranch branch transaction 46 | type TransBranch = storage.TransBranchStore 47 | 48 | type transProcessor interface { 49 | GenBranches() []TransBranch 50 | ProcessOnce(branches []TransBranch) error 51 | } 52 | 53 | type processorCreator func(*TransGlobal) transProcessor 54 | 55 | var processorFac = map[string]processorCreator{} 56 | 57 | func registorProcessorCreator(transType string, creator processorCreator) { 58 | processorFac[transType] = creator 59 | } 60 | 61 | func (t *TransGlobal) getProcessor() transProcessor { 62 | return processorFac[t.TransType](t) 63 | } 64 | 65 | type cronType int 66 | 67 | const ( 68 | cronBackoff cronType = iota 69 | cronReset 70 | cronKeep 71 | ) 72 | 73 | // TransFromContext TransFromContext 74 | func TransFromContext(c *gin.Context) *TransGlobal { 75 | b, err := c.GetRawData() 76 | e2p(err) 77 | m := TransGlobal{} 78 | dtmimp.MustUnmarshal(b, &m) 79 | m.Status = dtmimp.Escape(m.Status) 80 | m.Gid = dtmimp.Escape(m.Gid) 81 | logger.Debugf("creating trans in prepare") 82 | m.setupPayloads() 83 | m.Ext.Headers = map[string]string{} 84 | if len(m.PassthroughHeaders) > 0 { 85 | for _, h := range m.PassthroughHeaders { 86 | v := c.GetHeader(h) 87 | if v != "" { 88 | m.Ext.Headers[h] = v 89 | } 90 | } 91 | } 92 | return &m 93 | } 94 | 95 | // TransFromDtmRequest TransFromContext 96 | func TransFromDtmRequest(ctx context.Context, c *dtmgpb.DtmRequest) *TransGlobal { 97 | o := &dtmgpb.DtmTransOptions{} 98 | if c.TransOptions != nil { 99 | o = c.TransOptions 100 | } 101 | r := TransGlobal{TransGlobalStore: storage.TransGlobalStore{ 102 | Gid: c.Gid, 103 | TransType: c.TransType, 104 | QueryPrepared: c.QueryPrepared, 105 | Protocol: "grpc", 106 | BinPayloads: c.BinPayloads, 107 | CustomData: c.CustomedData, 108 | TransOptions: dtmcli.TransOptions{ 109 | WaitResult: o.WaitResult, 110 | TimeoutToFail: o.TimeoutToFail, 111 | RetryInterval: o.RetryInterval, 112 | PassthroughHeaders: o.PassthroughHeaders, 113 | BranchHeaders: o.BranchHeaders, 114 | RequestTimeout: o.RequestTimeout, 115 | }, 116 | }} 117 | if c.Steps != "" { 118 | dtmimp.MustUnmarshalString(c.Steps, &r.Steps) 119 | } 120 | if len(o.PassthroughHeaders) > 0 { 121 | r.Ext.Headers = map[string]string{} 122 | for _, h := range o.PassthroughHeaders { 123 | v := dtmgimp.GetMetaFromContext(ctx, h) 124 | if v != "" { 125 | r.Ext.Headers[h] = v 126 | } 127 | } 128 | } 129 | return &r 130 | } 131 | -------------------------------------------------------------------------------- /dtmcli/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/dtm/dtmcli/dtmimp" 15 | "github.com/dtm-labs/dtm/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 | } 29 | 30 | func (bb *BranchBarrier) String() string { 31 | return fmt.Sprintf("transInfo: %s %s %s %s", bb.TransType, bb.Gid, bb.BranchID, bb.Op) 32 | } 33 | 34 | func (bb *BranchBarrier) newBarrierID() string { 35 | bb.BarrierID++ 36 | return fmt.Sprintf("%02d", bb.BarrierID) 37 | } 38 | 39 | // BarrierFromQuery construct transaction info from request 40 | func BarrierFromQuery(qs url.Values) (*BranchBarrier, error) { 41 | return BarrierFrom(dtmimp.EscapeGet(qs, "trans_type"), dtmimp.EscapeGet(qs, "gid"), dtmimp.EscapeGet(qs, "branch_id"), dtmimp.EscapeGet(qs, "op")) 42 | } 43 | 44 | // BarrierFrom construct transaction info from request 45 | func BarrierFrom(transType, gid, branchID, op string) (*BranchBarrier, error) { 46 | ti := &BranchBarrier{ 47 | TransType: transType, 48 | Gid: gid, 49 | BranchID: branchID, 50 | Op: op, 51 | } 52 | if ti.TransType == "" || ti.Gid == "" || ti.BranchID == "" || ti.Op == "" { 53 | return nil, fmt.Errorf("invalid trans info: %v", ti) 54 | } 55 | return ti, nil 56 | } 57 | 58 | // Call see detail description in https://en.dtm.pub/practice/barrier.html 59 | // tx: local transaction connection 60 | // busiCall: busi func 61 | func (bb *BranchBarrier) Call(tx *sql.Tx, busiCall BarrierBusiFunc) (rerr error) { 62 | bid := bb.newBarrierID() 63 | defer dtmimp.DeferDo(&rerr, func() error { 64 | return tx.Commit() 65 | }, func() error { 66 | return tx.Rollback() 67 | }) 68 | originOp := map[string]string{ 69 | dtmimp.OpCancel: dtmimp.OpTry, 70 | dtmimp.OpCompensate: dtmimp.OpAction, 71 | }[bb.Op] 72 | 73 | originAffected, oerr := dtmimp.InsertBarrier(tx, bb.TransType, bb.Gid, bb.BranchID, originOp, bid, bb.Op) 74 | currentAffected, rerr := dtmimp.InsertBarrier(tx, bb.TransType, bb.Gid, bb.BranchID, bb.Op, bid, bb.Op) 75 | logger.Debugf("originAffected: %d currentAffected: %d", originAffected, currentAffected) 76 | 77 | if rerr == nil && bb.Op == dtmimp.MsgDoOp && currentAffected == 0 { // for msg's DoAndSubmit, repeated insert should be rejected. 78 | return ErrDuplicated 79 | } 80 | 81 | if rerr == nil { 82 | rerr = oerr 83 | } 84 | 85 | if (bb.Op == dtmimp.OpCancel || bb.Op == dtmimp.OpCompensate) && originAffected > 0 || // null compensate 86 | currentAffected == 0 { // repeated request or dangled request 87 | return 88 | } 89 | if rerr == nil { 90 | rerr = busiCall(tx) 91 | } 92 | return 93 | } 94 | 95 | // CallWithDB the same as Call, but with *sql.DB 96 | func (bb *BranchBarrier) CallWithDB(db *sql.DB, busiCall BarrierBusiFunc) error { 97 | tx, err := db.Begin() 98 | if err == nil { 99 | err = bb.Call(tx, busiCall) 100 | } 101 | return err 102 | } 103 | 104 | // QueryPrepared queries prepared data 105 | func (bb *BranchBarrier) QueryPrepared(db *sql.DB) error { 106 | _, err := dtmimp.InsertBarrier(db, bb.TransType, bb.Gid, dtmimp.MsgDoBranch0, dtmimp.MsgDoOp, dtmimp.MsgDoBarrier1, dtmimp.OpRollback) 107 | var reason string 108 | if err == nil { 109 | sql := fmt.Sprintf("select reason from %s where gid=? and branch_id=? and op=? and barrier_id=?", dtmimp.BarrierTableName) 110 | err = db.QueryRow(sql, bb.Gid, dtmimp.MsgDoBranch0, dtmimp.MsgDoOp, dtmimp.MsgDoBarrier1).Scan(&reason) 111 | } 112 | if reason == dtmimp.OpRollback { 113 | return ErrFailure 114 | } 115 | return err 116 | } 117 | --------------------------------------------------------------------------------