index.js 4.5 KB

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