@cap-js-community/event-queue 0.3.0 → 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 +118 -8
- package/src/housekeeping/EventQueueDeleteEvents.js +51 -0
- package/src/initialize.js +3 -3
- package/src/processEventQueue.js +38 -16
- package/src/redisPubSub.js +17 -0
- package/src/runner.js +23 -7
- 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,10 +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;
|
|
15
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
|
+
];
|
|
16
30
|
|
|
17
31
|
class Config {
|
|
18
32
|
#logger;
|
|
@@ -25,7 +39,7 @@ class Config {
|
|
|
25
39
|
#instanceLoadLimit;
|
|
26
40
|
#tableNameEventQueue;
|
|
27
41
|
#tableNameEventLock;
|
|
28
|
-
#
|
|
42
|
+
#isEventQueueActive;
|
|
29
43
|
#configFilePath;
|
|
30
44
|
#processEventsAfterPublish;
|
|
31
45
|
#skipCsnCheck;
|
|
@@ -34,6 +48,8 @@ class Config {
|
|
|
34
48
|
#env;
|
|
35
49
|
#eventMap;
|
|
36
50
|
#updatePeriodicEvents;
|
|
51
|
+
#blockedPeriodicEvents;
|
|
52
|
+
#isPeriodicEventBlockedCb;
|
|
37
53
|
static #instance;
|
|
38
54
|
constructor() {
|
|
39
55
|
this.#logger = cds.log(COMPONENT_NAME);
|
|
@@ -46,12 +62,13 @@ class Config {
|
|
|
46
62
|
this.#instanceLoadLimit = 100;
|
|
47
63
|
this.#tableNameEventQueue = null;
|
|
48
64
|
this.#tableNameEventLock = null;
|
|
49
|
-
this.#
|
|
65
|
+
this.#isEventQueueActive = true;
|
|
50
66
|
this.#configFilePath = null;
|
|
51
67
|
this.#processEventsAfterPublish = null;
|
|
52
68
|
this.#skipCsnCheck = null;
|
|
53
69
|
this.#disableRedis = null;
|
|
54
70
|
this.#env = getEnvInstance();
|
|
71
|
+
this.#blockedPeriodicEvents = {};
|
|
55
72
|
}
|
|
56
73
|
|
|
57
74
|
getEventConfig(type, subType) {
|
|
@@ -71,6 +88,7 @@ class Config {
|
|
|
71
88
|
}
|
|
72
89
|
|
|
73
90
|
attachConfigChangeHandler() {
|
|
91
|
+
this.#attachBlocklistChangeHandler();
|
|
74
92
|
redis.subscribeRedisChannel(REDIS_CONFIG_CHANNEL, (messageData) => {
|
|
75
93
|
try {
|
|
76
94
|
const { key, value } = JSON.parse(messageData);
|
|
@@ -79,7 +97,7 @@ class Config {
|
|
|
79
97
|
this[key] = value;
|
|
80
98
|
}
|
|
81
99
|
} catch (err) {
|
|
82
|
-
this.#logger.error("could not parse event config change", {
|
|
100
|
+
this.#logger.error("could not parse event config change", err, {
|
|
83
101
|
messageData,
|
|
84
102
|
});
|
|
85
103
|
}
|
|
@@ -96,18 +114,102 @@ class Config {
|
|
|
96
114
|
});
|
|
97
115
|
}
|
|
98
116
|
|
|
99
|
-
|
|
100
|
-
|
|
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;
|
|
101
203
|
}
|
|
102
204
|
|
|
103
|
-
set
|
|
104
|
-
this.#
|
|
205
|
+
set isEventQueueActive(value) {
|
|
206
|
+
this.#isEventQueueActive = value;
|
|
105
207
|
}
|
|
106
208
|
|
|
107
209
|
set fileContent(config) {
|
|
108
210
|
this.#config = config;
|
|
109
211
|
config.events = config.events ?? [];
|
|
110
|
-
config.periodicEvents = config.periodicEvents ?? [];
|
|
212
|
+
config.periodicEvents = (config.periodicEvents ?? []).concat(BASE_PERIODIC_EVENTS.map((event) => ({ ...event })));
|
|
111
213
|
this.#eventMap = config.events.reduce((result, event) => {
|
|
112
214
|
event.load = event.load ?? DEFAULT_LOAD;
|
|
113
215
|
this.validateAdHocEvents(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,7 +27,7 @@ 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
32
|
["tableNameEventQueue", BASE_TABLES.EVENT],
|
|
33
33
|
["tableNameEventLock", BASE_TABLES.LOCK],
|
|
@@ -40,7 +40,7 @@ const initialize = async ({
|
|
|
40
40
|
configFilePath,
|
|
41
41
|
registerAsEventProcessor,
|
|
42
42
|
processEventsAfterPublish,
|
|
43
|
-
|
|
43
|
+
isEventQueueActive,
|
|
44
44
|
runInterval,
|
|
45
45
|
tableNameEventQueue,
|
|
46
46
|
tableNameEventLock,
|
|
@@ -61,7 +61,7 @@ const initialize = async ({
|
|
|
61
61
|
configFilePath,
|
|
62
62
|
registerAsEventProcessor,
|
|
63
63
|
processEventsAfterPublish,
|
|
64
|
-
|
|
64
|
+
isEventQueueActive,
|
|
65
65
|
runInterval,
|
|
66
66
|
tableNameEventQueue,
|
|
67
67
|
tableNameEventLock,
|
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,10 +176,14 @@ 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
189
|
eventTypeInstance.transactionMode === TransactionMode.alwaysRollback ||
|
|
@@ -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,5 +1,7 @@
|
|
|
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");
|
|
@@ -27,6 +29,14 @@ const messageHandlerProcessEvents = async (messageData) => {
|
|
|
27
29
|
type,
|
|
28
30
|
subType,
|
|
29
31
|
});
|
|
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
|
+
|
|
30
40
|
const subdomain = await getSubdomainForTenantId(tenantId);
|
|
31
41
|
const tenantContext = {
|
|
32
42
|
tenant: tenantId,
|
|
@@ -46,6 +56,13 @@ const messageHandlerProcessEvents = async (messageData) => {
|
|
|
46
56
|
const broadcastEvent = async (tenantId, type, subType) => {
|
|
47
57
|
const logger = cds.log(COMPONENT_NAME);
|
|
48
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
|
+
}
|
|
49
66
|
if (!config.redisEnabled) {
|
|
50
67
|
if (config.registerAsEventProcessor) {
|
|
51
68
|
let context = {};
|
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) {
|
|
@@ -109,9 +112,10 @@ const _executeEventsAllTenants = (tenantIds, runId) => {
|
|
|
109
112
|
http: { req: { authInfo: { getSubdomain: () => subdomain } } },
|
|
110
113
|
};
|
|
111
114
|
return await cds.tx(tenantContext, async ({ context }) => {
|
|
112
|
-
|
|
115
|
+
const label = `${event.type}_${event.subType}`;
|
|
116
|
+
return await WorkerQueue.instance.addToQueue(event.load, label, async () => {
|
|
113
117
|
try {
|
|
114
|
-
const lockId = `${runId}_${
|
|
118
|
+
const lockId = `${runId}_${label}`;
|
|
115
119
|
const couldAcquireLock = await distributedLock.acquireLock(context, lockId, {
|
|
116
120
|
expiryTime: eventQueueConfig.runInterval * 0.95,
|
|
117
121
|
});
|
|
@@ -131,7 +135,8 @@ const _executeEventsAllTenants = (tenantIds, runId) => {
|
|
|
131
135
|
|
|
132
136
|
const _executePeriodicEventsAllTenants = (tenantIds, runId) => {
|
|
133
137
|
tenantIds.forEach((tenantId) => {
|
|
134
|
-
|
|
138
|
+
const label = `UPDATE_PERIODIC_EVENTS_${tenantId}`;
|
|
139
|
+
WorkerQueue.instance.addToQueue(1, label, async () => {
|
|
135
140
|
try {
|
|
136
141
|
const subdomain = await getSubdomainForTenantId(tenantId);
|
|
137
142
|
const tenantContext = {
|
|
@@ -159,11 +164,18 @@ const _executePeriodicEventsAllTenants = (tenantIds, runId) => {
|
|
|
159
164
|
};
|
|
160
165
|
|
|
161
166
|
const _singleTenantDb = async (tenantId) => {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
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 () => {
|
|
165
170
|
try {
|
|
166
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
|
+
}
|
|
167
179
|
await runEventCombinationForTenant(context, event.type, event.subType, true);
|
|
168
180
|
} catch (err) {
|
|
169
181
|
cds.log(COMPONENT_NAME).error("executing event-queue run for tenant failed", {
|
|
@@ -238,8 +250,10 @@ const runEventCombinationForTenant = async (context, type, subType, skipWorkerPo
|
|
|
238
250
|
return await processEventQueue(context, type, subType);
|
|
239
251
|
} else {
|
|
240
252
|
const config = eventQueueConfig.getEventConfig(type, subType);
|
|
253
|
+
const label = `${type}_${subType}`;
|
|
241
254
|
return await WorkerQueue.instance.addToQueue(
|
|
242
255
|
config.load,
|
|
256
|
+
label,
|
|
243
257
|
async () => await processEventQueue(context, type, subType)
|
|
244
258
|
);
|
|
245
259
|
}
|
|
@@ -307,10 +321,12 @@ module.exports = {
|
|
|
307
321
|
multiTenancyRedis,
|
|
308
322
|
runEventCombinationForTenant,
|
|
309
323
|
_: {
|
|
324
|
+
_singleTenantDb,
|
|
310
325
|
_multiTenancyRedis,
|
|
311
326
|
_multiTenancyDb,
|
|
312
327
|
_calculateOffsetForFirstRun,
|
|
313
328
|
_acquireRunId,
|
|
314
329
|
EVENT_QUEUE_RUN_TS,
|
|
330
|
+
clearHash: () => (tenantIdHash = null),
|
|
315
331
|
},
|
|
316
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
|