@cap-js-community/common 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +265 -0
- package/bin/cdsmc.js +51 -0
- package/cds-plugin.js +48 -0
- package/index.js +3 -0
- package/package.json +131 -0
- package/src/common/promise.js +24 -0
- package/src/index.js +8 -0
- package/src/migration-check/MigrationCheck.js +588 -0
- package/src/migration-check/index.js +5 -0
- package/src/rate-limiting/RateLimiting.js +213 -0
- package/src/rate-limiting/index.js +5 -0
- package/src/rate-limiting/redis/common.js +33 -0
- package/src/rate-limiting/redis/counter.js +63 -0
- package/src/rate-limiting/redis/resetTime.js +44 -0
- package/src/redis-client/RedisClient.js +238 -0
- package/src/redis-client/index.js +5 -0
- package/src/replication-cache/ReplicationCache.js +961 -0
- package/src/replication-cache/index.js +5 -0
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const cds = require("@sap/cds");
|
|
4
|
+
|
|
5
|
+
const redisCounter = require("./redis/counter");
|
|
6
|
+
const redisResetTime = require("./redis/resetTime");
|
|
7
|
+
|
|
8
|
+
const { connectionCheck } = require("./redis/common");
|
|
9
|
+
|
|
10
|
+
const COMPONENT_NAME = "rateLimiting";
|
|
11
|
+
|
|
12
|
+
class RateLimiting {
|
|
13
|
+
constructor(service, { maxConcurrent, maxInWindow, window } = {}) {
|
|
14
|
+
this.log = cds.log(COMPONENT_NAME);
|
|
15
|
+
this.service = service;
|
|
16
|
+
this.id = service.name;
|
|
17
|
+
this.resetTime = null;
|
|
18
|
+
this.tenantCounts = {}; // [<tenant>: {concurrent: 0, window: 0}]
|
|
19
|
+
this.maxConcurrent =
|
|
20
|
+
maxConcurrent || service.definition["@cds.rateLimt.maxConcurrent"] || cds.env.rateLimiting.maxConcurrent;
|
|
21
|
+
this.maxInWindow =
|
|
22
|
+
maxInWindow || service.definition["@cds.rateLimt.maxInWindow"] || cds.env.rateLimiting.maxInWindow;
|
|
23
|
+
this.window = window || service.definition["@cds.rateLimt.window"] || cds.env.rateLimiting.window;
|
|
24
|
+
this.redisActive = cds.env.rateLimiting.redis;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async setup() {
|
|
28
|
+
if (this.redisActive && !(await connectionCheck())) {
|
|
29
|
+
this.redisActive = cds.env.rateLimiting.redis = false;
|
|
30
|
+
}
|
|
31
|
+
this.redisTenantInWindowCounts = redisCounter({
|
|
32
|
+
name: `rateLimiting:${this.id}:inWindowCounts`,
|
|
33
|
+
});
|
|
34
|
+
this.redisTenantResetTime = redisResetTime({
|
|
35
|
+
name: `rateLimiting:${this.id}:resetTime`,
|
|
36
|
+
});
|
|
37
|
+
this.monitor(this.service);
|
|
38
|
+
this.log.info("using rate limiting", {
|
|
39
|
+
service: this.service.name,
|
|
40
|
+
maxConcurrent: this.maxConcurrent,
|
|
41
|
+
maxInWindow: this.maxInWindow,
|
|
42
|
+
window: this.window,
|
|
43
|
+
redis: this.redisActive,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
initTenant(tenant) {
|
|
48
|
+
tenant = tenant || "";
|
|
49
|
+
this.tenantCounts[tenant] = this.tenantCounts[tenant] || { concurrent: 0, window: 0 };
|
|
50
|
+
return tenant;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async calcResetTime() {
|
|
54
|
+
this.resetTime = new Date();
|
|
55
|
+
this.resetTime.setMilliseconds(this.resetTime.getMilliseconds() + this.window);
|
|
56
|
+
await (await this.redisTenantResetTime).set(this.resetTime);
|
|
57
|
+
return this.resetTime;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async nextResetTime() {
|
|
61
|
+
this.resetTime = await (await this.redisTenantResetTime).get();
|
|
62
|
+
if (this.resetTime) {
|
|
63
|
+
return this.resetTime;
|
|
64
|
+
}
|
|
65
|
+
return await this.calcResetTime();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async increment(tenant) {
|
|
69
|
+
tenant = this.initTenant(tenant);
|
|
70
|
+
const concurrentCount = this.tenantCounts[tenant].concurrent + 1;
|
|
71
|
+
this.tenantCounts[tenant].concurrent = concurrentCount;
|
|
72
|
+
const inWindowCount = await (await this.redisTenantInWindowCounts).increment(tenant);
|
|
73
|
+
this.tenantCounts[tenant].window = inWindowCount;
|
|
74
|
+
return {
|
|
75
|
+
ok: concurrentCount <= this.maxConcurrent && inWindowCount <= this.maxInWindow,
|
|
76
|
+
count: {
|
|
77
|
+
concurrent: concurrentCount,
|
|
78
|
+
inWindow: inWindowCount,
|
|
79
|
+
},
|
|
80
|
+
exceeds: {
|
|
81
|
+
concurrent: concurrentCount > this.maxConcurrent,
|
|
82
|
+
inWindow: inWindowCount > this.maxInWindow,
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async decrement(tenant) {
|
|
88
|
+
tenant = this.initTenant(tenant);
|
|
89
|
+
const concurrentCount = this.tenantCounts[tenant].concurrent - 1;
|
|
90
|
+
this.tenantCounts[tenant].concurrent = Math.max(concurrentCount, 0);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async clearInWindow(tenant) {
|
|
94
|
+
tenant = this.initTenant(tenant);
|
|
95
|
+
this.tenantCounts[tenant].window = await (await this.redisTenantInWindowCounts).reset(tenant);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async clearAllInWindow() {
|
|
99
|
+
await this.calcResetTime();
|
|
100
|
+
await Promise.allDone(
|
|
101
|
+
Object.keys(this.tenantCounts).map(async (tenant) => {
|
|
102
|
+
await this.clearInWindow(tenant);
|
|
103
|
+
}),
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
isExternal(req) {
|
|
108
|
+
return !!req.protocol?.match(/rest|odata/);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
monitor(srv) {
|
|
112
|
+
srv.rateLimiting = this;
|
|
113
|
+
|
|
114
|
+
if (parseInt(process.env.CF_INSTANCE_INDEX) === 0) {
|
|
115
|
+
(async () => {
|
|
116
|
+
try {
|
|
117
|
+
await this.calcResetTime();
|
|
118
|
+
} catch (err) {
|
|
119
|
+
this.log.error("Resetting rate limit time failed", err);
|
|
120
|
+
}
|
|
121
|
+
setInterval(async () => {
|
|
122
|
+
try {
|
|
123
|
+
await this.clearAllInWindow();
|
|
124
|
+
} catch (err) {
|
|
125
|
+
this.log.error("Resetting rate limit window failed", err);
|
|
126
|
+
}
|
|
127
|
+
}, this.window).unref();
|
|
128
|
+
})();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
srv.before("*", async (req) => {
|
|
132
|
+
if (!req.http?.req) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// decrement
|
|
137
|
+
req.on("succeeded", async () => {
|
|
138
|
+
if (this.isExternal(req)) {
|
|
139
|
+
await this.decrement(req.tenant);
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
req.on("failed", async () => {
|
|
143
|
+
if (this.isExternal(req)) {
|
|
144
|
+
await this.decrement(req.tenant);
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// increment
|
|
149
|
+
if (this.isExternal(req)) {
|
|
150
|
+
try {
|
|
151
|
+
const status = await this.increment(req.tenant);
|
|
152
|
+
if (status.ok) {
|
|
153
|
+
await this.accept(req, status);
|
|
154
|
+
} else {
|
|
155
|
+
await this.reject(req, status);
|
|
156
|
+
}
|
|
157
|
+
} catch (err) {
|
|
158
|
+
this.log.error("Incrementing rate limit counter failed", err);
|
|
159
|
+
await this.reject(req, { ok: false });
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async accept(req, status) {
|
|
166
|
+
try {
|
|
167
|
+
await this.addHeaders(req, status.count);
|
|
168
|
+
} catch (err) {
|
|
169
|
+
this.log.error("Adding rate limit headers failed", err);
|
|
170
|
+
await this.reject(req, { ok: false });
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async reject(req, status) {
|
|
175
|
+
try {
|
|
176
|
+
await this.addHeaders(req, status.count);
|
|
177
|
+
} catch (err) {
|
|
178
|
+
this.log.error("Adding rate limit headers failed", err);
|
|
179
|
+
}
|
|
180
|
+
if (status.exceeds?.inWindow) {
|
|
181
|
+
req.error({
|
|
182
|
+
code: "TOO_MANY_REQUESTS",
|
|
183
|
+
status: 429,
|
|
184
|
+
message: `Too many requests in time window (max ${this.maxInWindow}), please try again later.`,
|
|
185
|
+
});
|
|
186
|
+
} else if (status.exceeds?.concurrent) {
|
|
187
|
+
req.error({
|
|
188
|
+
code: "TOO_MANY_REQUESTS",
|
|
189
|
+
status: 429,
|
|
190
|
+
message: `Too many concurrent requests (max ${this.maxConcurrent}), please try again later.`,
|
|
191
|
+
});
|
|
192
|
+
} else {
|
|
193
|
+
req.error({
|
|
194
|
+
code: "TOO_MANY_REQUESTS",
|
|
195
|
+
status: 429,
|
|
196
|
+
message: "Too many requests, please try again later.",
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async addHeaders(req, count) {
|
|
202
|
+
const response = req.http.res;
|
|
203
|
+
if (response && !response.headersSent) {
|
|
204
|
+
response.setHeader("X-RateLimit-Limit", this.maxInWindow);
|
|
205
|
+
response.setHeader("X-RateLimit-Remaining", Math.max(this.maxInWindow - count.inWindow, 0));
|
|
206
|
+
response.setHeader("X-RateLimit-Reset", Math.ceil((await this.nextResetTime()).getTime() / 1000));
|
|
207
|
+
response.setHeader("Retry-After", Math.ceil(this.window / 1000));
|
|
208
|
+
response.setHeader("Date", new Date().toUTCString());
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
module.exports = RateLimiting;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const cds = require("@sap/cds");
|
|
4
|
+
|
|
5
|
+
const { RedisClient } = require("../../redis-client");
|
|
6
|
+
|
|
7
|
+
const COMPONENT_NAME = "rateLimiting";
|
|
8
|
+
|
|
9
|
+
async function connectionCheck() {
|
|
10
|
+
return await RedisClient.default(COMPONENT_NAME).connectionCheck();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async function perform(key, cb, cbFallback, retry = cds.env.rateLimiting.retry) {
|
|
14
|
+
const client = cds.env.rateLimiting.redis && (await RedisClient.default(COMPONENT_NAME).createMainClientAndConnect());
|
|
15
|
+
if (client) {
|
|
16
|
+
const value = await cb(client, key);
|
|
17
|
+
if (value === undefined) {
|
|
18
|
+
if (retry > 0) {
|
|
19
|
+
return await perform(key, cb, cbFallback, retry - 1);
|
|
20
|
+
} else {
|
|
21
|
+
cds.log(COMPONENT_NAME).error("Retry limit reached", { key });
|
|
22
|
+
throw new Error("Rate limiting retry limit reached");
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return value;
|
|
26
|
+
}
|
|
27
|
+
return cbFallback(key);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
module.exports = {
|
|
31
|
+
connectionCheck,
|
|
32
|
+
perform,
|
|
33
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { perform } = require("./common");
|
|
4
|
+
|
|
5
|
+
module.exports = async ({ name = "default" }) => {
|
|
6
|
+
const counts = {};
|
|
7
|
+
|
|
8
|
+
async function increment(key) {
|
|
9
|
+
return await perform(
|
|
10
|
+
`${name}/${key}`,
|
|
11
|
+
async (client, key) => {
|
|
12
|
+
return await client.incr(key);
|
|
13
|
+
},
|
|
14
|
+
(key) => {
|
|
15
|
+
counts[key] ??= 0;
|
|
16
|
+
counts[key]++;
|
|
17
|
+
return counts[key];
|
|
18
|
+
},
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function decrement(key) {
|
|
23
|
+
return await perform(
|
|
24
|
+
`${name}/${key}`,
|
|
25
|
+
async (client, key) => {
|
|
26
|
+
return await client.decr(key);
|
|
27
|
+
},
|
|
28
|
+
(key) => {
|
|
29
|
+
counts[key] ??= 0;
|
|
30
|
+
counts[key]--;
|
|
31
|
+
return counts[key];
|
|
32
|
+
},
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function reset(key) {
|
|
37
|
+
return await perform(
|
|
38
|
+
`${name}/${key}`,
|
|
39
|
+
async (client, key) => {
|
|
40
|
+
const status = await client.set(key, 0);
|
|
41
|
+
if (status === "OK") {
|
|
42
|
+
return 0;
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
(key) => {
|
|
46
|
+
counts[key] = 0;
|
|
47
|
+
return counts[key];
|
|
48
|
+
},
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
increment: async function (key) {
|
|
54
|
+
return await increment(key);
|
|
55
|
+
},
|
|
56
|
+
decrement: async function (key) {
|
|
57
|
+
return await decrement(key);
|
|
58
|
+
},
|
|
59
|
+
reset: async function (key) {
|
|
60
|
+
return await reset(key);
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { perform } = require("./common");
|
|
4
|
+
|
|
5
|
+
module.exports = async ({ name = "default" }) => {
|
|
6
|
+
let resetTime;
|
|
7
|
+
|
|
8
|
+
async function set(date) {
|
|
9
|
+
return await perform(
|
|
10
|
+
name,
|
|
11
|
+
async (client, key) => {
|
|
12
|
+
const status = await client.set(key, date.toISOString());
|
|
13
|
+
if (status === "OK") {
|
|
14
|
+
return date;
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
() => {
|
|
18
|
+
resetTime = date;
|
|
19
|
+
},
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function get() {
|
|
24
|
+
return await perform(
|
|
25
|
+
name,
|
|
26
|
+
async (client, key) => {
|
|
27
|
+
const value = await client.get(key);
|
|
28
|
+
return value ? new Date(value) : null;
|
|
29
|
+
},
|
|
30
|
+
() => {
|
|
31
|
+
return resetTime;
|
|
32
|
+
},
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
set: async function (date) {
|
|
38
|
+
return await set(date);
|
|
39
|
+
},
|
|
40
|
+
get: async function () {
|
|
41
|
+
return await get();
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
};
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const redis = require("redis");
|
|
4
|
+
|
|
5
|
+
const COMPONENT_NAME = "redisClient";
|
|
6
|
+
const LOG_AFTER_SEC = 5;
|
|
7
|
+
|
|
8
|
+
class RedisClient {
|
|
9
|
+
constructor(name) {
|
|
10
|
+
this.name = name;
|
|
11
|
+
this.log = cds.log(COMPONENT_NAME);
|
|
12
|
+
this.mainClientPromise = null;
|
|
13
|
+
this.additionalClientPromise = null;
|
|
14
|
+
this.subscriberClientPromise = null;
|
|
15
|
+
this.subscribedChannels = {};
|
|
16
|
+
this.lastErrorLog = Date.now();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
createMainClientAndConnect(options) {
|
|
20
|
+
if (this.mainClientPromise) {
|
|
21
|
+
return this.mainClientPromise;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const errorHandlerCreateClient = (err) => {
|
|
25
|
+
this.mainClientPromise?.then?.(this.resilientClientClose);
|
|
26
|
+
this.log.error("Error from main redis client", err);
|
|
27
|
+
this.mainClientPromise = null;
|
|
28
|
+
setTimeout(() => this.createMainClientAndConnect(options), LOG_AFTER_SEC * 1000).unref();
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
this.mainClientPromise = this.createClientAndConnect(options, errorHandlerCreateClient);
|
|
32
|
+
return this.mainClientPromise;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
createAdditionalClientAndConnect(options) {
|
|
36
|
+
if (this.additionalClientPromise) {
|
|
37
|
+
return this.additionalClientPromise;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const errorHandlerCreateClient = (err) => {
|
|
41
|
+
this.additionalClientPromise?.then?.(this.resilientClientClose);
|
|
42
|
+
this.log.error("Error from additional redis client", err);
|
|
43
|
+
this.additionalClientPromise = null;
|
|
44
|
+
setTimeout(() => this.createAdditionalClientAndConnect(options), LOG_AFTER_SEC * 1000).unref();
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
this.additionalClientPromise = this.createClientAndConnect(options, errorHandlerCreateClient);
|
|
48
|
+
return this.additionalClientPromise;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async createClientAndConnect(options, errorHandlerCreateClient, isConnectionCheck) {
|
|
52
|
+
try {
|
|
53
|
+
const client = this.createClientBase(options);
|
|
54
|
+
if (!client) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (!isConnectionCheck) {
|
|
58
|
+
client.on("error", (err) => {
|
|
59
|
+
const dateNow = Date.now();
|
|
60
|
+
if (dateNow - this.lastErrorLog > LOG_AFTER_SEC * 1000) {
|
|
61
|
+
this.log.error("Error from redis client", err);
|
|
62
|
+
this.lastErrorLog = dateNow;
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
client.on("reconnecting", () => {
|
|
67
|
+
const dateNow = Date.now();
|
|
68
|
+
if (dateNow - this.lastErrorLog > LOG_AFTER_SEC * 1000) {
|
|
69
|
+
this.log.info("Redis client trying reconnect...");
|
|
70
|
+
this.lastErrorLog = dateNow;
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
await client.connect();
|
|
75
|
+
return client;
|
|
76
|
+
} catch (err) {
|
|
77
|
+
errorHandlerCreateClient(err);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async connectionCheck(options) {
|
|
82
|
+
let error;
|
|
83
|
+
try {
|
|
84
|
+
const client = await this.createClientAndConnect(
|
|
85
|
+
options,
|
|
86
|
+
(err) => {
|
|
87
|
+
error = err;
|
|
88
|
+
},
|
|
89
|
+
true,
|
|
90
|
+
);
|
|
91
|
+
if (error) {
|
|
92
|
+
throw error;
|
|
93
|
+
}
|
|
94
|
+
if (client) {
|
|
95
|
+
await this.resilientClientClose(client);
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
} catch (err) {
|
|
99
|
+
this.log.warn("Falling back to no redis mode. Redis connection could not be established: ", err.message);
|
|
100
|
+
}
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
createClientBase(redisOptions = {}) {
|
|
105
|
+
const { credentials, options } =
|
|
106
|
+
(this.name ? cds.requires[`redis-${this.name}`] : undefined) || cds.requires["redis"] || {};
|
|
107
|
+
const socket = {
|
|
108
|
+
host: credentials?.hostname ?? "127.0.0.1",
|
|
109
|
+
tls: !!credentials?.tls,
|
|
110
|
+
port: credentials?.port ?? 6379,
|
|
111
|
+
...options?.socket,
|
|
112
|
+
...redisOptions.socket,
|
|
113
|
+
};
|
|
114
|
+
const socketOptions = {
|
|
115
|
+
...options,
|
|
116
|
+
...redisOptions,
|
|
117
|
+
password: options?.password ?? options?.password ?? credentials?.password,
|
|
118
|
+
socket,
|
|
119
|
+
};
|
|
120
|
+
try {
|
|
121
|
+
if (credentials?.cluster_mode) {
|
|
122
|
+
return redis.createCluster({
|
|
123
|
+
rootNodes: [socketOptions],
|
|
124
|
+
defaults: socketOptions,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
return redis.createClient(socketOptions);
|
|
128
|
+
} catch (err) {
|
|
129
|
+
throw new Error("Error during create client with redis-cache service" + err);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
subscribeChannel(options, channel, subscribeHandler) {
|
|
134
|
+
this.subscribedChannels[channel] = subscribeHandler;
|
|
135
|
+
const errorHandlerCreateClient = (err) => {
|
|
136
|
+
this.log.error(`Error from redis client for for channel ${channel}`, err);
|
|
137
|
+
this.subscriberClientPromise?.then?.(this.resilientClientClose);
|
|
138
|
+
this.subscriberClientPromise = null;
|
|
139
|
+
setTimeout(
|
|
140
|
+
() => this.subscribeChannels(options, this.subscribedChannels, subscribeHandler),
|
|
141
|
+
LOG_AFTER_SEC * 1000,
|
|
142
|
+
).unref();
|
|
143
|
+
};
|
|
144
|
+
this.subscribeChannels(options, { [channel]: subscribeHandler }, errorHandlerCreateClient);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
subscribeChannels(options, subscribedChannels, errorHandlerCreateClient) {
|
|
148
|
+
this.subscriberClientPromise = this.createClientAndConnect(options, errorHandlerCreateClient)
|
|
149
|
+
.then((client) => {
|
|
150
|
+
for (const channel in this.subscribedChannels) {
|
|
151
|
+
const fn = subscribedChannels[channel];
|
|
152
|
+
client._subscribedChannels ??= {};
|
|
153
|
+
if (client._subscribedChannels[channel]) {
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
this.log.info("Subscribe redis client connected channel", { channel });
|
|
157
|
+
client
|
|
158
|
+
.subscribe(channel, fn)
|
|
159
|
+
.then(() => {
|
|
160
|
+
client._subscribedChannels ??= {};
|
|
161
|
+
client._subscribedChannels[channel] = 1;
|
|
162
|
+
})
|
|
163
|
+
.catch(() => {
|
|
164
|
+
this.log.error("Error subscribe to channel - retrying...");
|
|
165
|
+
setTimeout(() => this.subscribeChannels(options, [channel], fn), LOG_AFTER_SEC * 1000).unref();
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
})
|
|
169
|
+
.catch((err) => {
|
|
170
|
+
cds
|
|
171
|
+
.log(COMPONENT_NAME)
|
|
172
|
+
.error(
|
|
173
|
+
`Error from redis client during startup - trying to reconnect - ${Object.keys(this.subscribedChannels).join(
|
|
174
|
+
", ",
|
|
175
|
+
)}`,
|
|
176
|
+
err,
|
|
177
|
+
);
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async publishMessage(options, channel, message) {
|
|
182
|
+
const client = await this.createMainClientAndConnect(options);
|
|
183
|
+
return await client.publish(channel, message);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async closeMainClient() {
|
|
187
|
+
const client = this.mainClientPromise;
|
|
188
|
+
this.mainClientPromise = null;
|
|
189
|
+
await this.resilientClientClose(await client);
|
|
190
|
+
this.log.info("Main redis client closed!");
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async closeAdditionalClient() {
|
|
194
|
+
const client = this.additionalClientPromise;
|
|
195
|
+
this.additionalClientPromise = null;
|
|
196
|
+
await this.resilientClientClose(await client);
|
|
197
|
+
this.log.info("Additional redis client closed!");
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async closeSubscribeClient() {
|
|
201
|
+
const client = this.subscriberClientPromise;
|
|
202
|
+
this.subscriberClientPromise = null;
|
|
203
|
+
await this.resilientClientClose(await client);
|
|
204
|
+
this.log.info("Subscribe redis client closed!");
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async closeClients() {
|
|
208
|
+
await this.closeMainClient();
|
|
209
|
+
await this.closeAdditionalClient();
|
|
210
|
+
await this.closeSubscribeClient();
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async resilientClientClose(client) {
|
|
214
|
+
try {
|
|
215
|
+
if (client?.quit) {
|
|
216
|
+
await client.quit();
|
|
217
|
+
}
|
|
218
|
+
} catch (err) {
|
|
219
|
+
this.log.info("Error during redis close - continuing...", err);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
static default(name = "default") {
|
|
224
|
+
RedisClient._default ??= {};
|
|
225
|
+
if (!RedisClient._default[name]) {
|
|
226
|
+
RedisClient._default[name] = new RedisClient(name);
|
|
227
|
+
}
|
|
228
|
+
return RedisClient._default[name];
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
static async closeAllClients() {
|
|
232
|
+
for (const entry of Object.values(RedisClient._default || {})) {
|
|
233
|
+
await entry.closeClients();
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
module.exports = RedisClient;
|