@cap-js-community/event-queue 0.2.3 → 0.2.5
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/README.md +20 -15
- package/package.json +6 -6
- package/src/EventQueueError.js +14 -0
- package/src/EventQueueProcessorBase.js +42 -20
- package/src/config.js +18 -6
- package/src/initialize.js +6 -5
- package/src/periodicEvents.js +16 -8
- package/src/processEventQueue.js +4 -15
- package/src/redisPubSub.js +5 -2
- package/src/runner.js +71 -50
- package/src/shared/WorkerQueue.js +68 -26
- package/src/shared/common.js +1 -72
- package/src/shared/distributedLock.js +3 -1
- package/src/shared/eventScheduler.js +1 -1
package/README.md
CHANGED
|
@@ -5,22 +5,24 @@
|
|
|
5
5
|
[](https://api.reuse.software/info/github.com/cap-js-community/event-queue)
|
|
6
6
|
[](https://github.com/cap-js-community/event-queue/commits/main)
|
|
7
7
|
|
|
8
|
-
The Event-Queue is a framework built on top of CAP Node.js,
|
|
9
|
-
asynchronous event processing
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
8
|
+
The Event-Queue is a framework built on top of CAP Node.js, designed specifically for efficient and
|
|
9
|
+
streamlined asynchronous event processing. With a focus on load balancing, this package ensures optimal
|
|
10
|
+
event distribution across all available application instances. By providing managed transactions similar to CAP
|
|
11
|
+
handlers, the Event-Queue framework simplifies event processing, enhancing the overall performance of your application.
|
|
12
|
+
|
|
13
|
+
Additionally, Event-Queue provides support for [periodic events](https://cap-js-community.github.io/event-queue/configure-event/#periodic-events),
|
|
14
|
+
allowing for processing at defined intervals. This feature further extends its capabilities in load balancing and
|
|
15
|
+
transaction management, ensuring that even regularly occurring tasks are handled efficiently and effectively without
|
|
16
|
+
overloading any single instance. This makes it an ideal solution for applications needing consistent, reliable event processing.
|
|
13
17
|
|
|
14
18
|
## Getting started
|
|
15
19
|
|
|
16
20
|
- Run `npm add @cap-js-community/event-queue` in `@sap/cds` project
|
|
17
|
-
- Activate the cds-plugin in the cds section of the package.
|
|
21
|
+
- Activate the cds-plugin in the cds section of the package.json.
|
|
18
22
|
|
|
19
23
|
### As cds-plugin
|
|
20
24
|
|
|
21
|
-
|
|
22
|
-
cds-plugin concept.
|
|
23
|
-
https://cap.cloud.sap/docs/releases/march23#new-cds-plugin-technique
|
|
25
|
+
For detailed information check out the [documentation](https://cap-js-community.github.io/event-queue/setup).
|
|
24
26
|
|
|
25
27
|
```json
|
|
26
28
|
{
|
|
@@ -35,13 +37,16 @@ https://cap.cloud.sap/docs/releases/march23#new-cds-plugin-technique
|
|
|
35
37
|
|
|
36
38
|
## Features
|
|
37
39
|
|
|
38
|
-
|
|
39
|
-
-
|
|
40
|
-
|
|
40
|
+
Learn more about features in the [documentation](https://cap-js-community.github.io/event-queue/#functionality-and-problem-solutions). To compare the
|
|
41
|
+
event-queue with other SAP products head over to [Distinction from other solutions](https://cap-js-community.github.io/event-queue/diff-to-outbox/).
|
|
42
|
+
|
|
43
|
+
- [load balancing](https://cap-js-community.github.io/event-queue/load-balancing) and concurrency control of event processing across app instances
|
|
44
|
+
- [periodic events](https://cap-js-community.github.io/event-queue/configure-event/#periodic-events) similar to running cron jobs for business processes
|
|
45
|
+
- [managed transactions](https://cap-js-community.github.io/event-queue/transaction-handling) for event processing
|
|
41
46
|
- async processing of processing intensive tasks for better UI responsiveness
|
|
42
47
|
- push/pull mechanism for reducing delay between publish an event and processing
|
|
43
48
|
- cluster published events during processing (e.g. for combining multiple E-Mail events to one E-Mail)
|
|
44
|
-
- plug and play via cds-plugin
|
|
49
|
+
- [plug and play](https://cap-js-community.github.io/event-queue/setup) via cds-plugin
|
|
45
50
|
|
|
46
51
|
## Documentation
|
|
47
52
|
|
|
@@ -50,7 +55,7 @@ Head over to our [Documentation](https://cap-js-community.github.io/event-queue/
|
|
|
50
55
|
## Support, Feedback, Contributing
|
|
51
56
|
|
|
52
57
|
This project is open to feature requests/suggestions, bug reports etc.
|
|
53
|
-
via [GitHub issues](https://github.com/cap-js-
|
|
58
|
+
via [GitHub issues](https://github.com/cap-js-communityevent-queue/issues). Contribution and feedback are encouraged
|
|
54
59
|
and always welcome. For more information about how to contribute, the project structure, as well as additional
|
|
55
60
|
contribution information, see our [Contribution Guidelines](CONTRIBUTING.md).
|
|
56
61
|
|
|
@@ -62,7 +67,7 @@ times.
|
|
|
62
67
|
|
|
63
68
|
## Licensing
|
|
64
69
|
|
|
65
|
-
Copyright 2023 SAP SE or an SAP affiliate company and `@cap-js-community/event-queue contributors
|
|
70
|
+
Copyright 2023 SAP SE or an SAP affiliate company and `@cap-js-community/event-queue` contributors. Please see
|
|
66
71
|
our [LICENSE](LICENSE) for copyright and license information. Detailed information including third-party components and
|
|
67
72
|
their licensing/copyright information is
|
|
68
73
|
available [via the REUSE tool](https://api.reuse.software/info/github.com/cap-js-community/<your-project>).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cap-js-community/event-queue",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.5",
|
|
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": [
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"lint:ci": "npm run eslint:ci && npm run prettier:ci",
|
|
32
32
|
"eslint": "eslint --fix .",
|
|
33
33
|
"eslint:ci": "eslint .",
|
|
34
|
-
"prettier": "prettier --write --
|
|
34
|
+
"prettier": "prettier --write --loglevel error .",
|
|
35
35
|
"prettier:ci": "prettier --check .",
|
|
36
36
|
"prepareRelease": "npm prune --production",
|
|
37
37
|
"docs": "cd docs && bundle exec jekyll serve",
|
|
@@ -42,14 +42,14 @@
|
|
|
42
42
|
"node": ">=16"
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
|
-
"redis": "4.6.
|
|
45
|
+
"redis": "4.6.11",
|
|
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.
|
|
50
|
+
"@sap/cds": "7.4.0",
|
|
51
|
+
"@sap/cds-dk": "7.4.0",
|
|
52
|
+
"eslint": "8.54.0",
|
|
53
53
|
"eslint-config-prettier": "9.0.0",
|
|
54
54
|
"eslint-plugin-jest": "27.6.0",
|
|
55
55
|
"eslint-plugin-node": "11.1.0",
|
package/src/EventQueueError.js
CHANGED
|
@@ -16,6 +16,7 @@ const ERROR_CODES = {
|
|
|
16
16
|
MISSING_IMPL: "MISSING_IMPL",
|
|
17
17
|
DUPLICATE_EVENT_REGISTRATION: "DUPLICATE_EVENT_REGISTRATION",
|
|
18
18
|
NO_MANUEL_INSERT_OF_PERIODIC: "NO_MANUEL_INSERT_OF_PERIODIC",
|
|
19
|
+
LOAD_HIGHER_THAN_LIMIT: "LOAD_HIGHER_THAN_LIMIT",
|
|
19
20
|
};
|
|
20
21
|
|
|
21
22
|
const ERROR_CODES_META = {
|
|
@@ -59,6 +60,9 @@ const ERROR_CODES_META = {
|
|
|
59
60
|
[ERROR_CODES.NO_MANUEL_INSERT_OF_PERIODIC]: {
|
|
60
61
|
message: "Periodic events are managed by the framework and are not allowed to insert manually.",
|
|
61
62
|
},
|
|
63
|
+
[ERROR_CODES.LOAD_HIGHER_THAN_LIMIT]: {
|
|
64
|
+
message: "The defined load of an event is higher than the maximum defined limit. Check your configuration!",
|
|
65
|
+
},
|
|
62
66
|
};
|
|
63
67
|
|
|
64
68
|
class EventQueueError extends VError {
|
|
@@ -206,6 +210,16 @@ class EventQueueError extends VError {
|
|
|
206
210
|
message
|
|
207
211
|
);
|
|
208
212
|
}
|
|
213
|
+
static loadHigherThanLimit(load) {
|
|
214
|
+
const { message } = ERROR_CODES_META[ERROR_CODES.LOAD_HIGHER_THAN_LIMIT];
|
|
215
|
+
return new EventQueueError(
|
|
216
|
+
{
|
|
217
|
+
name: ERROR_CODES.LOAD_HIGHER_THAN_LIMIT,
|
|
218
|
+
info: { load },
|
|
219
|
+
},
|
|
220
|
+
message
|
|
221
|
+
);
|
|
222
|
+
}
|
|
209
223
|
}
|
|
210
224
|
|
|
211
225
|
module.exports = EventQueueError;
|
|
@@ -98,6 +98,20 @@ class EventQueueProcessorBase {
|
|
|
98
98
|
throw new Error(IMPLEMENT_ERROR_MESSAGE);
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
+
/**
|
|
102
|
+
* Process one periodic event
|
|
103
|
+
* @param processContext the context valid for the event processing. This context is associated with a valid transaction
|
|
104
|
+
* Access to the context is also possible with this.getContextForEventProcessing(key).
|
|
105
|
+
* The associated tx can be accessed with this.getTxForEventProcessing(key).
|
|
106
|
+
* @param {string} key cluster key generated during the clustering step. By default, this is ID of the event queue entry
|
|
107
|
+
* @param {Object} queueEntry this is the queueEntry which should be processed
|
|
108
|
+
* @returns {Promise<undefined>}
|
|
109
|
+
*/
|
|
110
|
+
// eslint-disable-next-line no-unused-vars
|
|
111
|
+
async processPeriodicEvent(processContext, key, queueEntry) {
|
|
112
|
+
throw new Error(IMPLEMENT_ERROR_MESSAGE);
|
|
113
|
+
}
|
|
114
|
+
|
|
101
115
|
startPerformanceTracerEvents() {
|
|
102
116
|
this.__performanceLoggerEvents = new PerformanceTracer(this.logger, "Processing events");
|
|
103
117
|
}
|
|
@@ -254,7 +268,8 @@ class EventQueueProcessorBase {
|
|
|
254
268
|
this.#determineAndAddEventStatusToMap(queueEntry.ID, EventProcessingStatus.Error, statusMap)
|
|
255
269
|
);
|
|
256
270
|
this.logger.error(
|
|
257
|
-
|
|
271
|
+
"The supplied status tuple doesn't have the required structure. Setting all entries to error.",
|
|
272
|
+
error,
|
|
258
273
|
{
|
|
259
274
|
eventType: this.#eventType,
|
|
260
275
|
eventSubType: this.#eventSubType,
|
|
@@ -295,7 +310,8 @@ class EventQueueProcessorBase {
|
|
|
295
310
|
handleErrorDuringProcessing(error, queueEntries) {
|
|
296
311
|
queueEntries = Array.isArray(queueEntries) ? queueEntries : [queueEntries];
|
|
297
312
|
this.logger.error(
|
|
298
|
-
|
|
313
|
+
"Caught error during event processing - setting queue entry to error. Please catch your promises/exceptions",
|
|
314
|
+
error,
|
|
299
315
|
{
|
|
300
316
|
eventType: this.#eventType,
|
|
301
317
|
eventSubType: this.#eventSubType,
|
|
@@ -309,14 +325,11 @@ class EventQueueProcessorBase {
|
|
|
309
325
|
}
|
|
310
326
|
|
|
311
327
|
handleErrorDuringPeriodicEventProcessing(error, queueEntry) {
|
|
312
|
-
this.logger.error(
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
queueEntryId: queueEntry.ID,
|
|
318
|
-
}
|
|
319
|
-
);
|
|
328
|
+
this.logger.error("Caught error during event periodic processing. Please catch your promises/exceptions.", error, {
|
|
329
|
+
eventType: this.#eventType,
|
|
330
|
+
eventSubType: this.#eventSubType,
|
|
331
|
+
queueEntryId: queueEntry.ID,
|
|
332
|
+
});
|
|
320
333
|
}
|
|
321
334
|
|
|
322
335
|
async setPeriodicEventStatus(queueEntryIds) {
|
|
@@ -491,7 +504,7 @@ class EventQueueProcessorBase {
|
|
|
491
504
|
}
|
|
492
505
|
|
|
493
506
|
handleErrorDuringClustering(error) {
|
|
494
|
-
this.logger.error(
|
|
507
|
+
this.logger.error("Error during clustering of events - setting all queue entries to error.", error, {
|
|
495
508
|
eventType: this.#eventType,
|
|
496
509
|
eventSubType: this.#eventSubType,
|
|
497
510
|
});
|
|
@@ -521,7 +534,7 @@ class EventQueueProcessorBase {
|
|
|
521
534
|
*/
|
|
522
535
|
async getQueueEntriesAndSetToInProgress() {
|
|
523
536
|
let result = [];
|
|
524
|
-
const refDateStartAfter = new Date(Date.now() + this.#config.runInterval);
|
|
537
|
+
const refDateStartAfter = new Date(Date.now() + this.#config.runInterval * 1.2);
|
|
525
538
|
await executeInNewTransaction(this.__baseContext, "eventQueue-getQueueEntriesAndSetToInProgress", async (tx) => {
|
|
526
539
|
const entries = await tx.run(
|
|
527
540
|
SELECT.from(this.#config.tableNameEventQueue)
|
|
@@ -573,7 +586,7 @@ class EventQueueProcessorBase {
|
|
|
573
586
|
this.#handleDelayedEvents(delayedEvents);
|
|
574
587
|
|
|
575
588
|
result = openEvents;
|
|
576
|
-
this.logger.info("Selected event queue entries for processing", {
|
|
589
|
+
this.logger[eventsForProcessing.length ? "info" : "debug"]("Selected event queue entries for processing", {
|
|
577
590
|
openEvents: openEvents.length,
|
|
578
591
|
...(delayedEvents.length && { delayedEvents: delayedEvents.length }),
|
|
579
592
|
...(exceededTries.length && { exceededTries: exceededTries.length }),
|
|
@@ -626,7 +639,7 @@ class EventQueueProcessorBase {
|
|
|
626
639
|
}
|
|
627
640
|
|
|
628
641
|
#clusterEvents(events, refDateStartAfter) {
|
|
629
|
-
const refDate = new Date(refDateStartAfter.getTime() - this.#config.runInterval + EVENT_START_AFTER_HEADROOM);
|
|
642
|
+
const refDate = new Date(refDateStartAfter.getTime() - this.#config.runInterval * 1.2 + EVENT_START_AFTER_HEADROOM);
|
|
630
643
|
return events.reduce(
|
|
631
644
|
(result, event) => {
|
|
632
645
|
if (event.attempts === this.__retryAttempts + TRIES_FOR_EXCEEDED_EVENTS) {
|
|
@@ -677,7 +690,8 @@ class EventQueueProcessorBase {
|
|
|
677
690
|
await this.#persistEventQueueStatusForExceeded(this.tx, [exceededEvent], EventProcessingStatus.Exceeded);
|
|
678
691
|
} catch (err) {
|
|
679
692
|
this.logger.error(
|
|
680
|
-
|
|
693
|
+
"Caught error during hook for exceeded events - setting queue entry to error. Please catch your promises/exceptions.",
|
|
694
|
+
err,
|
|
681
695
|
{
|
|
682
696
|
eventType: this.#eventType,
|
|
683
697
|
eventSubType: this.#eventSubType,
|
|
@@ -822,6 +836,10 @@ class EventQueueProcessorBase {
|
|
|
822
836
|
[this.#eventType, this.#eventSubType].join("##")
|
|
823
837
|
);
|
|
824
838
|
if (!lockAcquired) {
|
|
839
|
+
this.logger.debug("no lock available, exit processing", {
|
|
840
|
+
type: this.#eventType,
|
|
841
|
+
subType: this.#eventSubType,
|
|
842
|
+
});
|
|
825
843
|
return false;
|
|
826
844
|
}
|
|
827
845
|
this.__lockAcquired = true;
|
|
@@ -835,16 +853,16 @@ class EventQueueProcessorBase {
|
|
|
835
853
|
try {
|
|
836
854
|
await distributedLock.releaseLock(this.context, [this.#eventType, this.#eventSubType].join("##"));
|
|
837
855
|
} catch (err) {
|
|
838
|
-
this.logger.error("Releasing distributed lock failed.
|
|
856
|
+
this.logger.error("Releasing distributed lock failed.", err);
|
|
839
857
|
}
|
|
840
858
|
}
|
|
841
859
|
|
|
842
860
|
async scheduleNextPeriodEvent(queueEntry) {
|
|
843
|
-
const
|
|
861
|
+
const intervalInSec = this.#eventConfig.interval * 1000;
|
|
844
862
|
const newEvent = {
|
|
845
863
|
type: this.#eventType,
|
|
846
864
|
subType: this.#eventSubType,
|
|
847
|
-
startAfter: new Date(new Date(queueEntry.startAfter).getTime() +
|
|
865
|
+
startAfter: new Date(new Date(queueEntry.startAfter).getTime() + intervalInSec),
|
|
848
866
|
};
|
|
849
867
|
const { relative } = this.#eventSchedulerInstance.calculateOffset(
|
|
850
868
|
this.#eventType,
|
|
@@ -853,7 +871,7 @@ class EventQueueProcessorBase {
|
|
|
853
871
|
);
|
|
854
872
|
|
|
855
873
|
// more than one interval behind - shift tick to keep up
|
|
856
|
-
if (relative < 0 && Math.abs(relative) >=
|
|
874
|
+
if (relative < 0 && Math.abs(relative) >= intervalInSec) {
|
|
857
875
|
newEvent.startAfter = new Date(Date.now() + 5 * 1000);
|
|
858
876
|
this.logger.info("interval adjusted because shifted more than one interval", {
|
|
859
877
|
eventType: this.#eventType,
|
|
@@ -865,7 +883,7 @@ class EventQueueProcessorBase {
|
|
|
865
883
|
this.tx._skipEventQueueBroadcase = true;
|
|
866
884
|
await this.tx.run(INSERT.into(this.#config.tableNameEventQueue).entries({ ...newEvent }));
|
|
867
885
|
this.tx._skipEventQueueBroadcase = false;
|
|
868
|
-
if (
|
|
886
|
+
if (intervalInSec < this.#config.runInterval * 1.5) {
|
|
869
887
|
this.#handleDelayedEvents([newEvent]);
|
|
870
888
|
const { relative: relativeAfterSchedule } = this.#eventSchedulerInstance.calculateOffset(
|
|
871
889
|
this.#eventType,
|
|
@@ -923,6 +941,10 @@ class EventQueueProcessorBase {
|
|
|
923
941
|
return this.__logger ?? this.__baseLogger;
|
|
924
942
|
}
|
|
925
943
|
|
|
944
|
+
set logger(value) {
|
|
945
|
+
this.__logger = value;
|
|
946
|
+
}
|
|
947
|
+
|
|
926
948
|
get queueEntriesWithPayloadMap() {
|
|
927
949
|
return this.#queueEntriesWithPayloadMap;
|
|
928
950
|
}
|
package/src/config.js
CHANGED
|
@@ -11,6 +11,7 @@ const GLOBAL_TX_TIMEOUT = 30 * 60 * 1000;
|
|
|
11
11
|
const REDIS_CONFIG_CHANNEL = "EVENT_QUEUE_CONFIG_CHANNEL";
|
|
12
12
|
const COMPONENT_NAME = "eventQueue/config";
|
|
13
13
|
const MIN_INTERVAL_SEC = 10;
|
|
14
|
+
const DEFAULT_LOAD = 1;
|
|
14
15
|
|
|
15
16
|
class Config {
|
|
16
17
|
#logger;
|
|
@@ -20,13 +21,14 @@ class Config {
|
|
|
20
21
|
#runInterval;
|
|
21
22
|
#redisEnabled;
|
|
22
23
|
#initialized;
|
|
23
|
-
#
|
|
24
|
+
#instanceLoadLimit;
|
|
24
25
|
#tableNameEventQueue;
|
|
25
26
|
#tableNameEventLock;
|
|
26
27
|
#isRunnerDeactivated;
|
|
27
28
|
#configFilePath;
|
|
28
29
|
#processEventsAfterPublish;
|
|
29
30
|
#skipCsnCheck;
|
|
31
|
+
#registerAsEventProcessor;
|
|
30
32
|
#disableRedis;
|
|
31
33
|
#env;
|
|
32
34
|
#eventMap;
|
|
@@ -40,7 +42,7 @@ class Config {
|
|
|
40
42
|
this.#runInterval = null;
|
|
41
43
|
this.#redisEnabled = null;
|
|
42
44
|
this.#initialized = false;
|
|
43
|
-
this.#
|
|
45
|
+
this.#instanceLoadLimit = null;
|
|
44
46
|
this.#tableNameEventQueue = null;
|
|
45
47
|
this.#tableNameEventLock = null;
|
|
46
48
|
this.#isRunnerDeactivated = false;
|
|
@@ -106,11 +108,13 @@ class Config {
|
|
|
106
108
|
config.events = config.events ?? [];
|
|
107
109
|
config.periodicEvents = config.periodicEvents ?? [];
|
|
108
110
|
this.#eventMap = config.events.reduce((result, event) => {
|
|
111
|
+
event.load = event.load ?? DEFAULT_LOAD;
|
|
109
112
|
this.validateAdHocEvents(result, event);
|
|
110
113
|
result[[event.type, event.subType].join("##")] = event;
|
|
111
114
|
return result;
|
|
112
115
|
}, {});
|
|
113
116
|
this.#eventMap = config.periodicEvents.reduce((result, event) => {
|
|
117
|
+
event.load = event.load ?? DEFAULT_LOAD;
|
|
114
118
|
const SUFFIX_PERIODIC = "_PERIODIC";
|
|
115
119
|
event.type = `${event.type}${SUFFIX_PERIODIC}`;
|
|
116
120
|
event.isPeriodic = true;
|
|
@@ -210,12 +214,12 @@ class Config {
|
|
|
210
214
|
this.#initialized = value;
|
|
211
215
|
}
|
|
212
216
|
|
|
213
|
-
get
|
|
214
|
-
return this.#
|
|
217
|
+
get instanceLoadLimit() {
|
|
218
|
+
return this.#instanceLoadLimit;
|
|
215
219
|
}
|
|
216
220
|
|
|
217
|
-
set
|
|
218
|
-
this.#
|
|
221
|
+
set instanceLoadLimit(value) {
|
|
222
|
+
this.#instanceLoadLimit = value;
|
|
219
223
|
}
|
|
220
224
|
|
|
221
225
|
get tableNameEventQueue() {
|
|
@@ -274,6 +278,14 @@ class Config {
|
|
|
274
278
|
return this.#updatePeriodicEvents;
|
|
275
279
|
}
|
|
276
280
|
|
|
281
|
+
set registerAsEventProcessor(value) {
|
|
282
|
+
this.#registerAsEventProcessor = value;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
get registerAsEventProcessor() {
|
|
286
|
+
return this.#registerAsEventProcessor;
|
|
287
|
+
}
|
|
288
|
+
|
|
277
289
|
get isMultiTenancy() {
|
|
278
290
|
return !!cds.requires.multitenancy;
|
|
279
291
|
}
|
package/src/initialize.js
CHANGED
|
@@ -29,7 +29,7 @@ const CONFIG_VARS = [
|
|
|
29
29
|
["processEventsAfterPublish", true],
|
|
30
30
|
["isRunnerDeactivated", false],
|
|
31
31
|
["runInterval", 5 * 60 * 1000],
|
|
32
|
-
["
|
|
32
|
+
["instanceLoadLimit", 20],
|
|
33
33
|
["tableNameEventQueue", BASE_TABLES.EVENT],
|
|
34
34
|
["tableNameEventLock", BASE_TABLES.LOCK],
|
|
35
35
|
["disableRedis", false],
|
|
@@ -43,7 +43,7 @@ const initialize = async ({
|
|
|
43
43
|
processEventsAfterPublish,
|
|
44
44
|
isRunnerDeactivated,
|
|
45
45
|
runInterval,
|
|
46
|
-
|
|
46
|
+
instanceLoadLimit,
|
|
47
47
|
tableNameEventQueue,
|
|
48
48
|
tableNameEventLock,
|
|
49
49
|
disableRedis,
|
|
@@ -52,7 +52,7 @@ const initialize = async ({
|
|
|
52
52
|
} = {}) => {
|
|
53
53
|
// TODO: initialize check:
|
|
54
54
|
// - content of yaml check
|
|
55
|
-
// - betweenRuns and
|
|
55
|
+
// - betweenRuns and instanceLoadLimit
|
|
56
56
|
|
|
57
57
|
if (config.initialized) {
|
|
58
58
|
return;
|
|
@@ -65,7 +65,7 @@ const initialize = async ({
|
|
|
65
65
|
processEventsAfterPublish,
|
|
66
66
|
isRunnerDeactivated,
|
|
67
67
|
runInterval,
|
|
68
|
-
|
|
68
|
+
instanceLoadLimit,
|
|
69
69
|
tableNameEventQueue,
|
|
70
70
|
tableNameEventLock,
|
|
71
71
|
disableRedis,
|
|
@@ -88,10 +88,11 @@ const initialize = async ({
|
|
|
88
88
|
registerCdsShutdown();
|
|
89
89
|
logger.info("event queue initialized", {
|
|
90
90
|
registerAsEventProcessor: config.registerAsEventProcessor,
|
|
91
|
+
processEventsAfterPublish: config.processEventsAfterPublish,
|
|
91
92
|
multiTenancyEnabled: config.isMultiTenancy,
|
|
92
93
|
redisEnabled: config.redisEnabled,
|
|
93
94
|
runInterval: config.runInterval,
|
|
94
|
-
config: config.
|
|
95
|
+
config: config.instanceLoadLimit,
|
|
95
96
|
});
|
|
96
97
|
};
|
|
97
98
|
|
package/src/periodicEvents.js
CHANGED
|
@@ -7,6 +7,7 @@ const { processChunkedSync } = require("./shared/common");
|
|
|
7
7
|
const eventConfig = require("./config");
|
|
8
8
|
|
|
9
9
|
const COMPONENT_NAME = "eventQueue/periodicEvents";
|
|
10
|
+
const CHUNK_SIZE_INSERT_PERIODIC_EVENTS = 4;
|
|
10
11
|
|
|
11
12
|
const checkAndInsertPeriodicEvents = async (context) => {
|
|
12
13
|
const tx = cds.tx(context);
|
|
@@ -63,12 +64,15 @@ const checkAndInsertPeriodicEvents = async (context) => {
|
|
|
63
64
|
cds.log(COMPONENT_NAME).info("deleting periodic events because they have changed", {
|
|
64
65
|
changedEvents: exitingWithNotMatchingInterval.map(({ type, subType }) => ({ type, subType })),
|
|
65
66
|
});
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
67
|
+
|
|
68
|
+
if (exitingWithNotMatchingInterval.length) {
|
|
69
|
+
await tx.run(
|
|
70
|
+
DELETE.from(eventConfig.tableNameEventQueue).where(
|
|
71
|
+
"ID IN",
|
|
72
|
+
exitingWithNotMatchingInterval.map(({ ID }) => ID)
|
|
73
|
+
)
|
|
74
|
+
);
|
|
75
|
+
}
|
|
72
76
|
|
|
73
77
|
const newOrChangedEvents = newEvents.concat(exitingWithNotMatchingInterval);
|
|
74
78
|
|
|
@@ -81,13 +85,17 @@ const checkAndInsertPeriodicEvents = async (context) => {
|
|
|
81
85
|
|
|
82
86
|
const insertPeriodEvents = async (tx, events) => {
|
|
83
87
|
const startAfter = new Date();
|
|
84
|
-
|
|
85
|
-
|
|
88
|
+
let counter = 1;
|
|
89
|
+
const chunks = Math.ceil(events.length / CHUNK_SIZE_INSERT_PERIODIC_EVENTS);
|
|
90
|
+
const logger = cds.log(COMPONENT_NAME);
|
|
91
|
+
processChunkedSync(events, CHUNK_SIZE_INSERT_PERIODIC_EVENTS, (chunk) => {
|
|
92
|
+
logger.info(`${counter}/${chunks} | inserting chunk of changed or new periodic events`, {
|
|
86
93
|
events: chunk.map(({ type, subType }) => {
|
|
87
94
|
const { interval } = eventConfig.getEventConfig(type, subType);
|
|
88
95
|
return { type, subType, interval };
|
|
89
96
|
}),
|
|
90
97
|
});
|
|
98
|
+
counter++;
|
|
91
99
|
});
|
|
92
100
|
const periodEventsInsert = events.map((periodicEvent) => ({
|
|
93
101
|
type: periodicEvent.type,
|
package/src/processEventQueue.js
CHANGED
|
@@ -6,23 +6,13 @@ const cds = require("@sap/cds");
|
|
|
6
6
|
|
|
7
7
|
const config = require("./config");
|
|
8
8
|
const { TransactionMode } = require("./constants");
|
|
9
|
-
const { limiter
|
|
9
|
+
const { limiter } = require("./shared/common");
|
|
10
10
|
|
|
11
11
|
const { executeInNewTransaction, TriggerRollback } = require("./shared/cdsHelper");
|
|
12
12
|
|
|
13
13
|
const COMPONENT_NAME = "eventQueue/processEventQueue";
|
|
14
14
|
const MAX_EXECUTION_TIME = 5 * 60 * 1000;
|
|
15
15
|
|
|
16
|
-
const eventQueueRunner = async (context, events) => {
|
|
17
|
-
const startTime = new Date();
|
|
18
|
-
const funnel = new Funnel();
|
|
19
|
-
await Promise.allSettled(
|
|
20
|
-
events.map((event) =>
|
|
21
|
-
funnel.run(event.load, async () => processEventQueue(context, event.type, event.subType, startTime))
|
|
22
|
-
)
|
|
23
|
-
);
|
|
24
|
-
};
|
|
25
|
-
|
|
26
16
|
const processEventQueue = async (context, eventType, eventSubType, startTime = new Date()) => {
|
|
27
17
|
let iterationCounter = 0;
|
|
28
18
|
let shouldContinue = true;
|
|
@@ -111,7 +101,7 @@ const processEventQueue = async (context, eventType, eventSubType, startTime = n
|
|
|
111
101
|
}
|
|
112
102
|
}
|
|
113
103
|
} catch (err) {
|
|
114
|
-
cds.log(COMPONENT_NAME).error("Processing event queue failed with unexpected error.
|
|
104
|
+
cds.log(COMPONENT_NAME).error("Processing event queue failed with unexpected error.", err, {
|
|
115
105
|
eventType,
|
|
116
106
|
eventSubType,
|
|
117
107
|
});
|
|
@@ -170,7 +160,7 @@ const processPeriodicEvent = async (eventTypeInstance) => {
|
|
|
170
160
|
eventTypeInstance.processEventContext = tx.context;
|
|
171
161
|
eventTypeInstance.setTxForEventProcessing(queueEntry.ID, cds.tx(tx.context));
|
|
172
162
|
try {
|
|
173
|
-
await eventTypeInstance.
|
|
163
|
+
await eventTypeInstance.processPeriodicEvent(tx.context, queueEntry.ID, queueEntry);
|
|
174
164
|
} catch (err) {
|
|
175
165
|
eventTypeInstance.handleErrorDuringPeriodicEventProcessing(err, queueEntry);
|
|
176
166
|
throw new TriggerRollback();
|
|
@@ -194,7 +184,7 @@ const processPeriodicEvent = async (eventTypeInstance) => {
|
|
|
194
184
|
);
|
|
195
185
|
}
|
|
196
186
|
} catch (err) {
|
|
197
|
-
cds.log(COMPONENT_NAME).error("Processing periodic events failed with unexpected error.
|
|
187
|
+
cds.log(COMPONENT_NAME).error("Processing periodic events failed with unexpected error.", err, {
|
|
198
188
|
eventType: eventTypeInstance?.eventType,
|
|
199
189
|
eventSubType: eventTypeInstance?.eventSubType,
|
|
200
190
|
});
|
|
@@ -278,5 +268,4 @@ const resilientRequire = (path) => {
|
|
|
278
268
|
|
|
279
269
|
module.exports = {
|
|
280
270
|
processEventQueue,
|
|
281
|
-
eventQueueRunner,
|
|
282
271
|
};
|
package/src/redisPubSub.js
CHANGED
|
@@ -48,7 +48,10 @@ const broadcastEvent = async (tenantId, type, subType) => {
|
|
|
48
48
|
[type, subType].join("##")
|
|
49
49
|
);
|
|
50
50
|
if (result) {
|
|
51
|
-
logger.info("skip publish redis event as no lock is available"
|
|
51
|
+
logger.info("skip publish redis event as no lock is available", {
|
|
52
|
+
type,
|
|
53
|
+
subType,
|
|
54
|
+
});
|
|
52
55
|
return;
|
|
53
56
|
}
|
|
54
57
|
logger.debug("publishing redis event", {
|
|
@@ -58,7 +61,7 @@ const broadcastEvent = async (tenantId, type, subType) => {
|
|
|
58
61
|
});
|
|
59
62
|
await redis.publishMessage(EVENT_MESSAGE_CHANNEL, JSON.stringify({ tenantId, type, subType }));
|
|
60
63
|
} catch (err) {
|
|
61
|
-
logger.error(
|
|
64
|
+
logger.error("publish event failed!", err, {
|
|
62
65
|
tenantId,
|
|
63
66
|
type,
|
|
64
67
|
subType,
|
package/src/runner.js
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
const { randomUUID } = require("crypto");
|
|
4
4
|
|
|
5
5
|
const eventQueueConfig = require("./config");
|
|
6
|
-
const {
|
|
7
|
-
const
|
|
6
|
+
const { processEventQueue } = require("./processEventQueue");
|
|
7
|
+
const WorkerQueue = require("./shared/WorkerQueue");
|
|
8
8
|
const cdsHelper = require("./shared/cdsHelper");
|
|
9
9
|
const distributedLock = require("./shared/distributedLock");
|
|
10
10
|
const SetIntervalDriftSafe = require("./shared/SetIntervalDriftSafe");
|
|
@@ -21,7 +21,7 @@ const OFFSET_FIRST_RUN = 10 * 1000;
|
|
|
21
21
|
let tenantIdHash;
|
|
22
22
|
let singleRunDone;
|
|
23
23
|
|
|
24
|
-
const singleTenant = () => _scheduleFunction(_checkPeriodicEventsSingleTenant,
|
|
24
|
+
const singleTenant = () => _scheduleFunction(_checkPeriodicEventsSingleTenant, _singleTenantDb);
|
|
25
25
|
|
|
26
26
|
const multiTenancyDb = () => _scheduleFunction(_multiTenancyPeriodicEvents, _multiTenancyDb);
|
|
27
27
|
|
|
@@ -75,7 +75,7 @@ const _multiTenancyRedis = async () => {
|
|
|
75
75
|
return;
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
|
|
78
|
+
return _executeEventsAllTenants(tenantIds, runId);
|
|
79
79
|
};
|
|
80
80
|
|
|
81
81
|
const _checkAndTriggerPeriodicEventUpdate = (tenantIds) => {
|
|
@@ -87,15 +87,43 @@ const _checkAndTriggerPeriodicEventUpdate = (tenantIds) => {
|
|
|
87
87
|
if (tenantIdHash && tenantIdHash !== hash) {
|
|
88
88
|
cds.log(COMPONENT_NAME).info("tenant id hash changed, triggering updating periodic events!");
|
|
89
89
|
_multiTenancyPeriodicEvents().catch((err) => {
|
|
90
|
-
cds.log(COMPONENT_NAME).error("Error during triggering updating periodic events!
|
|
90
|
+
cds.log(COMPONENT_NAME).error("Error during triggering updating periodic events!", err);
|
|
91
91
|
});
|
|
92
92
|
}
|
|
93
93
|
};
|
|
94
94
|
|
|
95
|
-
const
|
|
96
|
-
const
|
|
95
|
+
const _executeEventsAllTenants = (tenantIds, runId) => {
|
|
96
|
+
const events = eventQueueConfig.allEvents;
|
|
97
|
+
const promises = [];
|
|
97
98
|
tenantIds.forEach((tenantId) => {
|
|
98
|
-
|
|
99
|
+
events.forEach((event) => {
|
|
100
|
+
promises.push(
|
|
101
|
+
WorkerQueue.instance.addToQueue(event.load, async () => {
|
|
102
|
+
try {
|
|
103
|
+
const lockId = `${runId}_${event.type}_${event.subType}`;
|
|
104
|
+
const tenantContext = new cds.EventContext({ tenant: tenantId });
|
|
105
|
+
const couldAcquireLock = await distributedLock.acquireLock(tenantContext, lockId, {
|
|
106
|
+
expiryTime: eventQueueConfig.runInterval * 0.95,
|
|
107
|
+
});
|
|
108
|
+
if (!couldAcquireLock) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
await runEventCombinationForTenant(tenantId, event.type, event.subType, true);
|
|
112
|
+
} catch (err) {
|
|
113
|
+
cds.log(COMPONENT_NAME).error("executing event-queue run for tenant failed", {
|
|
114
|
+
tenantId,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
})
|
|
118
|
+
);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
return promises;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const _executePeriodicEventsAllTenants = (tenantIds, runId) => {
|
|
125
|
+
tenantIds.forEach((tenantId) => {
|
|
126
|
+
WorkerQueue.instance.addToQueue(1, async () => {
|
|
99
127
|
try {
|
|
100
128
|
const tenantContext = new cds.EventContext({ tenant: tenantId });
|
|
101
129
|
const couldAcquireLock = await distributedLock.acquireLock(tenantContext, runId, {
|
|
@@ -104,7 +132,7 @@ const _executeAllTenantsGeneric = (tenantIds, runId, fn) => {
|
|
|
104
132
|
if (!couldAcquireLock) {
|
|
105
133
|
return;
|
|
106
134
|
}
|
|
107
|
-
await
|
|
135
|
+
await _checkPeriodicEventsSingleTenant(tenantId);
|
|
108
136
|
} catch (err) {
|
|
109
137
|
cds.log(COMPONENT_NAME).error("executing event-queue run for tenant failed", {
|
|
110
138
|
tenantId,
|
|
@@ -114,34 +142,20 @@ const _executeAllTenantsGeneric = (tenantIds, runId, fn) => {
|
|
|
114
142
|
});
|
|
115
143
|
};
|
|
116
144
|
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
// NOTE: we need this because of logging otherwise logs would not contain the subdomain
|
|
130
|
-
http: { req: { authInfo: { getSubdomain: () => subdomain } } },
|
|
131
|
-
});
|
|
132
|
-
cds.context = context;
|
|
133
|
-
logger.info("executing eventQueue run", {
|
|
134
|
-
tenantId,
|
|
135
|
-
subdomain,
|
|
136
|
-
...(runId ? { runId } : null),
|
|
137
|
-
});
|
|
138
|
-
await eventQueueRunner(context, eventsForAutomaticRun);
|
|
139
|
-
} catch (err) {
|
|
140
|
-
logger.error(`Couldn't process eventQueue for tenant! Next try after defined interval. Error: ${err}`, {
|
|
141
|
-
tenantId,
|
|
142
|
-
redisEnabled: eventQueueConfig.redisEnabled,
|
|
145
|
+
const _singleTenantDb = async (tenantId) => {
|
|
146
|
+
const events = eventQueueConfig.allEvents;
|
|
147
|
+
events.forEach((event) => {
|
|
148
|
+
WorkerQueue.instance.addToQueue(event.load, async () => {
|
|
149
|
+
try {
|
|
150
|
+
await runEventCombinationForTenant(tenantId, event.type, event.subType, true);
|
|
151
|
+
} catch (err) {
|
|
152
|
+
cds.log(COMPONENT_NAME).error("executing event-queue run for tenant failed", {
|
|
153
|
+
tenantId,
|
|
154
|
+
redisEnabled: eventQueueConfig.redisEnabled,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
143
157
|
});
|
|
144
|
-
}
|
|
158
|
+
});
|
|
145
159
|
};
|
|
146
160
|
|
|
147
161
|
const _acquireRunId = async (context) => {
|
|
@@ -196,15 +210,12 @@ const _calculateOffsetForFirstRun = async () => {
|
|
|
196
210
|
} catch (err) {
|
|
197
211
|
cds
|
|
198
212
|
.log(COMPONENT_NAME)
|
|
199
|
-
.error(
|
|
200
|
-
"calculating offset for first run failed, falling back to default. Runs might be out-of-sync. Error:",
|
|
201
|
-
err
|
|
202
|
-
);
|
|
213
|
+
.error("calculating offset for first run failed, falling back to default. Runs might be out-of-sync.", err);
|
|
203
214
|
}
|
|
204
215
|
return offsetDependingOnLastRun;
|
|
205
216
|
};
|
|
206
217
|
|
|
207
|
-
const runEventCombinationForTenant = async (tenantId, type, subType) => {
|
|
218
|
+
const runEventCombinationForTenant = async (tenantId, type, subType, skipWorkerPool) => {
|
|
208
219
|
try {
|
|
209
220
|
const subdomain = await getSubdomainForTenantId(tenantId);
|
|
210
221
|
const context = new cds.EventContext({
|
|
@@ -213,7 +224,15 @@ const runEventCombinationForTenant = async (tenantId, type, subType) => {
|
|
|
213
224
|
http: { req: { authInfo: { getSubdomain: () => subdomain } } },
|
|
214
225
|
});
|
|
215
226
|
cds.context = context;
|
|
216
|
-
|
|
227
|
+
if (skipWorkerPool) {
|
|
228
|
+
return await processEventQueue(context, type, subType);
|
|
229
|
+
} else {
|
|
230
|
+
const config = eventQueueConfig.getEventConfig(type, subType);
|
|
231
|
+
return await WorkerQueue.instance.addToQueue(
|
|
232
|
+
config.load,
|
|
233
|
+
async () => await processEventQueue(context, type, subType)
|
|
234
|
+
);
|
|
235
|
+
}
|
|
217
236
|
} catch (err) {
|
|
218
237
|
const logger = cds.log(COMPONENT_NAME);
|
|
219
238
|
logger.error("error executing event combination for tenant", err, {
|
|
@@ -230,11 +249,9 @@ const _multiTenancyDb = async () => {
|
|
|
230
249
|
logger.info("executing event queue run for single instance and multi tenant");
|
|
231
250
|
const tenantIds = await cdsHelper.getAllTenantIds();
|
|
232
251
|
_checkAndTriggerPeriodicEventUpdate(tenantIds);
|
|
233
|
-
|
|
252
|
+
return _executeEventsAllTenants(tenantIds, EVENT_QUEUE_RUN_ID);
|
|
234
253
|
} catch (err) {
|
|
235
|
-
logger.error(
|
|
236
|
-
`Couldn't fetch tenant ids for event queue processing! Next try after defined interval. Error: ${err}`
|
|
237
|
-
);
|
|
254
|
+
logger.error("Couldn't fetch tenant ids for event queue processing! Next try after defined interval.", err);
|
|
238
255
|
}
|
|
239
256
|
};
|
|
240
257
|
|
|
@@ -245,14 +262,18 @@ const _multiTenancyPeriodicEvents = async () => {
|
|
|
245
262
|
const tenantIds = await cdsHelper.getAllTenantIds();
|
|
246
263
|
_executePeriodicEventsAllTenants(tenantIds, EVENT_QUEUE_RUN_PERIODIC_EVENT);
|
|
247
264
|
} catch (err) {
|
|
248
|
-
logger.error(
|
|
265
|
+
logger.error("Couldn't fetch tenant ids for updating periodic event processing!", err);
|
|
249
266
|
}
|
|
250
267
|
};
|
|
251
268
|
|
|
252
269
|
const _checkPeriodicEventsSingleTenant = async (tenantId) => {
|
|
253
270
|
const logger = cds.log(COMPONENT_NAME);
|
|
254
|
-
if (!eventQueueConfig.updatePeriodicEvents) {
|
|
255
|
-
logger.info("updating of periodic events is disabled"
|
|
271
|
+
if (!eventQueueConfig.updatePeriodicEvents || !eventQueueConfig.periodicEvents.length) {
|
|
272
|
+
logger.info("updating of periodic events is disabled or no periodic events configured", {
|
|
273
|
+
updateEnabled: eventQueueConfig.updatePeriodicEvents,
|
|
274
|
+
events: eventQueueConfig.periodicEvents.length,
|
|
275
|
+
});
|
|
276
|
+
return;
|
|
256
277
|
}
|
|
257
278
|
try {
|
|
258
279
|
const subdomain = await cdsHelper.getSubdomainForTenantId(tenantId);
|
|
@@ -270,7 +291,7 @@ const _checkPeriodicEventsSingleTenant = async (tenantId) => {
|
|
|
270
291
|
await periodicEvents.checkAndInsertPeriodicEvents(tx.context);
|
|
271
292
|
});
|
|
272
293
|
} catch (err) {
|
|
273
|
-
logger.error(
|
|
294
|
+
logger.error("Couldn't update periodic events for tenant! Next try after defined interval.", err, {
|
|
274
295
|
tenantId,
|
|
275
296
|
redisEnabled: eventQueueConfig.redisEnabled,
|
|
276
297
|
});
|
|
@@ -3,64 +3,106 @@
|
|
|
3
3
|
const cds = require("@sap/cds");
|
|
4
4
|
|
|
5
5
|
const config = require("../config");
|
|
6
|
+
const EventQueueError = require("../EventQueueError");
|
|
6
7
|
|
|
7
8
|
const COMPONENT_NAME = "eventQueue/WorkerQueue";
|
|
8
|
-
|
|
9
|
-
|
|
9
|
+
const NANO_TO_MS = 1e6;
|
|
10
|
+
const THRESHOLD = {
|
|
11
|
+
INFO: 5 * 1000,
|
|
12
|
+
WARN: 10 * 1000,
|
|
13
|
+
ERROR: 15 * 1000,
|
|
14
|
+
};
|
|
10
15
|
|
|
11
16
|
class WorkerQueue {
|
|
17
|
+
#concurrencyLimit;
|
|
18
|
+
#runningPromises;
|
|
19
|
+
#runningLoad;
|
|
20
|
+
#queue;
|
|
21
|
+
static #instance;
|
|
22
|
+
|
|
12
23
|
constructor(concurrency) {
|
|
13
24
|
if (Number.isNaN(concurrency) || concurrency <= 0) {
|
|
14
|
-
this
|
|
25
|
+
this.#concurrencyLimit = 1;
|
|
15
26
|
} else {
|
|
16
|
-
this
|
|
27
|
+
this.#concurrencyLimit = concurrency;
|
|
17
28
|
}
|
|
18
|
-
this
|
|
19
|
-
this
|
|
29
|
+
this.#runningPromises = [];
|
|
30
|
+
this.#runningLoad = 0;
|
|
31
|
+
this.#queue = [];
|
|
20
32
|
}
|
|
21
33
|
|
|
22
|
-
addToQueue(cb) {
|
|
34
|
+
addToQueue(load, cb) {
|
|
35
|
+
if (load > this.#concurrencyLimit) {
|
|
36
|
+
throw EventQueueError.loadHigherThanLimit(load);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const startTime = process.hrtime.bigint();
|
|
23
40
|
const p = new Promise((resolve, reject) => {
|
|
24
|
-
this.
|
|
41
|
+
this.#queue.push([load, cb, resolve, reject, startTime]);
|
|
25
42
|
});
|
|
26
43
|
this._checkForNext();
|
|
27
44
|
return p;
|
|
28
45
|
}
|
|
29
46
|
|
|
30
|
-
_executeFunction(cb, resolve, reject) {
|
|
47
|
+
_executeFunction(load, cb, resolve, reject, startTime) {
|
|
48
|
+
this.checkAndLogWaitingTime(startTime);
|
|
31
49
|
const promise = Promise.resolve().then(() => cb());
|
|
32
|
-
this.
|
|
50
|
+
this.#runningPromises.push(promise);
|
|
51
|
+
this.#runningLoad = this.#runningLoad + load;
|
|
33
52
|
promise
|
|
34
53
|
.finally(() => {
|
|
35
|
-
this
|
|
54
|
+
this.#runningLoad = this.#runningLoad - load;
|
|
55
|
+
this.#runningPromises.splice(this.#runningPromises.indexOf(promise), 1);
|
|
36
56
|
this._checkForNext();
|
|
37
57
|
})
|
|
38
58
|
.then((...results) => {
|
|
39
59
|
resolve(...results);
|
|
40
60
|
})
|
|
41
61
|
.catch((err) => {
|
|
42
|
-
cds.log(COMPONENT_NAME).error("Error happened in WorkQueue. Errors should be caught before!
|
|
62
|
+
cds.log(COMPONENT_NAME).error("Error happened in WorkQueue. Errors should be caught before!", err);
|
|
43
63
|
reject(err);
|
|
44
64
|
});
|
|
45
65
|
}
|
|
46
66
|
|
|
47
67
|
_checkForNext() {
|
|
48
|
-
|
|
68
|
+
const load = this.#queue[0]?.[0];
|
|
69
|
+
if (!this.#queue.length || this.#runningLoad + load > this.#concurrencyLimit) {
|
|
49
70
|
return;
|
|
50
71
|
}
|
|
51
|
-
const
|
|
52
|
-
this._executeFunction(
|
|
72
|
+
const args = this.#queue.shift();
|
|
73
|
+
this._executeFunction(...args);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
get runningPromises() {
|
|
77
|
+
return this.#runningPromises;
|
|
53
78
|
}
|
|
54
|
-
}
|
|
55
79
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
80
|
+
/**
|
|
81
|
+
@return { WorkerQueue }
|
|
82
|
+
**/
|
|
83
|
+
static get instance() {
|
|
84
|
+
if (!WorkerQueue.#instance) {
|
|
85
|
+
WorkerQueue.#instance = new WorkerQueue(config.instanceLoadLimit);
|
|
60
86
|
}
|
|
61
|
-
return instance;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
87
|
+
return WorkerQueue.#instance;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
checkAndLogWaitingTime(startTime) {
|
|
91
|
+
const diffMs = Math.round(Number(process.hrtime.bigint() - startTime) / NANO_TO_MS);
|
|
92
|
+
let logLevel;
|
|
93
|
+
if (diffMs >= THRESHOLD.ERROR) {
|
|
94
|
+
logLevel = "error";
|
|
95
|
+
} else if (diffMs >= THRESHOLD.WARN) {
|
|
96
|
+
logLevel = "warn";
|
|
97
|
+
} else if (diffMs >= THRESHOLD.INFO) {
|
|
98
|
+
logLevel = "info";
|
|
99
|
+
} else {
|
|
100
|
+
logLevel = "debug";
|
|
101
|
+
}
|
|
102
|
+
cds.log(COMPONENT_NAME)[logLevel]("Waiting time in worker queue", {
|
|
103
|
+
diffMs,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
module.exports = WorkerQueue;
|
package/src/shared/common.js
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
const crypto = require("crypto");
|
|
4
|
-
|
|
5
|
-
const { floor, abs, min } = Math;
|
|
6
|
-
|
|
7
4
|
const arrayToFlatMap = (array, key = "ID") => {
|
|
8
5
|
return array.reduce((result, element) => {
|
|
9
6
|
result[element[key]] = element;
|
|
@@ -11,74 +8,6 @@ const arrayToFlatMap = (array, key = "ID") => {
|
|
|
11
8
|
}, {});
|
|
12
9
|
};
|
|
13
10
|
|
|
14
|
-
/**
|
|
15
|
-
* Establish a "Funnel" instance to limit how much
|
|
16
|
-
* load can be processed in parallel. This is somewhat
|
|
17
|
-
* similar to the limiter function however it has some
|
|
18
|
-
* distinctintly different features. The Funnel will
|
|
19
|
-
* not know in advance which functions and how many
|
|
20
|
-
* loads it will have to process.
|
|
21
|
-
*/
|
|
22
|
-
class Funnel {
|
|
23
|
-
/**
|
|
24
|
-
* Create a funnel with specified capacity
|
|
25
|
-
* @param capacity - the capacity of the funnel (integer, sign will be ignored)
|
|
26
|
-
*/
|
|
27
|
-
constructor(capacity = 100) {
|
|
28
|
-
this.runningPromises = [];
|
|
29
|
-
this.capacity = floor(abs(capacity));
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Asynchronously run a function that will put a specified load to the funnel.
|
|
34
|
-
* The total amount of load of all running functions shall not
|
|
35
|
-
* exceed the capacity of the funnel. If the desired load exceeds the capacity
|
|
36
|
-
* the funnel will wait until sufficient capacity is available.
|
|
37
|
-
* If a function requires a load >= capacity, then it will run
|
|
38
|
-
* exclusively.
|
|
39
|
-
* @param load - the load (integer, sign will be ignored)
|
|
40
|
-
* @param f
|
|
41
|
-
* @param args
|
|
42
|
-
* @return {Promise<unknown>}
|
|
43
|
-
*/
|
|
44
|
-
async run(load, f, ...args) {
|
|
45
|
-
load = min(floor(abs(load)), Number.MAX_SAFE_INTEGER);
|
|
46
|
-
|
|
47
|
-
// wait for sufficient capacity
|
|
48
|
-
while (this.capacity < load && this.runningPromises.length > 0) {
|
|
49
|
-
try {
|
|
50
|
-
await Promise.race(this.runningPromises);
|
|
51
|
-
} catch {
|
|
52
|
-
// Yes, we must ignore exceptions here. The
|
|
53
|
-
// caller expects exceptions from f and no
|
|
54
|
-
// exceptions from other workloads.
|
|
55
|
-
// Other exceptions must be handled by the
|
|
56
|
-
// other callers. See (*) below.
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// map function call to promise
|
|
61
|
-
const p = f.constructor.name === "AsyncFunction" ? f(...args) : Promise.resolve().then(() => f(...args));
|
|
62
|
-
|
|
63
|
-
// create promise for book keeping
|
|
64
|
-
const workload = p.finally(() => {
|
|
65
|
-
// remove workload
|
|
66
|
-
this.runningPromises.splice(this.runningPromises.indexOf(workload), 1);
|
|
67
|
-
// and reclaim its capacity
|
|
68
|
-
this.capacity += load;
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
// claim the capacity and schedule workload
|
|
72
|
-
this.capacity -= load;
|
|
73
|
-
this.runningPromises.push(workload);
|
|
74
|
-
|
|
75
|
-
// make the caller wait for the workload
|
|
76
|
-
// this also establish the seemingly missing
|
|
77
|
-
// exception handling. See (*) above.
|
|
78
|
-
return workload;
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
11
|
/**
|
|
83
12
|
* Defines a promise that resolves when all payloads are processed by the iterator, but limits
|
|
84
13
|
* the number concurrent executions.
|
|
@@ -132,4 +61,4 @@ const processChunkedSync = (inputs, chunkSize, chunkHandler) => {
|
|
|
132
61
|
|
|
133
62
|
const hashStringTo32Bit = (value) => crypto.createHash("sha256").update(String(value)).digest("base64").slice(0, 32);
|
|
134
63
|
|
|
135
|
-
module.exports = { arrayToFlatMap,
|
|
64
|
+
module.exports = { arrayToFlatMap, limiter, isValidDate, processChunkedSync, hashStringTo32Bit };
|
|
@@ -4,6 +4,8 @@ const redis = require("./redis");
|
|
|
4
4
|
const config = require("../config");
|
|
5
5
|
const cdsHelper = require("./cdsHelper");
|
|
6
6
|
|
|
7
|
+
const KEY_PREFIX = "EVENT_QUEUE";
|
|
8
|
+
|
|
7
9
|
const acquireLock = async (context, key, { tenantScoped = true, expiryTime = config.globalTxTimeout } = {}) => {
|
|
8
10
|
const fullKey = _generateKey(context, tenantScoped, key);
|
|
9
11
|
if (config.redisEnabled) {
|
|
@@ -128,7 +130,7 @@ const _generateKey = (context, tenantScoped, key) => {
|
|
|
128
130
|
const keyParts = [];
|
|
129
131
|
tenantScoped && keyParts.push(context.tenant);
|
|
130
132
|
keyParts.push(key);
|
|
131
|
-
return keyParts.join("##")
|
|
133
|
+
return `${KEY_PREFIX}_${keyParts.join("##")}`;
|
|
132
134
|
};
|
|
133
135
|
|
|
134
136
|
module.exports = {
|
|
@@ -19,7 +19,7 @@ class EventScheduler {
|
|
|
19
19
|
return; // event combination already scheduled
|
|
20
20
|
}
|
|
21
21
|
this.#scheduledEvents[key] = true;
|
|
22
|
-
cds.log(COMPONENT_NAME).
|
|
22
|
+
cds.log(COMPONENT_NAME).debug("scheduling event queue run for delayed event", {
|
|
23
23
|
type,
|
|
24
24
|
subType,
|
|
25
25
|
delaySeconds: (date.getTime() - Date.now()) / 1000,
|