trpc.group/trpc-go/trpc-go@v1.0.3/docs/developer_guide/develop_plugins/database.md (about) 1 English | [中文](database.zh_CN.md) 2 3 # How to Develop a Database Plugin 4 5 In everyday development, we often need to access various storage systems like MySQL, Redis, Kafka, etc., for database read and write operations. While using open-source SDKs can fulfill the need for accessing databases, users are required to handle naming routing, monitoring, and configuration development on their own. 6 7 Considering that tRPC-Go provides various plugins for naming routing, monitoring, and configuration management, we can encapsulate open-source SDKs and leverage the capabilities of tRPC-Go plugins to reduce repetitive code. tRPC-Go provides wrappers for some open-source SDKs, allowing you to directly reuse tRPC-Go's naming routing and monitoring features. The code repository can be found at: https://github.com/trpc-ecosystem/go-database. 8 9 ## Producer 10 11 ### Define the `Client` Request Entry 12 13 Producers perform specific actions to produce messages. For example, if you want to execute a MySQL execution, you can define a `MysqlCli` struct that provides an `Exec` method. To reuse tRPC-Go plugins' capabilities, you need to enter the tRPC-Go Client layer, where various filters are called. Routing and monitoring plugins are implemented using filters. Therefore, in the `Exec` method, you call `Client.Invoke` to enter the Client layer: 14 15 ```golang 16 type MysqlCli struct { 17 serviceName string 18 client client.Client 19 } 20 21 func (c *MysqlCli) Exec(ctx context.Context, query string, args ...interface{}) (sql.Result, error) { 22 // Construct the request and response 23 mreq := &Request{ 24 Exec: query, 25 Args: args, 26 } 27 mrsp := &Response{} 28 29 ctx, msg := codec.WithCloneMessage(ctx) 30 defer codec.PutBackMessage(msg) 31 msg.WithClientRPCName(fmt.Sprintf("/%s/Exec", c.serviceName)) 32 msg.WithCalleeServiceName(c.serviceName) 33 msg.WithSerializationType(codec.SerializationTypeUnsupported) 34 msg.WithCompressType(codec.CompressTypeNoop) 35 36 // Put the request and response into the context 37 msg.WithClientReqHead(mreq) 38 msg.WithClientRspHead(mrsp) 39 40 // Enter the Client layer and specify the transport for MySQL using WithProtocol 41 err := c.client.Invoke(ctx, mreq, mrsp, client.WithProtocol("mysql")) 42 if err != nil { 43 return nil, err 44 } 45 46 return mrsp.Result, nil 47 } 48 ``` 49 50 ### Implement the `transport.ClientTransport` Interface 51 52 The `transport.ClientTransport` interface has a `RoundTrip` method, which is the starting point for making network requests after the tRPC-Go Client layer ends. The goal of the database plugin is not to create a database SDK but to wrap existing database SDKs with a tRPC-Go plugin, giving them the ability to route and monitor like a tRPC-Go client. 53 54 In the `RoundTrip` method, you need to call the database's SDK to initiate specific requests. For example, if you want to execute a MySQL execution: 55 56 ```golang 57 func init() { 58 // Register the MySQL transport 59 transport.RegisterClientTransport("mysql", &MysqlTransport{}) 60 } 61 62 type MysqlTransport struct{} 63 64 func (t *MysqlTransport) RoundTrip(ctx context.Context, req []byte, opts ...transport.RoundTripOption) ([]byte, error) { 65 // Retrieve the request and response from the context 66 msg := codec.Message(ctx) 67 req := msg.ClientReqHead().(*Request) 68 rsp := msg.ClientRspHead().(*Response) 69 70 opt := &transport.RoundTripOptions{} 71 for _, o := range opts { 72 o(opt) 73 } 74 75 // Ensure that the same address is only opened once 76 db, err := sql.Open(ct.DriverName, opt.Address) 77 rsp = db.ExecContext(ctx, req.Exec, req.Args...) 78 } 79 ``` 80 81 ### A Better Implementation Using Hooks 82 83 While defining request entry points and transport layers can achieve request interception for databases and implement routing and monitoring, it can become verbose and challenging to maintain as the number of interfaces increases. A more efficient approach is to use Hooks provided by open-source SDKs to inject filters. Almost all routing and monitoring plugins are implemented as filters, so by executing filters, routing and monitoring functionality is achieved without entering the Client layer. Additionally, using Hooks allows you to use the original request entry points provided by the open-source SDKs, avoiding the need for additional definitions. However, not all open-source SDKs provide Hook functionality. Let's take the example of the go-redis SDK to see how to leverage Hooks for filters. 84 85 The [go-redis](github.com/redis/go-redis/v9) UniversalClient provides an `AddHook(hook)` method, which allows you to execute custom logic before and after creating connections and making requests: 86 87 ```golang 88 type Hook interface { 89 DialHook(next DialHook) DialHook 90 ProcessHook(next ProcessHook) ProcessHook 91 ProcessPipelineHook(next ProcessPipelineHook) ProcessPipelineHook 92 } 93 ``` 94 95 For example, if you want to perform routing during connection creation: 96 97 ```golang 98 type hook struct { 99 filters *joinfilters.Filters // Filters 100 selector selector.Selector // Routing selector 101 } 102 103 func (h *hook) DialHook(next redis.DialHook) redis.DialHook { 104 return func(ctx context.Context, network, addr string) (net.Conn, error) { 105 start := time.Now() 106 node, err := h.selector.Select(h.options.Service) 107 if err != nil { 108 return nil, err 109 } 110 conn, err := next(ctx, network, node.Address) 111 return conn, err 112 } 113 } 114 ``` 115 116 For example, if you want to execute filters before and after making requests: 117 118 ```golang 119 func (h *hook) ProcessPipelineHook(next redis.ProcessPipelineHook) redis.ProcessPipelineHook { 120 return func(ctx context.Context, cmds []redis.Cmder) error { 121 ctx, redisMsg := WithMessage(ctx) 122 call := func(context.Context) (string, error) { 123 nextErr := next(ctx, cmds) 124 rspBody := pipelineRspBody(cmds) 125 return rspBody, nextErr 126 } 127 req := &invokeReq{ 128 cmds: cmds, 129 rpcName: "pipeline", 130 call: call, 131 } 132 req.calleeMethod, req.reqBody = pipelineReqBody(cmds) 133 trpcErr := h.filters.Invoke(ctx, rReq, rRsp, func(ctx context.Context, _, _ interface{}) error { 134 rRsp.Cmd, callErr = req.call(ctx) 135 return TRPCErr(callErr) 136 }, opts...) 137 } 138 } 139 ``` 140 141 Complete code can be found here: https://github.com/trpc-ecosystem/go-database/blob/main/goredis/client.go 142 143 ## Consumer 144 145 ### Define Stubs 146 147 Consumer services are typically callback functions similar to regular tRPC-Go servers. You can use stubs to register user processing logic with the framework. For example, in a Kafka consumer service that uses the [sarama](https://github.com/IBM/sarama) SDK, you can define the signature of the business processing interface as follows: 148 149 ```golang 150 type KafkaConsumer interface { 151 Handle(ctx context.Context, msg *sarama.ConsumerMessage) error 152 } 153 ``` 154 155 Define stubs and provide a method for users to register their processing logic: 156 157 ```golang 158 func RegisterKafkaConsumerService(s server.Service, svr KafkaConsumer) { 159 _ = s.Register(&KafkaConsumerServiceDesc, svr) 160 } 161 162 var KafkaConsumerServiceDesc = server.ServiceDesc{ 163 ServiceName: "trpc.kafka.consumer.service", 164 HandlerType: ((*KafkaConsumer)(nil)), 165 Methods: []server.Method{{ 166 Name: "/trpc.kafka.consumer.service/handle", 167 Func: KafkaConsumerHandle, 168 }}, 169 } 170 ``` 171 172 To ensure that the tRPC-Go framework executes the business processing logic correctly, you need to implement `server.Method.Func` to extract the `sarama.ConsumerMessage` from the context and pass it to the user's processing function: 173 174 ```golang 175 func(svr interface{}, ctx context.Context, f FilterFunc) (rspbody interface{}, err error) 176 177 func KafkaConsumerHandle(svr interface{}, ctx context.Context, f server.FilterFunc) (interface{}, error) { 178 filters, err := f(nil) 179 if err != nil { 180 return nil, err 181 } 182 handleFunc := func(ctx context.Context, _ interface{}, _ interface{}) error { 183 msg := codec.Message(ctx) 184 m, ok := msg.ServerReqHead().(*sarama.ConsumerMessage) 185 if !ok { 186 return errs.NewFrameError(errs.RetServerDecodeFail, "kafka consumer handler: message type invalid") 187 } 188 return svr.(KafkaConsumer).Handle(ctx, m) 189 } 190 if err := filters.Handle(ctx, nil, nil, handleFunc); err != nil { 191 return nil, err 192 } 193 return nil, nil 194 } 195 ``` 196 197 ### Implement the `transport.ServerTransport` 198 199 The `transport.ServerTransport` interface's `ListenAndServe` method is the entry point for network requests. In this function, you need to use the sarama consumer interface to fetch messages and pass them to users: 200 201 Complete code: https://github.com/trpc-ecosystem/go-database/blob/main/kafka/server_transport.go