code.gitea.io/gitea@v1.19.3/modules/nosql/manager_redis.go (about) 1 // Copyright 2020 The Gitea Authors. All rights reserved. 2 // SPDX-License-Identifier: MIT 3 4 package nosql 5 6 import ( 7 "crypto/tls" 8 "net/url" 9 "path" 10 "runtime/pprof" 11 "strconv" 12 "strings" 13 14 "code.gitea.io/gitea/modules/log" 15 16 "github.com/redis/go-redis/v9" 17 ) 18 19 var replacer = strings.NewReplacer("_", "", "-", "") 20 21 // CloseRedisClient closes a redis client 22 func (m *Manager) CloseRedisClient(connection string) error { 23 m.mutex.Lock() 24 defer m.mutex.Unlock() 25 client, ok := m.RedisConnections[connection] 26 if !ok { 27 connection = ToRedisURI(connection).String() 28 client, ok = m.RedisConnections[connection] 29 } 30 if !ok { 31 return nil 32 } 33 34 client.count-- 35 if client.count > 0 { 36 return nil 37 } 38 39 for _, name := range client.name { 40 delete(m.RedisConnections, name) 41 } 42 return client.UniversalClient.Close() 43 } 44 45 // GetRedisClient gets a redis client for a particular connection 46 func (m *Manager) GetRedisClient(connection string) (client redis.UniversalClient) { 47 // Because we want associate any goroutines created by this call to the main nosqldb context we need to 48 // wrap this in a goroutine labelled with the nosqldb context 49 done := make(chan struct{}) 50 var recovered interface{} 51 go func() { 52 defer func() { 53 recovered = recover() 54 if recovered != nil { 55 log.Critical("PANIC during GetRedisClient: %v\nStacktrace: %s", recovered, log.Stack(2)) 56 } 57 close(done) 58 }() 59 pprof.SetGoroutineLabels(m.ctx) 60 61 client = m.getRedisClient(connection) 62 }() 63 <-done 64 if recovered != nil { 65 panic(recovered) 66 } 67 return client 68 } 69 70 func (m *Manager) getRedisClient(connection string) redis.UniversalClient { 71 m.mutex.Lock() 72 defer m.mutex.Unlock() 73 client, ok := m.RedisConnections[connection] 74 if ok { 75 client.count++ 76 return client 77 } 78 79 uri := ToRedisURI(connection) 80 client, ok = m.RedisConnections[uri.String()] 81 if ok { 82 client.count++ 83 return client 84 } 85 client = &redisClientHolder{ 86 name: []string{connection, uri.String()}, 87 } 88 89 opts := getRedisOptions(uri) 90 tlsConfig := getRedisTLSOptions(uri) 91 92 clientName := uri.Query().Get("clientname") 93 94 if len(clientName) > 0 { 95 client.name = append(client.name, clientName) 96 } 97 98 switch uri.Scheme { 99 case "redis+sentinels": 100 fallthrough 101 case "rediss+sentinel": 102 opts.TLSConfig = tlsConfig 103 fallthrough 104 case "redis+sentinel": 105 client.UniversalClient = redis.NewFailoverClient(opts.Failover()) 106 case "redis+clusters": 107 fallthrough 108 case "rediss+cluster": 109 opts.TLSConfig = tlsConfig 110 fallthrough 111 case "redis+cluster": 112 client.UniversalClient = redis.NewClusterClient(opts.Cluster()) 113 case "redis+socket": 114 simpleOpts := opts.Simple() 115 simpleOpts.Network = "unix" 116 simpleOpts.Addr = path.Join(uri.Host, uri.Path) 117 client.UniversalClient = redis.NewClient(simpleOpts) 118 case "rediss": 119 opts.TLSConfig = tlsConfig 120 fallthrough 121 case "redis": 122 client.UniversalClient = redis.NewClient(opts.Simple()) 123 default: 124 return nil 125 } 126 127 for _, name := range client.name { 128 m.RedisConnections[name] = client 129 } 130 131 client.count++ 132 133 return client 134 } 135 136 // getRedisOptions pulls various configuration options based on the RedisUri format and converts them to go-redis's 137 // UniversalOptions fields. This function explicitly excludes fields related to TLS configuration, which is 138 // conditionally attached to this options struct before being converted to the specific type for the redis scheme being 139 // used, and only in scenarios where TLS is applicable (e.g. rediss://, redis+clusters://). 140 func getRedisOptions(uri *url.URL) *redis.UniversalOptions { 141 opts := &redis.UniversalOptions{} 142 143 // Handle username/password 144 if password, ok := uri.User.Password(); ok { 145 opts.Password = password 146 // Username does not appear to be handled by redis.Options 147 opts.Username = uri.User.Username() 148 } else if uri.User.Username() != "" { 149 // assume this is the password 150 opts.Password = uri.User.Username() 151 } 152 153 // Now handle the uri query sets 154 for k, v := range uri.Query() { 155 switch replacer.Replace(strings.ToLower(k)) { 156 case "addr": 157 opts.Addrs = append(opts.Addrs, v...) 158 case "addrs": 159 opts.Addrs = append(opts.Addrs, strings.Split(v[0], ",")...) 160 case "username": 161 opts.Username = v[0] 162 case "password": 163 opts.Password = v[0] 164 case "database": 165 fallthrough 166 case "db": 167 opts.DB, _ = strconv.Atoi(v[0]) 168 case "maxretries": 169 opts.MaxRetries, _ = strconv.Atoi(v[0]) 170 case "minretrybackoff": 171 opts.MinRetryBackoff = valToTimeDuration(v) 172 case "maxretrybackoff": 173 opts.MaxRetryBackoff = valToTimeDuration(v) 174 case "timeout": 175 timeout := valToTimeDuration(v) 176 if timeout != 0 { 177 if opts.DialTimeout == 0 { 178 opts.DialTimeout = timeout 179 } 180 if opts.ReadTimeout == 0 { 181 opts.ReadTimeout = timeout 182 } 183 } 184 case "dialtimeout": 185 opts.DialTimeout = valToTimeDuration(v) 186 case "readtimeout": 187 opts.ReadTimeout = valToTimeDuration(v) 188 case "writetimeout": 189 opts.WriteTimeout = valToTimeDuration(v) 190 case "poolsize": 191 opts.PoolSize, _ = strconv.Atoi(v[0]) 192 case "minidleconns": 193 opts.MinIdleConns, _ = strconv.Atoi(v[0]) 194 case "pooltimeout": 195 opts.PoolTimeout = valToTimeDuration(v) 196 case "maxredirects": 197 opts.MaxRedirects, _ = strconv.Atoi(v[0]) 198 case "readonly": 199 opts.ReadOnly, _ = strconv.ParseBool(v[0]) 200 case "routebylatency": 201 opts.RouteByLatency, _ = strconv.ParseBool(v[0]) 202 case "routerandomly": 203 opts.RouteRandomly, _ = strconv.ParseBool(v[0]) 204 case "sentinelmasterid": 205 fallthrough 206 case "mastername": 207 opts.MasterName = v[0] 208 case "sentinelusername": 209 opts.SentinelUsername = v[0] 210 case "sentinelpassword": 211 opts.SentinelPassword = v[0] 212 } 213 } 214 215 if uri.Host != "" { 216 opts.Addrs = append(opts.Addrs, strings.Split(uri.Host, ",")...) 217 } 218 219 // A redis connection string uses the path section of the URI in two different ways. In a TCP-based connection, the 220 // path will be a database index to automatically have the client SELECT. In a Unix socket connection, it will be the 221 // file path. We only want to try to coerce this to the database index when we're not expecting a file path so that 222 // the error log stays clean. 223 if uri.Path != "" && uri.Scheme != "redis+socket" { 224 if db, err := strconv.Atoi(uri.Path[1:]); err == nil { 225 opts.DB = db 226 } else { 227 log.Error("Provided database identifier '%s' is not a valid integer. Gitea will ignore this option.", uri.Path) 228 } 229 } 230 231 return opts 232 } 233 234 // getRedisTlsOptions parses RedisUri TLS configuration parameters and converts them to the go TLS configuration 235 // equivalent fields. 236 func getRedisTLSOptions(uri *url.URL) *tls.Config { 237 tlsConfig := &tls.Config{} 238 239 skipverify := uri.Query().Get("skipverify") 240 241 if len(skipverify) > 0 { 242 skipverify, err := strconv.ParseBool(skipverify) 243 if err == nil { 244 tlsConfig.InsecureSkipVerify = skipverify 245 } 246 } 247 248 insecureskipverify := uri.Query().Get("insecureskipverify") 249 250 if len(insecureskipverify) > 0 { 251 insecureskipverify, err := strconv.ParseBool(insecureskipverify) 252 if err == nil { 253 tlsConfig.InsecureSkipVerify = insecureskipverify 254 } 255 } 256 257 return tlsConfig 258 }