Code Overview

Project organization

The dtm project has the following main directories

  • app: there is only one main, which is the general entrance of dtm. You can pass in different parameters to run in different modes.

  • common: public functions and class libraries, including logging, json, database, network, etc.

  • dtmcli: dtm's http client, including tcc, saga, xa, msg transaction modes, and sub-transaction barrier

  • dtmgrpc: dtm's gRPC client, including the tcc, saga, xa, msg transaction modes, and sub-transaction barrier

  • dtmsvr: dtm's server side, including http, gRPC server APIs for various transaction modes implementation

  • examples: contains various examples

  • test: contains various test cases

code description

The idiomatic error handling method in Go is "error is a value", not exception. Therefore, the interfaces provided to users in dtmcli are all in line with this standard.

The example given, however, uses the function e2p, which is a custom function that turns error into a panic. Although it does not conform to the go specification, it reduces the amount of error-handling code and makes the code snippets shorter, allowing the user to focus on the core demo content

Example description

The example used in dtm is mainly a distributed transaction for a transfer. Suppose a scenario where there is a transfer from A to B, but A and B belong to different banks and are stored in different databases. This scenario is a typical distributed transaction scenario. We define this distributed transaction as two sub-transactions, one for the transfer out TransOut and one for the transfer in TransIn.

Since we will often call these two sub-transactions repeatedly in the later examples, we pull out the processing of these two sub-transactions separately

http

The http client defines the various basic operations related to TransIn and TransOut inside examples/base_http.go, posted as follows:

func handleGeneralBusiness(c *gin.Context, result1 string, result2 string, busi string) (interface{}, error) {
	info := infoFromContext(c)
	res := common.OrString(MainSwitch.TransInResult.Fetch(), result2, "SUCCESS")
	logrus.Printf("%s %s result: %s", busi, info.String(), res)
	return M{"dtm_result": res}, nil
}

// BaseAddRoute add base route handler
func BaseAddRoute(app *gin.Engine) {
	app.POST(BusiAPI+"/TransIn", common.WrapHandler(func(c *gin.Context) (interface{}, error) {
		return handleGeneralBusiness(c, MainSwitch.TransInResult.Fetch(), reqFrom(c).TransInResult, "transIn")
	}))
	app.POST(BusiAPI+"/TransOut", common.WrapHandler(func(c *gin.Context) (interface{}, error) {
		return handleGeneralBusiness(c, MainSwitch.TransOutResult.Fetch(), reqFrom(c).TransOutResult, "TransOut")
	}))
	app.POST(BusiAPI+"/TransInConfirm", common.WrapHandler(func(c *gin.Context) (interface{}, error) {
		return handleGeneralBusiness(c, MainSwitch.TransInConfirmResult.Fetch(), "", "TransInConfirm")
	}))
	app.POST(BusiAPI+"/TransOutConfirm", common.WrapHandler(func(c *gin.Context) (interface{}, error) {
		return handleGeneralBusiness(c, MainSwitch.TransOutConfirmResult.Fetch(), "", "TransOutConfirm")
	}))
	app.POST(BusiAPI+"/TransInRevert", common.WrapHandler(func(c *gin.Context) (interface{}, error) {
		return handleGeneralBusiness(c, MainSwitch.TransInRevertResult.Fetch(), "", "TransInRevert")
	}))
	app.POST(BusiAPI+"/TransOutRevert", common.WrapHandler(func(c *gin.Context) (interface{}, error) {
		return handleGeneralBusiness(c, MainSwitch.TransOutRevertResult.Fetch(), "", "TransOutRevert")
	}))
	app.GET(BusiAPI+"/CanSubmit", common.WrapHandler(func(c *gin.Context) (interface{}, error) {
		logrus.Printf("%s CanSubmit", c.Query("gid"))
		return common.OrString(MainSwitch.CanSubmitResult.Fetch(), "SUCCESS"), nil
	}))
}

grpc

The grpc client defines each basic operation related to TransIn, TransOut inside examples/base_grpc.go, as follows.

func handleGrpcBusiness(in *dtmgrpc.BusiRequest, result1 string, result2 string, busi string) error {
	res := dtmcli.OrString(result1, result2, "SUCCESS")
	dtmcli.Logf("grpc busi %s %s result: %s", busi, in.Info, res)
	if res == "SUCCESS" {
		return nil
	} else if res == "FAILURE" {
		return status.New(codes.Aborted, "user want to rollback").Err()
	}
	return status.New(codes.Internal, fmt.Sprintf("unknown result %s", res)).Err()
}

func (s *busiServer) CanSubmit(ctx context.Context, in *dtmgrpc.BusiRequest) (*emptypb.Empty, error) {
	res := MainSwitch.CanSubmitResult.Fetch()
	return &emptypb.Empty{}, dtmgrpc.Result2Error(res, nil)
}

func (s *busiServer) TransIn(ctx context.Context, in *dtmgrpc.BusiRequest) (*emptypb.Empty, error) {
	req := TransReq{}
	dtmcli.MustUnmarshal(in.BusiData, &req)
	return &emptypb.Empty{}, handleGrpcBusiness(in, MainSwitch.TransInResult.Fetch(), req.TransInResult, dtmcli.GetFuncName())
}

func (s *busiServer) TransOut(ctx context.Context, in *dtmgrpc.BusiRequest) (*emptypb.Empty, error) {
	req := TransReq{}
	dtmcli.MustUnmarshal(in.BusiData, &req)
	return &emptypb.Empty{}, handleGrpcBusiness(in, MainSwitch.TransOutResult.Fetch(), req.TransOutResult, dtmcli.GetFuncName())
}

func (s *busiServer) TransInRevert(ctx context.Context, in *dtmgrpc.BusiRequest) (*emptypb.Empty, error) {
	req := TransReq{}
	dtmcli.MustUnmarshal(in.BusiData, &req)
	return &emptypb.Empty{}, handleGrpcBusiness(in, MainSwitch.TransInRevertResult.Fetch(), "", dtmcli.GetFuncName())
}

func (s *busiServer) TransOutRevert(ctx context.Context, in *dtmgrpc.BusiRequest) (*emptypb.Empty, error) {
	req := TransReq{}
	dtmcli.MustUnmarshal(in.BusiData, &req)
	return &emptypb.Empty{}, handleGrpcBusiness(in, MainSwitch.TransOutRevertResult.Fetch(), "", dtmcli.GetFuncName())
}

func (s *busiServer) TransInConfirm(ctx context.Context, in *dtmgrpc.BusiRequest) (*emptypb.Empty, error) {
	req := TransReq{}
	dtmcli.MustUnmarshal(in.BusiData, &req)
	return &emptypb.Empty{}, handleGrpcBusiness(in, MainSwitch.TransInConfirmResult.Fetch(), "", dtmcli.GetFuncName())
}

func (s *busiServer) TransOutConfirm(ctx context.Context, in *dtmgrpc.BusiRequest) (*emptypb.Empty, error) {
	req := TransReq{}
	dtmcli.MustUnmarshal(in.BusiData, &req)
	return &emptypb.Empty{}, handleGrpcBusiness(in, MainSwitch.TransOutConfirmResult.Fetch(), "", dtmcli.GetFuncName())
}

Example Summary

In the above code, functions suffixed with Confirm will be called by Tcc transaction mode, those suffixed with Revert called by Tcc's Cancel and SAGA's compensate, and those suffixed with CanSubmit called by the transaction message.

In addition, MainSwitch is used for auxiliary testing, for simulating various failures.

Client for each language

go

Client sdk: https://github.com/yedf/dtmcli

Example: https://github.com/yedf/dtmcli-go-sample

dotnet

Client sdk (currently only supports TCC): https://github.com/yedf/dtmcli-csharp

Example: https://github.com/yedf/dtmcli-csharp-sample

Thanks to geffzhang for help with the C sdk and examples, all contributed independently by geffzhang

python

Client sdk (currently supports TCC, SAGA, sub-transaction barriers): https://github.com/yedf/dtmcli-py

Example: https://github.com/yedf/dtmcli-py-sample

Java

Client sdk (currently only supports TCC): https://github.com/yedf/dtmcli-java

Example: https://github.com/yedf/dtmcli-java-sample

Thanks to viticis for help with the Java sdk and examples, all contributed independently by viticis

php

Client sdk (currently only supports TCC): https://github.com/yedf/dtmcli-php

Example: https://github.com/yedf/dtmcli-php-sample

Thanks to onlyshow for help with the php sdk and examples, all done independently by onlyshow

node

Client sdk (currently only supports TCC): https://github.com/yedf/dtmcli-node

Example: https://github.com/yedf/dtmcli-node-sample