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