github.com/blend/go-sdk@v1.20240719.1/redis/radix_client.go (about)

     1  /*
     2  
     3  Copyright (c) 2024 - Present. Blend Labs, Inc. All rights reserved
     4  Use of this source code is governed by a MIT license that can be found in the LICENSE file.
     5  
     6  */
     7  
     8  package redis
     9  
    10  import (
    11  	"context"
    12  	"crypto/tls"
    13  	"net"
    14  	"time"
    15  
    16  	radix "github.com/mediocregopher/radix/v4"
    17  
    18  	"github.com/blend/go-sdk/async"
    19  	"github.com/blend/go-sdk/ex"
    20  	"github.com/blend/go-sdk/logger"
    21  	"github.com/blend/go-sdk/uuid"
    22  )
    23  
    24  var (
    25  	_ async.Checker = (*RadixClient)(nil)
    26  	_ Client        = (*RadixClient)(nil)
    27  )
    28  
    29  // New returns a new client.
    30  func New(ctx context.Context, opts ...Option) (*RadixClient, error) {
    31  	var rc RadixClient
    32  	var err error
    33  	for _, opt := range opts {
    34  		if err = opt(&rc); err != nil {
    35  			return nil, ex.New(err)
    36  		}
    37  	}
    38  	if rc.Config.ConnectTimeout > 0 {
    39  		var cancel func()
    40  		ctx, cancel = context.WithTimeout(ctx, rc.Config.ConnectTimeout)
    41  		defer cancel()
    42  	}
    43  
    44  	var dialer RadixNetDialer
    45  	if rc.Config.UseTLS {
    46  		dialer = new(tls.Dialer)
    47  	}
    48  
    49  	if len(rc.Config.SentinelAddrs) > 0 {
    50  		rc.Client, err = (radix.SentinelConfig{
    51  			PoolConfig: radix.PoolConfig{
    52  				Dialer: radix.Dialer{
    53  					SelectDB:  rc.Config.DB,
    54  					AuthUser:  rc.Config.AuthUser,
    55  					AuthPass:  rc.Config.AuthPassword,
    56  					NetDialer: dialer,
    57  				},
    58  			},
    59  		}).New(ctx, rc.Config.SentinelPrimaryName, rc.Config.SentinelAddrs)
    60  	} else if len(rc.Config.ClusterAddrs) > 0 {
    61  		rc.Client, err = (radix.ClusterConfig{
    62  			PoolConfig: radix.PoolConfig{
    63  				Dialer: radix.Dialer{
    64  					SelectDB:  rc.Config.DB,
    65  					AuthUser:  rc.Config.AuthUser,
    66  					AuthPass:  rc.Config.AuthPassword,
    67  					NetDialer: dialer,
    68  				},
    69  			},
    70  		}).New(ctx, rc.Config.ClusterAddrs)
    71  	} else {
    72  		rc.Client, err = (radix.PoolConfig{
    73  			Dialer: radix.Dialer{
    74  				SelectDB:  rc.Config.DB,
    75  				AuthUser:  rc.Config.AuthUser,
    76  				AuthPass:  rc.Config.AuthPassword,
    77  				NetDialer: dialer,
    78  			},
    79  		}).New(ctx, rc.Config.Network, rc.Config.Addr)
    80  	}
    81  	if err != nil {
    82  		return nil, ex.New(err)
    83  	}
    84  	return &rc, nil
    85  }
    86  
    87  // Assert `RadixClient` implements `Client`.
    88  var (
    89  	_ Client = (*RadixClient)(nil)
    90  )
    91  
    92  // RadixNetDialer is a dialer for radix connections.
    93  type RadixNetDialer interface {
    94  	DialContext(context.Context, string, string) (net.Conn, error)
    95  }
    96  
    97  // RadixDoCloser is an thin implementation of the radix client.
    98  type RadixDoCloser interface {
    99  	Do(context.Context, radix.Action) error
   100  	Close() error
   101  }
   102  
   103  // RadixClient is a wrapping client for the underling radix redis driver.
   104  type RadixClient struct {
   105  	Config Config
   106  	Log    logger.Triggerable
   107  	Tracer Tracer
   108  	Client RadixDoCloser
   109  }
   110  
   111  // Ping sends an echo to the server and validates the response.
   112  func (rc *RadixClient) Ping(ctx context.Context) error {
   113  	var actual string
   114  	expected := uuid.V4().String()
   115  	if err := rc.Client.Do(ctx, radix.Cmd(&actual, OpECHO, expected)); err != nil {
   116  		return ex.New(err)
   117  	}
   118  	if actual != expected {
   119  		return ex.New(ErrPingFailed)
   120  	}
   121  	return nil
   122  }
   123  
   124  // Check implements a status check.
   125  func (rc *RadixClient) Check(ctx context.Context) error {
   126  	return rc.Ping(ctx)
   127  }
   128  
   129  // Do runs a given command.
   130  func (rc *RadixClient) Do(ctx context.Context, out interface{}, op string, args ...string) (err error) {
   131  	if rc.Log != nil {
   132  		started := time.Now()
   133  		defer func() {
   134  			rc.Log.TriggerContext(ctx, NewEvent(op, args, time.Since(started),
   135  				OptEventNetwork(rc.Config.Network),
   136  				OptEventAddr(rc.Config.Addr),
   137  				OptEventAuthUser(rc.Config.AuthUser),
   138  				OptEventDB(rc.Config.DB),
   139  				OptEventErr(err),
   140  			))
   141  		}()
   142  	}
   143  	if rc.Tracer != nil {
   144  		finisher := rc.Tracer.Do(ctx, rc.Config, op, args)
   145  		defer finisher.Finish(ctx, err)
   146  	}
   147  	if rc.Config.Timeout > 0 {
   148  		var cancel func()
   149  		ctx, cancel = context.WithTimeout(ctx, rc.Config.Timeout)
   150  		defer cancel()
   151  	}
   152  	if radixErr := rc.Client.Do(ctx, radix.Cmd(out, op, args...)); radixErr != nil {
   153  		err = ex.New(radixErr)
   154  		return
   155  	}
   156  	return
   157  }
   158  
   159  // Pipeline runs the given commands in a pipeline
   160  func (rc *RadixClient) Pipeline(ctx context.Context, pipelineName string, ops ...Operation) (err error) {
   161  	// Create a parent span for the entire pipeline operation
   162  	if rc.Tracer != nil && pipelineName != "" {
   163  		pipelineFinisher := rc.Tracer.Do(ctx, rc.Config, pipelineName, nil)
   164  		defer pipelineFinisher.Finish(ctx, err)
   165  	}
   166  
   167  	var logEvents []Event
   168  	if rc.Log != nil {
   169  		started := time.Now()
   170  		for _, op := range ops {
   171  			event := NewEvent(op.Command, op.Args, time.Since(started),
   172  				OptEventNetwork(rc.Config.Network),
   173  				OptEventAddr(rc.Config.Addr),
   174  				OptEventAuthUser(rc.Config.AuthUser),
   175  				OptEventDB(rc.Config.DB),
   176  				OptEventErr(err),
   177  			)
   178  			logEvents = append(logEvents, event)
   179  		}
   180  
   181  		defer func() {
   182  			for _, event := range logEvents {
   183  				rc.Log.TriggerContext(ctx, event)
   184  			}
   185  		}()
   186  	}
   187  
   188  	// create a child span for each op
   189  	var finishers []TraceFinisher
   190  	if rc.Tracer != nil {
   191  		for _, op := range ops {
   192  			finisher := rc.Tracer.Do(ctx, rc.Config, op.Command, op.Args)
   193  			finishers = append(finishers, finisher)
   194  		}
   195  
   196  		defer func() {
   197  			for _, finisher := range finishers {
   198  				finisher.Finish(ctx, err)
   199  			}
   200  		}()
   201  	}
   202  
   203  	if rc.Config.Timeout > 0 {
   204  		var cancel func()
   205  		ctx, cancel = context.WithTimeout(ctx, rc.Config.Timeout)
   206  		defer cancel()
   207  	}
   208  
   209  	p := radix.NewPipeline()
   210  	for _, op := range ops {
   211  		p.Append(radix.Cmd(op.Out, op.Command, op.Args...))
   212  	}
   213  
   214  	// Execute pipeline
   215  	radixErr := rc.Client.Do(ctx, p)
   216  	if radixErr != nil {
   217  		err = ex.New(radixErr)
   218  		return
   219  	}
   220  	return
   221  }
   222  
   223  // Close closes the underlying connection.
   224  func (rc *RadixClient) Close() error {
   225  	return rc.Client.Close()
   226  }