|
@@ -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
|
|
|
|
|
+}
|