This repository has been archived by the owner on Aug 6, 2021. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 4
/
index.js
173 lines (155 loc) · 4.77 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
const crypto = require('crypto')
const os = require('os')
const { promisify } = require('util')
const Redis = require('ioredis')
const {
RedisHealthCheckTimedOut,
RedisHealthCheckWriteError,
RedisHealthCheckVerifyError,
} = require('./Errors')
const HEARTBEAT_TIMEOUT = 2000
// generate unique values for health check
const HOST = os.hostname()
const PID = process.pid
const RND = crypto.randomBytes(4).toString('hex')
let COUNT = 0
function createClient(opts) {
const standardOpts = Object.assign({}, opts)
delete standardOpts.key_schema
if (standardOpts.retry_max_delay == null) {
standardOpts.retry_max_delay = 5000 // ms
}
if (standardOpts.endpoints) {
throw new Error(
'@overleaf/redis-wrapper: redis-sentinel is no longer supported'
)
}
let client
if (standardOpts.cluster) {
delete standardOpts.cluster
client = new Redis.Cluster(opts.cluster, standardOpts)
} else {
client = new Redis(standardOpts)
}
monkeyPatchIoRedisExec(client)
client.healthCheck = (callback) => {
if (callback) {
// callback based invocation
healthCheck(client).then(callback).catch(callback)
} else {
// Promise based invocation
return healthCheck(client)
}
}
return client
}
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++}`
// o-error context
const context = {
uniqueToken,
stage: 'add context for a timeout',
}
await runWithTimeout({
runner: runCheck(client, uniqueToken, context),
timeout: HEARTBEAT_TIMEOUT,
context,
})
}
async function runCheck(client, uniqueToken, context) {
const healthCheckKey = `_redis-wrapper:healthCheckKey:{${uniqueToken}}`
const healthCheckValue = `_redis-wrapper:healthCheckValue:{${uniqueToken}}`
// set the unique key/value pair
context.stage = 'write'
const writeAck = await client
.set(healthCheckKey, healthCheckValue, 'EX', 60)
.catch((err) => {
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()
.catch((err) => {
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) {
return callback(err)
} else {
filteredResult.push(value)
}
}
callback(null, filteredResult)
}
const unwrapMultiResultPromisified = promisify(unwrapMultiResult)
function monkeyPatchIoRedisExec(client) {
const _multi = client.multi
client.multi = function () {
const multi = _multi.apply(client, arguments)
const _exec = multi.exec
multi.exec = (callback) => {
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)
}
}
return multi
}
}
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)),
])
}
module.exports = {
createClient,
}