index.js 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150
  1. /*
  2. * decaffeinate suggestions:
  3. * DS101: Remove unnecessary use of Array.from
  4. * DS102: Remove unnecessary code created because of implicit returns
  5. * DS103: Rewrite code to no longer use __guard__, or convert again using --optional-chaining
  6. * DS207: Consider shorter variations of null checks
  7. * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  8. */
  9. let RedisSharelatex
  10. const _ = require('underscore')
  11. const os = require('os')
  12. const crypto = require('crypto')
  13. // generate unique values for health check
  14. const HOST = os.hostname()
  15. const PID = process.pid
  16. const RND = crypto.randomBytes(4).toString('hex')
  17. let COUNT = 0
  18. module.exports = RedisSharelatex = {
  19. createClient(opts) {
  20. let client, standardOpts
  21. if (opts == null) {
  22. opts = { port: 6379, host: 'localhost' }
  23. }
  24. if (opts.retry_max_delay == null) {
  25. opts.retry_max_delay = 5000 // ms
  26. }
  27. if (opts.cluster != null) {
  28. const Redis = require('ioredis')
  29. standardOpts = _.clone(opts)
  30. delete standardOpts.cluster
  31. delete standardOpts.key_schema
  32. client = new Redis.Cluster(opts.cluster, standardOpts)
  33. client.healthCheck = RedisSharelatex.clusterHealthCheckBuilder(client)
  34. RedisSharelatex._monkeyPatchIoredisExec(client)
  35. } else {
  36. standardOpts = _.clone(opts)
  37. const ioredis = require('ioredis')
  38. client = new ioredis(standardOpts)
  39. RedisSharelatex._monkeyPatchIoredisExec(client)
  40. client.healthCheck = RedisSharelatex.singleInstanceHealthCheckBuilder(
  41. client
  42. )
  43. }
  44. return client
  45. },
  46. HEARTBEAT_TIMEOUT: 2000,
  47. singleInstanceHealthCheckBuilder(client) {
  48. const healthCheck = (callback) =>
  49. RedisSharelatex._checkClient(client, callback)
  50. return healthCheck
  51. },
  52. clusterHealthCheckBuilder(client) {
  53. return RedisSharelatex.singleInstanceHealthCheckBuilder(client)
  54. },
  55. _checkClient(client, callback) {
  56. callback = _.once(callback)
  57. // check the redis connection by storing and retrieving a unique key/value pair
  58. const uniqueToken = `host=${HOST}:pid=${PID}:random=${RND}:time=${Date.now()}:count=${COUNT++}`
  59. const timer = setTimeout(function () {
  60. const error = new Error(
  61. `redis client health check timed out ${__guard__(
  62. client != null ? client.options : undefined,
  63. (x) => x.host
  64. )}`
  65. )
  66. console.error(
  67. {
  68. err: error,
  69. key: client.options != null ? client.options.key : undefined, // only present for cluster
  70. clientOptions: client.options,
  71. uniqueToken,
  72. },
  73. 'client timed out'
  74. )
  75. return callback(error)
  76. }, RedisSharelatex.HEARTBEAT_TIMEOUT)
  77. const healthCheckKey = `_redis-wrapper:healthCheckKey:{${uniqueToken}}`
  78. const healthCheckValue = `_redis-wrapper:healthCheckValue:{${uniqueToken}}`
  79. // set the unique key/value pair
  80. let multi = client.multi()
  81. multi.set(healthCheckKey, healthCheckValue, 'EX', 60)
  82. return multi.exec(function (err, reply) {
  83. if (err != null) {
  84. clearTimeout(timer)
  85. return callback(err)
  86. }
  87. // check that we can retrieve the unique key/value pair
  88. multi = client.multi()
  89. multi.get(healthCheckKey)
  90. multi.del(healthCheckKey)
  91. return multi.exec(function (err, reply) {
  92. clearTimeout(timer)
  93. if (err != null) {
  94. return callback(err)
  95. }
  96. if (
  97. (reply != null ? reply[0] : undefined) !== healthCheckValue ||
  98. (reply != null ? reply[1] : undefined) !== 1
  99. ) {
  100. return callback(new Error('bad response from redis health check'))
  101. }
  102. return callback()
  103. })
  104. })
  105. },
  106. _monkeyPatchIoredisExec(client) {
  107. const _multi = client.multi
  108. return (client.multi = function (...args) {
  109. const multi = _multi.call(client, ...Array.from(args))
  110. const _exec = multi.exec
  111. multi.exec = function (callback) {
  112. if (callback == null) {
  113. callback = function () {}
  114. }
  115. return _exec.call(multi, function (error, result) {
  116. // ioredis exec returns an results like:
  117. // [ [null, 42], [null, "foo"] ]
  118. // where the first entries in each 2-tuple are
  119. // presumably errors for each individual command,
  120. // and the second entry is the result. We need to transform
  121. // this into the same result as the old redis driver:
  122. // [ 42, "foo" ]
  123. const filtered_result = []
  124. for (const entry of Array.from(result || [])) {
  125. if (entry[0] != null) {
  126. return callback(entry[0])
  127. } else {
  128. filtered_result.push(entry[1])
  129. }
  130. }
  131. return callback(error, filtered_result)
  132. })
  133. }
  134. return multi
  135. })
  136. },
  137. }
  138. function __guard__(value, transform) {
  139. return typeof value !== 'undefined' && value !== null
  140. ? transform(value)
  141. : undefined
  142. }