diff --git a/libraries/redis-wrapper/index.js b/libraries/redis-wrapper/index.js index da606655be..8fcb52a978 100644 --- a/libraries/redis-wrapper/index.js +++ b/libraries/redis-wrapper/index.js @@ -1,99 +1,150 @@ -_ = require("underscore") -os = require('os') -crypto = require('crypto') +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS103: Rewrite code to no longer use __guard__, or convert again using --optional-chaining + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let RedisSharelatex +const _ = require('underscore') +const os = require('os') +const crypto = require('crypto') -# generate unique values for health check -HOST = os.hostname() -PID = process.pid -RND = crypto.randomBytes(4).toString('hex') -COUNT = 0 +// generate unique values for health check +const HOST = os.hostname() +const PID = process.pid +const RND = crypto.randomBytes(4).toString('hex') +let COUNT = 0 -module.exports = RedisSharelatex = - createClient: (opts = {port: 6379, host: "localhost"})-> - if !opts.retry_max_delay? - opts.retry_max_delay = 5000 # ms - - if opts.cluster? - Redis = require("ioredis") - standardOpts = _.clone(opts) - delete standardOpts.cluster - delete standardOpts.key_schema - client = new Redis.Cluster(opts.cluster, standardOpts) - client.healthCheck = RedisSharelatex.clusterHealthCheckBuilder(client) - RedisSharelatex._monkeyPatchIoredisExec(client) - else - standardOpts = _.clone(opts) - ioredis = require("ioredis") - client = new ioredis(standardOpts) - RedisSharelatex._monkeyPatchIoredisExec(client) - client.healthCheck = RedisSharelatex.singleInstanceHealthCheckBuilder(client) - return client - - HEARTBEAT_TIMEOUT: 2000 - singleInstanceHealthCheckBuilder: (client) -> - healthCheck = (callback) -> - RedisSharelatex._checkClient(client, callback) - return healthCheck - - clusterHealthCheckBuilder: (client) -> - return RedisSharelatex.singleInstanceHealthCheckBuilder(client) - - _checkClient: (client, callback) -> - callback = _.once(callback) - # check the redis connection by storing and retrieving a unique key/value pair - uniqueToken = "host=#{HOST}:pid=#{PID}:random=#{RND}:time=#{Date.now()}:count=#{COUNT++}" - timer = setTimeout () -> - error = new Error("redis client health check timed out #{client?.options?.host}") - console.error { - err: error, - key: client.options?.key # only present for cluster - clientOptions: client.options - uniqueToken: uniqueToken - }, "client timed out" - callback(error) - , RedisSharelatex.HEARTBEAT_TIMEOUT - healthCheckKey = "_redis-wrapper:healthCheckKey:{#{uniqueToken}}" - healthCheckValue = "_redis-wrapper:healthCheckValue:{#{uniqueToken}}" - # set the unique key/value pair - multi = client.multi() - multi.set healthCheckKey, healthCheckValue, "EX", 60 - multi.exec (err, reply) -> - if err? - clearTimeout timer - return callback(err) - # check that we can retrieve the unique key/value pair - multi = client.multi() - multi.get healthCheckKey - multi.del healthCheckKey - multi.exec (err, reply) -> - clearTimeout timer - return callback(err) if err? - return callback(new Error("bad response from redis health check")) if reply?[0] isnt healthCheckValue or reply?[1] isnt 1 - return callback() +module.exports = RedisSharelatex = { + createClient(opts) { + let client, standardOpts + if (opts == null) { + opts = { port: 6379, host: 'localhost' } + } + if (opts.retry_max_delay == null) { + opts.retry_max_delay = 5000 // ms + } - _monkeyPatchIoredisExec: (client) -> - _multi = client.multi - client.multi = (args...) -> - multi = _multi.call(client, args...) - _exec = multi.exec - multi.exec = (callback = () ->) -> - _exec.call multi, (error, result) -> - # ioredis exec returns an results like: - # [ [null, 42], [null, "foo"] ] - # where the first entries in each 2-tuple are - # presumably errors for each individual command, - # and the second entry is the result. We need to transform - # this into the same result as the old redis driver: - # [ 42, "foo" ] - filtered_result = [] - for entry in result or [] - if entry[0]? - return callback(entry[0]) - else - filtered_result.push entry[1] - callback error, filtered_result - return multi - - - + if (opts.cluster != null) { + const Redis = require('ioredis') + standardOpts = _.clone(opts) + delete standardOpts.cluster + delete standardOpts.key_schema + client = new Redis.Cluster(opts.cluster, standardOpts) + client.healthCheck = RedisSharelatex.clusterHealthCheckBuilder(client) + RedisSharelatex._monkeyPatchIoredisExec(client) + } else { + standardOpts = _.clone(opts) + const ioredis = require('ioredis') + client = new ioredis(standardOpts) + RedisSharelatex._monkeyPatchIoredisExec(client) + client.healthCheck = RedisSharelatex.singleInstanceHealthCheckBuilder( + client + ) + } + return client + }, + HEARTBEAT_TIMEOUT: 2000, + singleInstanceHealthCheckBuilder(client) { + const healthCheck = (callback) => + RedisSharelatex._checkClient(client, callback) + return healthCheck + }, + + clusterHealthCheckBuilder(client) { + return RedisSharelatex.singleInstanceHealthCheckBuilder(client) + }, + + _checkClient(client, callback) { + callback = _.once(callback) + // check the redis connection by storing and retrieving a unique key/value pair + const uniqueToken = `host=${HOST}:pid=${PID}:random=${RND}:time=${Date.now()}:count=${COUNT++}` + const timer = setTimeout(function () { + const error = new Error( + `redis client health check timed out ${__guard__( + client != null ? client.options : undefined, + (x) => x.host + )}` + ) + console.error( + { + err: error, + key: client.options != null ? client.options.key : undefined, // only present for cluster + clientOptions: client.options, + uniqueToken, + }, + 'client timed out' + ) + return callback(error) + }, RedisSharelatex.HEARTBEAT_TIMEOUT) + const healthCheckKey = `_redis-wrapper:healthCheckKey:{${uniqueToken}}` + const healthCheckValue = `_redis-wrapper:healthCheckValue:{${uniqueToken}}` + // set the unique key/value pair + let multi = client.multi() + multi.set(healthCheckKey, healthCheckValue, 'EX', 60) + return multi.exec(function (err, reply) { + if (err != null) { + clearTimeout(timer) + return callback(err) + } + // check that we can retrieve the unique key/value pair + multi = client.multi() + multi.get(healthCheckKey) + multi.del(healthCheckKey) + return multi.exec(function (err, reply) { + clearTimeout(timer) + if (err != null) { + return callback(err) + } + if ( + (reply != null ? reply[0] : undefined) !== healthCheckValue || + (reply != null ? reply[1] : undefined) !== 1 + ) { + return callback(new Error('bad response from redis health check')) + } + return callback() + }) + }) + }, + + _monkeyPatchIoredisExec(client) { + const _multi = client.multi + return (client.multi = function (...args) { + const multi = _multi.call(client, ...Array.from(args)) + const _exec = multi.exec + multi.exec = function (callback) { + if (callback == null) { + callback = function () {} + } + return _exec.call(multi, function (error, result) { + // ioredis exec returns an results like: + // [ [null, 42], [null, "foo"] ] + // where the first entries in each 2-tuple are + // presumably errors for each individual command, + // and the second entry is the result. We need to transform + // this into the same result as the old redis driver: + // [ 42, "foo" ] + const filtered_result = [] + for (const entry of Array.from(result || [])) { + if (entry[0] != null) { + return callback(entry[0]) + } else { + filtered_result.push(entry[1]) + } + } + return callback(error, filtered_result) + }) + } + return multi + }) + }, +} + +function __guard__(value, transform) { + return typeof value !== 'undefined' && value !== null + ? transform(value) + : undefined +}