RedisBackend.coffee 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205
  1. Settings = require "settings-sharelatex"
  2. async = require "async"
  3. _ = require "underscore"
  4. logger = require "logger-sharelatex"
  5. Metrics = require "metrics-sharelatex"
  6. class Client
  7. constructor: (@clients) ->
  8. @SECONDARY_TIMEOUT = 600
  9. @HEARTBEAT_TIMEOUT = 2000
  10. multi: () ->
  11. return new MultiClient(
  12. @clients.map (client) -> {
  13. rclient: client.rclient.multi()
  14. key_schema: client.key_schema
  15. primary: client.primary
  16. driver: client.driver
  17. }
  18. )
  19. healthCheck: (callback) ->
  20. jobs = @clients.map (client) =>
  21. (cb) => @_healthCheckClient(client, cb)
  22. async.parallel jobs, callback
  23. _healthCheckClient: (client, callback) ->
  24. if client.driver == "ioredis"
  25. @_healthCheckClusterClient(client, callback)
  26. else
  27. @_healthCheckNodeRedisClient(client, callback)
  28. _healthCheckNodeRedisClient: (client, callback) ->
  29. client.healthCheck ?= require("redis-sharelatex").activeHealthCheckRedis(Settings.redis.web)
  30. if client.healthCheck.isAlive()
  31. return callback()
  32. else
  33. return callback(new Error("node-redis client failed health check"))
  34. _healthCheckClusterClient: (client, callback) ->
  35. jobs = client.rclient.nodes("all").map (n) =>
  36. (cb) => @_checkNode(n, cb)
  37. async.parallel jobs, callback
  38. _checkNode: (node, _callback) ->
  39. callback = (args...) ->
  40. _callback(args...)
  41. _callback = () ->
  42. timer = setTimeout () ->
  43. error = new Error("ioredis node ping check timed out")
  44. logger.error {err: error, key: node.options.key}, "node timed out"
  45. callback(error)
  46. , @HEARTBEAT_TIMEOUT
  47. node.ping (err) ->
  48. clearTimeout timer
  49. callback(err)
  50. class MultiClient
  51. constructor: (@clients) ->
  52. @SECONDARY_TIMEOUT = 600
  53. exec: (callback) ->
  54. primaryError = null
  55. primaryResult = null
  56. jobs = @clients.map (client) =>
  57. (cb) =>
  58. cb = _.once(cb)
  59. timer = new Metrics.Timer("redis.#{client.driver}.exec")
  60. timeout = null
  61. if !client.primary
  62. timeout = setTimeout () ->
  63. logger.error {err: new Error("#{client.driver} backend timed out")}, "backend timed out"
  64. cb()
  65. , @SECONDARY_TIMEOUT
  66. client.rclient.exec (error, result) =>
  67. timer.done()
  68. if client.driver == "ioredis"
  69. # ioredis returns an results like:
  70. # [ [null, 42], [null, "foo"] ]
  71. # where the first entries in each 2-tuple are
  72. # presumably errors for each individual command,
  73. # and the second entry is the result. We need to transform
  74. # this into the same result as the old redis driver:
  75. # [ 42, "foo" ]
  76. filtered_result = []
  77. for entry in result or []
  78. if entry[0]?
  79. return cb(entry[0])
  80. else
  81. filtered_result.push entry[1]
  82. result = filtered_result
  83. if client.primary
  84. primaryError = error
  85. primaryResult = result
  86. if timeout?
  87. clearTimeout(timeout)
  88. cb(error, result)
  89. async.parallel jobs, (error, results) ->
  90. if error?
  91. logger.error {err: error}, "error in redis backend"
  92. else
  93. compareResults(results, "exec")
  94. callback(primaryError, primaryResult)
  95. COMMANDS = {
  96. "get": 0,
  97. "smembers": 0,
  98. "set": 0,
  99. "srem": 0,
  100. "sadd": 0,
  101. "del": 0,
  102. "lrange": 0,
  103. "llen": 0,
  104. "rpush": 0,
  105. "expire": 0,
  106. "ltrim": 0,
  107. "incr": 0,
  108. "eval": 2
  109. }
  110. for command, key_pos of COMMANDS
  111. do (command, key_pos) ->
  112. Client.prototype[command] = (args..., callback) ->
  113. primaryError = null
  114. primaryResult = []
  115. jobs = @clients.map (client) =>
  116. (cb) =>
  117. cb = _.once(cb)
  118. key_builder = args[key_pos]
  119. key = key_builder(client.key_schema)
  120. args_with_key = args.slice(0)
  121. args_with_key[key_pos] = key
  122. timer = new Metrics.Timer("redis.#{client.driver}.#{command}")
  123. timeout = null
  124. if !client.primary
  125. timeout = setTimeout () ->
  126. logger.error {err: new Error("#{client.driver} backend timed out")}, "backend timed out"
  127. cb()
  128. , @SECONDARY_TIMEOUT
  129. client.rclient[command] args_with_key..., (error, result...) =>
  130. timer.done()
  131. if client.primary
  132. primaryError = error
  133. primaryResult = result
  134. if timeout?
  135. clearTimeout(timeout)
  136. cb(error, result...)
  137. async.parallel jobs, (error, results) ->
  138. if error?
  139. logger.error {err: error}, "error in redis backend"
  140. else
  141. compareResults(results, command)
  142. callback(primaryError, primaryResult...)
  143. MultiClient.prototype[command] = (args...) ->
  144. for client in @clients
  145. key_builder = args[key_pos]
  146. key = key_builder(client.key_schema)
  147. args_with_key = args.slice(0)
  148. args_with_key[key_pos] = key
  149. client.rclient[command] args_with_key...
  150. compareResults = (results, command) ->
  151. return if results.length < 2
  152. first = results[0]
  153. if command == "smembers" and first?
  154. first = first.slice().sort()
  155. for result in results.slice(1)
  156. if command == "smembers" and result?
  157. result = result.slice().sort()
  158. if not _.isEqual(first, result)
  159. logger.error results: results, "redis backend conflict"
  160. Metrics.inc "backend-conflict"
  161. else
  162. Metrics.inc "backend-match"
  163. module.exports =
  164. createClient: () ->
  165. client_configs = Settings.redis.documentupdater
  166. unless client_configs instanceof Array
  167. client_configs.primary = true
  168. client_configs = [client_configs]
  169. clients = client_configs.map (config) ->
  170. if config.cluster?
  171. Redis = require("ioredis")
  172. rclient = new Redis.Cluster(config.cluster)
  173. driver = "ioredis"
  174. else
  175. redis_config = {}
  176. for key in ["host", "port", "password", "endpoints", "masterName"]
  177. if config[key]?
  178. redis_config[key] = config[key]
  179. rclient = require("redis-sharelatex").createClient(redis_config)
  180. driver = "noderedis"
  181. return {
  182. rclient: rclient
  183. key_schema: config.key_schema
  184. primary: config.primary
  185. driver: driver
  186. }
  187. return new Client(clients)