@cap-js-community/event-queue 1.11.0 → 2.0.0-beta.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/db/Event.cds +1 -0
- package/package.json +16 -10
- package/src/EventQueueProcessorBase.js +18 -3
- package/src/config.js +68 -208
- package/src/dbHandler.js +7 -4
- package/src/index.d.ts +10 -9
- package/src/initialize.js +8 -23
- package/src/outbox/eventQueueAsOutbox.js +19 -9
- package/src/periodicEvents.js +13 -12
- package/src/processEventQueue.js +7 -11
- package/src/publishEvent.js +15 -6
- package/src/redis/redisPub.js +10 -8
- package/src/redis/redisSub.js +16 -5
- package/src/runner/openEvents.js +12 -10
- package/src/runner/runner.js +23 -12
- package/src/runner/runnerHelper.js +10 -4
- package/src/shared/common.js +6 -1
- package/src/shared/distributedLock.js +6 -6
- package/src/shared/eventScheduler.js +3 -3
- package/src/shared/redis/client.js +46 -0
- package/src/shared/redis/index.js +45 -0
- package/srv/service/admin-service.js +1 -1
- package/src/shared/redis.js +0 -199
|
@@ -84,7 +84,7 @@ const _acquireLockRedis = async (
|
|
|
84
84
|
expiryTime,
|
|
85
85
|
{ value = Date.now(), overrideValue = false, keepTrackOfLock } = {}
|
|
86
86
|
) => {
|
|
87
|
-
const client = await redis.createMainClientAndConnect(
|
|
87
|
+
const client = await redis.createMainClientAndConnect();
|
|
88
88
|
const result = await client.set(fullKey, value, {
|
|
89
89
|
PX: Math.round(expiryTime),
|
|
90
90
|
...(overrideValue ? null : { NX: true }),
|
|
@@ -97,7 +97,7 @@ const _acquireLockRedis = async (
|
|
|
97
97
|
};
|
|
98
98
|
|
|
99
99
|
const _renewLockRedis = async (context, fullKey, expiryTime, { value = "true" } = {}) => {
|
|
100
|
-
const client = await redis.createMainClientAndConnect(
|
|
100
|
+
const client = await redis.createMainClientAndConnect();
|
|
101
101
|
let result = await client.set(fullKey, value, {
|
|
102
102
|
PX: Math.round(expiryTime),
|
|
103
103
|
XX: true,
|
|
@@ -116,7 +116,7 @@ const _renewLockRedis = async (context, fullKey, expiryTime, { value = "true" }
|
|
|
116
116
|
};
|
|
117
117
|
|
|
118
118
|
const _getLockValueRedis = async (context, fullKey) => {
|
|
119
|
-
const client = await redis.createMainClientAndConnect(
|
|
119
|
+
const client = await redis.createMainClientAndConnect();
|
|
120
120
|
return await client.get(fullKey);
|
|
121
121
|
};
|
|
122
122
|
|
|
@@ -129,7 +129,7 @@ const _getLockValueDb = async (context, fullKey) => {
|
|
|
129
129
|
};
|
|
130
130
|
|
|
131
131
|
const _releaseLockRedis = async (context, fullKey) => {
|
|
132
|
-
const client = await redis.createMainClientAndConnect(
|
|
132
|
+
const client = await redis.createMainClientAndConnect();
|
|
133
133
|
const result = await client.del(fullKey);
|
|
134
134
|
delete existingLocks[fullKey];
|
|
135
135
|
return result === 1;
|
|
@@ -195,14 +195,14 @@ const _acquireLockDB = async (
|
|
|
195
195
|
};
|
|
196
196
|
|
|
197
197
|
const _generateKey = (context, tenantScoped, key) => {
|
|
198
|
-
const keyParts = [config.
|
|
198
|
+
const keyParts = [config.redisNamespace];
|
|
199
199
|
tenantScoped && keyParts.push(context.tenant);
|
|
200
200
|
keyParts.push(key);
|
|
201
201
|
return `${keyParts.join("##")}`;
|
|
202
202
|
};
|
|
203
203
|
|
|
204
204
|
const getAllLocksRedis = async () => {
|
|
205
|
-
const clientOrCluster = await redis.createMainClientAndConnect(
|
|
205
|
+
const clientOrCluster = await redis.createMainClientAndConnect();
|
|
206
206
|
const output = [];
|
|
207
207
|
const results = [];
|
|
208
208
|
|
|
@@ -15,9 +15,9 @@ class EventScheduler {
|
|
|
15
15
|
config.attachUnsubscribeHandler(this.clearForTenant.bind(this));
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
scheduleEvent(tenantId, type, subType, startAfter) {
|
|
18
|
+
scheduleEvent(tenantId, type, subType, namespace, startAfter) {
|
|
19
19
|
const { date, relative } = this.calculateOffset(type, subType, startAfter);
|
|
20
|
-
const key = [tenantId, type, subType, date.toISOString()].join("##");
|
|
20
|
+
const key = [tenantId, type, subType, namespace, date.toISOString()].join("##");
|
|
21
21
|
if (this.#scheduledEvents[key]) {
|
|
22
22
|
return; // event combination already scheduled
|
|
23
23
|
}
|
|
@@ -34,7 +34,7 @@ class EventScheduler {
|
|
|
34
34
|
clearTimeout(timeout);
|
|
35
35
|
delete this.#eventsByTenants[tenantId][timeout];
|
|
36
36
|
delete this.#scheduledEvents[key];
|
|
37
|
-
redisPub.broadcastEvent(tenantId, { type, subType }).catch((err) => {
|
|
37
|
+
redisPub.broadcastEvent(tenantId, { type, subType, namespace }).catch((err) => {
|
|
38
38
|
cds.log(COMPONENT_NAME).error("could not execute scheduled event", err, {
|
|
39
39
|
tenantId,
|
|
40
40
|
type,
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { RedisClient } = require("@cap-js-community/common");
|
|
4
|
+
|
|
5
|
+
const config = require("../../config");
|
|
6
|
+
|
|
7
|
+
const REDIS_CLIENT_NAME = "eventQueue";
|
|
8
|
+
|
|
9
|
+
const createMainClientAndConnect = async () => {
|
|
10
|
+
const redisClient = RedisClient.create(REDIS_CLIENT_NAME);
|
|
11
|
+
return await redisClient.createMainClientAndConnect(config.redisOptions);
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const subscribeRedisChannel = async (channel, subscribeHandler) => {
|
|
15
|
+
const redisClient = RedisClient.create(REDIS_CLIENT_NAME);
|
|
16
|
+
const channelWithNamespace = [config.redisNamespace, channel].join("##");
|
|
17
|
+
return await redisClient.subscribeChannel(config.redisOptions, channelWithNamespace, subscribeHandler);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const publishMessage = async (channel, message) => {
|
|
21
|
+
const redisClient = RedisClient.create(REDIS_CLIENT_NAME);
|
|
22
|
+
const channelWithNamespace = [config.redisNamespace, channel].join("##");
|
|
23
|
+
return await redisClient.publishMessage(config.redisOptions, channelWithNamespace, message);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const connectionCheck = async () => {
|
|
27
|
+
const redisClient = RedisClient.create(REDIS_CLIENT_NAME);
|
|
28
|
+
return await redisClient.connectionCheck(config.redisOptions);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const isClusterMode = () => {
|
|
32
|
+
return RedisClient.create(REDIS_CLIENT_NAME).isCluster;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const registerShutdownHandler = (cb) => {
|
|
36
|
+
RedisClient.create(REDIS_CLIENT_NAME).beforeCloseHandler = cb;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
module.exports = {
|
|
40
|
+
createMainClientAndConnect,
|
|
41
|
+
subscribeRedisChannel,
|
|
42
|
+
publishMessage,
|
|
43
|
+
connectionCheck,
|
|
44
|
+
isClusterMode,
|
|
45
|
+
registerShutdownHandler,
|
|
46
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const cds = require("@sap/cds");
|
|
4
|
+
|
|
5
|
+
const client = require("./client");
|
|
6
|
+
const config = require("../../config");
|
|
7
|
+
|
|
8
|
+
const COMPONENT_NAME = "/eventQueue/redis";
|
|
9
|
+
const REDIS_OFFBOARD_TENANT_CHANNEL = "REDIS_OFFBOARD_TENANT_CHANNEL";
|
|
10
|
+
|
|
11
|
+
const attachRedisUnsubscribeHandler = () => {
|
|
12
|
+
cds.log(COMPONENT_NAME).info("attached redis handle for unsubscribe events");
|
|
13
|
+
client
|
|
14
|
+
.subscribeRedisChannel(REDIS_OFFBOARD_TENANT_CHANNEL, (messageData) => {
|
|
15
|
+
try {
|
|
16
|
+
const { tenantId } = JSON.parse(messageData);
|
|
17
|
+
cds.log(COMPONENT_NAME).info("received unsubscribe broadcast event", { tenantId });
|
|
18
|
+
this.executeUnsubscribeHandlers(tenantId);
|
|
19
|
+
} catch (err) {
|
|
20
|
+
cds.log(COMPONENT_NAME).error("could not parse unsubscribe broadcast event", err, {
|
|
21
|
+
messageData,
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
})
|
|
25
|
+
.catch((err) => _errorHandlerSubscribeChannel(REDIS_OFFBOARD_TENANT_CHANNEL, err));
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const handleUnsubscribe = (tenantId) => {
|
|
29
|
+
if (config.redisEnabled) {
|
|
30
|
+
client.publishMessage(REDIS_OFFBOARD_TENANT_CHANNEL, JSON.stringify({ tenantId })).catch((error) => {
|
|
31
|
+
cds.log(COMPONENT_NAME).error(`publishing tenant unsubscribe failed. tenantId: ${tenantId}`, error);
|
|
32
|
+
});
|
|
33
|
+
} else {
|
|
34
|
+
config.executeUnsubscribeHandlers(tenantId);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const _errorHandlerSubscribeChannel = (channelName, err) =>
|
|
39
|
+
cds.log(COMPONENT_NAME).error("error subscribing to channel", err, { channelName });
|
|
40
|
+
|
|
41
|
+
module.exports = {
|
|
42
|
+
...client,
|
|
43
|
+
attachRedisUnsubscribeHandler,
|
|
44
|
+
handleUnsubscribe,
|
|
45
|
+
};
|
|
@@ -8,7 +8,7 @@ const redisPub = require("../../src/redis/redisPub");
|
|
|
8
8
|
|
|
9
9
|
module.exports = class AdminService extends cds.ApplicationService {
|
|
10
10
|
async init() {
|
|
11
|
-
const { Event: EventService, Lock: LockService } = this.entities
|
|
11
|
+
const { Event: EventService, Lock: LockService } = this.entities;
|
|
12
12
|
const { Event: EventDb } = cds.db.entities("sap.eventqueue");
|
|
13
13
|
const { landscape, space } = this.getLandscapeAndSpace();
|
|
14
14
|
|
package/src/shared/redis.js
DELETED
|
@@ -1,199 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
const redis = require("redis");
|
|
4
|
-
|
|
5
|
-
const { getEnvInstance } = require("./env");
|
|
6
|
-
const EventQueueError = require("../EventQueueError");
|
|
7
|
-
|
|
8
|
-
const COMPONENT_NAME = "/eventQueue/shared/redis";
|
|
9
|
-
const LOG_AFTER_SEC = 5;
|
|
10
|
-
|
|
11
|
-
let mainClientPromise;
|
|
12
|
-
let subscriberClientPromise;
|
|
13
|
-
const subscribedChannels = {};
|
|
14
|
-
let lastErrorLog = Date.now();
|
|
15
|
-
|
|
16
|
-
const createMainClientAndConnect = (options) => {
|
|
17
|
-
if (mainClientPromise) {
|
|
18
|
-
return mainClientPromise;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const errorHandlerCreateClient = (err) => {
|
|
22
|
-
mainClientPromise?.then?.(_resilientClientClose);
|
|
23
|
-
cds.log(COMPONENT_NAME).error("error from redis main client:", err);
|
|
24
|
-
mainClientPromise = null;
|
|
25
|
-
setTimeout(() => createMainClientAndConnect(options), LOG_AFTER_SEC * 1000).unref();
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
mainClientPromise = createClientAndConnect(options, errorHandlerCreateClient);
|
|
29
|
-
return mainClientPromise;
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
const _createClientBase = (redisOptions = {}) => {
|
|
33
|
-
const env = getEnvInstance();
|
|
34
|
-
try {
|
|
35
|
-
const { credentials, options } = env.redisRequires;
|
|
36
|
-
const socket = Object.assign(
|
|
37
|
-
{
|
|
38
|
-
host: credentials.hostname,
|
|
39
|
-
tls: !!credentials.tls,
|
|
40
|
-
port: credentials.port,
|
|
41
|
-
},
|
|
42
|
-
options?.socket,
|
|
43
|
-
redisOptions.socket
|
|
44
|
-
);
|
|
45
|
-
const socketOptions = Object.assign({}, options, redisOptions, {
|
|
46
|
-
password: redisOptions.password ?? options.password ?? credentials.password,
|
|
47
|
-
socket,
|
|
48
|
-
});
|
|
49
|
-
delete socketOptions.redisNamespace;
|
|
50
|
-
if (credentials.cluster_mode) {
|
|
51
|
-
return redis.createCluster({
|
|
52
|
-
rootNodes: [socketOptions],
|
|
53
|
-
defaults: socketOptions,
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
return redis.createClient(socketOptions);
|
|
57
|
-
} catch (err) {
|
|
58
|
-
throw EventQueueError.redisConnectionFailure(err);
|
|
59
|
-
}
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
const createClientAndConnect = async (options, errorHandlerCreateClient, isConnectionCheck) => {
|
|
63
|
-
try {
|
|
64
|
-
const client = _createClientBase(options);
|
|
65
|
-
if (!isConnectionCheck) {
|
|
66
|
-
client.on("error", (err) => {
|
|
67
|
-
const dateNow = Date.now();
|
|
68
|
-
if (dateNow - lastErrorLog > LOG_AFTER_SEC * 1000) {
|
|
69
|
-
cds.log(COMPONENT_NAME).error("error redis client:", err);
|
|
70
|
-
lastErrorLog = dateNow;
|
|
71
|
-
}
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
client.on("reconnecting", () => {
|
|
75
|
-
const dateNow = Date.now();
|
|
76
|
-
if (dateNow - lastErrorLog > LOG_AFTER_SEC * 1000) {
|
|
77
|
-
cds.log(COMPONENT_NAME).info("redis client trying reconnect...");
|
|
78
|
-
lastErrorLog = dateNow;
|
|
79
|
-
}
|
|
80
|
-
});
|
|
81
|
-
}
|
|
82
|
-
await client.connect();
|
|
83
|
-
return client;
|
|
84
|
-
} catch (err) {
|
|
85
|
-
errorHandlerCreateClient(err);
|
|
86
|
-
}
|
|
87
|
-
};
|
|
88
|
-
|
|
89
|
-
const subscribeRedisChannel = (options, channel, subscribeHandler) => {
|
|
90
|
-
subscribedChannels[channel] = subscribeHandler;
|
|
91
|
-
const errorHandlerCreateClient = (err) => {
|
|
92
|
-
cds.log(COMPONENT_NAME).error(`error from redis client for pub/sub failed for channel ${channel}`, err);
|
|
93
|
-
subscriberClientPromise?.then?.(_resilientClientClose);
|
|
94
|
-
subscriberClientPromise = null;
|
|
95
|
-
setTimeout(() => _subscribeChannels(options, subscribedChannels, subscribeHandler), LOG_AFTER_SEC * 1000).unref();
|
|
96
|
-
};
|
|
97
|
-
|
|
98
|
-
_subscribeChannels(options, { [channel]: subscribeHandler }, errorHandlerCreateClient);
|
|
99
|
-
};
|
|
100
|
-
|
|
101
|
-
const _subscribeChannels = (options, subscribedChannels, errorHandlerCreateClient) => {
|
|
102
|
-
subscriberClientPromise = createClientAndConnect(options, errorHandlerCreateClient)
|
|
103
|
-
.then((client) => {
|
|
104
|
-
for (const channel in subscribedChannels) {
|
|
105
|
-
const fn = subscribedChannels[channel];
|
|
106
|
-
client._subscribedChannels ??= {};
|
|
107
|
-
if (client._subscribedChannels[channel]) {
|
|
108
|
-
continue;
|
|
109
|
-
}
|
|
110
|
-
const prefixedChannelName = [options.redisNamespace, channel].join("_");
|
|
111
|
-
cds.log(COMPONENT_NAME).info("subscribe redis client connected channel", { channel: prefixedChannelName });
|
|
112
|
-
client
|
|
113
|
-
.subscribe(prefixedChannelName, fn)
|
|
114
|
-
.then(() => {
|
|
115
|
-
client._subscribedChannels ??= {};
|
|
116
|
-
client._subscribedChannels[channel] = 1;
|
|
117
|
-
})
|
|
118
|
-
.catch(() => {
|
|
119
|
-
cds.log(COMPONENT_NAME).error("error subscribe to channel - retrying...");
|
|
120
|
-
setTimeout(() => _subscribeChannels(options, [channel], fn), LOG_AFTER_SEC * 1000).unref();
|
|
121
|
-
});
|
|
122
|
-
}
|
|
123
|
-
})
|
|
124
|
-
.catch((err) => {
|
|
125
|
-
cds
|
|
126
|
-
.log(COMPONENT_NAME)
|
|
127
|
-
.error(
|
|
128
|
-
`error from redis client for pub/sub failed during startup - trying to reconnect - ${Object.keys(
|
|
129
|
-
subscribedChannels
|
|
130
|
-
).join(", ")}`,
|
|
131
|
-
err
|
|
132
|
-
);
|
|
133
|
-
});
|
|
134
|
-
};
|
|
135
|
-
|
|
136
|
-
const publishMessage = async (options, channel, message) => {
|
|
137
|
-
const client = await createMainClientAndConnect(options);
|
|
138
|
-
return await client.publish([options.redisNamespace, channel].join("_"), message);
|
|
139
|
-
};
|
|
140
|
-
|
|
141
|
-
const closeMainClient = async () => {
|
|
142
|
-
await _resilientClientClose(await mainClientPromise);
|
|
143
|
-
cds.log(COMPONENT_NAME).info("main redis client closed!");
|
|
144
|
-
};
|
|
145
|
-
|
|
146
|
-
const closeSubscribeClient = async () => {
|
|
147
|
-
await _resilientClientClose(await subscriberClientPromise);
|
|
148
|
-
cds.log(COMPONENT_NAME).info("subscribe redis client closed!");
|
|
149
|
-
};
|
|
150
|
-
|
|
151
|
-
const _resilientClientClose = async (client) => {
|
|
152
|
-
try {
|
|
153
|
-
if (client?.quit) {
|
|
154
|
-
await client.quit();
|
|
155
|
-
}
|
|
156
|
-
} catch (err) {
|
|
157
|
-
cds.log(COMPONENT_NAME).info("error during redis close - continuing...", err);
|
|
158
|
-
}
|
|
159
|
-
};
|
|
160
|
-
|
|
161
|
-
const connectionCheck = async (options) => {
|
|
162
|
-
return new Promise((resolve, reject) => {
|
|
163
|
-
createClientAndConnect(options, reject, true)
|
|
164
|
-
.then((client) => {
|
|
165
|
-
if (client) {
|
|
166
|
-
_resilientClientClose(client);
|
|
167
|
-
resolve();
|
|
168
|
-
} else {
|
|
169
|
-
reject(new Error());
|
|
170
|
-
}
|
|
171
|
-
})
|
|
172
|
-
.catch(reject);
|
|
173
|
-
})
|
|
174
|
-
.then(() => true)
|
|
175
|
-
.catch((err) => {
|
|
176
|
-
cds.log(COMPONENT_NAME).error("Redis connection check failed! Falling back to NO_REDIS mode", err);
|
|
177
|
-
return false;
|
|
178
|
-
});
|
|
179
|
-
};
|
|
180
|
-
|
|
181
|
-
const isClusterMode = () => {
|
|
182
|
-
if (!("__clusterMode" in isClusterMode)) {
|
|
183
|
-
const env = getEnvInstance();
|
|
184
|
-
const { credentials } = env.redisRequires;
|
|
185
|
-
isClusterMode.__clusterMode = credentials.cluster_mode;
|
|
186
|
-
}
|
|
187
|
-
return isClusterMode.__clusterMode;
|
|
188
|
-
};
|
|
189
|
-
|
|
190
|
-
module.exports = {
|
|
191
|
-
createClientAndConnect,
|
|
192
|
-
createMainClientAndConnect,
|
|
193
|
-
subscribeRedisChannel,
|
|
194
|
-
publishMessage,
|
|
195
|
-
closeMainClient,
|
|
196
|
-
closeSubscribeClient,
|
|
197
|
-
connectionCheck,
|
|
198
|
-
isClusterMode,
|
|
199
|
-
};
|