2024-11-08 05:21:56 -05:00
|
|
|
const crypto = require('node:crypto')
|
|
|
|
const os = require('node:os')
|
|
|
|
const { promisify } = require('node:util')
|
2020-11-10 05:34:06 -05:00
|
|
|
|
|
|
|
const Redis = require('ioredis')
|
|
|
|
|
|
|
|
const {
|
|
|
|
RedisHealthCheckTimedOut,
|
|
|
|
RedisHealthCheckWriteError,
|
|
|
|
RedisHealthCheckVerifyError,
|
|
|
|
} = require('./Errors')
|
|
|
|
|
|
|
|
const HEARTBEAT_TIMEOUT = 2000
|
2019-09-19 10:05:32 -04:00
|
|
|
|
2020-11-09 12:12:51 -05:00
|
|
|
// generate unique values for health check
|
|
|
|
const HOST = os.hostname()
|
|
|
|
const PID = process.pid
|
|
|
|
const RND = crypto.randomBytes(4).toString('hex')
|
|
|
|
let COUNT = 0
|
2014-09-26 09:46:23 -04:00
|
|
|
|
2020-11-09 12:15:36 -05:00
|
|
|
function createClient(opts) {
|
2020-11-10 05:34:06 -05:00
|
|
|
const standardOpts = Object.assign({}, opts)
|
|
|
|
delete standardOpts.key_schema
|
|
|
|
|
|
|
|
if (standardOpts.retry_max_delay == null) {
|
|
|
|
standardOpts.retry_max_delay = 5000 // ms
|
2020-11-09 12:15:36 -05:00
|
|
|
}
|
2019-09-19 10:05:32 -04:00
|
|
|
|
2020-11-11 06:31:44 -05:00
|
|
|
if (standardOpts.endpoints) {
|
|
|
|
throw new Error(
|
|
|
|
'@overleaf/redis-wrapper: redis-sentinel is no longer supported'
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2020-11-10 05:34:06 -05:00
|
|
|
let client
|
2020-11-11 06:35:33 -05:00
|
|
|
if (standardOpts.cluster) {
|
2020-11-09 12:15:36 -05:00
|
|
|
delete standardOpts.cluster
|
|
|
|
client = new Redis.Cluster(opts.cluster, standardOpts)
|
|
|
|
} else {
|
2020-11-10 05:34:06 -05:00
|
|
|
client = new Redis(standardOpts)
|
|
|
|
}
|
|
|
|
monkeyPatchIoRedisExec(client)
|
2021-12-16 04:04:32 -05:00
|
|
|
client.healthCheck = callback => {
|
2020-11-10 05:34:06 -05:00
|
|
|
if (callback) {
|
|
|
|
// callback based invocation
|
|
|
|
healthCheck(client).then(callback).catch(callback)
|
|
|
|
} else {
|
|
|
|
// Promise based invocation
|
|
|
|
return healthCheck(client)
|
|
|
|
}
|
2020-11-09 12:15:36 -05:00
|
|
|
}
|
|
|
|
return client
|
|
|
|
}
|
2014-11-19 18:18:56 -05:00
|
|
|
|
2020-11-10 05:34:06 -05:00
|
|
|
async function healthCheck(client) {
|
|
|
|
// 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++}`
|
2020-11-09 12:12:51 -05:00
|
|
|
|
2020-11-10 05:34:06 -05:00
|
|
|
// o-error context
|
|
|
|
const context = {
|
|
|
|
uniqueToken,
|
|
|
|
stage: 'add context for a timeout',
|
|
|
|
}
|
|
|
|
|
|
|
|
await runWithTimeout({
|
|
|
|
runner: runCheck(client, uniqueToken, context),
|
|
|
|
timeout: HEARTBEAT_TIMEOUT,
|
|
|
|
context,
|
|
|
|
})
|
2020-11-09 12:15:36 -05:00
|
|
|
}
|
2020-11-09 12:12:51 -05:00
|
|
|
|
2020-11-10 05:34:06 -05:00
|
|
|
async function runCheck(client, uniqueToken, context) {
|
2020-11-09 12:15:36 -05:00
|
|
|
const healthCheckKey = `_redis-wrapper:healthCheckKey:{${uniqueToken}}`
|
|
|
|
const healthCheckValue = `_redis-wrapper:healthCheckValue:{${uniqueToken}}`
|
2020-11-10 05:34:06 -05:00
|
|
|
|
2020-11-09 12:15:36 -05:00
|
|
|
// set the unique key/value pair
|
2020-11-10 05:34:06 -05:00
|
|
|
context.stage = 'write'
|
|
|
|
const writeAck = await client
|
|
|
|
.set(healthCheckKey, healthCheckValue, 'EX', 60)
|
2021-12-16 04:04:32 -05:00
|
|
|
.catch(err => {
|
2020-11-10 05:34:06 -05:00
|
|
|
throw new RedisHealthCheckWriteError('write errored', context, err)
|
|
|
|
})
|
|
|
|
if (writeAck !== 'OK') {
|
|
|
|
context.writeAck = writeAck
|
|
|
|
throw new RedisHealthCheckWriteError('write failed', context)
|
|
|
|
}
|
|
|
|
|
|
|
|
// check that we can retrieve the unique key/value pair
|
|
|
|
context.stage = 'verify'
|
|
|
|
const [roundTrippedHealthCheckValue, deleteAck] = await client
|
|
|
|
.multi()
|
|
|
|
.get(healthCheckKey)
|
|
|
|
.del(healthCheckKey)
|
|
|
|
.exec()
|
2021-12-16 04:04:32 -05:00
|
|
|
.catch(err => {
|
2020-11-10 05:34:06 -05:00
|
|
|
throw new RedisHealthCheckVerifyError(
|
|
|
|
'read/delete errored',
|
|
|
|
context,
|
|
|
|
err
|
|
|
|
)
|
|
|
|
})
|
|
|
|
if (roundTrippedHealthCheckValue !== healthCheckValue) {
|
|
|
|
context.roundTrippedHealthCheckValue = roundTrippedHealthCheckValue
|
|
|
|
throw new RedisHealthCheckVerifyError('read failed', context)
|
|
|
|
}
|
|
|
|
if (deleteAck !== 1) {
|
|
|
|
context.deleteAck = deleteAck
|
|
|
|
throw new RedisHealthCheckVerifyError('delete failed', context)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function unwrapMultiResult(result, callback) {
|
|
|
|
// ioredis exec returns a 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" ]
|
|
|
|
//
|
|
|
|
// Basically reverse:
|
|
|
|
// https://github.com/luin/ioredis/blob/v4.17.3/lib/utils/index.ts#L75-L92
|
|
|
|
const filteredResult = []
|
|
|
|
for (const [err, value] of result || []) {
|
|
|
|
if (err) {
|
2020-11-09 12:15:36 -05:00
|
|
|
return callback(err)
|
2020-11-10 05:34:06 -05:00
|
|
|
} else {
|
|
|
|
filteredResult.push(value)
|
2020-11-09 12:15:36 -05:00
|
|
|
}
|
2020-11-10 05:34:06 -05:00
|
|
|
}
|
|
|
|
callback(null, filteredResult)
|
2020-11-09 12:15:36 -05:00
|
|
|
}
|
2020-11-10 05:34:06 -05:00
|
|
|
const unwrapMultiResultPromisified = promisify(unwrapMultiResult)
|
2020-11-09 12:12:51 -05:00
|
|
|
|
2020-11-10 05:34:06 -05:00
|
|
|
function monkeyPatchIoRedisExec(client) {
|
2020-11-09 12:15:36 -05:00
|
|
|
const _multi = client.multi
|
2020-11-10 05:34:06 -05:00
|
|
|
client.multi = function () {
|
|
|
|
const multi = _multi.apply(client, arguments)
|
2020-11-09 12:15:36 -05:00
|
|
|
const _exec = multi.exec
|
2021-12-16 04:04:32 -05:00
|
|
|
multi.exec = callback => {
|
2020-11-10 05:34:06 -05:00
|
|
|
if (callback) {
|
|
|
|
// callback based invocation
|
|
|
|
_exec.call(multi, (error, result) => {
|
|
|
|
// The command can fail all-together due to syntax errors
|
|
|
|
if (error) return callback(error)
|
|
|
|
unwrapMultiResult(result, callback)
|
|
|
|
})
|
|
|
|
} else {
|
|
|
|
// Promise based invocation
|
|
|
|
return _exec.call(multi).then(unwrapMultiResultPromisified)
|
2020-11-09 12:12:51 -05:00
|
|
|
}
|
2020-11-09 12:15:36 -05:00
|
|
|
}
|
|
|
|
return multi
|
2020-11-10 05:34:06 -05:00
|
|
|
}
|
2020-11-09 12:12:51 -05:00
|
|
|
}
|
|
|
|
|
2020-11-10 05:34:06 -05:00
|
|
|
async function runWithTimeout({ runner, timeout, context }) {
|
|
|
|
let healthCheckDeadline
|
|
|
|
await Promise.race([
|
|
|
|
new Promise((resolve, reject) => {
|
|
|
|
healthCheckDeadline = setTimeout(() => {
|
|
|
|
// attach the timeout when hitting the timeout only
|
|
|
|
context.timeout = timeout
|
|
|
|
reject(new RedisHealthCheckTimedOut('timeout', context))
|
|
|
|
}, timeout)
|
|
|
|
}),
|
|
|
|
runner.finally(() => clearTimeout(healthCheckDeadline)),
|
|
|
|
])
|
2020-11-09 12:12:51 -05:00
|
|
|
}
|
2020-11-09 12:15:36 -05:00
|
|
|
|
|
|
|
module.exports = {
|
|
|
|
createClient,
|
|
|
|
}
|