Skip to content

Commit c2d83d1

Browse files
author
standlove
committed
updated backend
1 parent 7a2b4c6 commit c2d83d1

File tree

5 files changed

+150
-15
lines changed

5 files changed

+150
-15
lines changed

config/default.js

+3
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ let config = {
1717
PORT: process.env.PORT || 3003,
1818
API_VERSION: 'api/1.0',
1919
WORK_ROOT: path.join(__dirname, '..'),
20+
IDLE_TIMEOUT_IN_MINS: 30,
21+
IDLE_CONNECTION_POLL_INTERVAL: 1,
22+
MAX_RETRIES: 5,
2023
defaultExternalConfigLink: 'https://raw.githubusercontent.com/jiangliwu/static-files/master/config.json',
2124
defaultExternalConfigFileName: 'defaultExternalCache.json',
2225
externalConfigFileName: 'externalCache.json',

lib/redis.js

+107-15
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@ const Redis = require('ioredis');
22
const errors = require('./common/errors');
33
const _ = require('lodash');
44
const redisDump = require('./tools/redis-dump');
5+
const {
6+
IDLE_TIMEOUT_IN_MINS,
7+
IDLE_CONNECTION_POLL_INTERVAL,
8+
MAX_RETRIES
9+
} = require('config');
510

611
/**
712
* cached redis instance
@@ -16,21 +21,41 @@ const DISCONNECTED = 'disconnected';
1621
* @return {Promise<any>}
1722
*/
1823
async function connect(body) {
24+
let redisInstance = redisInstanceCache[body.id];
25+
if (redisInstance) {
26+
redisInstance.retries = 0;
27+
redisInstance.lastAccessed = Date.now();
28+
}
29+
30+
const newProps = getRedisInstanceProperties(body);
1931
// already exist
20-
if (redisInstanceCache[body.id]) {
21-
const redis = redisInstanceCache[body.id];
22-
if (redis.redisStatus == DISCONNECTED) {
23-
redis.connect();
32+
if (redisInstance && _.isEqual(redisInstance.props, newProps)) {
33+
if (redisInstance.status === DISCONNECTED) {
34+
throw errors.newConnectFailedError(body.id);
2435
}
2536
return Promise.resolve(body);
2637
}
2738

39+
// properties changed
40+
if (redisInstance) {
41+
redisInstanceCache[body.id] = null;
42+
redisInstance.connection.disconnect();
43+
} else {
44+
redisInstance = {
45+
props: getRedisInstanceProperties(body),
46+
retries: 0,
47+
};
48+
}
49+
2850
return new Promise((resolve, reject) => {
2951
const redis = new Redis({
30-
host: body.serverModel.ip, port: body.serverModel.port, db: body.serverModel.db,
52+
host: body.serverModel.ip,
53+
port: body.serverModel.port,
54+
db: body.serverModel.db,
3155
password: body.serverModel.password,
3256
showFriendlyErrorStack: true,
3357
autoResendUnfulfilledCommands: false,
58+
maxRetriesPerRequest: MAX_RETRIES,
3459
});
3560

3661
const timeoutHandler = setTimeout(() => {
@@ -40,34 +65,98 @@ async function connect(body) {
4065

4166
redis.on('error', (e) => {
4267
console.error(e);
43-
redis.redisStatus = DISCONNECTED;
68+
redisInstance.status = DISCONNECTED;
4469
});
4570

4671
redis.on('end', () => {
72+
console.log('end');
4773
redisInstanceCache[body.id] = null;
4874
});
4975

76+
redis.on('reconnecting', () => {
77+
console.log('reconnecting');
78+
redisInstance.retries++;
79+
if(redisInstance.retries >= MAX_RETRIES) {
80+
redisInstanceCache[body.id] = null;
81+
redis.disconnect();
82+
}
83+
})
84+
5085
redis.on('ready', () => {
51-
redis.redisStatus = CONNECTED;
52-
redisInstanceCache[body.id] = redis;
53-
86+
console.log('ready');
87+
redisInstance.connection = redis;
88+
redisInstance.status = CONNECTED;
89+
redisInstance.lastAccessed = Date.now();
90+
redisInstance.retries = 0;
91+
92+
redisInstanceCache[body.id] = redisInstance;
93+
5494
resolve(body);
5595
clearTimeout(timeoutHandler);
5696
});
5797
});
5898
}
5999

100+
101+
/**
102+
* disconnect redis
103+
* @param body
104+
* @return {Promise<any>}
105+
*/
106+
async function disconnect(body) {
107+
const redisInstance = redisInstanceCache[body.id];
108+
109+
if (redisInstance) {
110+
redisInstanceCache[body.id] = null;
111+
redisInstance.connection.disconnect();
112+
}
113+
}
114+
115+
/**
116+
* Get the properties of redis instance
117+
* @param body
118+
* @return {Object} - the properties used to identify modifications in instance config
119+
*/
120+
function getRedisInstanceProperties(body) {
121+
return _.pick(
122+
body,
123+
'serverModel.ip',
124+
'serverModel.port',
125+
'serverModel.db',
126+
'serverModel.password'
127+
);
128+
}
129+
130+
/**
131+
* Poll for idle instances. If found, disconnect and remove them from cache
132+
*/
133+
function pollIdleConnections() {
134+
const idleTimeoutInMS = IDLE_TIMEOUT_IN_MINS * 60 * 1000;
135+
setInterval(() => {
136+
const idleThreshold = Date.now() - idleTimeoutInMS;
137+
for (let id in redisInstanceCache) {
138+
const redisInstance = redisInstanceCache[id];
139+
if (redisInstance && redisInstance.lastAccessed < idleThreshold) {
140+
redisInstanceCache[id] = null;
141+
redisInstance.connection.disconnect();
142+
}
143+
}
144+
}, IDLE_CONNECTION_POLL_INTERVAL * 60 * 1000);
145+
}
146+
60147
/**
61148
* Get redis connection
62-
*
149+
*
63150
* @param query the query params
64151
*/
65152
function getRedisConnection(query) {
66-
const redis = redisInstanceCache[query.id];
67-
if (!redis || redis.redisStatus !== CONNECTED) {
153+
const redisInstance = redisInstanceCache[query.id];
154+
if (!redisInstance || redisInstance.status !== CONNECTED) {
68155
throw errors.newConnectFailedError(query.id);
69156
}
70-
return redis;
157+
158+
redisInstance.lastAccessed = Date.now();
159+
return redisInstance.connection;
71160
}
72161

73162
/**
@@ -88,7 +177,7 @@ async function fetchTree(query) {
88177
let keys;
89178
try {
90179
keys = query.keys || (await redis.keys('*'));
91-
180+
92181
for (let i = 0; i < keys.length; i++) { // process types
93182
const key = keys[i];
94183
const type = await redis.type(key);
@@ -203,6 +292,9 @@ async function dump(query) {
203292
}
204293
}
205294

295+
// trigger the poller on start
296+
pollIdleConnections();
297+
206298
module.exports = {
207-
connect, fetchTree, call, dump
299+
connect, fetchTree, call, dump, disconnect
208300
};

lib/route.js

+5
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@ module.exports = {
2424
method: async req => await redis.connect(req.payload),
2525
},
2626
},
27+
'/redis/disconnect': {
28+
post: {
29+
method: async req => await redis.disconnect(req.payload),
30+
},
31+
},
2732
'/redis/fetch': {
2833
get: {
2934
method: async req => await redis.fetchTree(req.query),

redis-cluster.conf

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
port 7000
2+
cluster-enabled yes
3+
cluster-config-file nodes.conf
4+
cluster-node-timeout 5000
5+
appendonly yes

verification.md

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
## Verfication Guide
2+
3+
#1 Support redis cluster
4+
* To verify the first point, set up a redis cluster following the updated readme and connect from the UI.
5+
* Once connected, you should be able to send commands and recieve response from cluster like this:
6+
```
7+
> get foo
8+
(nil)
9+
> set foo bar
10+
Ok
11+
> get foo
12+
bar
13+
```
14+
#1.1 And in the connect endpoint, when the connection with id already exists, you should also check if the properties (e.g. host name, port, cluster etc...) are changed or not. If changed, disconnect the old one and reconnect.
15+
16+
* To verify this, manually edit the localStorage, refresh the page and try to connect.
17+
18+
#2 Add a disconnect endpoint to the backend.
19+
20+
* This is implemented and integrated with the disconnect button above the instance info
21+
22+
#3 To verify if the backend removes the cache,
23+
* run the server in dev mode (using ndb is recommended for running in debug mode) put a breakpoint in reconnecting callback in redis.js.
24+
* set MAX_RETRIES in config/default.js.
25+
26+
#3.1 To verify idle timeout
27+
* set a very low value (say 1) in the config/default.js > IDLE_TIMEOUT_IN_MINS variable.
28+
* wait for 1 minute and run `get foo` from the UI. This will fail unless you manually reconnected in the meantime.
29+
30+

0 commit comments

Comments
 (0)