@cap-js-community/event-queue 0.2.5 → 1.0.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 +9 -9
- package/src/EventQueueError.js +2 -2
- package/src/EventQueueProcessorBase.js +129 -59
- package/src/config.js +120 -10
- package/src/housekeeping/EventQueueDeleteEvents.js +51 -0
- package/src/initialize.js +4 -8
- package/src/processEventQueue.js +39 -17
- package/src/redisPubSub.js +45 -7
- package/src/runner.js +73 -54
- package/src/shared/PerformanceTracer.js +2 -0
- package/src/shared/WorkerQueue.js +11 -10
- package/src/shared/redis.js +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cap-js-community/event-queue",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "An event queue that enables secure transactional processing of asynchronous 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": [
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"scripts": {
|
|
23
23
|
"test:unit": "jest --testPathIgnorePatterns=\"/test-integration/\"",
|
|
24
24
|
"test:integration": "jest --testPathIgnorePatterns=\"/test/\" --runInBand --forceExit",
|
|
25
|
-
"test
|
|
25
|
+
"test": "npm run test:unit && npm run test:integration",
|
|
26
26
|
"test:all:coverage": "jest --runInBand --forceExit --collect-coverage",
|
|
27
27
|
"test:prepare": "npm run build:ci --prefix=./test-integration/_env",
|
|
28
28
|
"test:deploySchema": "node test-integration/_env/srv/hana/deploy.js",
|
|
@@ -39,22 +39,22 @@
|
|
|
39
39
|
"upgrade-lock": "npx shx rm -rf package-lock.json node_modules && npm i --package-lock"
|
|
40
40
|
},
|
|
41
41
|
"engines": {
|
|
42
|
-
"node": ">=
|
|
42
|
+
"node": ">=18"
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
|
-
"redis": "4.6.
|
|
45
|
+
"redis": "4.6.12",
|
|
46
46
|
"verror": "1.10.1",
|
|
47
47
|
"yaml": "2.3.4"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
|
-
"@sap/cds": "7.
|
|
51
|
-
"@sap/cds-dk": "7.
|
|
52
|
-
"eslint": "8.
|
|
53
|
-
"eslint-config-prettier": "9.
|
|
50
|
+
"@sap/cds": "7.5.0",
|
|
51
|
+
"@sap/cds-dk": "7.5.0",
|
|
52
|
+
"eslint": "8.56.0",
|
|
53
|
+
"eslint-config-prettier": "9.1.0",
|
|
54
54
|
"eslint-plugin-jest": "27.6.0",
|
|
55
55
|
"eslint-plugin-node": "11.1.0",
|
|
56
56
|
"express": "4.18.2",
|
|
57
|
-
"hdb": "0.19.
|
|
57
|
+
"hdb": "0.19.7",
|
|
58
58
|
"jest": "29.7.0",
|
|
59
59
|
"prettier": "2.8.8",
|
|
60
60
|
"sqlite3": "5.1.6"
|
package/src/EventQueueError.js
CHANGED
|
@@ -210,12 +210,12 @@ class EventQueueError extends VError {
|
|
|
210
210
|
message
|
|
211
211
|
);
|
|
212
212
|
}
|
|
213
|
-
static loadHigherThanLimit(load) {
|
|
213
|
+
static loadHigherThanLimit(load, label) {
|
|
214
214
|
const { message } = ERROR_CODES_META[ERROR_CODES.LOAD_HIGHER_THAN_LIMIT];
|
|
215
215
|
return new EventQueueError(
|
|
216
216
|
{
|
|
217
217
|
name: ERROR_CODES.LOAD_HIGHER_THAN_LIMIT,
|
|
218
|
-
info: { load },
|
|
218
|
+
info: { load, label },
|
|
219
219
|
},
|
|
220
220
|
message
|
|
221
221
|
);
|
|
@@ -18,8 +18,6 @@ const DEFAULT_RETRY_ATTEMPTS = 3;
|
|
|
18
18
|
const DEFAULT_PARALLEL_EVENT_PROCESSING = 1;
|
|
19
19
|
const LIMIT_PARALLEL_EVENT_PROCESSING = 10;
|
|
20
20
|
const SELECT_LIMIT_EVENTS_PER_TICK = 100;
|
|
21
|
-
const DEFAULT_DELETE_FINISHED_EVENTS_AFTER = 0;
|
|
22
|
-
const DAYS_TO_MS = 24 * 60 * 60 * 1000;
|
|
23
21
|
const TRIES_FOR_EXCEEDED_EVENTS = 3;
|
|
24
22
|
const EVENT_START_AFTER_HEADROOM = 3 * 1000;
|
|
25
23
|
|
|
@@ -34,6 +32,7 @@ class EventQueueProcessorBase {
|
|
|
34
32
|
#eventSchedulerInstance = null;
|
|
35
33
|
#eventConfig;
|
|
36
34
|
#isPeriodic;
|
|
35
|
+
#lastSuccessfulRunTimestamp;
|
|
37
36
|
|
|
38
37
|
constructor(context, eventType, eventSubType, config) {
|
|
39
38
|
this.__context = context;
|
|
@@ -57,21 +56,12 @@ class EventQueueProcessorBase {
|
|
|
57
56
|
// NOTE: keep the feature, this might be needed again
|
|
58
57
|
this.__concurrentEventProcessing = false;
|
|
59
58
|
this.__startTime = this.#eventConfig.startTime ?? new Date();
|
|
60
|
-
this.__retryAttempts = this.#eventConfig.retryAttempts ?? DEFAULT_RETRY_ATTEMPTS;
|
|
59
|
+
this.__retryAttempts = this.#isPeriodic ? 1 : this.#eventConfig.retryAttempts ?? DEFAULT_RETRY_ATTEMPTS;
|
|
61
60
|
this.__selectMaxChunkSize = this.#eventConfig.selectMaxChunkSize ?? SELECT_LIMIT_EVENTS_PER_TICK;
|
|
62
61
|
this.__selectNextChunk = !!this.#eventConfig.checkForNextChunk;
|
|
63
62
|
this.__keepalivePromises = {};
|
|
64
63
|
this.__outdatedCheckEnabled = this.#eventConfig.eventOutdatedCheck ?? true;
|
|
65
64
|
this.__transactionMode = this.#eventConfig.transactionMode ?? TransactionMode.isolated;
|
|
66
|
-
if (this.#eventConfig.deleteFinishedEventsAfterDays) {
|
|
67
|
-
this.__deleteFinishedEventsAfter =
|
|
68
|
-
Number.isInteger(this.#eventConfig.deleteFinishedEventsAfterDays) &&
|
|
69
|
-
this.#eventConfig.deleteFinishedEventsAfterDays > 0
|
|
70
|
-
? this.#eventConfig.deleteFinishedEventsAfterDays
|
|
71
|
-
: DEFAULT_DELETE_FINISHED_EVENTS_AFTER;
|
|
72
|
-
} else {
|
|
73
|
-
this.__deleteFinishedEventsAfter = DEFAULT_DELETE_FINISHED_EVENTS_AFTER;
|
|
74
|
-
}
|
|
75
65
|
this.__emptyChunkSelected = false;
|
|
76
66
|
this.__lockAcquired = false;
|
|
77
67
|
this.__txUsageAllowed = true;
|
|
@@ -113,11 +103,30 @@ class EventQueueProcessorBase {
|
|
|
113
103
|
}
|
|
114
104
|
|
|
115
105
|
startPerformanceTracerEvents() {
|
|
116
|
-
this.__performanceLoggerEvents = new PerformanceTracer(this.logger, "Processing events"
|
|
106
|
+
this.__performanceLoggerEvents = new PerformanceTracer(this.logger, "Processing events", {
|
|
107
|
+
properties: {
|
|
108
|
+
type: this.eventType,
|
|
109
|
+
subType: this.eventSubType,
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
startPerformanceTracerPeriodicEvents() {
|
|
115
|
+
this.__performanceLoggerPeriodicEvents = new PerformanceTracer(this.logger, "Processing periodic event", {
|
|
116
|
+
properties: {
|
|
117
|
+
type: this.eventType,
|
|
118
|
+
subType: this.eventSubType,
|
|
119
|
+
},
|
|
120
|
+
});
|
|
117
121
|
}
|
|
118
122
|
|
|
119
123
|
startPerformanceTracerPreprocessing() {
|
|
120
|
-
this.__performanceLoggerPreprocessing = new PerformanceTracer(this.logger, "Preprocessing events"
|
|
124
|
+
this.__performanceLoggerPreprocessing = new PerformanceTracer(this.logger, "Preprocessing events", {
|
|
125
|
+
properties: {
|
|
126
|
+
type: this.eventType,
|
|
127
|
+
subType: this.eventSubType,
|
|
128
|
+
},
|
|
129
|
+
});
|
|
121
130
|
}
|
|
122
131
|
|
|
123
132
|
endPerformanceTracerEvents() {
|
|
@@ -130,6 +139,16 @@ class EventQueueProcessorBase {
|
|
|
130
139
|
);
|
|
131
140
|
}
|
|
132
141
|
|
|
142
|
+
endPerformanceTracerPeriodicEvents() {
|
|
143
|
+
this.__performanceLoggerPeriodicEvents?.endPerformanceTrace(
|
|
144
|
+
{ threshold: 50 },
|
|
145
|
+
{
|
|
146
|
+
eventType: this.#eventType,
|
|
147
|
+
eventSubType: this.#eventSubType,
|
|
148
|
+
}
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
133
152
|
endPerformanceTracerPreprocessing() {
|
|
134
153
|
this.__performanceLoggerPreprocessing?.endPerformanceTrace(
|
|
135
154
|
{ threshold: 50 },
|
|
@@ -148,10 +167,9 @@ class EventQueueProcessorBase {
|
|
|
148
167
|
});
|
|
149
168
|
}
|
|
150
169
|
|
|
151
|
-
logStartMessage(
|
|
152
|
-
// TODO: how to handle custom fields
|
|
170
|
+
logStartMessage() {
|
|
153
171
|
this.logger.info("Processing queue event", {
|
|
154
|
-
|
|
172
|
+
numberClusterEntries: Object.keys(this.eventProcessingMap).length,
|
|
155
173
|
eventType: this.#eventType,
|
|
156
174
|
eventSubType: this.#eventSubType,
|
|
157
175
|
});
|
|
@@ -332,11 +350,11 @@ class EventQueueProcessorBase {
|
|
|
332
350
|
});
|
|
333
351
|
}
|
|
334
352
|
|
|
335
|
-
async setPeriodicEventStatus(queueEntryIds) {
|
|
353
|
+
async setPeriodicEventStatus(queueEntryIds, status) {
|
|
336
354
|
await this.tx.run(
|
|
337
355
|
UPDATE.entity(this.#config.tableNameEventQueue)
|
|
338
356
|
.set({
|
|
339
|
-
status:
|
|
357
|
+
status: status,
|
|
340
358
|
})
|
|
341
359
|
.where({
|
|
342
360
|
ID: queueEntryIds,
|
|
@@ -426,28 +444,6 @@ class EventQueueProcessorBase {
|
|
|
426
444
|
});
|
|
427
445
|
}
|
|
428
446
|
|
|
429
|
-
async deleteFinishedEvents(tx) {
|
|
430
|
-
if (!this.__deleteFinishedEventsAfter) {
|
|
431
|
-
return;
|
|
432
|
-
}
|
|
433
|
-
const deleteCount = await tx.run(
|
|
434
|
-
DELETE.from(this.#config.tableNameEventQueue).where(
|
|
435
|
-
"type =",
|
|
436
|
-
this.#eventType,
|
|
437
|
-
"AND subType=",
|
|
438
|
-
this.#eventSubType,
|
|
439
|
-
"AND lastAttemptTimestamp <=",
|
|
440
|
-
new Date(Date.now() - this.__deleteFinishedEventsAfter * DAYS_TO_MS).toISOString()
|
|
441
|
-
)
|
|
442
|
-
);
|
|
443
|
-
this.logger.debug("Deleted finished events", {
|
|
444
|
-
eventType: this.#eventType,
|
|
445
|
-
eventSubType: this.#eventSubType,
|
|
446
|
-
deleteFinishedEventsAfter: this.__deleteFinishedEventsAfter,
|
|
447
|
-
deleteCount,
|
|
448
|
-
});
|
|
449
|
-
}
|
|
450
|
-
|
|
451
447
|
#ensureEveryQueueEntryHasStatus() {
|
|
452
448
|
this.__queueEntries.forEach((queueEntry) => {
|
|
453
449
|
if (queueEntry.ID in this.__statusMap || queueEntry.ID in this.__commitedStatusMap) {
|
|
@@ -549,15 +545,27 @@ class EventQueueProcessorBase {
|
|
|
549
545
|
refDateStartAfter.toISOString(),
|
|
550
546
|
" ) AND ( status =",
|
|
551
547
|
EventProcessingStatus.Open,
|
|
552
|
-
"
|
|
553
|
-
EventProcessingStatus.Error,
|
|
554
|
-
"AND lastAttemptTimestamp <=",
|
|
548
|
+
"AND ( lastAttemptTimestamp <=",
|
|
555
549
|
this.__startTime.toISOString(),
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
550
|
+
...(this.isPeriodicEvent
|
|
551
|
+
? [
|
|
552
|
+
"OR lastAttemptTimestamp IS NULL ) OR ( status =",
|
|
553
|
+
EventProcessingStatus.InProgress,
|
|
554
|
+
"AND lastAttemptTimestamp <=",
|
|
555
|
+
new Date(new Date().getTime() - this.#config.globalTxTimeout).toISOString(),
|
|
556
|
+
") )",
|
|
557
|
+
]
|
|
558
|
+
: [
|
|
559
|
+
"OR lastAttemptTimestamp IS NULL ) OR ( status =",
|
|
560
|
+
EventProcessingStatus.Error,
|
|
561
|
+
"AND lastAttemptTimestamp <=",
|
|
562
|
+
this.__startTime.toISOString(),
|
|
563
|
+
") OR ( status =",
|
|
564
|
+
EventProcessingStatus.InProgress,
|
|
565
|
+
"AND lastAttemptTimestamp <=",
|
|
566
|
+
new Date(new Date().getTime() - this.#config.globalTxTimeout).toISOString(),
|
|
567
|
+
") )",
|
|
568
|
+
])
|
|
561
569
|
)
|
|
562
570
|
.orderBy("createdAt", "ID")
|
|
563
571
|
);
|
|
@@ -575,9 +583,11 @@ class EventQueueProcessorBase {
|
|
|
575
583
|
entries,
|
|
576
584
|
refDateStartAfter
|
|
577
585
|
);
|
|
578
|
-
const eventsForProcessing =
|
|
586
|
+
const eventsForProcessing = openEvents
|
|
587
|
+
.concat(exceededTriesExceeded)
|
|
588
|
+
.concat(this.#isPeriodic ? [] : exceededTries);
|
|
579
589
|
this.#selectedEventMap = arrayToFlatMap(eventsForProcessing);
|
|
580
|
-
if (exceededTries.length) {
|
|
590
|
+
if (!this.#isPeriodic && exceededTries.length) {
|
|
581
591
|
this.#eventsWithExceededTries = exceededTries;
|
|
582
592
|
}
|
|
583
593
|
if (exceededTriesExceeded.length) {
|
|
@@ -586,13 +596,30 @@ class EventQueueProcessorBase {
|
|
|
586
596
|
this.#handleDelayedEvents(delayedEvents);
|
|
587
597
|
|
|
588
598
|
result = openEvents;
|
|
589
|
-
this.logger[eventsForProcessing.length ? "info" : "debug"](
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
599
|
+
this.logger[eventsForProcessing.length && !this.isPeriodicEvent ? "info" : "debug"](
|
|
600
|
+
"Selected event queue entries for processing",
|
|
601
|
+
{
|
|
602
|
+
openEvents: openEvents.length,
|
|
603
|
+
...(delayedEvents.length && { delayedEvents: delayedEvents.length }),
|
|
604
|
+
...(exceededTries.length && { exceededTries: exceededTries.length }),
|
|
605
|
+
eventType: this.#eventType,
|
|
606
|
+
eventSubType: this.#eventSubType,
|
|
607
|
+
}
|
|
608
|
+
);
|
|
609
|
+
|
|
610
|
+
if (this.#isPeriodic && exceededTries.length) {
|
|
611
|
+
await tx.run(
|
|
612
|
+
UPDATE.entity(this.#config.tableNameEventQueue)
|
|
613
|
+
.set({
|
|
614
|
+
status: EventProcessingStatus.Error,
|
|
615
|
+
lastAttemptTimestamp: new Date(),
|
|
616
|
+
})
|
|
617
|
+
.where(
|
|
618
|
+
"ID IN",
|
|
619
|
+
exceededTries.map(({ ID }) => ID)
|
|
620
|
+
)
|
|
621
|
+
);
|
|
622
|
+
}
|
|
596
623
|
|
|
597
624
|
if (!eventsForProcessing.length) {
|
|
598
625
|
this.__emptyChunkSelected = true;
|
|
@@ -623,10 +650,26 @@ class EventQueueProcessorBase {
|
|
|
623
650
|
});
|
|
624
651
|
this.__queueEntries = result;
|
|
625
652
|
this.__queueEntriesMap = arrayToFlatMap(result);
|
|
653
|
+
|
|
654
|
+
if (this.#isPeriodic && this.#eventConfig.lastSuccessfulRunTimestamp) {
|
|
655
|
+
this.#lastSuccessfulRunTimestamp = await this.#selectLastSuccessfulPeriodicTimestamp(tx);
|
|
656
|
+
}
|
|
626
657
|
});
|
|
627
658
|
return result;
|
|
628
659
|
}
|
|
629
660
|
|
|
661
|
+
async #selectLastSuccessfulPeriodicTimestamp() {
|
|
662
|
+
const entry = await SELECT.one
|
|
663
|
+
.from(this.#config.tableNameEventQueue)
|
|
664
|
+
.where({
|
|
665
|
+
type: this.#eventType,
|
|
666
|
+
subType: this.#eventSubType,
|
|
667
|
+
status: EventProcessingStatus.Done,
|
|
668
|
+
})
|
|
669
|
+
.columns("max (lastAttemptTimestamp) as lastAttemptsTs");
|
|
670
|
+
return entry.lastAttemptsTs;
|
|
671
|
+
}
|
|
672
|
+
|
|
630
673
|
#handleDelayedEvents(delayedEvents) {
|
|
631
674
|
for (const delayedEvent of delayedEvents) {
|
|
632
675
|
this.#eventSchedulerInstance.scheduleEvent(
|
|
@@ -924,10 +967,37 @@ class EventQueueProcessorBase {
|
|
|
924
967
|
obsoleteEntries.push(queueEntry);
|
|
925
968
|
}
|
|
926
969
|
}
|
|
927
|
-
await this.setPeriodicEventStatus(
|
|
970
|
+
await this.setPeriodicEventStatus(
|
|
971
|
+
obsoleteEntries.map(({ ID }) => ID),
|
|
972
|
+
EventProcessingStatus.Done
|
|
973
|
+
);
|
|
928
974
|
return queueEntryToUse;
|
|
929
975
|
}
|
|
930
976
|
|
|
977
|
+
/**
|
|
978
|
+
* Asynchronously gets the timestamp of the last successful run.
|
|
979
|
+
*
|
|
980
|
+
* @returns {Promise<string|null>} A Promise that resolves to a string representation of the timestamp
|
|
981
|
+
* of the last successful run (in ISO 8601 format: YYYY-MM-DDTHH:mm:ss.sss),
|
|
982
|
+
* or null if there has been no successful run yet.
|
|
983
|
+
*
|
|
984
|
+
* @example
|
|
985
|
+
* const timestamp = await instance.getLastSuccessfulRunTimestamp();
|
|
986
|
+
* console.log(timestamp); // Outputs: 2023-12-07T09:15:44.237
|
|
987
|
+
*
|
|
988
|
+
* @throws {Error} If an error occurs while fetching the timestamp.
|
|
989
|
+
*/
|
|
990
|
+
async getLastSuccessfulRunTimestamp() {
|
|
991
|
+
if (!this.#isPeriodic) {
|
|
992
|
+
return null;
|
|
993
|
+
}
|
|
994
|
+
if (this.#lastSuccessfulRunTimestamp === undefined) {
|
|
995
|
+
this.#lastSuccessfulRunTimestamp = await this.#selectLastSuccessfulPeriodicTimestamp();
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
return this.#lastSuccessfulRunTimestamp;
|
|
999
|
+
}
|
|
1000
|
+
|
|
931
1001
|
statusMapContainsError(statusMap) {
|
|
932
1002
|
return Object.values(statusMap).includes(EventProcessingStatus.Error);
|
|
933
1003
|
}
|
package/src/config.js
CHANGED
|
@@ -9,9 +9,24 @@ const EventQueueError = require("./EventQueueError");
|
|
|
9
9
|
const FOR_UPDATE_TIMEOUT = 10;
|
|
10
10
|
const GLOBAL_TX_TIMEOUT = 30 * 60 * 1000;
|
|
11
11
|
const REDIS_CONFIG_CHANNEL = "EVENT_QUEUE_CONFIG_CHANNEL";
|
|
12
|
+
const REDIS_CONFIG_BLOCKLIST_CHANNEL = "REDIS_CONFIG_BLOCKLIST_CHANNEL";
|
|
12
13
|
const COMPONENT_NAME = "eventQueue/config";
|
|
13
14
|
const MIN_INTERVAL_SEC = 10;
|
|
14
15
|
const DEFAULT_LOAD = 1;
|
|
16
|
+
const SUFFIX_PERIODIC = "_PERIODIC";
|
|
17
|
+
const COMMAND_BLOCK = "EVENT_QUEUE_EVENT_BLOCK";
|
|
18
|
+
const COMMAND_UNBLOCK = "EVENT_QUEUE_EVENT_UNBLOCK";
|
|
19
|
+
|
|
20
|
+
const BASE_PERIODIC_EVENTS = [
|
|
21
|
+
{
|
|
22
|
+
type: "EVENT_QUEUE_BASE",
|
|
23
|
+
subType: "DELETE_EVENTS",
|
|
24
|
+
impl: "./housekeeping/EventQueueDeleteEvents",
|
|
25
|
+
load: 1,
|
|
26
|
+
interval: 86400, // 1 day,
|
|
27
|
+
internalEvent: true,
|
|
28
|
+
},
|
|
29
|
+
];
|
|
15
30
|
|
|
16
31
|
class Config {
|
|
17
32
|
#logger;
|
|
@@ -24,7 +39,7 @@ class Config {
|
|
|
24
39
|
#instanceLoadLimit;
|
|
25
40
|
#tableNameEventQueue;
|
|
26
41
|
#tableNameEventLock;
|
|
27
|
-
#
|
|
42
|
+
#isEventQueueActive;
|
|
28
43
|
#configFilePath;
|
|
29
44
|
#processEventsAfterPublish;
|
|
30
45
|
#skipCsnCheck;
|
|
@@ -33,6 +48,8 @@ class Config {
|
|
|
33
48
|
#env;
|
|
34
49
|
#eventMap;
|
|
35
50
|
#updatePeriodicEvents;
|
|
51
|
+
#blockedPeriodicEvents;
|
|
52
|
+
#isPeriodicEventBlockedCb;
|
|
36
53
|
static #instance;
|
|
37
54
|
constructor() {
|
|
38
55
|
this.#logger = cds.log(COMPONENT_NAME);
|
|
@@ -42,15 +59,16 @@ class Config {
|
|
|
42
59
|
this.#runInterval = null;
|
|
43
60
|
this.#redisEnabled = null;
|
|
44
61
|
this.#initialized = false;
|
|
45
|
-
this.#instanceLoadLimit =
|
|
62
|
+
this.#instanceLoadLimit = 100;
|
|
46
63
|
this.#tableNameEventQueue = null;
|
|
47
64
|
this.#tableNameEventLock = null;
|
|
48
|
-
this.#
|
|
65
|
+
this.#isEventQueueActive = true;
|
|
49
66
|
this.#configFilePath = null;
|
|
50
67
|
this.#processEventsAfterPublish = null;
|
|
51
68
|
this.#skipCsnCheck = null;
|
|
52
69
|
this.#disableRedis = null;
|
|
53
70
|
this.#env = getEnvInstance();
|
|
71
|
+
this.#blockedPeriodicEvents = {};
|
|
54
72
|
}
|
|
55
73
|
|
|
56
74
|
getEventConfig(type, subType) {
|
|
@@ -70,6 +88,7 @@ class Config {
|
|
|
70
88
|
}
|
|
71
89
|
|
|
72
90
|
attachConfigChangeHandler() {
|
|
91
|
+
this.#attachBlocklistChangeHandler();
|
|
73
92
|
redis.subscribeRedisChannel(REDIS_CONFIG_CHANNEL, (messageData) => {
|
|
74
93
|
try {
|
|
75
94
|
const { key, value } = JSON.parse(messageData);
|
|
@@ -78,7 +97,7 @@ class Config {
|
|
|
78
97
|
this[key] = value;
|
|
79
98
|
}
|
|
80
99
|
} catch (err) {
|
|
81
|
-
this.#logger.error("could not parse event config change", {
|
|
100
|
+
this.#logger.error("could not parse event config change", err, {
|
|
82
101
|
messageData,
|
|
83
102
|
});
|
|
84
103
|
}
|
|
@@ -95,18 +114,102 @@ class Config {
|
|
|
95
114
|
});
|
|
96
115
|
}
|
|
97
116
|
|
|
98
|
-
|
|
99
|
-
|
|
117
|
+
#attachBlocklistChangeHandler() {
|
|
118
|
+
redis.subscribeRedisChannel(REDIS_CONFIG_BLOCKLIST_CHANNEL, (messageData) => {
|
|
119
|
+
try {
|
|
120
|
+
const { command, key, tenant } = JSON.parse(messageData);
|
|
121
|
+
if (command === COMMAND_BLOCK) {
|
|
122
|
+
this.#blockPeriodicEventLocalState(key, tenant);
|
|
123
|
+
} else {
|
|
124
|
+
this.#unblockPeriodicEventLocalState(key, tenant);
|
|
125
|
+
}
|
|
126
|
+
} catch (err) {
|
|
127
|
+
this.#logger.error("could not parse event blocklist change", err, {
|
|
128
|
+
messageData,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
blockPeriodicEvent(type, subType, tenant = "*") {
|
|
135
|
+
const typeWithSuffix = `${type}${SUFFIX_PERIODIC}`;
|
|
136
|
+
const config = this.getEventConfig(typeWithSuffix, subType);
|
|
137
|
+
if (!config) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
const key = this.generateKey(typeWithSuffix, subType);
|
|
141
|
+
this.#blockPeriodicEventLocalState(key, tenant);
|
|
142
|
+
if (!this.redisEnabled) {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
redis
|
|
147
|
+
.publishMessage(REDIS_CONFIG_BLOCKLIST_CHANNEL, JSON.stringify({ command: COMMAND_BLOCK, key, tenant }))
|
|
148
|
+
.catch((error) => {
|
|
149
|
+
this.#logger.error(`publishing config block failed key: ${key}`, error);
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
#blockPeriodicEventLocalState(key, tenant) {
|
|
154
|
+
this.#blockedPeriodicEvents[key] ??= {};
|
|
155
|
+
this.#blockedPeriodicEvents[key][tenant] = true;
|
|
156
|
+
return key;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
clearPeriodicEventBlockList() {
|
|
160
|
+
this.#blockedPeriodicEvents = {};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
unblockPeriodicEvent(type, subType, tenant = "*") {
|
|
164
|
+
const typeWithSuffix = `${type}${SUFFIX_PERIODIC}`;
|
|
165
|
+
const key = this.generateKey(typeWithSuffix, subType);
|
|
166
|
+
const config = this.getEventConfig(typeWithSuffix, subType);
|
|
167
|
+
if (!config) {
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
this.#unblockPeriodicEventLocalState(key, tenant);
|
|
171
|
+
if (!this.redisEnabled) {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
redis
|
|
176
|
+
.publishMessage(REDIS_CONFIG_BLOCKLIST_CHANNEL, JSON.stringify({ command: COMMAND_UNBLOCK, key, tenant }))
|
|
177
|
+
.catch((error) => {
|
|
178
|
+
this.#logger.error(`publishing config block failed key: ${key}`, error);
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
#unblockPeriodicEventLocalState(key, tenant) {
|
|
183
|
+
const map = this.#blockedPeriodicEvents[key];
|
|
184
|
+
if (!map) {
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
this.#blockedPeriodicEvents[key][tenant] = false;
|
|
188
|
+
return key;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
isPeriodicEventBlocked(type, subType, tenant) {
|
|
192
|
+
const map = this.#blockedPeriodicEvents[this.generateKey(type, subType)];
|
|
193
|
+
if (!map) {
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
const tenantSpecific = map[tenant];
|
|
197
|
+
const allTenants = map["*"];
|
|
198
|
+
return tenantSpecific ?? allTenants;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
get isEventQueueActive() {
|
|
202
|
+
return this.#isEventQueueActive;
|
|
100
203
|
}
|
|
101
204
|
|
|
102
|
-
set
|
|
103
|
-
this.#
|
|
205
|
+
set isEventQueueActive(value) {
|
|
206
|
+
this.#isEventQueueActive = value;
|
|
104
207
|
}
|
|
105
208
|
|
|
106
209
|
set fileContent(config) {
|
|
107
210
|
this.#config = config;
|
|
108
211
|
config.events = config.events ?? [];
|
|
109
|
-
config.periodicEvents = config.periodicEvents ?? [];
|
|
212
|
+
config.periodicEvents = (config.periodicEvents ?? []).concat(BASE_PERIODIC_EVENTS.map((event) => ({ ...event })));
|
|
110
213
|
this.#eventMap = config.events.reduce((result, event) => {
|
|
111
214
|
event.load = event.load ?? DEFAULT_LOAD;
|
|
112
215
|
this.validateAdHocEvents(result, event);
|
|
@@ -115,7 +218,6 @@ class Config {
|
|
|
115
218
|
}, {});
|
|
116
219
|
this.#eventMap = config.periodicEvents.reduce((result, event) => {
|
|
117
220
|
event.load = event.load ?? DEFAULT_LOAD;
|
|
118
|
-
const SUFFIX_PERIODIC = "_PERIODIC";
|
|
119
221
|
event.type = `${event.type}${SUFFIX_PERIODIC}`;
|
|
120
222
|
event.isPeriodic = true;
|
|
121
223
|
this.validatePeriodicConfig(result, event);
|
|
@@ -222,6 +324,14 @@ class Config {
|
|
|
222
324
|
this.#instanceLoadLimit = value;
|
|
223
325
|
}
|
|
224
326
|
|
|
327
|
+
get isPeriodicEventBlockedCb() {
|
|
328
|
+
return this.#isPeriodicEventBlockedCb;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
set isPeriodicEventBlockedCb(value) {
|
|
332
|
+
this.#isPeriodicEventBlockedCb = value;
|
|
333
|
+
}
|
|
334
|
+
|
|
225
335
|
get tableNameEventQueue() {
|
|
226
336
|
return this.#tableNameEventQueue;
|
|
227
337
|
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const EventQueueBaseClass = require("../EventQueueProcessorBase");
|
|
4
|
+
const config = require("../config");
|
|
5
|
+
const eventConfig = require("../config");
|
|
6
|
+
|
|
7
|
+
const DELETE_DEFAULT_PERIOD_IN_DAYS = 7;
|
|
8
|
+
const DAY_IN_MS = 24 * 60 * 60 * 1000;
|
|
9
|
+
|
|
10
|
+
class EventQueueDeleteEvents extends EventQueueBaseClass {
|
|
11
|
+
constructor(context, eventType, eventSubType, config) {
|
|
12
|
+
super(context, eventType, eventSubType, config);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async processPeriodicEvent(processContext, key) {
|
|
16
|
+
const tx = this.getTxForEventProcessing(key);
|
|
17
|
+
const deleteByDaysMap = config.events.concat(config.periodicEvents).reduce((result, event) => {
|
|
18
|
+
if (!event.deleteFinishedEventsAfterDays) {
|
|
19
|
+
event.deleteFinishedEventsAfterDays = DELETE_DEFAULT_PERIOD_IN_DAYS;
|
|
20
|
+
}
|
|
21
|
+
result[event.deleteFinishedEventsAfterDays] ??= [];
|
|
22
|
+
result[event.deleteFinishedEventsAfterDays].push(event);
|
|
23
|
+
return result;
|
|
24
|
+
}, {});
|
|
25
|
+
|
|
26
|
+
for (const [deleteAfter, events] of Object.entries(deleteByDaysMap)) {
|
|
27
|
+
const deleteCount = await tx.run(
|
|
28
|
+
DELETE.from(eventConfig.tableNameEventQueue).where([
|
|
29
|
+
{ list: [{ ref: ["type"] }, { ref: ["subType"] }] },
|
|
30
|
+
"IN",
|
|
31
|
+
{
|
|
32
|
+
list: events.map((event) => ({
|
|
33
|
+
list: [{ val: event.type }, { val: event.subType }],
|
|
34
|
+
})),
|
|
35
|
+
},
|
|
36
|
+
"AND",
|
|
37
|
+
{ ref: ["lastAttemptTimestamp"] },
|
|
38
|
+
"<=",
|
|
39
|
+
{ val: new Date(processContext.timestamp.getTime() - deleteAfter * DAY_IN_MS).toISOString() },
|
|
40
|
+
])
|
|
41
|
+
);
|
|
42
|
+
this.logger.info("deleted eligible events", {
|
|
43
|
+
deleteAfterDays: deleteAfter,
|
|
44
|
+
deleteCount,
|
|
45
|
+
events: events.map((event) => `${event.type}_${event.subType}`),
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
module.exports = EventQueueDeleteEvents;
|
package/src/initialize.js
CHANGED
|
@@ -27,9 +27,8 @@ const CONFIG_VARS = [
|
|
|
27
27
|
["configFilePath", null],
|
|
28
28
|
["registerAsEventProcessor", true],
|
|
29
29
|
["processEventsAfterPublish", true],
|
|
30
|
-
["
|
|
30
|
+
["isEventQueueActive", true],
|
|
31
31
|
["runInterval", 5 * 60 * 1000],
|
|
32
|
-
["instanceLoadLimit", 20],
|
|
33
32
|
["tableNameEventQueue", BASE_TABLES.EVENT],
|
|
34
33
|
["tableNameEventLock", BASE_TABLES.LOCK],
|
|
35
34
|
["disableRedis", false],
|
|
@@ -41,9 +40,8 @@ const initialize = async ({
|
|
|
41
40
|
configFilePath,
|
|
42
41
|
registerAsEventProcessor,
|
|
43
42
|
processEventsAfterPublish,
|
|
44
|
-
|
|
43
|
+
isEventQueueActive,
|
|
45
44
|
runInterval,
|
|
46
|
-
instanceLoadLimit,
|
|
47
45
|
tableNameEventQueue,
|
|
48
46
|
tableNameEventLock,
|
|
49
47
|
disableRedis,
|
|
@@ -52,7 +50,7 @@ const initialize = async ({
|
|
|
52
50
|
} = {}) => {
|
|
53
51
|
// TODO: initialize check:
|
|
54
52
|
// - content of yaml check
|
|
55
|
-
// - betweenRuns
|
|
53
|
+
// - betweenRuns
|
|
56
54
|
|
|
57
55
|
if (config.initialized) {
|
|
58
56
|
return;
|
|
@@ -63,9 +61,8 @@ const initialize = async ({
|
|
|
63
61
|
configFilePath,
|
|
64
62
|
registerAsEventProcessor,
|
|
65
63
|
processEventsAfterPublish,
|
|
66
|
-
|
|
64
|
+
isEventQueueActive,
|
|
67
65
|
runInterval,
|
|
68
|
-
instanceLoadLimit,
|
|
69
66
|
tableNameEventQueue,
|
|
70
67
|
tableNameEventLock,
|
|
71
68
|
disableRedis,
|
|
@@ -92,7 +89,6 @@ const initialize = async ({
|
|
|
92
89
|
multiTenancyEnabled: config.isMultiTenancy,
|
|
93
90
|
redisEnabled: config.redisEnabled,
|
|
94
91
|
runInterval: config.runInterval,
|
|
95
|
-
config: config.instanceLoadLimit,
|
|
96
92
|
});
|
|
97
93
|
};
|
|
98
94
|
|
package/src/processEventQueue.js
CHANGED
|
@@ -5,7 +5,7 @@ const pathLib = require("path");
|
|
|
5
5
|
const cds = require("@sap/cds");
|
|
6
6
|
|
|
7
7
|
const config = require("./config");
|
|
8
|
-
const { TransactionMode } = require("./constants");
|
|
8
|
+
const { TransactionMode, EventProcessingStatus } = require("./constants");
|
|
9
9
|
const { limiter } = require("./shared/common");
|
|
10
10
|
|
|
11
11
|
const { executeInNewTransaction, TriggerRollback } = require("./shared/cdsHelper");
|
|
@@ -20,7 +20,7 @@ const processEventQueue = async (context, eventType, eventSubType, startTime = n
|
|
|
20
20
|
try {
|
|
21
21
|
let eventTypeInstance;
|
|
22
22
|
const eventConfig = config.getEventConfig(eventType, eventSubType);
|
|
23
|
-
const [err, EventTypeClass] = resilientRequire(eventConfig
|
|
23
|
+
const [err, EventTypeClass] = resilientRequire(eventConfig);
|
|
24
24
|
if (!eventConfig || err || !(typeof EventTypeClass.constructor === "function")) {
|
|
25
25
|
cds.log(COMPONENT_NAME).error("No Implementation found in the provided configuration file.", {
|
|
26
26
|
eventType,
|
|
@@ -90,15 +90,6 @@ const processEventQueue = async (context, eventType, eventSubType, startTime = n
|
|
|
90
90
|
await eventTypeInstance.persistEventStatus(tx);
|
|
91
91
|
});
|
|
92
92
|
shouldContinue = reevaluateShouldContinue(eventTypeInstance, iterationCounter, startTime);
|
|
93
|
-
if (!shouldContinue) {
|
|
94
|
-
await executeInNewTransaction(
|
|
95
|
-
context,
|
|
96
|
-
`eventQueue-deleteFinishedEvents-${eventType}##${eventSubType}`,
|
|
97
|
-
async (tx) => {
|
|
98
|
-
await eventTypeInstance.deleteFinishedEvents(tx);
|
|
99
|
-
}
|
|
100
|
-
);
|
|
101
|
-
}
|
|
102
93
|
}
|
|
103
94
|
} catch (err) {
|
|
104
95
|
cds.log(COMPONENT_NAME).error("Processing event queue failed with unexpected error.", err, {
|
|
@@ -124,11 +115,35 @@ const reevaluateShouldContinue = (eventTypeInstance, iterationCounter, startTime
|
|
|
124
115
|
return false;
|
|
125
116
|
};
|
|
126
117
|
|
|
127
|
-
// TODO: don't forget to release lock
|
|
128
118
|
const processPeriodicEvent = async (eventTypeInstance) => {
|
|
129
119
|
let queueEntry;
|
|
130
120
|
let processNext = true;
|
|
131
121
|
|
|
122
|
+
const isPeriodicEventBlockedCb = config.isPeriodicEventBlockedCb;
|
|
123
|
+
const params = [eventTypeInstance.eventType, eventTypeInstance.eventSubType, eventTypeInstance.context.tenant];
|
|
124
|
+
let eventBlocked = false;
|
|
125
|
+
if (isPeriodicEventBlockedCb) {
|
|
126
|
+
try {
|
|
127
|
+
eventBlocked = await isPeriodicEventBlockedCb(...params);
|
|
128
|
+
} catch (err) {
|
|
129
|
+
eventBlocked = true;
|
|
130
|
+
eventTypeInstance.logger.error("skipping run because periodic event blocked check failed!", err, {
|
|
131
|
+
type: eventTypeInstance.eventType,
|
|
132
|
+
subType: eventTypeInstance.eventSubType,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
} else {
|
|
136
|
+
eventBlocked = config.isPeriodicEventBlocked(...params);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (eventBlocked) {
|
|
140
|
+
eventTypeInstance.logger.info("skipping run because periodic event is blocked by configuration", {
|
|
141
|
+
type: eventTypeInstance.eventType,
|
|
142
|
+
subType: eventTypeInstance.eventSubType,
|
|
143
|
+
});
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
132
147
|
try {
|
|
133
148
|
while (processNext) {
|
|
134
149
|
await executeInNewTransaction(
|
|
@@ -153,6 +168,7 @@ const processPeriodicEvent = async (eventTypeInstance) => {
|
|
|
153
168
|
return;
|
|
154
169
|
}
|
|
155
170
|
|
|
171
|
+
let status = EventProcessingStatus.Done;
|
|
156
172
|
await executeInNewTransaction(
|
|
157
173
|
eventTypeInstance.context,
|
|
158
174
|
`eventQueue-periodic-process-${eventTypeInstance.eventType}##${eventTypeInstance.eventSubType}`,
|
|
@@ -160,13 +176,17 @@ const processPeriodicEvent = async (eventTypeInstance) => {
|
|
|
160
176
|
eventTypeInstance.processEventContext = tx.context;
|
|
161
177
|
eventTypeInstance.setTxForEventProcessing(queueEntry.ID, cds.tx(tx.context));
|
|
162
178
|
try {
|
|
179
|
+
eventTypeInstance.startPerformanceTracerPeriodicEvents();
|
|
163
180
|
await eventTypeInstance.processPeriodicEvent(tx.context, queueEntry.ID, queueEntry);
|
|
164
181
|
} catch (err) {
|
|
182
|
+
status = EventProcessingStatus.Error;
|
|
165
183
|
eventTypeInstance.handleErrorDuringPeriodicEventProcessing(err, queueEntry);
|
|
166
184
|
throw new TriggerRollback();
|
|
185
|
+
} finally {
|
|
186
|
+
eventTypeInstance.endPerformanceTracerPeriodicEvents();
|
|
167
187
|
}
|
|
168
188
|
if (
|
|
169
|
-
eventTypeInstance.transactionMode
|
|
189
|
+
eventTypeInstance.transactionMode === TransactionMode.alwaysRollback ||
|
|
170
190
|
eventTypeInstance.shouldRollbackTransaction(queueEntry.ID)
|
|
171
191
|
) {
|
|
172
192
|
throw new TriggerRollback();
|
|
@@ -179,7 +199,7 @@ const processPeriodicEvent = async (eventTypeInstance) => {
|
|
|
179
199
|
`eventQueue-periodic-setStatus-${eventTypeInstance.eventType}##${eventTypeInstance.eventSubType}`,
|
|
180
200
|
async (tx) => {
|
|
181
201
|
eventTypeInstance.processEventContext = tx.context;
|
|
182
|
-
await eventTypeInstance.setPeriodicEventStatus(queueEntry.ID);
|
|
202
|
+
await eventTypeInstance.setPeriodicEventStatus(queueEntry.ID, status);
|
|
183
203
|
}
|
|
184
204
|
);
|
|
185
205
|
}
|
|
@@ -196,6 +216,7 @@ const processPeriodicEvent = async (eventTypeInstance) => {
|
|
|
196
216
|
const processEventMap = async (eventTypeInstance) => {
|
|
197
217
|
eventTypeInstance.startPerformanceTracerEvents();
|
|
198
218
|
await eventTypeInstance.beforeProcessingEvents();
|
|
219
|
+
eventTypeInstance.logStartMessage();
|
|
199
220
|
if (eventTypeInstance.commitOnEventLevel) {
|
|
200
221
|
eventTypeInstance.txUsageAllowed = false;
|
|
201
222
|
}
|
|
@@ -244,7 +265,6 @@ const processEventMap = async (eventTypeInstance) => {
|
|
|
244
265
|
|
|
245
266
|
const _processEvent = async (eventTypeInstance, processContext, key, queueEntries, payload) => {
|
|
246
267
|
try {
|
|
247
|
-
eventTypeInstance.logStartMessage(queueEntries);
|
|
248
268
|
const eventOutdated = await eventTypeInstance.isOutdatedAndKeepalive(queueEntries);
|
|
249
269
|
if (eventOutdated) {
|
|
250
270
|
return;
|
|
@@ -257,9 +277,11 @@ const _processEvent = async (eventTypeInstance, processContext, key, queueEntrie
|
|
|
257
277
|
}
|
|
258
278
|
};
|
|
259
279
|
|
|
260
|
-
const resilientRequire = (
|
|
280
|
+
const resilientRequire = (eventConfig) => {
|
|
261
281
|
try {
|
|
262
|
-
const
|
|
282
|
+
const path = eventConfig?.impl;
|
|
283
|
+
const internal = eventConfig?.internalEvent;
|
|
284
|
+
const module = require(pathLib.join(internal ? __dirname : process.cwd(), path));
|
|
263
285
|
return [null, module];
|
|
264
286
|
} catch (err) {
|
|
265
287
|
return [err, null];
|
package/src/redisPubSub.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
+
const cds = require("@sap/cds");
|
|
4
|
+
|
|
3
5
|
const redis = require("./shared/redis");
|
|
4
6
|
const { checkLockExistsAndReturnValue } = require("./shared/distributedLock");
|
|
5
7
|
const config = require("./config");
|
|
6
8
|
const { runEventCombinationForTenant } = require("./runner");
|
|
9
|
+
const { getSubdomainForTenantId } = require("./shared/cdsHelper");
|
|
7
10
|
|
|
8
11
|
const EVENT_MESSAGE_CHANNEL = "EVENT_QUEUE_MESSAGE_CHANNEL";
|
|
9
12
|
const COMPONENT_NAME = "eventQueue/redisPubSub";
|
|
@@ -26,7 +29,23 @@ const messageHandlerProcessEvents = async (messageData) => {
|
|
|
26
29
|
type,
|
|
27
30
|
subType,
|
|
28
31
|
});
|
|
29
|
-
|
|
32
|
+
if (!config.isEventQueueActive) {
|
|
33
|
+
cds.log(COMPONENT_NAME).info("Skipping processing because runner is deactivated!", {
|
|
34
|
+
type,
|
|
35
|
+
subType,
|
|
36
|
+
});
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const subdomain = await getSubdomainForTenantId(tenantId);
|
|
41
|
+
const tenantContext = {
|
|
42
|
+
tenant: tenantId,
|
|
43
|
+
// NOTE: we need this because of logging otherwise logs would not contain the subdomain
|
|
44
|
+
http: { req: { authInfo: { getSubdomain: () => subdomain } } },
|
|
45
|
+
};
|
|
46
|
+
return await cds.tx(tenantContext, async ({ context }) => {
|
|
47
|
+
return await runEventCombinationForTenant(context, type, subType);
|
|
48
|
+
});
|
|
30
49
|
} catch (err) {
|
|
31
50
|
logger.error("could not parse event information", {
|
|
32
51
|
messageData,
|
|
@@ -36,13 +55,32 @@ const messageHandlerProcessEvents = async (messageData) => {
|
|
|
36
55
|
|
|
37
56
|
const broadcastEvent = async (tenantId, type, subType) => {
|
|
38
57
|
const logger = cds.log(COMPONENT_NAME);
|
|
39
|
-
if (!config.redisEnabled) {
|
|
40
|
-
if (config.registerAsEventProcessor) {
|
|
41
|
-
await runEventCombinationForTenant(tenantId, type, subType);
|
|
42
|
-
}
|
|
43
|
-
return;
|
|
44
|
-
}
|
|
45
58
|
try {
|
|
59
|
+
if (!config.isEventQueueActive) {
|
|
60
|
+
cds.log(COMPONENT_NAME).info("Skipping processing because runner is deactivated!", {
|
|
61
|
+
type,
|
|
62
|
+
subType,
|
|
63
|
+
});
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (!config.redisEnabled) {
|
|
67
|
+
if (config.registerAsEventProcessor) {
|
|
68
|
+
let context = {};
|
|
69
|
+
if (tenantId) {
|
|
70
|
+
const subdomain = await getSubdomainForTenantId(tenantId);
|
|
71
|
+
context = {
|
|
72
|
+
// NOTE: we need this because of logging otherwise logs would not contain the subdomain
|
|
73
|
+
tenant: tenantId,
|
|
74
|
+
http: { req: { authInfo: { getSubdomain: () => subdomain } } },
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return await cds.tx(context, async ({ context }) => {
|
|
79
|
+
return await runEventCombinationForTenant(context, type, subType);
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
46
84
|
const result = await checkLockExistsAndReturnValue(
|
|
47
85
|
new cds.EventContext({ tenant: tenantId }),
|
|
48
86
|
[type, subType].join("##")
|
package/src/runner.js
CHANGED
|
@@ -37,7 +37,7 @@ const _scheduleFunction = async (singleRunFn, periodicFn) => {
|
|
|
37
37
|
|
|
38
38
|
const fnWithRunningCheck = () => {
|
|
39
39
|
const logger = cds.log(COMPONENT_NAME);
|
|
40
|
-
if (eventQueueConfig.
|
|
40
|
+
if (!eventQueueConfig.isEventQueueActive) {
|
|
41
41
|
logger.info("runner is deactivated via config variable. Skipping this run.");
|
|
42
42
|
return;
|
|
43
43
|
}
|
|
@@ -82,6 +82,9 @@ const _checkAndTriggerPeriodicEventUpdate = (tenantIds) => {
|
|
|
82
82
|
const hash = hashStringTo32Bit(JSON.stringify(tenantIds));
|
|
83
83
|
if (!tenantIdHash) {
|
|
84
84
|
tenantIdHash = hash;
|
|
85
|
+
_multiTenancyPeriodicEvents().catch((err) => {
|
|
86
|
+
cds.log(COMPONENT_NAME).error("Error during triggering updating periodic events!", err);
|
|
87
|
+
});
|
|
85
88
|
return;
|
|
86
89
|
}
|
|
87
90
|
if (tenantIdHash && tenantIdHash !== hash) {
|
|
@@ -94,45 +97,63 @@ const _checkAndTriggerPeriodicEventUpdate = (tenantIds) => {
|
|
|
94
97
|
|
|
95
98
|
const _executeEventsAllTenants = (tenantIds, runId) => {
|
|
96
99
|
const events = eventQueueConfig.allEvents;
|
|
97
|
-
const
|
|
98
|
-
tenantIds.forEach((tenantId) => {
|
|
100
|
+
const product = tenantIds.reduce((result, tenantId) => {
|
|
99
101
|
events.forEach((event) => {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
102
|
+
result.push([tenantId, event]);
|
|
103
|
+
});
|
|
104
|
+
return result;
|
|
105
|
+
}, []);
|
|
106
|
+
|
|
107
|
+
return product.map(async ([tenantId, event]) => {
|
|
108
|
+
const subdomain = await getSubdomainForTenantId(tenantId);
|
|
109
|
+
const tenantContext = {
|
|
110
|
+
tenant: tenantId,
|
|
111
|
+
// NOTE: we need this because of logging otherwise logs would not contain the subdomain
|
|
112
|
+
http: { req: { authInfo: { getSubdomain: () => subdomain } } },
|
|
113
|
+
};
|
|
114
|
+
return await cds.tx(tenantContext, async ({ context }) => {
|
|
115
|
+
const label = `${event.type}_${event.subType}`;
|
|
116
|
+
return await WorkerQueue.instance.addToQueue(event.load, label, async () => {
|
|
117
|
+
try {
|
|
118
|
+
const lockId = `${runId}_${label}`;
|
|
119
|
+
const couldAcquireLock = await distributedLock.acquireLock(context, lockId, {
|
|
120
|
+
expiryTime: eventQueueConfig.runInterval * 0.95,
|
|
121
|
+
});
|
|
122
|
+
if (!couldAcquireLock) {
|
|
123
|
+
return;
|
|
116
124
|
}
|
|
117
|
-
|
|
118
|
-
|
|
125
|
+
await runEventCombinationForTenant(context, event.type, event.subType, true);
|
|
126
|
+
} catch (err) {
|
|
127
|
+
cds.log(COMPONENT_NAME).error("executing event-queue run for tenant failed", {
|
|
128
|
+
tenantId,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
});
|
|
119
132
|
});
|
|
120
133
|
});
|
|
121
|
-
return promises;
|
|
122
134
|
};
|
|
123
135
|
|
|
124
136
|
const _executePeriodicEventsAllTenants = (tenantIds, runId) => {
|
|
125
137
|
tenantIds.forEach((tenantId) => {
|
|
126
|
-
|
|
138
|
+
const label = `UPDATE_PERIODIC_EVENTS_${tenantId}`;
|
|
139
|
+
WorkerQueue.instance.addToQueue(1, label, async () => {
|
|
127
140
|
try {
|
|
128
|
-
const
|
|
129
|
-
const
|
|
130
|
-
|
|
141
|
+
const subdomain = await getSubdomainForTenantId(tenantId);
|
|
142
|
+
const tenantContext = {
|
|
143
|
+
tenant: tenantId,
|
|
144
|
+
// NOTE: we need this because of logging otherwise logs would not contain the subdomain
|
|
145
|
+
http: { req: { authInfo: { getSubdomain: () => subdomain } } },
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
return await cds.tx(tenantContext, async ({ context }) => {
|
|
149
|
+
const couldAcquireLock = await distributedLock.acquireLock(context, runId, {
|
|
150
|
+
expiryTime: eventQueueConfig.runInterval * 0.95,
|
|
151
|
+
});
|
|
152
|
+
if (!couldAcquireLock) {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
await _checkPeriodicEventsSingleTenant(context);
|
|
131
156
|
});
|
|
132
|
-
if (!couldAcquireLock) {
|
|
133
|
-
return;
|
|
134
|
-
}
|
|
135
|
-
await _checkPeriodicEventsSingleTenant(tenantId);
|
|
136
157
|
} catch (err) {
|
|
137
158
|
cds.log(COMPONENT_NAME).error("executing event-queue run for tenant failed", {
|
|
138
159
|
tenantId,
|
|
@@ -143,11 +164,19 @@ const _executePeriodicEventsAllTenants = (tenantIds, runId) => {
|
|
|
143
164
|
};
|
|
144
165
|
|
|
145
166
|
const _singleTenantDb = async (tenantId) => {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
WorkerQueue.instance.addToQueue(event.load, async () => {
|
|
167
|
+
return eventQueueConfig.allEvents.map((event) => {
|
|
168
|
+
const label = `${event.type}_${event.subType}`;
|
|
169
|
+
return WorkerQueue.instance.addToQueue(event.load, label, async () => {
|
|
149
170
|
try {
|
|
150
|
-
|
|
171
|
+
const context = new cds.EventContext({ tenant: tenantId });
|
|
172
|
+
const lockId = `${EVENT_QUEUE_RUN_ID}_${label}`;
|
|
173
|
+
const couldAcquireLock = await distributedLock.acquireLock(context, lockId, {
|
|
174
|
+
expiryTime: eventQueueConfig.runInterval * 0.95,
|
|
175
|
+
});
|
|
176
|
+
if (!couldAcquireLock) {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
await runEventCombinationForTenant(context, event.type, event.subType, true);
|
|
151
180
|
} catch (err) {
|
|
152
181
|
cds.log(COMPONENT_NAME).error("executing event-queue run for tenant failed", {
|
|
153
182
|
tenantId,
|
|
@@ -215,28 +244,23 @@ const _calculateOffsetForFirstRun = async () => {
|
|
|
215
244
|
return offsetDependingOnLastRun;
|
|
216
245
|
};
|
|
217
246
|
|
|
218
|
-
const runEventCombinationForTenant = async (
|
|
247
|
+
const runEventCombinationForTenant = async (context, type, subType, skipWorkerPool) => {
|
|
219
248
|
try {
|
|
220
|
-
const subdomain = await getSubdomainForTenantId(tenantId);
|
|
221
|
-
const context = new cds.EventContext({
|
|
222
|
-
tenant: tenantId,
|
|
223
|
-
// NOTE: we need this because of logging otherwise logs would not contain the subdomain
|
|
224
|
-
http: { req: { authInfo: { getSubdomain: () => subdomain } } },
|
|
225
|
-
});
|
|
226
|
-
cds.context = context;
|
|
227
249
|
if (skipWorkerPool) {
|
|
228
250
|
return await processEventQueue(context, type, subType);
|
|
229
251
|
} else {
|
|
230
252
|
const config = eventQueueConfig.getEventConfig(type, subType);
|
|
253
|
+
const label = `${type}_${subType}`;
|
|
231
254
|
return await WorkerQueue.instance.addToQueue(
|
|
232
255
|
config.load,
|
|
256
|
+
label,
|
|
233
257
|
async () => await processEventQueue(context, type, subType)
|
|
234
258
|
);
|
|
235
259
|
}
|
|
236
260
|
} catch (err) {
|
|
237
261
|
const logger = cds.log(COMPONENT_NAME);
|
|
238
262
|
logger.error("error executing event combination for tenant", err, {
|
|
239
|
-
tenantId,
|
|
263
|
+
tenantId: context.tenant,
|
|
240
264
|
type,
|
|
241
265
|
subType,
|
|
242
266
|
});
|
|
@@ -266,7 +290,7 @@ const _multiTenancyPeriodicEvents = async () => {
|
|
|
266
290
|
}
|
|
267
291
|
};
|
|
268
292
|
|
|
269
|
-
const _checkPeriodicEventsSingleTenant = async (
|
|
293
|
+
const _checkPeriodicEventsSingleTenant = async (context = {}) => {
|
|
270
294
|
const logger = cds.log(COMPONENT_NAME);
|
|
271
295
|
if (!eventQueueConfig.updatePeriodicEvents || !eventQueueConfig.periodicEvents.length) {
|
|
272
296
|
logger.info("updating of periodic events is disabled or no periodic events configured", {
|
|
@@ -276,23 +300,16 @@ const _checkPeriodicEventsSingleTenant = async (tenantId) => {
|
|
|
276
300
|
return;
|
|
277
301
|
}
|
|
278
302
|
try {
|
|
279
|
-
const subdomain = await cdsHelper.getSubdomainForTenantId(tenantId);
|
|
280
|
-
const context = new cds.EventContext({
|
|
281
|
-
tenant: tenantId,
|
|
282
|
-
// NOTE: we need this because of logging otherwise logs would not contain the subdomain
|
|
283
|
-
http: { req: { authInfo: { getSubdomain: () => subdomain } } },
|
|
284
|
-
});
|
|
285
|
-
cds.context = context;
|
|
286
303
|
logger.info("executing updating periotic events", {
|
|
287
|
-
tenantId,
|
|
288
|
-
subdomain,
|
|
304
|
+
tenantId: context.tenant,
|
|
305
|
+
subdomain: context.http?.req.authInfo.getSubdomain(),
|
|
289
306
|
});
|
|
290
307
|
await cdsHelper.executeInNewTransaction(context, "update-periodic-events", async (tx) => {
|
|
291
308
|
await periodicEvents.checkAndInsertPeriodicEvents(tx.context);
|
|
292
309
|
});
|
|
293
310
|
} catch (err) {
|
|
294
311
|
logger.error("Couldn't update periodic events for tenant! Next try after defined interval.", err, {
|
|
295
|
-
tenantId,
|
|
312
|
+
tenantId: context.tenant,
|
|
296
313
|
redisEnabled: eventQueueConfig.redisEnabled,
|
|
297
314
|
});
|
|
298
315
|
}
|
|
@@ -304,10 +321,12 @@ module.exports = {
|
|
|
304
321
|
multiTenancyRedis,
|
|
305
322
|
runEventCombinationForTenant,
|
|
306
323
|
_: {
|
|
324
|
+
_singleTenantDb,
|
|
307
325
|
_multiTenancyRedis,
|
|
308
326
|
_multiTenancyDb,
|
|
309
327
|
_calculateOffsetForFirstRun,
|
|
310
328
|
_acquireRunId,
|
|
311
329
|
EVENT_QUEUE_RUN_TS,
|
|
330
|
+
clearHash: () => (tenantIdHash = null),
|
|
312
331
|
},
|
|
313
332
|
};
|
|
@@ -17,6 +17,7 @@ class PerformanceTracer {
|
|
|
17
17
|
this.__start = new Date();
|
|
18
18
|
this.__logger = logger;
|
|
19
19
|
this.__name = name;
|
|
20
|
+
this.__properties = options.properties ??= {};
|
|
20
21
|
options.startMessage &&
|
|
21
22
|
logger.info("Performance measurement started", {
|
|
22
23
|
name: name,
|
|
@@ -64,6 +65,7 @@ class PerformanceTracer {
|
|
|
64
65
|
this.__logger.info("Performance measurement executed", {
|
|
65
66
|
name: this.__name,
|
|
66
67
|
milliseconds: executionTime,
|
|
68
|
+
...this.__properties,
|
|
67
69
|
customFields,
|
|
68
70
|
});
|
|
69
71
|
}
|
|
@@ -8,9 +8,9 @@ const EventQueueError = require("../EventQueueError");
|
|
|
8
8
|
const COMPONENT_NAME = "eventQueue/WorkerQueue";
|
|
9
9
|
const NANO_TO_MS = 1e6;
|
|
10
10
|
const THRESHOLD = {
|
|
11
|
-
INFO:
|
|
12
|
-
WARN:
|
|
13
|
-
ERROR:
|
|
11
|
+
INFO: 35 * 1000,
|
|
12
|
+
WARN: 55 * 1000,
|
|
13
|
+
ERROR: 75 * 1000,
|
|
14
14
|
};
|
|
15
15
|
|
|
16
16
|
class WorkerQueue {
|
|
@@ -31,21 +31,21 @@ class WorkerQueue {
|
|
|
31
31
|
this.#queue = [];
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
addToQueue(load, cb) {
|
|
34
|
+
addToQueue(load, label, cb) {
|
|
35
35
|
if (load > this.#concurrencyLimit) {
|
|
36
|
-
throw EventQueueError.loadHigherThanLimit(load);
|
|
36
|
+
throw EventQueueError.loadHigherThanLimit(load, label);
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
const startTime = process.hrtime.bigint();
|
|
40
40
|
const p = new Promise((resolve, reject) => {
|
|
41
|
-
this.#queue.push([load, cb, resolve, reject, startTime]);
|
|
41
|
+
this.#queue.push([load, label, cb, resolve, reject, startTime]);
|
|
42
42
|
});
|
|
43
43
|
this._checkForNext();
|
|
44
44
|
return p;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
_executeFunction(load, cb, resolve, reject, startTime) {
|
|
48
|
-
this.checkAndLogWaitingTime(startTime);
|
|
47
|
+
_executeFunction(load, label, cb, resolve, reject, startTime) {
|
|
48
|
+
this.checkAndLogWaitingTime(startTime, label);
|
|
49
49
|
const promise = Promise.resolve().then(() => cb());
|
|
50
50
|
this.#runningPromises.push(promise);
|
|
51
51
|
this.#runningLoad = this.#runningLoad + load;
|
|
@@ -59,7 +59,7 @@ class WorkerQueue {
|
|
|
59
59
|
resolve(...results);
|
|
60
60
|
})
|
|
61
61
|
.catch((err) => {
|
|
62
|
-
cds.log(COMPONENT_NAME).error("Error happened in WorkQueue. Errors should be caught before!", err);
|
|
62
|
+
cds.log(COMPONENT_NAME).error("Error happened in WorkQueue. Errors should be caught before!", err, { label });
|
|
63
63
|
reject(err);
|
|
64
64
|
});
|
|
65
65
|
}
|
|
@@ -87,7 +87,7 @@ class WorkerQueue {
|
|
|
87
87
|
return WorkerQueue.#instance;
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
-
checkAndLogWaitingTime(startTime) {
|
|
90
|
+
checkAndLogWaitingTime(startTime, label) {
|
|
91
91
|
const diffMs = Math.round(Number(process.hrtime.bigint() - startTime) / NANO_TO_MS);
|
|
92
92
|
let logLevel;
|
|
93
93
|
if (diffMs >= THRESHOLD.ERROR) {
|
|
@@ -101,6 +101,7 @@ class WorkerQueue {
|
|
|
101
101
|
}
|
|
102
102
|
cds.log(COMPONENT_NAME)[logLevel]("Waiting time in worker queue", {
|
|
103
103
|
diffMs,
|
|
104
|
+
label,
|
|
104
105
|
});
|
|
105
106
|
}
|
|
106
107
|
}
|
package/src/shared/redis.js
CHANGED
|
@@ -80,7 +80,7 @@ const subscribeRedisChannel = (channel, subscribeCb) => {
|
|
|
80
80
|
subscriberChannelClientPromise[channel]
|
|
81
81
|
.then((client) => {
|
|
82
82
|
cds.log(COMPONENT_NAME).info("subscribe redis client connected channel", { channel });
|
|
83
|
-
client.subscribe(channel, subscribeCb);
|
|
83
|
+
client.subscribe(channel, subscribeCb).catch(errorHandlerCreateClient);
|
|
84
84
|
})
|
|
85
85
|
.catch((err) => {
|
|
86
86
|
cds
|