go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/server/redisconn/redisconn.go (about) 1 // Copyright 2019 The LUCI Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 // Package redisconn implements integration with a Redis connection pool. 16 // 17 // Usage as a server module: 18 // 19 // func main() { 20 // modules := []module.Module{ 21 // redisconn.NewModuleFromFlags(), 22 // } 23 // server.Main(nil, modules, func(srv *server.Server) error { 24 // srv.Routes.GET("/", ..., func(c *router.Context) { 25 // conn, err := redisconn.Get(c.Context) 26 // if err != nil { 27 // // handle error 28 // } 29 // defer conn.Close() 30 // // use Redis API via `conn` 31 // }) 32 // return nil 33 // }) 34 // } 35 // 36 // When used that way, Redis is also installed as the default implementation 37 // of caching.BlobCache (which basically speeds up various internal guts of 38 // the LUCI server framework). 39 // 40 // Can also be used as a low-level Redis connection pool library, see 41 // NewPool(...) 42 package redisconn 43 44 import ( 45 "context" 46 "time" 47 48 "github.com/gomodule/redigo/redis" 49 50 "go.chromium.org/luci/common/errors" 51 "go.chromium.org/luci/common/logging" 52 "go.chromium.org/luci/common/tsmon/field" 53 "go.chromium.org/luci/common/tsmon/metric" 54 "go.chromium.org/luci/common/tsmon/types" 55 ) 56 57 // ErrNotConfigured is returned by Get if the context has no Redis pool inside. 58 var ErrNotConfigured = errors.New("Redis connection pool is not configured") 59 60 // Per-pool metrics derived from redis.Pool.Stats() by ReportStats. 61 var ( 62 connsMetric = metric.NewInt( 63 "redis/pool/conns", 64 "The number of connections in the pool (idle or in-use depending if state field)", 65 &types.MetricMetadata{}, 66 field.String("pool"), // e.g. "default" 67 field.String("state"), // either "idle" or "in-use" 68 ) 69 70 waitCountMetric = metric.NewCounter( 71 "redis/pool/wait_count", 72 "The total number of connections waited for.", 73 &types.MetricMetadata{}, 74 field.String("pool"), // e.g. "default" 75 ) 76 77 waitDurationMetric = metric.NewCounter( 78 "redis/pool/wait_duration", 79 "The total time blocked waiting for a new connection.", 80 &types.MetricMetadata{Units: types.Microseconds}, 81 field.String("pool"), // e.g. "default" 82 ) 83 ) 84 85 // NewPool returns a new pool configured with default parameters. 86 // 87 // "addr" is TCP "host:port" of a Redis server to connect to. No actual 88 // connection is established yet (this happens first time the pool is used). 89 // 90 // "db" is a index of a logical DB to SELECT in the connection by default, 91 // see https://redis.io/commands/select. It can be used as a weak form of 92 // namespacing. It is easy to bypass though, so please do not depend on it 93 // for anything critical (better to setup multiple Redis instances in this 94 // case). 95 // 96 // Doesn't use any authentication or encryption. 97 func NewPool(addr string, db int) *redis.Pool { 98 // TODO(vadimsh): Tune the parameters or make them configurable. The values 99 // below were picked somewhat arbitrarily. 100 return &redis.Pool{ 101 MaxIdle: 64, 102 MaxActive: 512, 103 IdleTimeout: 3 * time.Minute, 104 Wait: true, // if all connections are busy, wait for an available one 105 106 DialContext: func(ctx context.Context) (redis.Conn, error) { 107 logging.Debugf(ctx, "Opening new Redis connection to %q...", addr) 108 conn, err := redis.Dial("tcp", addr, 109 redis.DialDatabase(db), 110 redis.DialConnectTimeout(5*time.Second), 111 redis.DialReadTimeout(5*time.Second), 112 redis.DialWriteTimeout(5*time.Second), 113 ) 114 if err != nil { 115 return nil, errors.Annotate(err, "redis").Err() 116 } 117 return conn, nil 118 }, 119 120 // If the connection was idle for more than a minute, verify it is still 121 // alive by pinging the server. 122 TestOnBorrow: func(c redis.Conn, t time.Time) error { 123 if time.Since(t) < time.Minute { 124 return nil 125 } 126 _, err := c.Do("PING") 127 return err 128 }, 129 } 130 } 131 132 var contextKey = "redisconn.Pool" 133 134 // UsePool installs a connection pool into the context, to be used by Get. 135 func UsePool(ctx context.Context, pool *redis.Pool) context.Context { 136 return context.WithValue(ctx, &contextKey, pool) 137 } 138 139 // GetPool returns a connection pool in the context or nil if not there. 140 func GetPool(ctx context.Context) *redis.Pool { 141 p, _ := ctx.Value(&contextKey).(*redis.Pool) 142 return p 143 } 144 145 // ReportStats reports the connection pool stats as tsmon metrics. 146 // 147 // For best results should be called once a minute or right before tsmon flush. 148 // 149 // "name" is used as "pool" metric field, to distinguish pools between each 150 // other. 151 func ReportStats(ctx context.Context, pool *redis.Pool, name string) { 152 stats := pool.Stats() 153 connsMetric.Set(ctx, int64(stats.IdleCount), name, "idle") 154 connsMetric.Set(ctx, int64(stats.ActiveCount-stats.IdleCount), name, "in-use") 155 waitCountMetric.Set(ctx, int64(stats.WaitCount), name) 156 waitDurationMetric.Set(ctx, int64(stats.WaitDuration.Nanoseconds()/1000), name) 157 } 158 159 // Get returns a Redis connection using the pool installed in the context. 160 // 161 // May block until such connection is available. Returns an error if the 162 // context expires before that. The returned connection itself is not associated 163 // with the context and can outlive it. 164 // 165 // The connection MUST be explicitly closed as soon as it's no longer needed, 166 // otherwise leaks and slow downs are eminent. 167 func Get(ctx context.Context) (redis.Conn, error) { 168 if p := GetPool(ctx); p != nil { 169 return p.GetContext(ctx) 170 } 171 return nil, ErrNotConfigured 172 }