forked from github/docs
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcreate-client.js
More file actions
161 lines (137 loc) · 5.33 KB
/
create-client.js
File metadata and controls
161 lines (137 loc) · 5.33 KB
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
const Redis = require('redis')
const { REDIS_MIN_DB, REDIS_MAX_DB } = process.env
// By default, every Redis instance supports database numbers 0 - 15
const redisMinDb = REDIS_MIN_DB || 0
const redisMaxDb = REDIS_MAX_DB || 15
// Maximum delay between reconnection attempts after backoff
const maxReconnectDelay = 5000
function formatRedisError (error) {
const errorCode = error ? error.code : null
const errorName = error ? error.constructor.name : 'Server disconnection'
const errorMsg = error ? error.toString() : 'unknown (commonly a server idle timeout)'
const preamble = errorName + (errorCode ? ` with code "${errorCode}"` : '')
return preamble + ': ' + errorMsg
}
module.exports = function createClient (options = {}) {
const { db, name, url } = options
// If no Redis URL is provided, bail out
// NOTE: Could support other options like `host`, `port`, and `path` but
// choosing not to for the time being!
if (!url) return null
// Verify database number is within range
if (db != null) {
if (!Number.isInteger(db) || db < redisMinDb || db > redisMaxDb) {
throw new TypeError(
`Redis database number must be an integer between ${redisMinDb} and ${redisMaxDb} but was: ${JSON.stringify(db)}`
)
}
}
let pingInterval = null
function stopPinging () {
if (pingInterval) {
clearInterval(pingInterval)
pingInterval = null
}
}
// Create the client
const client = Redis.createClient(url, {
// Only add this configuration for TLS-enabled Redis URL values.
// Otherwise, it breaks for local Redis instances without TLS enabled.
...url.startsWith('rediss://') && {
tls: {
// Required for production Heroku Redis
rejectUnauthorized: false
}
},
// Any running command that is unfulfilled when a connection is lost should
// NOT be retried after the connection has been reestablished.
retry_unfulfilled_commands: false,
// If we failed to send a new command during a disconnection, do NOT
// enqueue it to send later after the connection has been [re-]established.
// This is also critical to preventing a backend pile-up!
enable_offline_queue: false,
// This timeout value will be applied to both the initial connection
// and any auto-reconnect attempts (if the `retry_strategy` option is
// provided). If not using the `retry_strategy` option, this value can be
// set to a very low number. If using the `retry_strategy` option to allow
// more than one reconnection attempt, this value must be set to a higher
// number. Defaults to 1 hour if not configured!
connect_timeout: 60 * 60 * 1000, // 60 minutes
// Be aware that this retry (NOT just reconnection) strategy appears to
// be a major point of confusion (and possibly legitimate issues) between
// reconnecting and retrying failed commands.
retry_strategy:
function ({
attempt,
error,
total_retry_time: totalRetryTime,
times_connected: timesConnected
}) {
let delayPerAttempt = 100
// If the server appears to be unavailable, slow down faster
if (error && error.code === 'ECONNREFUSED') {
delayPerAttempt *= 5
}
// Reconnect after delay
return Math.min(attempt * delayPerAttempt, maxReconnectDelay)
},
// Expand whatever other options and overrides were provided
...options
})
// Handle connection errors to prevent killing the Node.js process
client.on('error', (connectError) => {
try {
// Forcibly close the connection to the Redis server.
// Allow all still running commands to silently fail immediately.
client.end(false)
} catch (disconnectError) {
// Swallow any failure
}
// Also, stop pinging the Redis server
stopPinging()
})
client.on('connect', () => {
// Stop pinging the Redis server, if any such timer already exists
stopPinging()
// Start pinging the server once per minute to prevent Redis connection
// from closing when its idle `timeout` configuration value expires
pingInterval = setInterval(
() => { client.ping(() => {}) },
60 * 1000
)
})
client.on('end', () => {
// Stop pinging the Redis server
stopPinging()
})
// If a `name` was provided, use it in the prefix for logging event messages
const logPrefix = '[redis' + (name ? ` (${name})` : '') + ']'
// Add event listeners for basic logging
client.on('connect', () => { console.log(logPrefix, 'Connection opened') })
client.on('ready', () => { console.log(logPrefix, 'Ready to receive commands') })
client.on('end', () => { console.log(logPrefix, 'Connection closed') })
client.on(
'reconnecting',
({
attempt,
delay,
// The rest are unofficial properties but currently supported
error,
total_retry_time: totalRetryTime,
times_connected: timesConnected
}) => {
console.log(
logPrefix,
'Reconnecting,',
`attempt ${attempt}`,
`with ${delay} delay`,
`due to ${formatRedisError(error)}.`,
`Elapsed time: ${totalRetryTime}.`,
`Successful connections: ${timesConnected}.`
)
}
)
client.on('warning', (msg) => { console.warn(logPrefix, 'Warning:', msg) })
client.on('error', (error) => { console.error(logPrefix, formatRedisError(error)) })
return client
}