選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。

manager_redis.go 6.8KB

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