diff --git a/libraries/redis-wrapper/index.js b/libraries/redis-wrapper/index.js index 8fcb52a978..80be830845 100644 --- a/libraries/redis-wrapper/index.js +++ b/libraries/redis-wrapper/index.js @@ -6,7 +6,6 @@ * 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') @@ -17,130 +16,125 @@ const PID = process.pid const RND = crypto.randomBytes(4).toString('hex') let COUNT = 0 -module.exports = RedisSharelatex = { - createClient(opts) { - let client, standardOpts - if (opts == null) { - opts = { port: 6379, host: 'localhost' } +function 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 + } + + 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 = clusterHealthCheckBuilder(client) + _monkeyPatchIoredisExec(client) + } else { + standardOpts = _.clone(opts) + const ioredis = require('ioredis') + client = new ioredis(standardOpts) + _monkeyPatchIoredisExec(client) + client.healthCheck = singleInstanceHealthCheckBuilder(client) + } + return client +} + +const HEARTBEAT_TIMEOUT = 2000 +function singleInstanceHealthCheckBuilder(client) { + const healthCheck = (callback) => _checkClient(client, callback) + return healthCheck +} + +function clusterHealthCheckBuilder(client) { + return singleInstanceHealthCheckBuilder(client) +} + +function _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) + }, 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) } - if (opts.retry_max_delay == null) { - opts.retry_max_delay = 5000 // ms - } - - 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) + // 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) { - 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) - }) + if ( + (reply != null ? reply[0] : undefined) !== healthCheckValue || + (reply != null ? reply[1] : undefined) !== 1 + ) { + return callback(new Error('bad response from redis health check')) } - return multi + return callback() }) - }, + }) +} + +function _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) { @@ -148,3 +142,7 @@ function __guard__(value, transform) { ? transform(value) : undefined } + +module.exports = { + createClient, +} diff --git a/libraries/redis-wrapper/test/unit/src/test.js b/libraries/redis-wrapper/test/unit/src/test.js index 23cea9adb5..fee144c990 100644 --- a/libraries/redis-wrapper/test/unit/src/test.js +++ b/libraries/redis-wrapper/test/unit/src/test.js @@ -126,7 +126,8 @@ describe('index', function () { this.results = [] this.multiOrig = { exec: sinon.stub().yields(null, this.results) } this.client = { multi: sinon.stub().returns(this.multiOrig) } - this.redis._monkeyPatchIoredisExec(this.client) + this.ioredisConstructor.returns(this.client) + this.redis.createClient(this.client) return (this.multi = this.client.multi()) })