@cap-js-community/event-queue 1.3.6 → 1.4.1
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/cds-plugin.js +1 -1
- package/package.json +6 -5
- package/src/EventQueueProcessorBase.js +2 -102
- package/src/config.js +29 -24
- package/src/dbHandler.js +11 -8
- package/src/initialize.js +8 -67
- package/src/processEventQueue.js +0 -6
- package/src/redis/redisPub.js +90 -0
- package/src/redis/redisSub.js +92 -0
- package/src/runner/openEvents.js +52 -0
- package/src/{runner.js → runner/runner.js} +129 -117
- package/src/runner/runnerHelper.js +37 -0
- package/src/shared/cdsHelper.js +10 -28
- package/src/shared/common.js +52 -1
- package/src/shared/distributedLock.js +3 -3
- package/src/shared/eventScheduler.js +2 -2
- package/src/shared/redis.js +15 -14
- package/src/redisPubSub.js +0 -170
package/cds-plugin.js
CHANGED
|
@@ -6,5 +6,5 @@ const eventQueue = require("./src");
|
|
|
6
6
|
const COMPONENT_NAME = "/eventQueue/plugin";
|
|
7
7
|
|
|
8
8
|
if (!cds.build.register && cds.env.eventQueue) {
|
|
9
|
-
eventQueue.initialize().catch((err) => cds.log(COMPONENT_NAME).error(err));
|
|
9
|
+
module.exports = eventQueue.initialize().catch((err) => cds.log(COMPONENT_NAME).error(err));
|
|
10
10
|
}
|
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.1",
|
|
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": [
|
|
@@ -42,12 +42,13 @@
|
|
|
42
42
|
"node": ">=18"
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
|
-
"
|
|
46
|
-
"
|
|
47
|
-
"
|
|
45
|
+
"@sap/xssec": "^3.6.1",
|
|
46
|
+
"redis": "^4.6.13",
|
|
47
|
+
"verror": "^1.10.1",
|
|
48
|
+
"yaml": "^2.4.1"
|
|
48
49
|
},
|
|
49
50
|
"devDependencies": {
|
|
50
|
-
"@cap-js/hana": "^0.0
|
|
51
|
+
"@cap-js/hana": "^0.1.0",
|
|
51
52
|
"@cap-js/sqlite": "^1.5.0",
|
|
52
53
|
"@sap/cds": "^7.7.0",
|
|
53
54
|
"@sap/cds-dk": "^7.5.1",
|
|
@@ -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";
|
|
@@ -23,8 +23,6 @@ const TRIES_FOR_EXCEEDED_EVENTS = 3;
|
|
|
23
23
|
const EVENT_START_AFTER_HEADROOM = 3 * 1000;
|
|
24
24
|
const ETAG_CHECK_AFTER_MIN = 10;
|
|
25
25
|
|
|
26
|
-
let serviceBindingCache = null;
|
|
27
|
-
|
|
28
26
|
class EventQueueProcessorBase {
|
|
29
27
|
#eventsWithExceededTries = [];
|
|
30
28
|
#exceededTriesExceeded = [];
|
|
@@ -72,8 +70,6 @@ class EventQueueProcessorBase {
|
|
|
72
70
|
this.__txMap = {};
|
|
73
71
|
this.__txRollback = {};
|
|
74
72
|
this.__queueEntries = [];
|
|
75
|
-
|
|
76
|
-
this.#checkGlobalContextToLocalContext();
|
|
77
73
|
}
|
|
78
74
|
|
|
79
75
|
/**
|
|
@@ -537,10 +533,7 @@ class EventQueueProcessorBase {
|
|
|
537
533
|
async getQueueEntriesAndSetToInProgress() {
|
|
538
534
|
let result = [];
|
|
539
535
|
const refDateStartAfter = new Date(Date.now() + this.#config.runInterval * 1.2);
|
|
540
|
-
this.#checkGlobalContextToLocalContext();
|
|
541
536
|
await executeInNewTransaction(this.__baseContext, "eventQueue-getQueueEntriesAndSetToInProgress", async (tx) => {
|
|
542
|
-
this.#checkGlobalContextToLocalContext();
|
|
543
|
-
await this.checkTxConsistency(tx);
|
|
544
537
|
const entries = await tx.run(
|
|
545
538
|
SELECT.from(this.#config.tableNameEventQueue)
|
|
546
539
|
.forUpdate({ wait: this.#config.forUpdateTimeout })
|
|
@@ -667,57 +660,6 @@ class EventQueueProcessorBase {
|
|
|
667
660
|
return result;
|
|
668
661
|
}
|
|
669
662
|
|
|
670
|
-
async checkTxConsistency(tx) {
|
|
671
|
-
if (!this.#config.enableTxConsistencyCheck) {
|
|
672
|
-
return;
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
const errorHandler = (err) =>
|
|
676
|
-
this.logger.error("tx consistency check failed!", err, {
|
|
677
|
-
type: this.eventType,
|
|
678
|
-
subType: this.eventSubType,
|
|
679
|
-
txTenant: tx.context.tenant,
|
|
680
|
-
globalCdsTenant: cds.context.tenant,
|
|
681
|
-
});
|
|
682
|
-
let txSchema, serviceManagerSchema;
|
|
683
|
-
try {
|
|
684
|
-
const schemaPromise = tx.run("SELECT CURRENT_SCHEMA FROM DUMMY");
|
|
685
|
-
const [schema, serviceManagerBindings] = await Promise.allSettled([schemaPromise, this.#getServiceBindings()]);
|
|
686
|
-
if (schema.reason) {
|
|
687
|
-
errorHandler(schema.reason);
|
|
688
|
-
return;
|
|
689
|
-
}
|
|
690
|
-
if (serviceManagerBindings.reason) {
|
|
691
|
-
errorHandler(serviceManagerBindings.reason);
|
|
692
|
-
return;
|
|
693
|
-
}
|
|
694
|
-
|
|
695
|
-
txSchema = schema.value[0].CURRENT_SCHEMA;
|
|
696
|
-
serviceManagerSchema = serviceManagerBindings.value.find((t) => t.labels.tenant_id[0] === tx.context.tenant)
|
|
697
|
-
.credentials.schema;
|
|
698
|
-
} catch (err) {
|
|
699
|
-
errorHandler(err);
|
|
700
|
-
}
|
|
701
|
-
if (serviceManagerSchema && txSchema !== serviceManagerSchema) {
|
|
702
|
-
const err = EventQueueError.dbClientSchemaMismatch(tx.context.tenant, txSchema, serviceManagerSchema);
|
|
703
|
-
errorHandler(err);
|
|
704
|
-
throw err;
|
|
705
|
-
}
|
|
706
|
-
}
|
|
707
|
-
|
|
708
|
-
async #getServiceBindings() {
|
|
709
|
-
if (!(serviceBindingCache && serviceBindingCache.expireTs >= Date.now())) {
|
|
710
|
-
const mtxServiceManager = require("@sap/cds-mtxs/srv/plugins/hana/srv-mgr");
|
|
711
|
-
serviceBindingCache = {
|
|
712
|
-
expireTs: Date.now() + 10 * 60 * 1000,
|
|
713
|
-
value: mtxServiceManager.getAll().catch(() => {
|
|
714
|
-
serviceBindingCache = null;
|
|
715
|
-
}),
|
|
716
|
-
};
|
|
717
|
-
}
|
|
718
|
-
return await serviceBindingCache.value;
|
|
719
|
-
}
|
|
720
|
-
|
|
721
663
|
async #selectLastSuccessfulPeriodicTimestamp() {
|
|
722
664
|
const entry = await SELECT.one
|
|
723
665
|
.from(this.#config.tableNameEventQueue)
|
|
@@ -730,48 +672,6 @@ class EventQueueProcessorBase {
|
|
|
730
672
|
return entry.lastAttemptsTs;
|
|
731
673
|
}
|
|
732
674
|
|
|
733
|
-
#checkGlobalContextToLocalContext() {
|
|
734
|
-
if (!this.#config.enableTxConsistencyCheck) {
|
|
735
|
-
return;
|
|
736
|
-
}
|
|
737
|
-
if (this.__context.tenant !== cds.context.tenant) {
|
|
738
|
-
throw EventQueueError.globalCdsContextNotMatchingLocal(
|
|
739
|
-
JSON.stringify(
|
|
740
|
-
{
|
|
741
|
-
correlationId: cds.context.id,
|
|
742
|
-
tenantId: cds.context.tenant,
|
|
743
|
-
timestamp: cds.context.timestamp,
|
|
744
|
-
base: JSON.stringify(
|
|
745
|
-
{
|
|
746
|
-
correlationId: cds.context.context?.id,
|
|
747
|
-
tenantId: cds.context.context?.tenant,
|
|
748
|
-
timestamp: cds.context.context?.timestamp,
|
|
749
|
-
},
|
|
750
|
-
null,
|
|
751
|
-
2
|
|
752
|
-
),
|
|
753
|
-
},
|
|
754
|
-
null,
|
|
755
|
-
2
|
|
756
|
-
),
|
|
757
|
-
JSON.stringify(
|
|
758
|
-
{
|
|
759
|
-
correlationId: this.__context.id,
|
|
760
|
-
tenantId: this.__context.tenant,
|
|
761
|
-
timestamp: this.__context.timestamp,
|
|
762
|
-
base: JSON.stringify({
|
|
763
|
-
correlationId: this.__context.context?.id,
|
|
764
|
-
tenantId: this.__context.context?.tenant,
|
|
765
|
-
timestamp: this.__context.context?.timestamp,
|
|
766
|
-
}),
|
|
767
|
-
},
|
|
768
|
-
null,
|
|
769
|
-
2
|
|
770
|
-
)
|
|
771
|
-
);
|
|
772
|
-
}
|
|
773
|
-
}
|
|
774
|
-
|
|
775
675
|
#handleDelayedEvents(delayedEvents) {
|
|
776
676
|
for (const delayedEvent of delayedEvents) {
|
|
777
677
|
this.#eventSchedulerInstance.scheduleEvent(
|
|
@@ -1113,7 +1013,7 @@ class EventQueueProcessorBase {
|
|
|
1113
1013
|
|
|
1114
1014
|
broadCastEvent() {
|
|
1115
1015
|
setTimeout(() => {
|
|
1116
|
-
broadcastEvent(this.__baseContext.tenant, this.#eventType, this.#eventSubType).catch((err) => {
|
|
1016
|
+
broadcastEvent(this.__baseContext.tenant, { type: this.#eventType, subType: this.#eventSubType }).catch((err) => {
|
|
1117
1017
|
this.logger.error("could not execute scheduled event", err, {
|
|
1118
1018
|
tenantId: this.__baseContext.tenant,
|
|
1119
1019
|
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;
|
|
@@ -59,8 +64,8 @@ class Config {
|
|
|
59
64
|
#thresholdLoggingEventProcessing;
|
|
60
65
|
#useAsCAPOutbox;
|
|
61
66
|
#userId;
|
|
62
|
-
#enableTxConsistencyCheck;
|
|
63
67
|
#cleanupLocksAndEventsForDev;
|
|
68
|
+
#redisOptions;
|
|
64
69
|
static #instance;
|
|
65
70
|
constructor() {
|
|
66
71
|
this.#logger = cds.log(COMPONENT_NAME);
|
|
@@ -105,7 +110,7 @@ class Config {
|
|
|
105
110
|
|
|
106
111
|
attachConfigChangeHandler() {
|
|
107
112
|
this.#attachBlockListChangeHandler();
|
|
108
|
-
redis.subscribeRedisChannel(REDIS_CONFIG_CHANNEL, (messageData) => {
|
|
113
|
+
redis.subscribeRedisChannel(this.#redisOptions, REDIS_CONFIG_CHANNEL, (messageData) => {
|
|
109
114
|
try {
|
|
110
115
|
const { key, value } = JSON.parse(messageData);
|
|
111
116
|
if (this[key] !== value) {
|
|
@@ -125,13 +130,13 @@ class Config {
|
|
|
125
130
|
this.#logger.info("redis not connected, config change won't be published", { key, value });
|
|
126
131
|
return;
|
|
127
132
|
}
|
|
128
|
-
redis.publishMessage(REDIS_CONFIG_CHANNEL, JSON.stringify({ key, value })).catch((error) => {
|
|
133
|
+
redis.publishMessage(this.#redisOptions, REDIS_CONFIG_CHANNEL, JSON.stringify({ key, value })).catch((error) => {
|
|
129
134
|
this.#logger.error(`publishing config change failed key: ${key}, value: ${value}`, error);
|
|
130
135
|
});
|
|
131
136
|
}
|
|
132
137
|
|
|
133
138
|
#attachBlockListChangeHandler() {
|
|
134
|
-
redis.subscribeRedisChannel(REDIS_CONFIG_BLOCKLIST_CHANNEL, (messageData) => {
|
|
139
|
+
redis.subscribeRedisChannel(this.#redisOptions, REDIS_CONFIG_BLOCKLIST_CHANNEL, (messageData) => {
|
|
135
140
|
try {
|
|
136
141
|
const { command, key, tenant } = JSON.parse(messageData);
|
|
137
142
|
if (command === COMMAND_BLOCK) {
|
|
@@ -160,7 +165,11 @@ class Config {
|
|
|
160
165
|
}
|
|
161
166
|
|
|
162
167
|
redis
|
|
163
|
-
.publishMessage(
|
|
168
|
+
.publishMessage(
|
|
169
|
+
this.#redisOptions,
|
|
170
|
+
REDIS_CONFIG_BLOCKLIST_CHANNEL,
|
|
171
|
+
JSON.stringify({ command: COMMAND_BLOCK, key, tenant })
|
|
172
|
+
)
|
|
164
173
|
.catch((error) => {
|
|
165
174
|
this.#logger.error(`publishing config block failed key: ${key}`, error);
|
|
166
175
|
});
|
|
@@ -189,7 +198,11 @@ class Config {
|
|
|
189
198
|
}
|
|
190
199
|
|
|
191
200
|
redis
|
|
192
|
-
.publishMessage(
|
|
201
|
+
.publishMessage(
|
|
202
|
+
this.#redisOptions,
|
|
203
|
+
REDIS_CONFIG_BLOCKLIST_CHANNEL,
|
|
204
|
+
JSON.stringify({ command: COMMAND_UNBLOCK, key, tenant })
|
|
205
|
+
)
|
|
193
206
|
.catch((error) => {
|
|
194
207
|
this.#logger.error(`publishing config block failed key: ${key}`, error);
|
|
195
208
|
});
|
|
@@ -386,19 +399,11 @@ class Config {
|
|
|
386
399
|
}
|
|
387
400
|
|
|
388
401
|
get tableNameEventQueue() {
|
|
389
|
-
return
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
set tableNameEventQueue(value) {
|
|
393
|
-
this.#tableNameEventQueue = value;
|
|
402
|
+
return BASE_TABLES.EVENT;
|
|
394
403
|
}
|
|
395
404
|
|
|
396
405
|
get tableNameEventLock() {
|
|
397
|
-
return
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
set tableNameEventLock(value) {
|
|
401
|
-
this.#tableNameEventLock = value;
|
|
406
|
+
return BASE_TABLES.LOCK;
|
|
402
407
|
}
|
|
403
408
|
|
|
404
409
|
set configFilePath(value) {
|
|
@@ -473,14 +478,6 @@ class Config {
|
|
|
473
478
|
return this.#userId;
|
|
474
479
|
}
|
|
475
480
|
|
|
476
|
-
set enableTxConsistencyCheck(value) {
|
|
477
|
-
this.#enableTxConsistencyCheck = value;
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
get enableTxConsistencyCheck() {
|
|
481
|
-
return this.#enableTxConsistencyCheck;
|
|
482
|
-
}
|
|
483
|
-
|
|
484
481
|
set cleanupLocksAndEventsForDev(value) {
|
|
485
482
|
this.#cleanupLocksAndEventsForDev = value;
|
|
486
483
|
}
|
|
@@ -489,6 +486,14 @@ class Config {
|
|
|
489
486
|
return this.#cleanupLocksAndEventsForDev;
|
|
490
487
|
}
|
|
491
488
|
|
|
489
|
+
set redisOptions(value) {
|
|
490
|
+
this.#redisOptions = value;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
get redisOptions() {
|
|
494
|
+
return this.#redisOptions;
|
|
495
|
+
}
|
|
496
|
+
|
|
492
497
|
get isMultiTenancy() {
|
|
493
498
|
return !!cds.requires.multitenancy;
|
|
494
499
|
}
|
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,20 @@ 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
|
-
["enableTxConsistencyCheck", false],
|
|
43
35
|
["cleanupLocksAndEventsForDev", false],
|
|
36
|
+
["redisOptions", {}],
|
|
44
37
|
];
|
|
45
38
|
|
|
46
39
|
const initialize = async ({
|
|
@@ -49,16 +42,13 @@ const initialize = async ({
|
|
|
49
42
|
processEventsAfterPublish,
|
|
50
43
|
isEventQueueActive,
|
|
51
44
|
runInterval,
|
|
52
|
-
tableNameEventQueue,
|
|
53
|
-
tableNameEventLock,
|
|
54
45
|
disableRedis,
|
|
55
|
-
skipCsnCheck,
|
|
56
46
|
updatePeriodicEvents,
|
|
57
47
|
thresholdLoggingEventProcessing,
|
|
58
48
|
useAsCAPOutbox,
|
|
59
49
|
userId,
|
|
60
|
-
enableTxConsistencyCheck,
|
|
61
50
|
cleanupLocksAndEventsForDev,
|
|
51
|
+
redisOptions,
|
|
62
52
|
} = {}) => {
|
|
63
53
|
if (config.initialized) {
|
|
64
54
|
return;
|
|
@@ -71,16 +61,13 @@ const initialize = async ({
|
|
|
71
61
|
processEventsAfterPublish,
|
|
72
62
|
isEventQueueActive,
|
|
73
63
|
runInterval,
|
|
74
|
-
tableNameEventQueue,
|
|
75
|
-
tableNameEventLock,
|
|
76
64
|
disableRedis,
|
|
77
|
-
skipCsnCheck,
|
|
78
65
|
updatePeriodicEvents,
|
|
79
66
|
thresholdLoggingEventProcessing,
|
|
80
67
|
useAsCAPOutbox,
|
|
81
68
|
userId,
|
|
82
|
-
|
|
83
|
-
|
|
69
|
+
cleanupLocksAndEventsForDev,
|
|
70
|
+
redisOptions
|
|
84
71
|
);
|
|
85
72
|
|
|
86
73
|
const logger = cds.log(COMPONENT);
|
|
@@ -95,12 +82,10 @@ const initialize = async ({
|
|
|
95
82
|
}
|
|
96
83
|
});
|
|
97
84
|
if (redisEnabled) {
|
|
98
|
-
config.redisEnabled = await redis.connectionCheck();
|
|
85
|
+
config.redisEnabled = await redis.connectionCheck(config.redisOptions);
|
|
99
86
|
}
|
|
100
87
|
config.fileContent = await readConfigFromFile(config.configFilePath);
|
|
101
88
|
|
|
102
|
-
!config.skipCsnCheck && (await csnCheck());
|
|
103
|
-
|
|
104
89
|
monkeyPatchCAPOutbox();
|
|
105
90
|
registerCdsShutdown();
|
|
106
91
|
logger.info("event queue initialized", {
|
|
@@ -172,50 +157,6 @@ const monkeyPatchCAPOutbox = () => {
|
|
|
172
157
|
}
|
|
173
158
|
};
|
|
174
159
|
|
|
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
160
|
const mixConfigVarsWithEnv = (...args) => {
|
|
220
161
|
CONFIG_VARS.forEach(([configName, defaultValue], index) => {
|
|
221
162
|
const configValue = args[index];
|
package/src/processEventQueue.js
CHANGED
|
@@ -96,9 +96,6 @@ const processEventQueue = async (context, eventType, eventSubType, startTime = n
|
|
|
96
96
|
}
|
|
97
97
|
} catch (err) {
|
|
98
98
|
cds.log(COMPONENT_NAME).error("Processing event queue failed with unexpected error.", err, {
|
|
99
|
-
tenantId: context?.tenant,
|
|
100
|
-
tenantIdBase: baseInstance?.context?.tenant,
|
|
101
|
-
globalTenantId: cds.context?.tenant,
|
|
102
99
|
eventType,
|
|
103
100
|
eventSubType,
|
|
104
101
|
});
|
|
@@ -188,9 +185,6 @@ const processPeriodicEvent = async (context, eventTypeInstance) => {
|
|
|
188
185
|
cds.log(COMPONENT_NAME).error("Processing periodic events failed with unexpected error.", err, {
|
|
189
186
|
eventType: eventTypeInstance?.eventType,
|
|
190
187
|
eventSubType: eventTypeInstance?.eventSubType,
|
|
191
|
-
tenantId: context?.tenant,
|
|
192
|
-
tenantIdBase: eventTypeInstance?.context?.tenant,
|
|
193
|
-
globalTenantId: cds.context?.tenant,
|
|
194
188
|
});
|
|
195
189
|
} finally {
|
|
196
190
|
await eventTypeInstance?.handleReleaseLock();
|
|
@@ -0,0 +1,90 @@
|
|
|
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 common = require("../shared/common");
|
|
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 user = new cds.User.Privileged({ id: config.userId, authInfo: await common.getAuthInfo(tenantId) });
|
|
33
|
+
context = {
|
|
34
|
+
tenant: tenantId,
|
|
35
|
+
user,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return await cds.tx(context, async ({ context }) => {
|
|
40
|
+
for (const { type, subType } of events) {
|
|
41
|
+
await runEventCombinationForTenant(context, type, subType);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
for (const { type, subType } of events) {
|
|
48
|
+
const eventConfig = config.getEventConfig(type, subType);
|
|
49
|
+
for (let i = 0; i < TRIES_FOR_PUBLISH_PERIODIC_EVENT; i++) {
|
|
50
|
+
const result = await checkLockExistsAndReturnValue(
|
|
51
|
+
new cds.EventContext({ tenant: tenantId }),
|
|
52
|
+
[type, subType].join("##")
|
|
53
|
+
);
|
|
54
|
+
if (result) {
|
|
55
|
+
logger.debug("skip publish redis event as no lock is available", {
|
|
56
|
+
type,
|
|
57
|
+
subType,
|
|
58
|
+
index: i,
|
|
59
|
+
isPeriodic: eventConfig.isPeriodic,
|
|
60
|
+
waitInterval: SLEEP_TIME_FOR_PUBLISH_PERIODIC_EVENT,
|
|
61
|
+
});
|
|
62
|
+
if (!eventConfig.isPeriodic) {
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
await wait(SLEEP_TIME_FOR_PUBLISH_PERIODIC_EVENT);
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
logger.debug("publishing redis event", {
|
|
69
|
+
tenantId,
|
|
70
|
+
type,
|
|
71
|
+
subType,
|
|
72
|
+
});
|
|
73
|
+
await redis.publishMessage(
|
|
74
|
+
config.redisOptions,
|
|
75
|
+
EVENT_MESSAGE_CHANNEL,
|
|
76
|
+
JSON.stringify({ tenantId, type, subType })
|
|
77
|
+
);
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
} catch (err) {
|
|
82
|
+
logger.error("publish events failed!", err, {
|
|
83
|
+
tenantId,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
module.exports = {
|
|
89
|
+
broadcastEvent,
|
|
90
|
+
};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const cds = require("@sap/cds");
|
|
4
|
+
|
|
5
|
+
const redis = require("../shared/redis");
|
|
6
|
+
const config = require("../config");
|
|
7
|
+
const runnerHelper = require("../runner/runnerHelper");
|
|
8
|
+
const common = require("../shared/common");
|
|
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 user = new cds.User.Privileged({ id: config.userId, authInfo: await common.getAuthInfo(tenantId) });
|
|
39
|
+
const tenantContext = {
|
|
40
|
+
tenant: tenantId,
|
|
41
|
+
user,
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
if (!config.getEventConfig(type, subType)) {
|
|
45
|
+
if (config.isCapOutboxEvent(type)) {
|
|
46
|
+
try {
|
|
47
|
+
const service = await cds.connect.to(subType);
|
|
48
|
+
cds.outboxed(service);
|
|
49
|
+
} catch (err) {
|
|
50
|
+
logger.error("could not connect to outboxed service", err, {
|
|
51
|
+
type,
|
|
52
|
+
subType,
|
|
53
|
+
});
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
} else {
|
|
57
|
+
logger.error("cannot find configuration for published event. Event won't be processed", {
|
|
58
|
+
type,
|
|
59
|
+
subType,
|
|
60
|
+
});
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return await cds.tx(tenantContext, async ({ context }) => {
|
|
66
|
+
return await runnerHelper.runEventCombinationForTenant(context, type, subType);
|
|
67
|
+
});
|
|
68
|
+
} catch (err) {
|
|
69
|
+
logger.error("could not parse event information", {
|
|
70
|
+
messageData,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const closeSubscribeClient = async () => {
|
|
76
|
+
try {
|
|
77
|
+
const client = await subscriberClientPromise;
|
|
78
|
+
if (client?.quit) {
|
|
79
|
+
await client.quit();
|
|
80
|
+
}
|
|
81
|
+
} catch (err) {
|
|
82
|
+
// ignore errors during shutdown
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
module.exports = {
|
|
87
|
+
initEventQueueRedisSubscribe,
|
|
88
|
+
closeSubscribeClient,
|
|
89
|
+
__: {
|
|
90
|
+
_messageHandlerProcessEvents,
|
|
91
|
+
},
|
|
92
|
+
};
|