Skip to content

Commit a06b9be

Browse files
authored
Fb/more redis client polish (#95)
* wip 1 * wip 2 * wip 3 * wip 4 * wip 5 * wip 6 * naming consistency between initialize and cds-plugin options * make tests great again * lower redis ping interval to 4min * new harmonized approach * new harmonized approach 2 * new harmonized approach 3 * clearer option naming * remove different behavior for local reconnect * minimal defaulting for cds.requires.redis-featureToggles * no more reconnect strategy in snaps * no defaulting for redis-featureToggles * update jsdocs a little * ternary for createClient/createCluster
1 parent 9d01c00 commit a06b9be

File tree

6 files changed

+74
-56
lines changed

6 files changed

+74
-56
lines changed

src/feature-toggles.js

+5-2
Original file line numberDiff line numberDiff line change
@@ -808,7 +808,8 @@ class FeatureToggles {
808808
configFile: configFilepath,
809809
configFiles: configFilepaths,
810810
configAuto,
811-
redisClientOptions,
811+
customRedisCredentials,
812+
customRedisClientOptions,
812813
} = {}) {
813814
if (this.__isInitialized) {
814815
return;
@@ -867,7 +868,7 @@ class FeatureToggles {
867868
);
868869
}
869870

870-
redis.setClientOptions(redisClientOptions);
871+
redis.setCustomOptions(customRedisCredentials, customRedisClientOptions);
871872
const redisIntegrationMode = await redis.getIntegrationMode();
872873
if (redisIntegrationMode !== REDIS_INTEGRATION_MODE.NO_REDIS) {
873874
try {
@@ -953,6 +954,8 @@ class FeatureToggles {
953954
* @property {Config} [config]
954955
* @property {string} [configFile]
955956
* @property {Config} [configAuto]
957+
* @property {object} [customRedisCredentials]
958+
* @property {object} [customRedisClientOptions]
956959
*/
957960
/**
958961
* Initialize needs to run and finish before other APIs are called. It processes the configuration, sets up

src/plugin.js

+7-3
Original file line numberDiff line numberDiff line change
@@ -169,13 +169,17 @@ const activate = async () => {
169169
return;
170170
}
171171

172-
const ftsAutoConfig = await _discoverFtsAutoConfig();
172+
const configAuto = await _discoverFtsAutoConfig();
173+
const { credentials: customRedisCredentials, options: customRedisClientOptions } =
174+
cds.requires["redis-featureToggles"] || cds.requires["redis"] || {};
175+
173176
await toggles.initializeFeatures({
174177
config: envFeatureToggles?.config,
175178
configFile: envFeatureToggles?.configFile,
176179
configFiles: envFeatureToggles?.configFiles,
177-
configAuto: ftsAutoConfig,
178-
redisClientOptions: envFeatureToggles?.clientOptions,
180+
configAuto,
181+
customRedisCredentials,
182+
customRedisClientOptions,
179183
});
180184

181185
_registerFeatureProvider(envFeatureToggles);

src/redis-adapter.js

+56-44
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ const INTEGRATION_MODE = Object.freeze({
2424
NO_REDIS: "NO_REDIS",
2525
});
2626
const CF_REDIS_SERVICE_LABEL = "redis-cache";
27-
const REDIS_CLIENT_DEFAULT_PING_INTERVAL = 5 * 60000;
27+
const REDIS_CLIENT_DEFAULT_PING_INTERVAL = 4 * 60000;
2828

2929
const cfEnv = CfEnv.getInstance();
3030
const logger = new Logger(COMPONENT_NAME);
@@ -35,14 +35,18 @@ const MODE = Object.freeze({
3535
});
3636

3737
let __messageHandlers;
38-
let __clientOptions;
38+
let __customCredentials;
39+
let __customClientOptions;
40+
let __activeOptionsTuple;
3941
let __canGetClientPromise;
4042
let __mainClientPromise;
4143
let __subscriberClientPromise;
4244
let __integrationModePromise;
4345
const _reset = () => {
4446
__messageHandlers = new HandlerCollection();
45-
__clientOptions = null;
47+
__customCredentials = null;
48+
__customClientOptions = null;
49+
__activeOptionsTuple = null;
4650
__canGetClientPromise = null;
4751
__mainClientPromise = null;
4852
__subscriberClientPromise = null;
@@ -80,25 +84,20 @@ const _subscribedMessageHandler = async (message, channel) => {
8084
);
8185
};
8286

83-
const _localReconnectStrategy = () =>
84-
new VError({ name: VERROR_CLUSTER_NAME }, "disabled reconnect, because we are not running on cloud foundry");
85-
86-
/**
87-
* Lazily create a new redis client. Client creation transparently handles both the Cloud Foundry "redis-cache" service
88-
* (hyperscaler option) and a local redis-server.
89-
*
90-
* @returns {RedisClient|RedisCluster}
91-
* @private
92-
*/
93-
const _createClientBase = (clientName) => {
94-
try {
95-
const localSocketOptions = {
96-
host: "localhost",
97-
port: 6379,
87+
const _getRedisOptionsTuple = () => {
88+
if (!__activeOptionsTuple) {
89+
const defaultClientOptions = {
90+
pingInterval: REDIS_CLIENT_DEFAULT_PING_INTERVAL,
91+
socket: {
92+
host: "localhost",
93+
port: 6379,
94+
},
9895
};
99-
const credentials = cfEnv.cfServiceCredentialsForLabel(CF_REDIS_SERVICE_LABEL);
96+
97+
const credentials = __customCredentials || cfEnv.cfServiceCredentialsForLabel(CF_REDIS_SERVICE_LABEL);
10098
const hasCredentials = Object.keys(credentials).length > 0;
101-
const { cluster_mode: isCluster } = credentials;
99+
100+
const isCluster = !!credentials.cluster_mode;
102101
const credentialClientOptions = hasCredentials
103102
? {
104103
password: credentials.password,
@@ -112,13 +111,16 @@ const _createClientBase = (clientName) => {
112111

113112
// NOTE: documentation is buried here https://github.com/redis/node-redis/blob/master/docs/client-configuration.md
114113
const redisClientOptions = {
114+
...defaultClientOptions,
115115
...credentialClientOptions,
116-
pingInterval: REDIS_CLIENT_DEFAULT_PING_INTERVAL,
117-
...__clientOptions,
116+
...__customClientOptions,
117+
// https://nodejs.org/docs/latest-v22.x/api/net.html#socketconnectoptions-connectlistener
118+
// https://nodejs.org/docs/latest-v22.x/api/tls.html#tlsconnectoptions-callback
119+
// https://nodejs.org/docs/latest-v22.x/api/tls.html#tlscreatesecurecontextoptions
118120
socket: {
119-
...localSocketOptions,
121+
...defaultClientOptions.socket,
120122
...credentialClientOptions?.socket,
121-
...__clientOptions?.socket,
123+
...__customClientOptions?.socket,
122124
},
123125
};
124126

@@ -132,21 +134,31 @@ const _createClientBase = (clientName) => {
132134
redisClientOptions.socket.tls = !!redisClientOptions.socket.tls;
133135
}
134136

135-
if (
136-
redisClientOptions.socket.host === "localhost" &&
137-
!Object.prototype.hasOwnProperty.call(redisClientOptions.socket, "reconnectStrategy")
138-
) {
139-
redisClientOptions.socket.reconnectStrategy = _localReconnectStrategy;
140-
}
137+
__activeOptionsTuple = [isCluster, redisClientOptions];
138+
}
141139

142-
if (isCluster) {
143-
return redis.createCluster({
144-
rootNodes: [redisClientOptions], // NOTE: assume this ignores everything but socket/url
145-
// https://github.com/redis/node-redis/issues/1782
146-
defaults: redisClientOptions, // NOTE: assume this ignores socket/url
147-
});
148-
}
149-
return redis.createClient(redisClientOptions);
140+
return __activeOptionsTuple;
141+
};
142+
143+
/**
144+
* Lazily create a new redis client. Client creation transparently handles
145+
* - custom credentials and client options passed in via {@link setCustomOptions},
146+
* - Cloud Foundry service with label "redis-cache" (hyperscaler option), and
147+
* - local redis-server.
148+
*
149+
* @returns {RedisClient|RedisCluster}
150+
* @private
151+
*/
152+
const _createClientBase = (clientName) => {
153+
try {
154+
const [isCluster, redisClientOptions] = _getRedisOptionsTuple();
155+
return isCluster
156+
? redis.createCluster({
157+
rootNodes: [redisClientOptions], // NOTE: assume this ignores everything but socket/url
158+
// https://github.com/redis/node-redis/issues/1782
159+
defaults: redisClientOptions, // NOTE: assume this ignores socket/url
160+
})
161+
: redis.createClient(redisClientOptions);
150162
} catch (err) {
151163
throw new VError(
152164
{ name: VERROR_CLUSTER_NAME, cause: err, info: { clientName } },
@@ -189,8 +201,9 @@ const _closeClientBase = async (client) => {
189201
}
190202
};
191203

192-
const setClientOptions = (clientOptions) => {
193-
__clientOptions = clientOptions;
204+
const setCustomOptions = (customCredentials, customClientOptions) => {
205+
__customCredentials = customCredentials;
206+
__customClientOptions = customClientOptions;
194207
};
195208

196209
const _canGetClient = async () => {
@@ -213,7 +226,7 @@ const _getIntegrationMode = async () => {
213226
return INTEGRATION_MODE.NO_REDIS;
214227
}
215228
if (cfEnv.isOnCf) {
216-
const { cluster_mode: isCluster } = cfEnv.cfServiceCredentialsForLabel(CF_REDIS_SERVICE_LABEL);
229+
const [isCluster] = _getRedisOptionsTuple();
217230
return isCluster ? INTEGRATION_MODE.CF_REDIS_CLUSTER : INTEGRATION_MODE.CF_REDIS;
218231
}
219232
return INTEGRATION_MODE.LOCAL_REDIS;
@@ -304,7 +317,7 @@ const sendCommand = async (command) => {
304317
const mainClient = await getMainClient();
305318

306319
try {
307-
const { cluster_mode: isCluster } = cfEnv.cfServiceCredentialsForLabel(CF_REDIS_SERVICE_LABEL);
320+
const [isCluster] = _getRedisOptionsTuple();
308321
if (isCluster) {
309322
// NOTE: the cluster sendCommand API has a different signature, where it takes two optional args: firstKey and
310323
// isReadonly before the command
@@ -594,7 +607,7 @@ const removeAllMessageHandlers = (channel) => __messageHandlers.removeAllHandler
594607

595608
module.exports = {
596609
REDIS_INTEGRATION_MODE: INTEGRATION_MODE,
597-
setClientOptions,
610+
setCustomOptions,
598611
getIntegrationMode,
599612
getMainClient,
600613
closeMainClient,
@@ -628,7 +641,6 @@ module.exports = {
628641
_getSubscriberClient: () => __subscriberClientPromise,
629642
_setSubscriberClient: (value) => (__subscriberClientPromise = value),
630643
_subscribedMessageHandler,
631-
_localReconnectStrategy,
632644
_createClientBase,
633645
_createClientAndConnect,
634646
_clientExec,

test/__mocks__/redis-adapter.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ module.exports = {
7070
watchedHashGetSetObject,
7171
subscribe: jest.fn(),
7272
getIntegrationMode: jest.fn(() => REDIS_INTEGRATION_MODE.CF_REDIS),
73-
setClientOptions: jest.fn(),
73+
setCustomOptions: jest.fn(),
7474
_reset,
7575
_setValues,
7676
_setValue,

test/__snapshots__/redis-adapter.test.js.snap

+4-4
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ exports[`redis-adapter test _createClientBase on CF | AWS-cluster 1`] = `
55
{
66
"defaults": {
77
"password": "mock-password",
8-
"pingInterval": 300000,
8+
"pingInterval": 240000,
99
"socket": {
1010
"host": "clustercfg.rg-0c94f0c1-db0a-495d-b385-4a44a87fc398.vwvwdz.euc1.cache.amazonaws.com",
1111
"port": 3991,
@@ -15,7 +15,7 @@ exports[`redis-adapter test _createClientBase on CF | AWS-cluster 1`] = `
1515
"rootNodes": [
1616
{
1717
"password": "mock-password",
18-
"pingInterval": 300000,
18+
"pingInterval": 240000,
1919
"socket": {
2020
"host": "clustercfg.rg-0c94f0c1-db0a-495d-b385-4a44a87fc398.vwvwdz.euc1.cache.amazonaws.com",
2121
"port": 3991,
@@ -31,7 +31,7 @@ exports[`redis-adapter test _createClientBase on CF | AZURE 1`] = `
3131
[
3232
{
3333
"password": "mock-password",
34-
"pingInterval": 300000,
34+
"pingInterval": 240000,
3535
"socket": {
3636
"host": "redis-389395bc-9765-4d9a-ba6b-7252c136abab.redis.cache.windows.net",
3737
"port": 6380,
@@ -45,7 +45,7 @@ exports[`redis-adapter test _createClientBase on CF | GCP 1`] = `
4545
[
4646
{
4747
"password": "mock-password",
48-
"pingInterval": 300000,
48+
"pingInterval": 240000,
4949
"socket": {
5050
"host": "10.160.68.75",
5151
"port": 6378,

test/redis-adapter.test.js

+1-2
Original file line numberDiff line numberDiff line change
@@ -73,11 +73,10 @@ describe("redis-adapter test", () => {
7373
expect(redis.createClient.mock.calls[0]).toMatchInlineSnapshot(`
7474
[
7575
{
76-
"pingInterval": 300000,
76+
"pingInterval": 240000,
7777
"socket": {
7878
"host": "localhost",
7979
"port": 6379,
80-
"reconnectStrategy": [Function],
8180
},
8281
},
8382
]

0 commit comments

Comments
 (0)