@cap-js-community/event-queue 1.3.5 → 1.4.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/package.json +1 -1
- package/src/EventQueueProcessorBase.js +2 -2
- package/src/config.js +29 -15
- package/src/dbHandler.js +11 -8
- package/src/initialize.js +8 -64
- package/src/redis/redisPub.js +93 -0
- package/src/redis/redisSub.js +95 -0
- package/src/runner/openEvents.js +52 -0
- package/src/{runner.js → runner/runner.js} +140 -117
- package/src/runner/runnerHelper.js +37 -0
- package/src/shared/cdsHelper.js +9 -0
- package/src/shared/distributedLock.js +3 -3
- package/src/shared/eventScheduler.js +2 -2
- package/src/shared/redis.js +35 -23
- package/src/redisPubSub.js +0 -170
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cap-js-community/event-queue",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "An event queue that enables secure transactional processing of asynchronous and periodic events, featuring instant event processing with Redis Pub/Sub and load distribution across all application instances.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"files": [
|
|
@@ -10,7 +10,7 @@ const { arrayToFlatMap } = require("./shared/common");
|
|
|
10
10
|
const eventScheduler = require("./shared/eventScheduler");
|
|
11
11
|
const eventConfig = require("./config");
|
|
12
12
|
const PerformanceTracer = require("./shared/PerformanceTracer");
|
|
13
|
-
const { broadcastEvent } = require("./
|
|
13
|
+
const { broadcastEvent } = require("./redis/redisPub");
|
|
14
14
|
|
|
15
15
|
const IMPLEMENT_ERROR_MESSAGE = "needs to be reimplemented";
|
|
16
16
|
const COMPONENT_NAME = "/eventQueue/EventQueueProcessorBase";
|
|
@@ -1113,7 +1113,7 @@ class EventQueueProcessorBase {
|
|
|
1113
1113
|
|
|
1114
1114
|
broadCastEvent() {
|
|
1115
1115
|
setTimeout(() => {
|
|
1116
|
-
broadcastEvent(this.__baseContext.tenant, this.#eventType, this.#eventSubType).catch((err) => {
|
|
1116
|
+
broadcastEvent(this.__baseContext.tenant, { type: this.#eventType, subType: this.#eventSubType }).catch((err) => {
|
|
1117
1117
|
this.logger.error("could not execute scheduled event", err, {
|
|
1118
1118
|
tenantId: this.__baseContext.tenant,
|
|
1119
1119
|
type: this.#eventType,
|
package/src/config.js
CHANGED
|
@@ -34,6 +34,11 @@ const BASE_PERIODIC_EVENTS = [
|
|
|
34
34
|
},
|
|
35
35
|
];
|
|
36
36
|
|
|
37
|
+
const BASE_TABLES = {
|
|
38
|
+
EVENT: "sap.eventqueue.Event",
|
|
39
|
+
LOCK: "sap.eventqueue.Lock",
|
|
40
|
+
};
|
|
41
|
+
|
|
37
42
|
class Config {
|
|
38
43
|
#logger;
|
|
39
44
|
#config;
|
|
@@ -61,6 +66,7 @@ class Config {
|
|
|
61
66
|
#userId;
|
|
62
67
|
#enableTxConsistencyCheck;
|
|
63
68
|
#cleanupLocksAndEventsForDev;
|
|
69
|
+
#redisOptions;
|
|
64
70
|
static #instance;
|
|
65
71
|
constructor() {
|
|
66
72
|
this.#logger = cds.log(COMPONENT_NAME);
|
|
@@ -105,7 +111,7 @@ class Config {
|
|
|
105
111
|
|
|
106
112
|
attachConfigChangeHandler() {
|
|
107
113
|
this.#attachBlockListChangeHandler();
|
|
108
|
-
redis.subscribeRedisChannel(REDIS_CONFIG_CHANNEL, (messageData) => {
|
|
114
|
+
redis.subscribeRedisChannel(this.#redisOptions, REDIS_CONFIG_CHANNEL, (messageData) => {
|
|
109
115
|
try {
|
|
110
116
|
const { key, value } = JSON.parse(messageData);
|
|
111
117
|
if (this[key] !== value) {
|
|
@@ -125,13 +131,13 @@ class Config {
|
|
|
125
131
|
this.#logger.info("redis not connected, config change won't be published", { key, value });
|
|
126
132
|
return;
|
|
127
133
|
}
|
|
128
|
-
redis.publishMessage(REDIS_CONFIG_CHANNEL, JSON.stringify({ key, value })).catch((error) => {
|
|
134
|
+
redis.publishMessage(this.#redisOptions, REDIS_CONFIG_CHANNEL, JSON.stringify({ key, value })).catch((error) => {
|
|
129
135
|
this.#logger.error(`publishing config change failed key: ${key}, value: ${value}`, error);
|
|
130
136
|
});
|
|
131
137
|
}
|
|
132
138
|
|
|
133
139
|
#attachBlockListChangeHandler() {
|
|
134
|
-
redis.subscribeRedisChannel(REDIS_CONFIG_BLOCKLIST_CHANNEL, (messageData) => {
|
|
140
|
+
redis.subscribeRedisChannel(this.#redisOptions, REDIS_CONFIG_BLOCKLIST_CHANNEL, (messageData) => {
|
|
135
141
|
try {
|
|
136
142
|
const { command, key, tenant } = JSON.parse(messageData);
|
|
137
143
|
if (command === COMMAND_BLOCK) {
|
|
@@ -160,7 +166,11 @@ class Config {
|
|
|
160
166
|
}
|
|
161
167
|
|
|
162
168
|
redis
|
|
163
|
-
.publishMessage(
|
|
169
|
+
.publishMessage(
|
|
170
|
+
this.#redisOptions,
|
|
171
|
+
REDIS_CONFIG_BLOCKLIST_CHANNEL,
|
|
172
|
+
JSON.stringify({ command: COMMAND_BLOCK, key, tenant })
|
|
173
|
+
)
|
|
164
174
|
.catch((error) => {
|
|
165
175
|
this.#logger.error(`publishing config block failed key: ${key}`, error);
|
|
166
176
|
});
|
|
@@ -189,7 +199,11 @@ class Config {
|
|
|
189
199
|
}
|
|
190
200
|
|
|
191
201
|
redis
|
|
192
|
-
.publishMessage(
|
|
202
|
+
.publishMessage(
|
|
203
|
+
this.#redisOptions,
|
|
204
|
+
REDIS_CONFIG_BLOCKLIST_CHANNEL,
|
|
205
|
+
JSON.stringify({ command: COMMAND_UNBLOCK, key, tenant })
|
|
206
|
+
)
|
|
193
207
|
.catch((error) => {
|
|
194
208
|
this.#logger.error(`publishing config block failed key: ${key}`, error);
|
|
195
209
|
});
|
|
@@ -386,19 +400,11 @@ class Config {
|
|
|
386
400
|
}
|
|
387
401
|
|
|
388
402
|
get tableNameEventQueue() {
|
|
389
|
-
return
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
set tableNameEventQueue(value) {
|
|
393
|
-
this.#tableNameEventQueue = value;
|
|
403
|
+
return BASE_TABLES.EVENT;
|
|
394
404
|
}
|
|
395
405
|
|
|
396
406
|
get tableNameEventLock() {
|
|
397
|
-
return
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
set tableNameEventLock(value) {
|
|
401
|
-
this.#tableNameEventLock = value;
|
|
407
|
+
return BASE_TABLES.LOCK;
|
|
402
408
|
}
|
|
403
409
|
|
|
404
410
|
set configFilePath(value) {
|
|
@@ -489,6 +495,14 @@ class Config {
|
|
|
489
495
|
return this.#cleanupLocksAndEventsForDev;
|
|
490
496
|
}
|
|
491
497
|
|
|
498
|
+
set redisOptions(value) {
|
|
499
|
+
this.#redisOptions = value;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
get redisOptions() {
|
|
503
|
+
return this.#redisOptions;
|
|
504
|
+
}
|
|
505
|
+
|
|
492
506
|
get isMultiTenancy() {
|
|
493
507
|
return !!cds.requires.multitenancy;
|
|
494
508
|
}
|
package/src/dbHandler.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const cds = require("@sap/cds");
|
|
4
4
|
|
|
5
|
-
const { broadcastEvent } = require("./
|
|
5
|
+
const { broadcastEvent } = require("./redis/redisPub");
|
|
6
6
|
const config = require("./config");
|
|
7
7
|
|
|
8
8
|
const COMPONENT_NAME = "/eventQueue/dbHandler";
|
|
@@ -31,14 +31,17 @@ const registerEventQueueDbHandler = (dbService) => {
|
|
|
31
31
|
|
|
32
32
|
eventCombinations.length &&
|
|
33
33
|
req.on("succeeded", () => {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
34
|
+
const events = eventCombinations.map((eventCombination) => {
|
|
35
|
+
const [type, subType] = eventCombination.split("##");
|
|
36
|
+
return { type, subType };
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
broadcastEvent(req.tenant, events).catch((err) => {
|
|
40
|
+
cds.log(COMPONENT_NAME).error("db handler failure during broadcasting event", err, {
|
|
41
|
+
tenant: req.tenant,
|
|
42
|
+
events,
|
|
40
43
|
});
|
|
41
|
-
}
|
|
44
|
+
});
|
|
42
45
|
});
|
|
43
46
|
});
|
|
44
47
|
};
|
package/src/initialize.js
CHANGED
|
@@ -7,11 +7,10 @@ const cds = require("@sap/cds");
|
|
|
7
7
|
const yaml = require("yaml");
|
|
8
8
|
const VError = require("verror");
|
|
9
9
|
|
|
10
|
-
const
|
|
11
|
-
const runner = require("./runner");
|
|
10
|
+
const runner = require("./runner/runner");
|
|
12
11
|
const dbHandler = require("./dbHandler");
|
|
13
12
|
const config = require("./config");
|
|
14
|
-
const { initEventQueueRedisSubscribe, closeSubscribeClient } = require("./
|
|
13
|
+
const { initEventQueueRedisSubscribe, closeSubscribeClient } = require("./redis/redisSub");
|
|
15
14
|
const redis = require("./shared/redis");
|
|
16
15
|
const eventQueueAsOutbox = require("./outbox/eventQueueAsOutbox");
|
|
17
16
|
const { getAllTenantIds } = require("./shared/cdsHelper");
|
|
@@ -21,26 +20,21 @@ const readFileAsync = promisify(fs.readFile);
|
|
|
21
20
|
|
|
22
21
|
const VERROR_CLUSTER_NAME = "EventQueueInitialization";
|
|
23
22
|
const COMPONENT = "eventQueue/initialize";
|
|
24
|
-
|
|
25
|
-
EVENT: "sap.eventqueue.Event",
|
|
26
|
-
LOCK: "sap.eventqueue.Lock",
|
|
27
|
-
};
|
|
23
|
+
|
|
28
24
|
const CONFIG_VARS = [
|
|
29
25
|
["configFilePath", null],
|
|
30
26
|
["registerAsEventProcessor", true],
|
|
31
27
|
["processEventsAfterPublish", true],
|
|
32
28
|
["isEventQueueActive", true],
|
|
33
29
|
["runInterval", 25 * 60 * 1000],
|
|
34
|
-
["tableNameEventQueue", BASE_TABLES.EVENT],
|
|
35
|
-
["tableNameEventLock", BASE_TABLES.LOCK],
|
|
36
30
|
["disableRedis", true],
|
|
37
|
-
["skipCsnCheck", false],
|
|
38
31
|
["updatePeriodicEvents", true],
|
|
39
32
|
["thresholdLoggingEventProcessing", 50],
|
|
40
33
|
["useAsCAPOutbox", false],
|
|
41
34
|
["userId", null],
|
|
42
35
|
["enableTxConsistencyCheck", false],
|
|
43
36
|
["cleanupLocksAndEventsForDev", false],
|
|
37
|
+
["redisOptions", {}],
|
|
44
38
|
];
|
|
45
39
|
|
|
46
40
|
const initialize = async ({
|
|
@@ -49,16 +43,14 @@ const initialize = async ({
|
|
|
49
43
|
processEventsAfterPublish,
|
|
50
44
|
isEventQueueActive,
|
|
51
45
|
runInterval,
|
|
52
|
-
tableNameEventQueue,
|
|
53
|
-
tableNameEventLock,
|
|
54
46
|
disableRedis,
|
|
55
|
-
skipCsnCheck,
|
|
56
47
|
updatePeriodicEvents,
|
|
57
48
|
thresholdLoggingEventProcessing,
|
|
58
49
|
useAsCAPOutbox,
|
|
59
50
|
userId,
|
|
60
51
|
enableTxConsistencyCheck,
|
|
61
52
|
cleanupLocksAndEventsForDev,
|
|
53
|
+
redisOptions,
|
|
62
54
|
} = {}) => {
|
|
63
55
|
if (config.initialized) {
|
|
64
56
|
return;
|
|
@@ -71,16 +63,14 @@ const initialize = async ({
|
|
|
71
63
|
processEventsAfterPublish,
|
|
72
64
|
isEventQueueActive,
|
|
73
65
|
runInterval,
|
|
74
|
-
tableNameEventQueue,
|
|
75
|
-
tableNameEventLock,
|
|
76
66
|
disableRedis,
|
|
77
|
-
skipCsnCheck,
|
|
78
67
|
updatePeriodicEvents,
|
|
79
68
|
thresholdLoggingEventProcessing,
|
|
80
69
|
useAsCAPOutbox,
|
|
81
70
|
userId,
|
|
82
71
|
enableTxConsistencyCheck,
|
|
83
|
-
cleanupLocksAndEventsForDev
|
|
72
|
+
cleanupLocksAndEventsForDev,
|
|
73
|
+
redisOptions
|
|
84
74
|
);
|
|
85
75
|
|
|
86
76
|
const logger = cds.log(COMPONENT);
|
|
@@ -95,12 +85,10 @@ const initialize = async ({
|
|
|
95
85
|
}
|
|
96
86
|
});
|
|
97
87
|
if (redisEnabled) {
|
|
98
|
-
config.redisEnabled = await redis.connectionCheck();
|
|
88
|
+
config.redisEnabled = await redis.connectionCheck(config.redisOptions);
|
|
99
89
|
}
|
|
100
90
|
config.fileContent = await readConfigFromFile(config.configFilePath);
|
|
101
91
|
|
|
102
|
-
!config.skipCsnCheck && (await csnCheck());
|
|
103
|
-
|
|
104
92
|
monkeyPatchCAPOutbox();
|
|
105
93
|
registerCdsShutdown();
|
|
106
94
|
logger.info("event queue initialized", {
|
|
@@ -172,50 +160,6 @@ const monkeyPatchCAPOutbox = () => {
|
|
|
172
160
|
}
|
|
173
161
|
};
|
|
174
162
|
|
|
175
|
-
const csnCheck = async () => {
|
|
176
|
-
cds.on("loaded", async (csn) => {
|
|
177
|
-
if (csn.namespace === "cds.xt") {
|
|
178
|
-
return;
|
|
179
|
-
}
|
|
180
|
-
const eventCsn = csn.definitions[config.tableNameEventQueue];
|
|
181
|
-
if (!eventCsn) {
|
|
182
|
-
throw EventQueueError.missingTableInCsn(config.tableNameEventQueue);
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
const lockCsn = csn.definitions[config.tableNameEventLock];
|
|
186
|
-
if (!lockCsn) {
|
|
187
|
-
throw EventQueueError.missingTableInCsn(config.tableNameEventLock);
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
if (config.tableNameEventQueue === BASE_TABLES.EVENT && config.tableNameEventLock === BASE_TABLES.LOCK) {
|
|
191
|
-
return; // no need to check base tables
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
const baseEvent = csn.definitions["sap.eventqueue.Event"];
|
|
195
|
-
const baseLock = csn.definitions["sap.eventqueue.Lock"];
|
|
196
|
-
|
|
197
|
-
checkCustomTable(baseEvent, eventCsn);
|
|
198
|
-
checkCustomTable(baseLock, lockCsn);
|
|
199
|
-
});
|
|
200
|
-
};
|
|
201
|
-
|
|
202
|
-
const checkCustomTable = (baseCsn, customCsn) => {
|
|
203
|
-
for (const columnName in baseCsn.elements) {
|
|
204
|
-
if (!customCsn.elements[columnName]) {
|
|
205
|
-
throw EventQueueError.missingElementInTable(customCsn.name, columnName);
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
if (
|
|
209
|
-
customCsn.elements[columnName].type !== "cds.Association" &&
|
|
210
|
-
customCsn.elements[columnName].type !== baseCsn.elements[columnName].type &&
|
|
211
|
-
columnName === "status" &&
|
|
212
|
-
customCsn.elements[columnName].type !== "cds.Integer"
|
|
213
|
-
) {
|
|
214
|
-
throw EventQueueError.typeMismatchInTable(customCsn.name, columnName);
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
};
|
|
218
|
-
|
|
219
163
|
const mixConfigVarsWithEnv = (...args) => {
|
|
220
164
|
CONFIG_VARS.forEach(([configName, defaultValue], index) => {
|
|
221
165
|
const configValue = args[index];
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { promisify } = require("util");
|
|
4
|
+
|
|
5
|
+
const cds = require("@sap/cds");
|
|
6
|
+
|
|
7
|
+
const redis = require("../shared/redis");
|
|
8
|
+
const { checkLockExistsAndReturnValue } = require("../shared/distributedLock");
|
|
9
|
+
const config = require("../config");
|
|
10
|
+
const { getSubdomainForTenantId } = require("../shared/cdsHelper");
|
|
11
|
+
const { runEventCombinationForTenant } = require("../runner/runnerHelper");
|
|
12
|
+
|
|
13
|
+
const EVENT_MESSAGE_CHANNEL = "EVENT_QUEUE_MESSAGE_CHANNEL";
|
|
14
|
+
const COMPONENT_NAME = "/eventQueue/redisPub";
|
|
15
|
+
const TRIES_FOR_PUBLISH_PERIODIC_EVENT = 10;
|
|
16
|
+
const SLEEP_TIME_FOR_PUBLISH_PERIODIC_EVENT = 30 * 1000;
|
|
17
|
+
|
|
18
|
+
const wait = promisify(setTimeout);
|
|
19
|
+
|
|
20
|
+
const broadcastEvent = async (tenantId, events) => {
|
|
21
|
+
const logger = cds.log(COMPONENT_NAME);
|
|
22
|
+
events = Array.isArray(events) ? events : [events];
|
|
23
|
+
try {
|
|
24
|
+
if (!config.isEventQueueActive) {
|
|
25
|
+
cds.log(COMPONENT_NAME).info("Skipping processing because runner is deactivated!", {});
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
if (!config.redisEnabled) {
|
|
29
|
+
if (config.registerAsEventProcessor) {
|
|
30
|
+
let context = {};
|
|
31
|
+
if (tenantId) {
|
|
32
|
+
const subdomain = await getSubdomainForTenantId(tenantId);
|
|
33
|
+
const user = new cds.User.Privileged(config.userId);
|
|
34
|
+
context = {
|
|
35
|
+
// NOTE: we need this because of logging otherwise logs would not contain the subdomain
|
|
36
|
+
tenant: tenantId,
|
|
37
|
+
user,
|
|
38
|
+
http: { req: { authInfo: { getSubdomain: () => subdomain } } },
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return await cds.tx(context, async ({ context }) => {
|
|
43
|
+
for (const { type, subType } of events) {
|
|
44
|
+
await runEventCombinationForTenant(context, type, subType);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
for (const { type, subType } of events) {
|
|
51
|
+
const eventConfig = config.getEventConfig(type, subType);
|
|
52
|
+
for (let i = 0; i < TRIES_FOR_PUBLISH_PERIODIC_EVENT; i++) {
|
|
53
|
+
const result = await checkLockExistsAndReturnValue(
|
|
54
|
+
new cds.EventContext({ tenant: tenantId }),
|
|
55
|
+
[type, subType].join("##")
|
|
56
|
+
);
|
|
57
|
+
if (result) {
|
|
58
|
+
logger.debug("skip publish redis event as no lock is available", {
|
|
59
|
+
type,
|
|
60
|
+
subType,
|
|
61
|
+
index: i,
|
|
62
|
+
isPeriodic: eventConfig.isPeriodic,
|
|
63
|
+
waitInterval: SLEEP_TIME_FOR_PUBLISH_PERIODIC_EVENT,
|
|
64
|
+
});
|
|
65
|
+
if (!eventConfig.isPeriodic) {
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
await wait(SLEEP_TIME_FOR_PUBLISH_PERIODIC_EVENT);
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
logger.debug("publishing redis event", {
|
|
72
|
+
tenantId,
|
|
73
|
+
type,
|
|
74
|
+
subType,
|
|
75
|
+
});
|
|
76
|
+
await redis.publishMessage(
|
|
77
|
+
config.redisOptions,
|
|
78
|
+
EVENT_MESSAGE_CHANNEL,
|
|
79
|
+
JSON.stringify({ tenantId, type, subType })
|
|
80
|
+
);
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
} catch (err) {
|
|
85
|
+
logger.error("publish events failed!", err, {
|
|
86
|
+
tenantId,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
module.exports = {
|
|
92
|
+
broadcastEvent,
|
|
93
|
+
};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const cds = require("@sap/cds");
|
|
4
|
+
|
|
5
|
+
const redis = require("../shared/redis");
|
|
6
|
+
const config = require("../config");
|
|
7
|
+
const { getSubdomainForTenantId } = require("../shared/cdsHelper");
|
|
8
|
+
const runnerHelper = require("../runner/runnerHelper");
|
|
9
|
+
|
|
10
|
+
const EVENT_MESSAGE_CHANNEL = "EVENT_QUEUE_MESSAGE_CHANNEL";
|
|
11
|
+
const COMPONENT_NAME = "/eventQueue/redisSub";
|
|
12
|
+
let subscriberClientPromise;
|
|
13
|
+
|
|
14
|
+
const initEventQueueRedisSubscribe = () => {
|
|
15
|
+
if (subscriberClientPromise || !config.redisEnabled) {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
redis.subscribeRedisChannel(config.redisOptions, EVENT_MESSAGE_CHANNEL, _messageHandlerProcessEvents);
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const _messageHandlerProcessEvents = async (messageData) => {
|
|
22
|
+
const logger = cds.log(COMPONENT_NAME);
|
|
23
|
+
try {
|
|
24
|
+
const { tenantId, type, subType } = JSON.parse(messageData);
|
|
25
|
+
logger.debug("received redis event", {
|
|
26
|
+
tenantId,
|
|
27
|
+
type,
|
|
28
|
+
subType,
|
|
29
|
+
});
|
|
30
|
+
if (!config.isEventQueueActive) {
|
|
31
|
+
cds.log(COMPONENT_NAME).info("Skipping processing because runner is deactivated!", {
|
|
32
|
+
type,
|
|
33
|
+
subType,
|
|
34
|
+
});
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const subdomain = await getSubdomainForTenantId(tenantId);
|
|
39
|
+
const user = new cds.User.Privileged(config.userId);
|
|
40
|
+
const tenantContext = {
|
|
41
|
+
tenant: tenantId,
|
|
42
|
+
user,
|
|
43
|
+
// NOTE: we need this because of logging otherwise logs would not contain the subdomain
|
|
44
|
+
http: { req: { authInfo: { getSubdomain: () => subdomain } } },
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
if (!config.getEventConfig(type, subType)) {
|
|
48
|
+
if (config.isCapOutboxEvent(type)) {
|
|
49
|
+
try {
|
|
50
|
+
const service = await cds.connect.to(subType);
|
|
51
|
+
cds.outboxed(service);
|
|
52
|
+
} catch (err) {
|
|
53
|
+
logger.error("could not connect to outboxed service", err, {
|
|
54
|
+
type,
|
|
55
|
+
subType,
|
|
56
|
+
});
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
} else {
|
|
60
|
+
logger.error("cannot find configuration for published event. Event won't be processed", {
|
|
61
|
+
type,
|
|
62
|
+
subType,
|
|
63
|
+
});
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return await cds.tx(tenantContext, async ({ context }) => {
|
|
69
|
+
return await runnerHelper.runEventCombinationForTenant(context, type, subType);
|
|
70
|
+
});
|
|
71
|
+
} catch (err) {
|
|
72
|
+
logger.error("could not parse event information", {
|
|
73
|
+
messageData,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const closeSubscribeClient = async () => {
|
|
79
|
+
try {
|
|
80
|
+
const client = await subscriberClientPromise;
|
|
81
|
+
if (client?.quit) {
|
|
82
|
+
await client.quit();
|
|
83
|
+
}
|
|
84
|
+
} catch (err) {
|
|
85
|
+
// ignore errors during shutdown
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
module.exports = {
|
|
90
|
+
initEventQueueRedisSubscribe,
|
|
91
|
+
closeSubscribeClient,
|
|
92
|
+
__: {
|
|
93
|
+
_messageHandlerProcessEvents,
|
|
94
|
+
},
|
|
95
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const cds = require("@sap/cds");
|
|
4
|
+
|
|
5
|
+
const eventConfig = require("../config");
|
|
6
|
+
const { EventProcessingStatus } = require("../constants");
|
|
7
|
+
|
|
8
|
+
const getOpenQueueEntries = async (tx) => {
|
|
9
|
+
const startTime = new Date();
|
|
10
|
+
const refDateStartAfter = new Date(startTime.getTime() + eventConfig.runInterval * 1.2);
|
|
11
|
+
const entries = await tx.run(
|
|
12
|
+
SELECT.from(eventConfig.tableNameEventQueue)
|
|
13
|
+
.where(
|
|
14
|
+
"( startAfter IS NULL OR startAfter <=",
|
|
15
|
+
refDateStartAfter.toISOString(),
|
|
16
|
+
" ) AND ( status =",
|
|
17
|
+
EventProcessingStatus.Open,
|
|
18
|
+
"OR ( status =",
|
|
19
|
+
EventProcessingStatus.Error,
|
|
20
|
+
") OR ( status =",
|
|
21
|
+
EventProcessingStatus.InProgress,
|
|
22
|
+
"AND lastAttemptTimestamp <=",
|
|
23
|
+
new Date(startTime.getTime() - eventConfig.globalTxTimeout).toISOString(),
|
|
24
|
+
") )"
|
|
25
|
+
)
|
|
26
|
+
.columns("type", "subType")
|
|
27
|
+
.groupBy("type", "subType")
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
const result = [];
|
|
31
|
+
for (const { type, subType } of entries) {
|
|
32
|
+
if (type.startsWith("CAP_OUTBOX")) {
|
|
33
|
+
if (cds.requires[subType]) {
|
|
34
|
+
result.push({ type, subType });
|
|
35
|
+
} else {
|
|
36
|
+
const service = await cds.connect.to(subType).catch(() => {});
|
|
37
|
+
if (service) {
|
|
38
|
+
result.push({ type, subType });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
} else {
|
|
42
|
+
if (eventConfig.getEventConfig(type, subType)) {
|
|
43
|
+
result.push({ type, subType });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return result;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
module.exports = {
|
|
51
|
+
getOpenQueueEntries,
|
|
52
|
+
};
|
|
@@ -1,32 +1,33 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
const { randomUUID } = require("crypto");
|
|
4
|
-
const { AsyncResource } = require("async_hooks");
|
|
5
4
|
|
|
6
5
|
const cds = require("@sap/cds");
|
|
7
6
|
|
|
8
|
-
const eventQueueConfig = require("
|
|
9
|
-
const
|
|
10
|
-
const
|
|
11
|
-
const
|
|
12
|
-
const
|
|
13
|
-
const
|
|
14
|
-
const
|
|
15
|
-
const
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
-
const
|
|
7
|
+
const eventQueueConfig = require("../config");
|
|
8
|
+
const WorkerQueue = require("../shared/WorkerQueue");
|
|
9
|
+
const cdsHelper = require("../shared/cdsHelper");
|
|
10
|
+
const distributedLock = require("../shared/distributedLock");
|
|
11
|
+
const SetIntervalDriftSafe = require("../shared/SetIntervalDriftSafe");
|
|
12
|
+
const { getSubdomainForTenantId } = require("../shared/cdsHelper");
|
|
13
|
+
const periodicEvents = require("../periodicEvents");
|
|
14
|
+
const { hashStringTo32Bit } = require("../shared/common");
|
|
15
|
+
const config = require("../config");
|
|
16
|
+
const redisPub = require("../redis/redisPub");
|
|
17
|
+
const openEvents = require("./openEvents");
|
|
18
|
+
const { runEventCombinationForTenant } = require("./runnerHelper");
|
|
19
19
|
|
|
20
20
|
const COMPONENT_NAME = "/eventQueue/runner";
|
|
21
21
|
const EVENT_QUEUE_RUN_ID = "EVENT_QUEUE_RUN_ID";
|
|
22
22
|
const EVENT_QUEUE_RUN_TS = "EVENT_QUEUE_RUN_TS";
|
|
23
|
-
const
|
|
23
|
+
const EVENT_QUEUE_RUN_REDIS_CHECK = "EVENT_QUEUE_RUN_REDIS_CHECK";
|
|
24
|
+
const EVENT_QUEUE_UPDATE_PERIODIC_EVENTS = "EVENT_QUEUE_UPDATE_PERIODIC_EVENTS";
|
|
24
25
|
const OFFSET_FIRST_RUN = 10 * 1000;
|
|
25
26
|
|
|
26
27
|
let tenantIdHash;
|
|
27
28
|
let singleRunDone;
|
|
28
29
|
|
|
29
|
-
const singleTenant = () => _scheduleFunction(
|
|
30
|
+
const singleTenant = () => _scheduleFunction(_checkPeriodicEventsSingleTenantOneTime, _singleTenantDb);
|
|
30
31
|
|
|
31
32
|
const multiTenancyDb = () => _scheduleFunction(async () => {}, _multiTenancyDb);
|
|
32
33
|
|
|
@@ -68,19 +69,14 @@ const _scheduleFunction = async (singleRunFn, periodicFn) => {
|
|
|
68
69
|
|
|
69
70
|
const _multiTenancyRedis = async () => {
|
|
70
71
|
const logger = cds.log(COMPONENT_NAME);
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
if (!runId) {
|
|
79
|
-
logger.error("could not acquire runId, skip processing events!");
|
|
80
|
-
return;
|
|
72
|
+
try {
|
|
73
|
+
logger.info("executing event queue run for multi instance and tenant");
|
|
74
|
+
const tenantIds = await cdsHelper.getAllTenantIds();
|
|
75
|
+
await _checkPeriodicEventUpdate(tenantIds);
|
|
76
|
+
return await _executeEventsAllTenantsRedis(tenantIds);
|
|
77
|
+
} catch (err) {
|
|
78
|
+
logger.info("executing event queue run for multi instance and tenant failed", err);
|
|
81
79
|
}
|
|
82
|
-
|
|
83
|
-
return await _executeEventsAllTenants(tenantIds, runId);
|
|
84
80
|
};
|
|
85
81
|
|
|
86
82
|
const _checkPeriodicEventUpdate = async (tenantIds) => {
|
|
@@ -100,80 +96,119 @@ const _checkPeriodicEventUpdate = async (tenantIds) => {
|
|
|
100
96
|
}
|
|
101
97
|
};
|
|
102
98
|
|
|
103
|
-
const
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
99
|
+
const _executeEventsAllTenantsRedis = async (tenantIds) => {
|
|
100
|
+
const logger = cds.log(COMPONENT_NAME);
|
|
101
|
+
try {
|
|
102
|
+
// NOTE: do checks for all tenants on the same app instance --> acquire lock tenant independent
|
|
103
|
+
// distribute from this instance to all others
|
|
104
|
+
const dummyContext = new cds.EventContext({});
|
|
105
|
+
const couldAcquireLock = await distributedLock.acquireLock(dummyContext, EVENT_QUEUE_RUN_REDIS_CHECK, {
|
|
106
|
+
expiryTime: eventQueueConfig.runInterval * 0.95,
|
|
107
|
+
tenantScoped: false,
|
|
108
108
|
});
|
|
109
|
-
|
|
110
|
-
|
|
109
|
+
if (!couldAcquireLock) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
111
112
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
const couldAcquireLock = await distributedLock.acquireLock(context, lockId, {
|
|
128
|
-
expiryTime: eventQueueConfig.runInterval * 0.95,
|
|
129
|
-
});
|
|
130
|
-
if (!couldAcquireLock) {
|
|
131
|
-
return;
|
|
132
|
-
}
|
|
133
|
-
await runEventCombinationForTenant(context, eventConfig.type, eventConfig.subType, true);
|
|
134
|
-
} catch (err) {
|
|
135
|
-
cds.log(COMPONENT_NAME).error("executing event-queue run for tenant failed", {
|
|
136
|
-
tenantId,
|
|
137
|
-
});
|
|
138
|
-
}
|
|
113
|
+
for (const tenantId of tenantIds) {
|
|
114
|
+
await cds.tx({ tenant: tenantId }, async (tx) => {
|
|
115
|
+
const entries = await openEvents.getOpenQueueEntries(tx);
|
|
116
|
+
logger.info("broadcasting events for run", {
|
|
117
|
+
tenantId,
|
|
118
|
+
entries: entries.length,
|
|
119
|
+
});
|
|
120
|
+
if (!entries.length) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
await redisPub.broadcastEvent(tenantId, entries).catch((err) => {
|
|
124
|
+
logger.error("broadcasting event failed", err, {
|
|
125
|
+
tenantId,
|
|
126
|
+
entries: entries.length,
|
|
127
|
+
});
|
|
139
128
|
});
|
|
140
129
|
});
|
|
141
|
-
}
|
|
142
|
-
)
|
|
130
|
+
}
|
|
131
|
+
} catch (err) {
|
|
132
|
+
logger.info("executing event queue run for multi instance and tenant failed", err);
|
|
133
|
+
}
|
|
143
134
|
};
|
|
144
135
|
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
136
|
+
const _executeEventsAllTenants = async (tenantIds, runId) => {
|
|
137
|
+
const promises = [];
|
|
138
|
+
|
|
139
|
+
for (const tenantId of tenantIds) {
|
|
140
|
+
const subdomain = await getSubdomainForTenantId(tenantId);
|
|
141
|
+
const user = new cds.User.Privileged(config.userId);
|
|
142
|
+
const tenantContext = {
|
|
143
|
+
tenant: tenantId,
|
|
144
|
+
user,
|
|
145
|
+
// NOTE: we need this because of logging otherwise logs would not contain the subdomain
|
|
146
|
+
http: { req: { authInfo: { getSubdomain: () => subdomain } } },
|
|
147
|
+
};
|
|
148
|
+
const events = await cds.tx(tenantContext, async (tx) => {
|
|
149
|
+
return await openEvents.getOpenQueueEntries(tx);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
if (!events.length) {
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
159
155
|
|
|
156
|
+
promises.concat(
|
|
157
|
+
events.map(async (openEvent) => {
|
|
158
|
+
const eventConfig = config.getEventConfig(openEvent.type, openEvent.subType);
|
|
159
|
+
const label = `${eventConfig.type}_${eventConfig.subType}`;
|
|
160
|
+
return await WorkerQueue.instance.addToQueue(eventConfig.load, label, eventConfig.priority, async () => {
|
|
160
161
|
return await cds.tx(tenantContext, async ({ context }) => {
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
162
|
+
try {
|
|
163
|
+
const lockId = `${runId}_${label}`;
|
|
164
|
+
const couldAcquireLock = await distributedLock.acquireLock(context, lockId, {
|
|
165
|
+
expiryTime: eventQueueConfig.runInterval * 0.95,
|
|
166
|
+
});
|
|
167
|
+
if (!couldAcquireLock) {
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
await runEventCombinationForTenant(context, eventConfig.type, eventConfig.subType, true);
|
|
171
|
+
} catch (err) {
|
|
172
|
+
cds.log(COMPONENT_NAME).error("executing event-queue run for tenant failed", {
|
|
173
|
+
tenantId,
|
|
174
|
+
});
|
|
166
175
|
}
|
|
167
|
-
await _checkPeriodicEventsSingleTenant(context);
|
|
168
176
|
});
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
|
|
177
|
+
});
|
|
178
|
+
})
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
return Promise.allSettled(promises);
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
const _executePeriodicEventsAllTenants = async (tenantIds) => {
|
|
185
|
+
for (const tenantId of tenantIds) {
|
|
186
|
+
try {
|
|
187
|
+
const subdomain = await getSubdomainForTenantId(tenantId);
|
|
188
|
+
const user = new cds.User.Privileged(config.userId);
|
|
189
|
+
const tenantContext = {
|
|
190
|
+
tenant: tenantId,
|
|
191
|
+
user,
|
|
192
|
+
// NOTE: we need this because of logging otherwise logs would not contain the subdomain
|
|
193
|
+
http: { req: { authInfo: { getSubdomain: () => subdomain } } },
|
|
194
|
+
};
|
|
195
|
+
await cds.tx(tenantContext, async ({ context }) => {
|
|
196
|
+
if (!config.redisEnabled) {
|
|
197
|
+
const couldAcquireLock = await distributedLock.acquireLock(context, EVENT_QUEUE_UPDATE_PERIODIC_EVENTS, {
|
|
198
|
+
expiryTime: eventQueueConfig.runInterval * 0.95,
|
|
172
199
|
});
|
|
200
|
+
if (!couldAcquireLock) {
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
173
203
|
}
|
|
204
|
+
await _checkPeriodicEventsSingleTenant(context);
|
|
174
205
|
});
|
|
175
|
-
})
|
|
176
|
-
|
|
206
|
+
} catch (err) {
|
|
207
|
+
cds.log(COMPONENT_NAME).error("executing event-queue run for tenant failed", {
|
|
208
|
+
tenantId,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
}
|
|
177
212
|
};
|
|
178
213
|
|
|
179
214
|
const _singleTenantDb = async (tenantId) => {
|
|
@@ -265,30 +300,6 @@ const _calculateOffsetForFirstRun = async () => {
|
|
|
265
300
|
return offsetDependingOnLastRun;
|
|
266
301
|
};
|
|
267
302
|
|
|
268
|
-
const runEventCombinationForTenant = async (context, type, subType, skipWorkerPool) => {
|
|
269
|
-
try {
|
|
270
|
-
if (skipWorkerPool) {
|
|
271
|
-
return await processEventQueue(context, type, subType);
|
|
272
|
-
} else {
|
|
273
|
-
const eventConfig = eventQueueConfig.getEventConfig(type, subType);
|
|
274
|
-
const label = `${type}_${subType}`;
|
|
275
|
-
return await WorkerQueue.instance.addToQueue(
|
|
276
|
-
eventConfig.load,
|
|
277
|
-
label,
|
|
278
|
-
eventConfig.priority,
|
|
279
|
-
AsyncResource.bind(async () => await processEventQueue(context, type, subType))
|
|
280
|
-
);
|
|
281
|
-
}
|
|
282
|
-
} catch (err) {
|
|
283
|
-
const logger = cds.log(COMPONENT_NAME);
|
|
284
|
-
logger.error("error executing event combination for tenant", err, {
|
|
285
|
-
tenantId: context.tenant,
|
|
286
|
-
type,
|
|
287
|
-
subType,
|
|
288
|
-
});
|
|
289
|
-
}
|
|
290
|
-
};
|
|
291
|
-
|
|
292
303
|
const _multiTenancyDb = async () => {
|
|
293
304
|
const logger = cds.log(COMPONENT_NAME);
|
|
294
305
|
try {
|
|
@@ -305,14 +316,29 @@ const _multiTenancyPeriodicEvents = async (tenantIds) => {
|
|
|
305
316
|
const logger = cds.log(COMPONENT_NAME);
|
|
306
317
|
try {
|
|
307
318
|
logger.info("executing event queue update periodic events");
|
|
319
|
+
|
|
320
|
+
if (config.redisEnabled) {
|
|
321
|
+
const dummyContext = new cds.EventContext({});
|
|
322
|
+
const couldAcquireLock = await distributedLock.acquireLock(dummyContext, EVENT_QUEUE_UPDATE_PERIODIC_EVENTS, {
|
|
323
|
+
expiryTime: 60 * 1000, // short living lock --> assume we do not have 2 onboards within 1 minute
|
|
324
|
+
tenantScoped: false,
|
|
325
|
+
});
|
|
326
|
+
if (!couldAcquireLock) {
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
308
331
|
tenantIds = tenantIds ?? (await cdsHelper.getAllTenantIds());
|
|
309
|
-
return await _executePeriodicEventsAllTenants(tenantIds
|
|
332
|
+
return await _executePeriodicEventsAllTenants(tenantIds);
|
|
310
333
|
} catch (err) {
|
|
311
334
|
logger.error("Couldn't fetch tenant ids for updating periodic event processing!", err);
|
|
312
335
|
}
|
|
313
336
|
};
|
|
314
337
|
|
|
315
|
-
const
|
|
338
|
+
const _checkPeriodicEventsSingleTenantOneTime = () =>
|
|
339
|
+
cds.tx({}, async (tx) => await periodicEvents.checkAndInsertPeriodicEvents(tx.context));
|
|
340
|
+
|
|
341
|
+
const _checkPeriodicEventsSingleTenant = async (context) => {
|
|
316
342
|
const logger = cds.log(COMPONENT_NAME);
|
|
317
343
|
if (!eventQueueConfig.updatePeriodicEvents || !eventQueueConfig.periodicEvents.length) {
|
|
318
344
|
logger.info("updating of periodic events is disabled or no periodic events configured", {
|
|
@@ -326,9 +352,7 @@ const _checkPeriodicEventsSingleTenant = async (context = {}) => {
|
|
|
326
352
|
tenantId: context.tenant,
|
|
327
353
|
subdomain: context.http?.req.authInfo.getSubdomain(),
|
|
328
354
|
});
|
|
329
|
-
await
|
|
330
|
-
await periodicEvents.checkAndInsertPeriodicEvents(tx.context);
|
|
331
|
-
});
|
|
355
|
+
await periodicEvents.checkAndInsertPeriodicEvents(context);
|
|
332
356
|
} catch (err) {
|
|
333
357
|
logger.error("Couldn't update periodic events for tenant! Next try after defined interval.", err, {
|
|
334
358
|
tenantId: context.tenant,
|
|
@@ -341,7 +365,6 @@ module.exports = {
|
|
|
341
365
|
singleTenant,
|
|
342
366
|
multiTenancyDb,
|
|
343
367
|
multiTenancyRedis,
|
|
344
|
-
runEventCombinationForTenant,
|
|
345
368
|
__: {
|
|
346
369
|
_singleTenantDb,
|
|
347
370
|
_multiTenancyRedis,
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { AsyncResource } = require("async_hooks");
|
|
4
|
+
|
|
5
|
+
const cds = require("@sap/cds");
|
|
6
|
+
|
|
7
|
+
const { processEventQueue } = require("../processEventQueue");
|
|
8
|
+
const eventQueueConfig = require("../config");
|
|
9
|
+
const WorkerQueue = require("../shared/WorkerQueue");
|
|
10
|
+
|
|
11
|
+
const COMPONENT_NAME = "/eventQueue/runnerHelper";
|
|
12
|
+
|
|
13
|
+
const runEventCombinationForTenant = async (context, type, subType, skipWorkerPool) => {
|
|
14
|
+
try {
|
|
15
|
+
if (skipWorkerPool) {
|
|
16
|
+
return await processEventQueue(context, type, subType);
|
|
17
|
+
} else {
|
|
18
|
+
const eventConfig = eventQueueConfig.getEventConfig(type, subType);
|
|
19
|
+
const label = `${type}_${subType}`;
|
|
20
|
+
return await WorkerQueue.instance.addToQueue(
|
|
21
|
+
eventConfig.load,
|
|
22
|
+
label,
|
|
23
|
+
eventConfig.priority,
|
|
24
|
+
AsyncResource.bind(async () => await processEventQueue(context, type, subType))
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
} catch (err) {
|
|
28
|
+
const logger = cds.log(COMPONENT_NAME);
|
|
29
|
+
logger.error("error executing event combination for tenant", err, {
|
|
30
|
+
tenantId: context.tenant,
|
|
31
|
+
type,
|
|
32
|
+
subType,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
module.exports = { runEventCombinationForTenant };
|
package/src/shared/cdsHelper.js
CHANGED
|
@@ -129,6 +129,15 @@ const getAllTenantIds = async () => {
|
|
|
129
129
|
if (!config.isMultiTenancy) {
|
|
130
130
|
return null;
|
|
131
131
|
}
|
|
132
|
+
|
|
133
|
+
// NOTE: tmp workaround until cds-mtxs fixes the connect.to service
|
|
134
|
+
for (let i = 0; i < 10; i++) {
|
|
135
|
+
if (cds.services["saas-registry"]) {
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
139
|
+
}
|
|
140
|
+
|
|
132
141
|
const ssp = await cds.connect.to("cds.xt.SaasProvisioningService");
|
|
133
142
|
const response = await ssp.get("/tenant");
|
|
134
143
|
return response
|
|
@@ -54,7 +54,7 @@ const checkLockExistsAndReturnValue = async (context, key, { tenantScoped = true
|
|
|
54
54
|
};
|
|
55
55
|
|
|
56
56
|
const _acquireLockRedis = async (context, fullKey, expiryTime, { value = "true", overrideValue = false } = {}) => {
|
|
57
|
-
const client = await redis.createMainClientAndConnect();
|
|
57
|
+
const client = await redis.createMainClientAndConnect(config.redisOptions);
|
|
58
58
|
const result = await client.set(fullKey, value, {
|
|
59
59
|
PX: expiryTime,
|
|
60
60
|
...(overrideValue ? null : { NX: true }),
|
|
@@ -63,7 +63,7 @@ const _acquireLockRedis = async (context, fullKey, expiryTime, { value = "true",
|
|
|
63
63
|
};
|
|
64
64
|
|
|
65
65
|
const _checkLockExistsRedis = async (context, fullKey) => {
|
|
66
|
-
const client = await redis.createMainClientAndConnect();
|
|
66
|
+
const client = await redis.createMainClientAndConnect(config.redisOptions);
|
|
67
67
|
return await client.get(fullKey);
|
|
68
68
|
};
|
|
69
69
|
|
|
@@ -76,7 +76,7 @@ const _checkLockExistsDb = async (context, fullKey) => {
|
|
|
76
76
|
};
|
|
77
77
|
|
|
78
78
|
const _releaseLockRedis = async (context, fullKey) => {
|
|
79
|
-
const client = await redis.createMainClientAndConnect();
|
|
79
|
+
const client = await redis.createMainClientAndConnect(config.redisOptions);
|
|
80
80
|
await client.del(fullKey);
|
|
81
81
|
};
|
|
82
82
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const cds = require("@sap/cds");
|
|
4
4
|
|
|
5
|
-
const
|
|
5
|
+
const redisPub = require("../redis/redisPub");
|
|
6
6
|
const config = require("./../config");
|
|
7
7
|
|
|
8
8
|
const COMPONENT_NAME = "/eventQueue/shared/eventScheduler";
|
|
@@ -26,7 +26,7 @@ class EventScheduler {
|
|
|
26
26
|
});
|
|
27
27
|
setTimeout(() => {
|
|
28
28
|
delete this.#scheduledEvents[key];
|
|
29
|
-
broadcastEvent(tenantId, type, subType).catch((err) => {
|
|
29
|
+
redisPub.broadcastEvent(tenantId, { type, subType }).catch((err) => {
|
|
30
30
|
cds.log(COMPONENT_NAME).error("could not execute scheduled event", err, {
|
|
31
31
|
tenantId,
|
|
32
32
|
type,
|
package/src/shared/redis.js
CHANGED
|
@@ -6,11 +6,13 @@ const { getEnvInstance } = require("./env");
|
|
|
6
6
|
const EventQueueError = require("../EventQueueError");
|
|
7
7
|
|
|
8
8
|
const COMPONENT_NAME = "/eventQueue/shared/redis";
|
|
9
|
+
const LOG_AFTER_SEC = 5;
|
|
9
10
|
|
|
10
11
|
let mainClientPromise;
|
|
11
12
|
const subscriberChannelClientPromise = {};
|
|
13
|
+
let lastErrorLog = Date.now();
|
|
12
14
|
|
|
13
|
-
const createMainClientAndConnect = () => {
|
|
15
|
+
const createMainClientAndConnect = (options) => {
|
|
14
16
|
if (mainClientPromise) {
|
|
15
17
|
return mainClientPromise;
|
|
16
18
|
}
|
|
@@ -18,13 +20,14 @@ const createMainClientAndConnect = () => {
|
|
|
18
20
|
const errorHandlerCreateClient = (err) => {
|
|
19
21
|
cds.log(COMPONENT_NAME).error("error from redis client for pub/sub failed", err);
|
|
20
22
|
mainClientPromise = null;
|
|
21
|
-
setTimeout(createMainClientAndConnect,
|
|
23
|
+
setTimeout(() => createMainClientAndConnect(options), LOG_AFTER_SEC * 1000).unref();
|
|
22
24
|
};
|
|
23
|
-
|
|
25
|
+
|
|
26
|
+
mainClientPromise = createClientAndConnect(options, errorHandlerCreateClient);
|
|
24
27
|
return mainClientPromise;
|
|
25
28
|
};
|
|
26
29
|
|
|
27
|
-
const _createClientBase = () => {
|
|
30
|
+
const _createClientBase = (redisOptions) => {
|
|
28
31
|
const env = getEnvInstance();
|
|
29
32
|
try {
|
|
30
33
|
const credentials = env.getRedisCredentialsFromEnv();
|
|
@@ -37,40 +40,49 @@ const _createClientBase = () => {
|
|
|
37
40
|
defaults: {
|
|
38
41
|
password: credentials.password,
|
|
39
42
|
socket: { tls: credentials.tls },
|
|
43
|
+
...redisOptions,
|
|
40
44
|
},
|
|
41
45
|
});
|
|
42
46
|
}
|
|
43
|
-
return redis.createClient({ url });
|
|
47
|
+
return redis.createClient({ url, ...redisOptions });
|
|
44
48
|
} catch (err) {
|
|
45
49
|
throw EventQueueError.redisConnectionFailure(err);
|
|
46
50
|
}
|
|
47
51
|
};
|
|
48
52
|
|
|
49
|
-
const createClientAndConnect = async (errorHandlerCreateClient) => {
|
|
50
|
-
let client = null;
|
|
51
|
-
try {
|
|
52
|
-
client = _createClientBase();
|
|
53
|
-
} catch (err) {
|
|
54
|
-
throw EventQueueError.redisConnectionFailure(err);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
client.on("error", errorHandlerCreateClient);
|
|
58
|
-
|
|
53
|
+
const createClientAndConnect = async (options, errorHandlerCreateClient) => {
|
|
59
54
|
try {
|
|
55
|
+
const client = _createClientBase(options);
|
|
60
56
|
await client.connect();
|
|
57
|
+
client.on("error", (err) => {
|
|
58
|
+
const dateNow = Date.now();
|
|
59
|
+
if (dateNow - lastErrorLog > LOG_AFTER_SEC * 1000) {
|
|
60
|
+
cds.log(COMPONENT_NAME).error("error from redis client for pub/sub failed", err);
|
|
61
|
+
lastErrorLog = dateNow;
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
client.on("reconnecting", () => {
|
|
66
|
+
const dateNow = Date.now();
|
|
67
|
+
if (dateNow - lastErrorLog > LOG_AFTER_SEC * 1000) {
|
|
68
|
+
cds.log(COMPONENT_NAME).info("redis client trying reconnect...");
|
|
69
|
+
lastErrorLog = dateNow;
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
return client;
|
|
61
73
|
} catch (err) {
|
|
62
74
|
errorHandlerCreateClient(err);
|
|
63
75
|
}
|
|
64
|
-
return client;
|
|
65
76
|
};
|
|
66
77
|
|
|
67
|
-
const subscribeRedisChannel = (channel, subscribeHandler) => {
|
|
78
|
+
const subscribeRedisChannel = (options, channel, subscribeHandler) => {
|
|
68
79
|
const errorHandlerCreateClient = (err) => {
|
|
69
80
|
cds.log(COMPONENT_NAME).error(`error from redis client for pub/sub failed for channel ${channel}`, err);
|
|
70
81
|
subscriberChannelClientPromise[channel] = null;
|
|
71
|
-
setTimeout(() => subscribeRedisChannel(channel, subscribeHandler),
|
|
82
|
+
setTimeout(() => subscribeRedisChannel(options, channel, subscribeHandler), LOG_AFTER_SEC * 1000).unref();
|
|
72
83
|
};
|
|
73
|
-
|
|
84
|
+
|
|
85
|
+
subscriberChannelClientPromise[channel] = createClientAndConnect(options, errorHandlerCreateClient)
|
|
74
86
|
.then((client) => {
|
|
75
87
|
cds.log(COMPONENT_NAME).info("subscribe redis client connected channel", { channel });
|
|
76
88
|
client.subscribe(channel, subscribeHandler).catch(errorHandlerCreateClient);
|
|
@@ -82,8 +94,8 @@ const subscribeRedisChannel = (channel, subscribeHandler) => {
|
|
|
82
94
|
});
|
|
83
95
|
};
|
|
84
96
|
|
|
85
|
-
const publishMessage = async (channel, message) => {
|
|
86
|
-
const client = await createMainClientAndConnect();
|
|
97
|
+
const publishMessage = async (options, channel, message) => {
|
|
98
|
+
const client = await createMainClientAndConnect(options);
|
|
87
99
|
return await client.publish(channel, message);
|
|
88
100
|
};
|
|
89
101
|
|
|
@@ -105,9 +117,9 @@ const _resilientClientClose = async (client) => {
|
|
|
105
117
|
}
|
|
106
118
|
};
|
|
107
119
|
|
|
108
|
-
const connectionCheck = async () => {
|
|
120
|
+
const connectionCheck = async (options) => {
|
|
109
121
|
return new Promise((resolve, reject) => {
|
|
110
|
-
createClientAndConnect(reject)
|
|
122
|
+
createClientAndConnect(options, reject)
|
|
111
123
|
.then((client) => {
|
|
112
124
|
if (client) {
|
|
113
125
|
_resilientClientClose(client);
|
package/src/redisPubSub.js
DELETED
|
@@ -1,170 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
const { promisify } = require("util");
|
|
4
|
-
|
|
5
|
-
const cds = require("@sap/cds");
|
|
6
|
-
|
|
7
|
-
const redis = require("./shared/redis");
|
|
8
|
-
const { checkLockExistsAndReturnValue } = require("./shared/distributedLock");
|
|
9
|
-
const config = require("./config");
|
|
10
|
-
const runner = require("./runner");
|
|
11
|
-
const { getSubdomainForTenantId } = require("./shared/cdsHelper");
|
|
12
|
-
|
|
13
|
-
const EVENT_MESSAGE_CHANNEL = "EVENT_QUEUE_MESSAGE_CHANNEL";
|
|
14
|
-
const COMPONENT_NAME = "/eventQueue/redisPubSub";
|
|
15
|
-
const TRIES_FOR_PUBLISH_PERIODIC_EVENT = 10;
|
|
16
|
-
const SLEEP_TIME_FOR_PUBLISH_PERIODIC_EVENT = 30 * 1000;
|
|
17
|
-
|
|
18
|
-
const wait = promisify(setTimeout);
|
|
19
|
-
let subscriberClientPromise;
|
|
20
|
-
|
|
21
|
-
const initEventQueueRedisSubscribe = () => {
|
|
22
|
-
if (subscriberClientPromise || !config.redisEnabled) {
|
|
23
|
-
return;
|
|
24
|
-
}
|
|
25
|
-
redis.subscribeRedisChannel(EVENT_MESSAGE_CHANNEL, _messageHandlerProcessEvents);
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
const _messageHandlerProcessEvents = async (messageData) => {
|
|
29
|
-
const logger = cds.log(COMPONENT_NAME);
|
|
30
|
-
try {
|
|
31
|
-
const { tenantId, type, subType } = JSON.parse(messageData);
|
|
32
|
-
logger.debug("received redis event", {
|
|
33
|
-
tenantId,
|
|
34
|
-
type,
|
|
35
|
-
subType,
|
|
36
|
-
});
|
|
37
|
-
if (!config.isEventQueueActive) {
|
|
38
|
-
cds.log(COMPONENT_NAME).info("Skipping processing because runner is deactivated!", {
|
|
39
|
-
type,
|
|
40
|
-
subType,
|
|
41
|
-
});
|
|
42
|
-
return;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const subdomain = await getSubdomainForTenantId(tenantId);
|
|
46
|
-
const user = new cds.User.Privileged(config.userId);
|
|
47
|
-
const tenantContext = {
|
|
48
|
-
tenant: tenantId,
|
|
49
|
-
user,
|
|
50
|
-
// NOTE: we need this because of logging otherwise logs would not contain the subdomain
|
|
51
|
-
http: { req: { authInfo: { getSubdomain: () => subdomain } } },
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
if (!config.getEventConfig(type, subType)) {
|
|
55
|
-
if (config.isCapOutboxEvent(type)) {
|
|
56
|
-
try {
|
|
57
|
-
const service = await cds.connect.to(subType);
|
|
58
|
-
cds.outboxed(service);
|
|
59
|
-
} catch (err) {
|
|
60
|
-
logger.error("could not connect to outboxed service", err, {
|
|
61
|
-
type,
|
|
62
|
-
subType,
|
|
63
|
-
});
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
66
|
-
} else {
|
|
67
|
-
logger.error("cannot find configuration for published event. Event won't be processed", {
|
|
68
|
-
type,
|
|
69
|
-
subType,
|
|
70
|
-
});
|
|
71
|
-
return;
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
return await cds.tx(tenantContext, async ({ context }) => {
|
|
76
|
-
return await runner.runEventCombinationForTenant(context, type, subType);
|
|
77
|
-
});
|
|
78
|
-
} catch (err) {
|
|
79
|
-
logger.error("could not parse event information", {
|
|
80
|
-
messageData,
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
|
-
};
|
|
84
|
-
|
|
85
|
-
const broadcastEvent = async (tenantId, type, subType) => {
|
|
86
|
-
const logger = cds.log(COMPONENT_NAME);
|
|
87
|
-
try {
|
|
88
|
-
if (!config.isEventQueueActive) {
|
|
89
|
-
cds.log(COMPONENT_NAME).info("Skipping processing because runner is deactivated!", {
|
|
90
|
-
type,
|
|
91
|
-
subType,
|
|
92
|
-
});
|
|
93
|
-
return;
|
|
94
|
-
}
|
|
95
|
-
if (!config.redisEnabled) {
|
|
96
|
-
if (config.registerAsEventProcessor) {
|
|
97
|
-
let context = {};
|
|
98
|
-
if (tenantId) {
|
|
99
|
-
const subdomain = await getSubdomainForTenantId(tenantId);
|
|
100
|
-
const user = new cds.User.Privileged(config.userId);
|
|
101
|
-
context = {
|
|
102
|
-
// NOTE: we need this because of logging otherwise logs would not contain the subdomain
|
|
103
|
-
tenant: tenantId,
|
|
104
|
-
user,
|
|
105
|
-
http: { req: { authInfo: { getSubdomain: () => subdomain } } },
|
|
106
|
-
};
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
return await cds.tx(context, async ({ context }) => {
|
|
110
|
-
return await runner.runEventCombinationForTenant(context, type, subType);
|
|
111
|
-
});
|
|
112
|
-
}
|
|
113
|
-
return;
|
|
114
|
-
}
|
|
115
|
-
const eventConfig = config.getEventConfig(type, subType);
|
|
116
|
-
for (let i = 0; i < TRIES_FOR_PUBLISH_PERIODIC_EVENT; i++) {
|
|
117
|
-
const result = await checkLockExistsAndReturnValue(
|
|
118
|
-
new cds.EventContext({ tenant: tenantId }),
|
|
119
|
-
[type, subType].join("##")
|
|
120
|
-
);
|
|
121
|
-
if (result) {
|
|
122
|
-
logger.debug("skip publish redis event as no lock is available", {
|
|
123
|
-
type,
|
|
124
|
-
subType,
|
|
125
|
-
index: i,
|
|
126
|
-
isPeriodic: eventConfig.isPeriodic,
|
|
127
|
-
waitInterval: SLEEP_TIME_FOR_PUBLISH_PERIODIC_EVENT,
|
|
128
|
-
});
|
|
129
|
-
if (!eventConfig.isPeriodic) {
|
|
130
|
-
break;
|
|
131
|
-
}
|
|
132
|
-
await wait(SLEEP_TIME_FOR_PUBLISH_PERIODIC_EVENT);
|
|
133
|
-
continue;
|
|
134
|
-
}
|
|
135
|
-
logger.debug("publishing redis event", {
|
|
136
|
-
tenantId,
|
|
137
|
-
type,
|
|
138
|
-
subType,
|
|
139
|
-
});
|
|
140
|
-
await redis.publishMessage(EVENT_MESSAGE_CHANNEL, JSON.stringify({ tenantId, type, subType }));
|
|
141
|
-
break;
|
|
142
|
-
}
|
|
143
|
-
} catch (err) {
|
|
144
|
-
logger.error("publish event failed!", err, {
|
|
145
|
-
tenantId,
|
|
146
|
-
type,
|
|
147
|
-
subType,
|
|
148
|
-
});
|
|
149
|
-
}
|
|
150
|
-
};
|
|
151
|
-
|
|
152
|
-
const closeSubscribeClient = async () => {
|
|
153
|
-
try {
|
|
154
|
-
const client = await subscriberClientPromise;
|
|
155
|
-
if (client?.quit) {
|
|
156
|
-
await client.quit();
|
|
157
|
-
}
|
|
158
|
-
} catch (err) {
|
|
159
|
-
// ignore errors during shutdown
|
|
160
|
-
}
|
|
161
|
-
};
|
|
162
|
-
|
|
163
|
-
module.exports = {
|
|
164
|
-
initEventQueueRedisSubscribe,
|
|
165
|
-
broadcastEvent,
|
|
166
|
-
closeSubscribeClient,
|
|
167
|
-
__: {
|
|
168
|
-
_messageHandlerProcessEvents,
|
|
169
|
-
},
|
|
170
|
-
};
|