2020-09-28 00:09:46 +03:00
// Copyright 2020 The Gitea Authors. All rights reserved.
2022-11-27 21:20:29 +03:00
// SPDX-License-Identifier: MIT
2020-09-28 00:09:46 +03:00
package nosql
import (
"crypto/tls"
2022-03-30 22:12:02 +03:00
"net/url"
2020-09-28 00:09:46 +03:00
"path"
2022-03-31 20:01:43 +03:00
"runtime/pprof"
2020-09-28 00:09:46 +03:00
"strconv"
"strings"
2022-03-30 22:12:02 +03:00
"code.gitea.io/gitea/modules/log"
2023-04-14 01:41:04 +03:00
"github.com/redis/go-redis/v9"
2020-09-28 00:09:46 +03:00
)
var replacer = strings . NewReplacer ( "_" , "" , "-" , "" )
// CloseRedisClient closes a redis client
func ( m * Manager ) CloseRedisClient ( connection string ) error {
m . mutex . Lock ( )
defer m . mutex . Unlock ( )
client , ok := m . RedisConnections [ connection ]
if ! ok {
connection = ToRedisURI ( connection ) . String ( )
client , ok = m . RedisConnections [ connection ]
}
if ! ok {
return nil
}
client . count --
if client . count > 0 {
return nil
}
for _ , name := range client . name {
delete ( m . RedisConnections , name )
}
return client . UniversalClient . Close ( )
}
// GetRedisClient gets a redis client for a particular connection
2022-03-31 20:01:43 +03:00
func ( m * Manager ) GetRedisClient ( connection string ) ( client redis . UniversalClient ) {
// Because we want associate any goroutines created by this call to the main nosqldb context we need to
// wrap this in a goroutine labelled with the nosqldb context
done := make ( chan struct { } )
var recovered interface { }
go func ( ) {
defer func ( ) {
recovered = recover ( )
if recovered != nil {
log . Critical ( "PANIC during GetRedisClient: %v\nStacktrace: %s" , recovered , log . Stack ( 2 ) )
}
close ( done )
} ( )
pprof . SetGoroutineLabels ( m . ctx )
client = m . getRedisClient ( connection )
} ( )
<- done
if recovered != nil {
panic ( recovered )
}
2022-06-20 13:02:49 +03:00
return client
2022-03-31 20:01:43 +03:00
}
func ( m * Manager ) getRedisClient ( connection string ) redis . UniversalClient {
2020-09-28 00:09:46 +03:00
m . mutex . Lock ( )
defer m . mutex . Unlock ( )
client , ok := m . RedisConnections [ connection ]
if ok {
client . count ++
return client
}
uri := ToRedisURI ( connection )
client , ok = m . RedisConnections [ uri . String ( ) ]
if ok {
client . count ++
return client
}
client = & redisClientHolder {
name : [ ] string { connection , uri . String ( ) } ,
}
2022-03-30 22:12:02 +03:00
opts := getRedisOptions ( uri )
tlsConfig := getRedisTLSOptions ( uri )
clientName := uri . Query ( ) . Get ( "clientname" )
if len ( clientName ) > 0 {
client . name = append ( client . name , clientName )
}
switch uri . Scheme {
case "redis+sentinels" :
fallthrough
case "rediss+sentinel" :
opts . TLSConfig = tlsConfig
fallthrough
case "redis+sentinel" :
client . UniversalClient = redis . NewFailoverClient ( opts . Failover ( ) )
case "redis+clusters" :
fallthrough
case "rediss+cluster" :
opts . TLSConfig = tlsConfig
fallthrough
case "redis+cluster" :
client . UniversalClient = redis . NewClusterClient ( opts . Cluster ( ) )
case "redis+socket" :
simpleOpts := opts . Simple ( )
simpleOpts . Network = "unix"
simpleOpts . Addr = path . Join ( uri . Host , uri . Path )
client . UniversalClient = redis . NewClient ( simpleOpts )
case "rediss" :
opts . TLSConfig = tlsConfig
fallthrough
case "redis" :
client . UniversalClient = redis . NewClient ( opts . Simple ( ) )
default :
return nil
}
for _ , name := range client . name {
m . RedisConnections [ name ] = client
}
client . count ++
return client
}
// getRedisOptions pulls various configuration options based on the RedisUri format and converts them to go-redis's
// UniversalOptions fields. This function explicitly excludes fields related to TLS configuration, which is
// conditionally attached to this options struct before being converted to the specific type for the redis scheme being
// used, and only in scenarios where TLS is applicable (e.g. rediss://, redis+clusters://).
func getRedisOptions ( uri * url . URL ) * redis . UniversalOptions {
2020-09-28 00:09:46 +03:00
opts := & redis . UniversalOptions { }
// Handle username/password
if password , ok := uri . User . Password ( ) ; ok {
opts . Password = password
// Username does not appear to be handled by redis.Options
opts . Username = uri . User . Username ( )
} else if uri . User . Username ( ) != "" {
// assume this is the password
opts . Password = uri . User . Username ( )
}
// Now handle the uri query sets
for k , v := range uri . Query ( ) {
switch replacer . Replace ( strings . ToLower ( k ) ) {
case "addr" :
opts . Addrs = append ( opts . Addrs , v ... )
case "addrs" :
opts . Addrs = append ( opts . Addrs , strings . Split ( v [ 0 ] , "," ) ... )
case "username" :
opts . Username = v [ 0 ]
case "password" :
opts . Password = v [ 0 ]
case "database" :
fallthrough
case "db" :
opts . DB , _ = strconv . Atoi ( v [ 0 ] )
case "maxretries" :
opts . MaxRetries , _ = strconv . Atoi ( v [ 0 ] )
case "minretrybackoff" :
opts . MinRetryBackoff = valToTimeDuration ( v )
case "maxretrybackoff" :
opts . MaxRetryBackoff = valToTimeDuration ( v )
case "timeout" :
timeout := valToTimeDuration ( v )
if timeout != 0 {
if opts . DialTimeout == 0 {
opts . DialTimeout = timeout
}
if opts . ReadTimeout == 0 {
opts . ReadTimeout = timeout
}
}
case "dialtimeout" :
opts . DialTimeout = valToTimeDuration ( v )
case "readtimeout" :
opts . ReadTimeout = valToTimeDuration ( v )
case "writetimeout" :
opts . WriteTimeout = valToTimeDuration ( v )
case "poolsize" :
opts . PoolSize , _ = strconv . Atoi ( v [ 0 ] )
case "minidleconns" :
opts . MinIdleConns , _ = strconv . Atoi ( v [ 0 ] )
case "pooltimeout" :
opts . PoolTimeout = valToTimeDuration ( v )
case "maxredirects" :
opts . MaxRedirects , _ = strconv . Atoi ( v [ 0 ] )
case "readonly" :
opts . ReadOnly , _ = strconv . ParseBool ( v [ 0 ] )
case "routebylatency" :
opts . RouteByLatency , _ = strconv . ParseBool ( v [ 0 ] )
case "routerandomly" :
opts . RouteRandomly , _ = strconv . ParseBool ( v [ 0 ] )
case "sentinelmasterid" :
fallthrough
case "mastername" :
opts . MasterName = v [ 0 ]
2022-03-30 22:12:02 +03:00
case "sentinelusername" :
opts . SentinelUsername = v [ 0 ]
case "sentinelpassword" :
opts . SentinelPassword = v [ 0 ]
2020-09-28 00:09:46 +03:00
}
}
2022-03-30 22:12:02 +03:00
if uri . Host != "" {
opts . Addrs = append ( opts . Addrs , strings . Split ( uri . Host , "," ) ... )
}
2020-09-28 00:09:46 +03:00
2022-03-30 22:12:02 +03:00
// A redis connection string uses the path section of the URI in two different ways. In a TCP-based connection, the
// path will be a database index to automatically have the client SELECT. In a Unix socket connection, it will be the
// file path. We only want to try to coerce this to the database index when we're not expecting a file path so that
// the error log stays clean.
if uri . Path != "" && uri . Scheme != "redis+socket" {
if db , err := strconv . Atoi ( uri . Path [ 1 : ] ) ; err == nil {
opts . DB = db
} else {
log . Error ( "Provided database identifier '%s' is not a valid integer. Gitea will ignore this option." , uri . Path )
2020-09-28 00:09:46 +03:00
}
}
2022-03-30 22:12:02 +03:00
return opts
}
// getRedisTlsOptions parses RedisUri TLS configuration parameters and converts them to the go TLS configuration
// equivalent fields.
func getRedisTLSOptions ( uri * url . URL ) * tls . Config {
tlsConfig := & tls . Config { }
skipverify := uri . Query ( ) . Get ( "skipverify" )
if len ( skipverify ) > 0 {
skipverify , err := strconv . ParseBool ( skipverify )
2022-08-29 17:38:49 +03:00
if err == nil {
2022-03-30 22:12:02 +03:00
tlsConfig . InsecureSkipVerify = skipverify
}
2020-09-28 00:09:46 +03:00
}
2022-03-30 22:12:02 +03:00
insecureskipverify := uri . Query ( ) . Get ( "insecureskipverify" )
2020-09-28 00:09:46 +03:00
2022-03-30 22:12:02 +03:00
if len ( insecureskipverify ) > 0 {
insecureskipverify , err := strconv . ParseBool ( insecureskipverify )
2022-08-29 17:38:49 +03:00
if err == nil {
2022-03-30 22:12:02 +03:00
tlsConfig . InsecureSkipVerify = insecureskipverify
}
}
return tlsConfig
2020-09-28 00:09:46 +03:00
}