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 }