@cap-js-community/event-queue 1.4.7 → 1.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cds-plugin.js +1 -1
- package/package.json +4 -1
- package/src/EventQueueProcessorBase.js +124 -115
- package/src/config.js +9 -0
- package/src/index.d.ts +7 -1
- package/src/initialize.js +4 -1
- package/src/processEventQueue.js +74 -65
- package/src/publishEvent.js +8 -5
- package/src/redis/redisPub.js +2 -2
- package/src/redis/redisSub.js +2 -2
- package/src/runner/runner.js +132 -78
- package/src/runner/runnerHelper.js +20 -2
- package/src/shared/eventScheduler.js +12 -2
- package/src/shared/openTelemetry.js +64 -0
package/cds-plugin.js
CHANGED
|
@@ -5,6 +5,6 @@ const cds = require("@sap/cds");
|
|
|
5
5
|
const eventQueue = require("./src");
|
|
6
6
|
const COMPONENT_NAME = "/eventQueue/plugin";
|
|
7
7
|
|
|
8
|
-
if (!cds.build
|
|
8
|
+
if (!cds.build?.register && cds.env.eventQueue) {
|
|
9
9
|
module.exports = eventQueue.initialize().catch((err) => cds.log(COMPONENT_NAME).error(err));
|
|
10
10
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cap-js-community/event-queue",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.1",
|
|
4
4
|
"description": "An event queue that enables secure transactional processing of asynchronous and periodic events, featuring instant event processing with Redis Pub/Sub and load distribution across all application instances.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"types": "src/index.d.ts",
|
|
@@ -63,6 +63,9 @@
|
|
|
63
63
|
"prettier": "^2.8.8",
|
|
64
64
|
"sqlite3": "^5.1.7"
|
|
65
65
|
},
|
|
66
|
+
"peerDependencies": {
|
|
67
|
+
"@cap-js/telemetry": "^0.2.2"
|
|
68
|
+
},
|
|
66
69
|
"homepage": "https://cap-js-community.github.io/event-queue/",
|
|
67
70
|
"repository": {
|
|
68
71
|
"type": "git",
|
|
@@ -11,6 +11,7 @@ const eventScheduler = require("./shared/eventScheduler");
|
|
|
11
11
|
const eventConfig = require("./config");
|
|
12
12
|
const PerformanceTracer = require("./shared/PerformanceTracer");
|
|
13
13
|
const { broadcastEvent } = require("./redis/redisPub");
|
|
14
|
+
const trace = require("./shared/openTelemetry");
|
|
14
15
|
|
|
15
16
|
const IMPLEMENT_ERROR_MESSAGE = "needs to be reimplemented";
|
|
16
17
|
const COMPONENT_NAME = "/eventQueue/EventQueueProcessorBase";
|
|
@@ -382,79 +383,81 @@ class EventQueueProcessorBase {
|
|
|
382
383
|
* The function accepts no arguments as there are dedicated functions to set the status of events (e.g. setEventStatus)
|
|
383
384
|
*/
|
|
384
385
|
async persistEventStatus(tx, { skipChecks, statusMap = this.__statusMap } = {}) {
|
|
385
|
-
this.
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
this.#ensureEveryStatusIsAllowed(statusMap);
|
|
394
|
-
|
|
395
|
-
const { success, failed, exceeded, invalidAttempts } = Object.entries(statusMap).reduce(
|
|
396
|
-
(result, [notificationEntityId, processingStatus]) => {
|
|
397
|
-
this.__commitedStatusMap[notificationEntityId] = processingStatus;
|
|
398
|
-
if (processingStatus === EventProcessingStatus.Open) {
|
|
399
|
-
result.invalidAttempts.push(notificationEntityId);
|
|
400
|
-
} else if (processingStatus === EventProcessingStatus.Done) {
|
|
401
|
-
result.success.push(notificationEntityId);
|
|
402
|
-
} else if (processingStatus === EventProcessingStatus.Error) {
|
|
403
|
-
result.failed.push(notificationEntityId);
|
|
404
|
-
} else if (processingStatus === EventProcessingStatus.Exceeded) {
|
|
405
|
-
result.exceeded.push(notificationEntityId);
|
|
406
|
-
}
|
|
407
|
-
return result;
|
|
408
|
-
},
|
|
409
|
-
{
|
|
410
|
-
success: [],
|
|
411
|
-
failed: [],
|
|
412
|
-
exceeded: [],
|
|
413
|
-
invalidAttempts: [],
|
|
386
|
+
return await trace(this.baseContext, "persist-event-status", async () => {
|
|
387
|
+
this.logger.debug("entering persistEventStatus", {
|
|
388
|
+
eventType: this.#eventType,
|
|
389
|
+
eventSubType: this.#eventSubType,
|
|
390
|
+
});
|
|
391
|
+
this.#ensureOnlySelectedQueueEntries(statusMap);
|
|
392
|
+
if (!skipChecks) {
|
|
393
|
+
this.#ensureEveryQueueEntryHasStatus();
|
|
414
394
|
}
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
395
|
+
this.#ensureEveryStatusIsAllowed(statusMap);
|
|
396
|
+
|
|
397
|
+
const { success, failed, exceeded, invalidAttempts } = Object.entries(statusMap).reduce(
|
|
398
|
+
(result, [notificationEntityId, processingStatus]) => {
|
|
399
|
+
this.__commitedStatusMap[notificationEntityId] = processingStatus;
|
|
400
|
+
if (processingStatus === EventProcessingStatus.Open) {
|
|
401
|
+
result.invalidAttempts.push(notificationEntityId);
|
|
402
|
+
} else if (processingStatus === EventProcessingStatus.Done) {
|
|
403
|
+
result.success.push(notificationEntityId);
|
|
404
|
+
} else if (processingStatus === EventProcessingStatus.Error) {
|
|
405
|
+
result.failed.push(notificationEntityId);
|
|
406
|
+
} else if (processingStatus === EventProcessingStatus.Exceeded) {
|
|
407
|
+
result.exceeded.push(notificationEntityId);
|
|
408
|
+
}
|
|
409
|
+
return result;
|
|
410
|
+
},
|
|
411
|
+
{
|
|
412
|
+
success: [],
|
|
413
|
+
failed: [],
|
|
414
|
+
exceeded: [],
|
|
415
|
+
invalidAttempts: [],
|
|
416
|
+
}
|
|
433
417
|
);
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
418
|
+
this.logger.debug("persistEventStatus for entries", {
|
|
419
|
+
eventType: this.#eventType,
|
|
420
|
+
eventSubType: this.#eventSubType,
|
|
421
|
+
invalidAttempts,
|
|
422
|
+
failed,
|
|
423
|
+
exceeded,
|
|
424
|
+
success,
|
|
425
|
+
});
|
|
426
|
+
if (invalidAttempts.length) {
|
|
427
|
+
await tx.run(
|
|
428
|
+
UPDATE.entity(this.#config.tableNameEventQueue)
|
|
429
|
+
.set({
|
|
430
|
+
status: EventProcessingStatus.Open,
|
|
431
|
+
lastAttemptTimestamp: new Date().toISOString(),
|
|
432
|
+
attempts: { "-=": 1 },
|
|
433
|
+
})
|
|
434
|
+
.where("ID IN", invalidAttempts)
|
|
435
|
+
);
|
|
445
436
|
}
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
)
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
437
|
+
const ts = new Date().toISOString();
|
|
438
|
+
const updateTuples = [
|
|
439
|
+
[success, EventProcessingStatus.Done],
|
|
440
|
+
[failed, EventProcessingStatus.Error],
|
|
441
|
+
[exceeded, EventProcessingStatus.Exceeded],
|
|
442
|
+
];
|
|
443
|
+
|
|
444
|
+
for (const [eventIds, status] of updateTuples) {
|
|
445
|
+
if (!eventIds.length) {
|
|
446
|
+
continue;
|
|
447
|
+
}
|
|
448
|
+
await tx.run(
|
|
449
|
+
UPDATE.entity(this.#config.tableNameEventQueue)
|
|
450
|
+
.set({
|
|
451
|
+
status: status,
|
|
452
|
+
lastAttemptTimestamp: ts,
|
|
453
|
+
})
|
|
454
|
+
.where("ID IN", eventIds)
|
|
455
|
+
);
|
|
456
|
+
}
|
|
457
|
+
this.logger.debug("exiting persistEventStatus", {
|
|
458
|
+
eventType: this.#eventType,
|
|
459
|
+
eventSubType: this.#eventSubType,
|
|
460
|
+
});
|
|
458
461
|
});
|
|
459
462
|
}
|
|
460
463
|
|
|
@@ -728,43 +731,45 @@ class EventQueueProcessorBase {
|
|
|
728
731
|
return;
|
|
729
732
|
}
|
|
730
733
|
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
eventSubType: this.#eventSubType,
|
|
743
|
-
retryAttempts: this.__retryAttempts,
|
|
744
|
-
queueEntriesId: exceededEvent.ID,
|
|
745
|
-
currentAttempt: exceededEvent.attempts,
|
|
746
|
-
});
|
|
747
|
-
await this.#persistEventQueueStatusForExceeded(this.tx, [exceededEvent], EventProcessingStatus.Exceeded);
|
|
748
|
-
} catch (err) {
|
|
749
|
-
this.logger.error(
|
|
750
|
-
"Caught error during hook for exceeded events - setting queue entry to error. Please catch your promises/exceptions.",
|
|
751
|
-
err,
|
|
752
|
-
{
|
|
734
|
+
return await trace(this.baseContext, "handle-exceeded-events", async () => {
|
|
735
|
+
for (const exceededEvent of this.#eventsWithExceededTries) {
|
|
736
|
+
await executeInNewTransaction(
|
|
737
|
+
this.context,
|
|
738
|
+
`eventQueue-handleExceededEvents-${this.#eventType}##${this.#eventSubType}`,
|
|
739
|
+
async (tx) => {
|
|
740
|
+
try {
|
|
741
|
+
this.processEventContext = tx.context;
|
|
742
|
+
this.modifyQueueEntry(exceededEvent);
|
|
743
|
+
await this.hookForExceededEvents({ ...exceededEvent });
|
|
744
|
+
this.logger.warn("The retry attempts for the following events are exceeded", {
|
|
753
745
|
eventType: this.#eventType,
|
|
754
746
|
eventSubType: this.#eventSubType,
|
|
755
747
|
retryAttempts: this.__retryAttempts,
|
|
756
748
|
queueEntriesId: exceededEvent.ID,
|
|
757
749
|
currentAttempt: exceededEvent.attempts,
|
|
758
|
-
}
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
this
|
|
762
|
-
|
|
763
|
-
|
|
750
|
+
});
|
|
751
|
+
await this.#persistEventQueueStatusForExceeded(this.tx, [exceededEvent], EventProcessingStatus.Exceeded);
|
|
752
|
+
} catch (err) {
|
|
753
|
+
this.logger.error(
|
|
754
|
+
"Caught error during hook for exceeded events - setting queue entry to error. Please catch your promises/exceptions.",
|
|
755
|
+
err,
|
|
756
|
+
{
|
|
757
|
+
eventType: this.#eventType,
|
|
758
|
+
eventSubType: this.#eventSubType,
|
|
759
|
+
retryAttempts: this.__retryAttempts,
|
|
760
|
+
queueEntriesId: exceededEvent.ID,
|
|
761
|
+
currentAttempt: exceededEvent.attempts,
|
|
762
|
+
}
|
|
763
|
+
);
|
|
764
|
+
await executeInNewTransaction(this.context, "error-hookForExceededEvents", async (tx) =>
|
|
765
|
+
this.#persistEventQueueStatusForExceeded(tx, [exceededEvent], EventProcessingStatus.Error)
|
|
766
|
+
);
|
|
767
|
+
throw new TriggerRollback();
|
|
768
|
+
}
|
|
764
769
|
}
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
}
|
|
770
|
+
);
|
|
771
|
+
}
|
|
772
|
+
});
|
|
768
773
|
}
|
|
769
774
|
|
|
770
775
|
async #handleExceededTriesExceeded() {
|
|
@@ -888,19 +893,21 @@ class EventQueueProcessorBase {
|
|
|
888
893
|
return true;
|
|
889
894
|
}
|
|
890
895
|
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
896
|
+
return await trace(this.baseContext, "acquire-lock", async () => {
|
|
897
|
+
const lockAcquired = await distributedLock.acquireLock(
|
|
898
|
+
this.context,
|
|
899
|
+
[this.#eventType, this.#eventSubType].join("##")
|
|
900
|
+
);
|
|
901
|
+
if (!lockAcquired) {
|
|
902
|
+
this.logger.debug("no lock available, exit processing", {
|
|
903
|
+
type: this.#eventType,
|
|
904
|
+
subType: this.#eventSubType,
|
|
905
|
+
});
|
|
906
|
+
return false;
|
|
907
|
+
}
|
|
908
|
+
this.__lockAcquired = true;
|
|
909
|
+
return true;
|
|
910
|
+
});
|
|
904
911
|
}
|
|
905
912
|
|
|
906
913
|
async handleReleaseLock() {
|
|
@@ -908,7 +915,9 @@ class EventQueueProcessorBase {
|
|
|
908
915
|
return;
|
|
909
916
|
}
|
|
910
917
|
try {
|
|
911
|
-
await
|
|
918
|
+
await trace(this.baseContext, "persist-release-lock", async () => {
|
|
919
|
+
await distributedLock.releaseLock(this.context, [this.#eventType, this.#eventSubType].join("##"));
|
|
920
|
+
});
|
|
912
921
|
} catch (err) {
|
|
913
922
|
this.logger.error("Releasing distributed lock failed.", err);
|
|
914
923
|
}
|
package/src/config.js
CHANGED
|
@@ -68,6 +68,7 @@ class Config {
|
|
|
68
68
|
#cleanupLocksAndEventsForDev;
|
|
69
69
|
#redisOptions;
|
|
70
70
|
#insertEventsBeforeCommit;
|
|
71
|
+
#enableCAPTelemetry;
|
|
71
72
|
#unsubscribeHandlers = [];
|
|
72
73
|
#unsubscribedTenants = {};
|
|
73
74
|
static #instance;
|
|
@@ -555,6 +556,14 @@ class Config {
|
|
|
555
556
|
return this.#insertEventsBeforeCommit;
|
|
556
557
|
}
|
|
557
558
|
|
|
559
|
+
set enableCAPTelemetry(value) {
|
|
560
|
+
this.#enableCAPTelemetry = value;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
get enableCAPTelemetry() {
|
|
564
|
+
return this.#enableCAPTelemetry;
|
|
565
|
+
}
|
|
566
|
+
|
|
558
567
|
get isMultiTenancy() {
|
|
559
568
|
return !!cds.requires.multitenancy;
|
|
560
569
|
}
|
package/src/index.d.ts
CHANGED
|
@@ -116,18 +116,24 @@ export declare class EventQueueProcessorBase {
|
|
|
116
116
|
getTxForEventProcessing(key: string): cds.Transaction;
|
|
117
117
|
setShouldRollbackTransaction(key: string): void;
|
|
118
118
|
shouldRollbackTransaction(key: string): boolean;
|
|
119
|
+
beforeProcessingEvents(): Promise<void>;
|
|
119
120
|
|
|
120
121
|
set logger(value: CdsLogger);
|
|
121
122
|
get logger(): CdsLogger;
|
|
122
123
|
get tx(): cds.Transaction;
|
|
123
124
|
get context(): cds.EventContext;
|
|
124
125
|
get isPeriodicEvent(): boolean;
|
|
126
|
+
get eventType(): String;
|
|
127
|
+
get eventSubType(): String;
|
|
125
128
|
}
|
|
126
129
|
|
|
127
130
|
export function publishEvent(
|
|
128
131
|
tx: cds.Transaction,
|
|
129
132
|
events: EventEntityPublish[] | EventEntityPublish,
|
|
130
|
-
|
|
133
|
+
options?: {
|
|
134
|
+
skipBroadcast?: boolean;
|
|
135
|
+
skipInsertEventsBeforeCommit?: boolean;
|
|
136
|
+
}
|
|
131
137
|
): Promise<any>;
|
|
132
138
|
|
|
133
139
|
export function processEventQueue(
|
package/src/initialize.js
CHANGED
|
@@ -36,6 +36,7 @@ const CONFIG_VARS = [
|
|
|
36
36
|
["cleanupLocksAndEventsForDev", false],
|
|
37
37
|
["redisOptions", {}],
|
|
38
38
|
["insertEventsBeforeCommit", false],
|
|
39
|
+
["enableCAPTelemetry", false],
|
|
39
40
|
];
|
|
40
41
|
|
|
41
42
|
const initialize = async ({
|
|
@@ -52,6 +53,7 @@ const initialize = async ({
|
|
|
52
53
|
cleanupLocksAndEventsForDev,
|
|
53
54
|
redisOptions,
|
|
54
55
|
insertEventsBeforeCommit,
|
|
56
|
+
enableCAPTelemetry,
|
|
55
57
|
} = {}) => {
|
|
56
58
|
if (config.initialized) {
|
|
57
59
|
return;
|
|
@@ -71,7 +73,8 @@ const initialize = async ({
|
|
|
71
73
|
userId,
|
|
72
74
|
cleanupLocksAndEventsForDev,
|
|
73
75
|
redisOptions,
|
|
74
|
-
insertEventsBeforeCommit
|
|
76
|
+
insertEventsBeforeCommit,
|
|
77
|
+
enableCAPTelemetry
|
|
75
78
|
);
|
|
76
79
|
|
|
77
80
|
const logger = cds.log(COMPONENT);
|
package/src/processEventQueue.js
CHANGED
|
@@ -9,6 +9,7 @@ const { TransactionMode, EventProcessingStatus } = require("./constants");
|
|
|
9
9
|
const { limiter } = require("./shared/common");
|
|
10
10
|
|
|
11
11
|
const { executeInNewTransaction, TriggerRollback } = require("./shared/cdsHelper");
|
|
12
|
+
const trace = require("./shared/openTelemetry");
|
|
12
13
|
|
|
13
14
|
const COMPONENT_NAME = "/eventQueue/processEventQueue";
|
|
14
15
|
|
|
@@ -44,26 +45,28 @@ const processEventQueue = async (context, eventType, eventSubType, startTime = n
|
|
|
44
45
|
iterationCounter++;
|
|
45
46
|
await executeInNewTransaction(context, `eventQueue-pre-processing-${eventType}##${eventSubType}`, async (tx) => {
|
|
46
47
|
eventTypeInstance = new EventTypeClass(tx.context, eventType, eventSubType, eventConfig);
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
48
|
+
await trace(eventTypeInstance.context, "preparation", async () => {
|
|
49
|
+
const queueEntries = await eventTypeInstance.getQueueEntriesAndSetToInProgress();
|
|
50
|
+
eventTypeInstance.startPerformanceTracerPreprocessing();
|
|
51
|
+
for (const queueEntry of queueEntries) {
|
|
52
|
+
try {
|
|
53
|
+
eventTypeInstance.modifyQueueEntry(queueEntry);
|
|
54
|
+
const payload = await eventTypeInstance.checkEventAndGeneratePayload(queueEntry);
|
|
55
|
+
if (payload === null) {
|
|
56
|
+
eventTypeInstance.setStatusToDone(queueEntry);
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (payload === undefined) {
|
|
60
|
+
eventTypeInstance.handleInvalidPayloadReturned(queueEntry);
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
eventTypeInstance.addEventWithPayloadForProcessing(queueEntry, payload);
|
|
64
|
+
} catch (err) {
|
|
65
|
+
eventTypeInstance.handleErrorDuringProcessing(err, queueEntry);
|
|
60
66
|
}
|
|
61
|
-
eventTypeInstance.addEventWithPayloadForProcessing(queueEntry, payload);
|
|
62
|
-
} catch (err) {
|
|
63
|
-
eventTypeInstance.handleErrorDuringProcessing(err, queueEntry);
|
|
64
67
|
}
|
|
65
|
-
|
|
66
|
-
|
|
68
|
+
throw new TriggerRollback();
|
|
69
|
+
});
|
|
67
70
|
});
|
|
68
71
|
await eventTypeInstance.handleExceededEvents();
|
|
69
72
|
if (!eventTypeInstance) {
|
|
@@ -73,20 +76,22 @@ const processEventQueue = async (context, eventType, eventSubType, startTime = n
|
|
|
73
76
|
if (Object.keys(eventTypeInstance.queueEntriesWithPayloadMap).length) {
|
|
74
77
|
await executeInNewTransaction(context, `eventQueue-processing-${eventType}##${eventSubType}`, async (tx) => {
|
|
75
78
|
eventTypeInstance.processEventContext = tx.context;
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
eventTypeInstance.
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
79
|
+
await trace(eventTypeInstance.context, "process-events", async () => {
|
|
80
|
+
try {
|
|
81
|
+
eventTypeInstance.clusterQueueEntries(eventTypeInstance.queueEntriesWithPayloadMap);
|
|
82
|
+
await processEventMap(eventTypeInstance);
|
|
83
|
+
} catch (err) {
|
|
84
|
+
eventTypeInstance.handleErrorDuringClustering(err);
|
|
85
|
+
}
|
|
86
|
+
if (
|
|
87
|
+
eventTypeInstance.transactionMode !== TransactionMode.alwaysCommit ||
|
|
88
|
+
Object.entries(eventTypeInstance.eventProcessingMap).some(([key]) =>
|
|
89
|
+
eventTypeInstance.shouldRollbackTransaction(key)
|
|
90
|
+
)
|
|
91
|
+
) {
|
|
92
|
+
throw new TriggerRollback();
|
|
93
|
+
}
|
|
94
|
+
});
|
|
90
95
|
});
|
|
91
96
|
}
|
|
92
97
|
await executeInNewTransaction(context, `eventQueue-persistStatus-${eventType}##${eventSubType}`, async (tx) => {
|
|
@@ -128,17 +133,19 @@ const processPeriodicEvent = async (context, eventTypeInstance) => {
|
|
|
128
133
|
eventTypeInstance.context,
|
|
129
134
|
`eventQueue-periodic-scheduleNext-${eventTypeInstance.eventType}##${eventTypeInstance.eventSubType}`,
|
|
130
135
|
async (tx) => {
|
|
131
|
-
eventTypeInstance.
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
136
|
+
await trace(eventTypeInstance.context, "periodic-event-preparation", async () => {
|
|
137
|
+
eventTypeInstance.processEventContext = tx.context;
|
|
138
|
+
const queueEntries = await eventTypeInstance.getQueueEntriesAndSetToInProgress();
|
|
139
|
+
if (!queueEntries.length) {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
if (queueEntries.length > 1) {
|
|
143
|
+
queueEntry = await eventTypeInstance.handleDuplicatedPeriodicEventEntry(queueEntries);
|
|
144
|
+
} else {
|
|
145
|
+
queueEntry = queueEntries[0];
|
|
146
|
+
}
|
|
147
|
+
processNext = await eventTypeInstance.scheduleNextPeriodEvent(queueEntry);
|
|
148
|
+
});
|
|
142
149
|
}
|
|
143
150
|
);
|
|
144
151
|
|
|
@@ -151,24 +158,26 @@ const processPeriodicEvent = async (context, eventTypeInstance) => {
|
|
|
151
158
|
eventTypeInstance.context,
|
|
152
159
|
`eventQueue-periodic-process-${eventTypeInstance.eventType}##${eventTypeInstance.eventSubType}`,
|
|
153
160
|
async (tx) => {
|
|
154
|
-
eventTypeInstance.
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
161
|
+
await trace(eventTypeInstance.context, "process-periodic-event", async () => {
|
|
162
|
+
eventTypeInstance.processEventContext = tx.context;
|
|
163
|
+
eventTypeInstance.setTxForEventProcessing(queueEntry.ID, cds.tx(tx.context));
|
|
164
|
+
try {
|
|
165
|
+
eventTypeInstance.startPerformanceTracerPeriodicEvents();
|
|
166
|
+
await eventTypeInstance.processPeriodicEvent(tx.context, queueEntry.ID, queueEntry);
|
|
167
|
+
} catch (err) {
|
|
168
|
+
status = EventProcessingStatus.Error;
|
|
169
|
+
eventTypeInstance.handleErrorDuringPeriodicEventProcessing(err, queueEntry);
|
|
170
|
+
throw new TriggerRollback();
|
|
171
|
+
} finally {
|
|
172
|
+
eventTypeInstance.endPerformanceTracerPeriodicEvents();
|
|
173
|
+
}
|
|
174
|
+
if (
|
|
175
|
+
eventTypeInstance.transactionMode === TransactionMode.alwaysRollback ||
|
|
176
|
+
eventTypeInstance.shouldRollbackTransaction(queueEntry.ID)
|
|
177
|
+
) {
|
|
178
|
+
throw new TriggerRollback();
|
|
179
|
+
}
|
|
180
|
+
});
|
|
172
181
|
}
|
|
173
182
|
);
|
|
174
183
|
|
|
@@ -176,8 +185,10 @@ const processPeriodicEvent = async (context, eventTypeInstance) => {
|
|
|
176
185
|
eventTypeInstance.context,
|
|
177
186
|
`eventQueue-periodic-setStatus-${eventTypeInstance.eventType}##${eventTypeInstance.eventSubType}`,
|
|
178
187
|
async (tx) => {
|
|
179
|
-
eventTypeInstance.
|
|
180
|
-
|
|
188
|
+
await trace(eventTypeInstance.context, "periodic-event-set-status", async () => {
|
|
189
|
+
eventTypeInstance.processEventContext = tx.context;
|
|
190
|
+
await eventTypeInstance.setPeriodicEventStatus(queueEntry.ID, status);
|
|
191
|
+
});
|
|
181
192
|
}
|
|
182
193
|
);
|
|
183
194
|
}
|
|
@@ -186,8 +197,6 @@ const processPeriodicEvent = async (context, eventTypeInstance) => {
|
|
|
186
197
|
eventType: eventTypeInstance?.eventType,
|
|
187
198
|
eventSubType: eventTypeInstance?.eventSubType,
|
|
188
199
|
});
|
|
189
|
-
} finally {
|
|
190
|
-
await eventTypeInstance?.handleReleaseLock();
|
|
191
200
|
}
|
|
192
201
|
};
|
|
193
202
|
|
package/src/publishEvent.js
CHANGED
|
@@ -21,13 +21,15 @@ const EventQueueError = require("./EventQueueError");
|
|
|
21
21
|
* createdAt: Timestamp, // Timestamp of event creation. This field is automatically set on insert.
|
|
22
22
|
* startAfter: Timestamp, // Timestamp indicating when the event should start after.
|
|
23
23
|
* }
|
|
24
|
-
* @param {
|
|
24
|
+
* @param {Object} [options] - Optional settings.
|
|
25
|
+
* @param {Boolean} [options.skipBroadcast=false] - If set to true, event broadcasting will be skipped. Defaults to false.
|
|
26
|
+
* @param {Boolean} [options.skipInsertEventsBeforeCommit=false] - If set to true, events will not be inserted before the transaction commit. Defaults to false.
|
|
25
27
|
* @throws {EventQueueError} Throws an error if the configuration is not initialized.
|
|
26
28
|
* @throws {EventQueueError} Throws an error if the event type is unknown.
|
|
27
29
|
* @throws {EventQueueError} Throws an error if the startAfter field is not a valid date.
|
|
28
|
-
* @returns {Promise} Returns a promise which resolves to the result of the database insert operation.
|
|
30
|
+
* @returns {Promise<*>} Returns a promise which resolves to the result of the database insert operation.
|
|
29
31
|
*/
|
|
30
|
-
const publishEvent = async (tx, events, skipBroadcast = false) => {
|
|
32
|
+
const publishEvent = async (tx, events, { skipBroadcast = false, skipInsertEventsBeforeCommit = false } = {}) => {
|
|
31
33
|
if (!config.initialized) {
|
|
32
34
|
throw EventQueueError.notInitialized();
|
|
33
35
|
}
|
|
@@ -46,11 +48,12 @@ const publishEvent = async (tx, events, skipBroadcast = false) => {
|
|
|
46
48
|
}
|
|
47
49
|
}
|
|
48
50
|
|
|
49
|
-
if (config.insertEventsBeforeCommit) {
|
|
51
|
+
if (config.insertEventsBeforeCommit && !skipInsertEventsBeforeCommit) {
|
|
50
52
|
_registerHandlerAndAddEvents(tx, events);
|
|
51
53
|
} else {
|
|
54
|
+
let result;
|
|
52
55
|
tx._skipEventQueueBroadcase = skipBroadcast;
|
|
53
|
-
|
|
56
|
+
result = await tx.run(INSERT.into(config.tableNameEventQueue).entries(events));
|
|
54
57
|
tx._skipEventQueueBroadcase = false;
|
|
55
58
|
return result;
|
|
56
59
|
}
|
package/src/redis/redisPub.js
CHANGED
|
@@ -40,7 +40,7 @@ const broadcastEvent = async (tenantId, events) => {
|
|
|
40
40
|
|
|
41
41
|
return await cds.tx(context, async ({ context }) => {
|
|
42
42
|
for (const { type, subType } of events) {
|
|
43
|
-
await runEventCombinationForTenant(context, type, subType);
|
|
43
|
+
await runEventCombinationForTenant(context, type, subType, { shouldTrace: true });
|
|
44
44
|
}
|
|
45
45
|
});
|
|
46
46
|
}
|
|
@@ -75,7 +75,7 @@ const broadcastEvent = async (tenantId, events) => {
|
|
|
75
75
|
await redis.publishMessage(
|
|
76
76
|
config.redisOptions,
|
|
77
77
|
EVENT_MESSAGE_CHANNEL,
|
|
78
|
-
JSON.stringify({ tenantId, type, subType })
|
|
78
|
+
JSON.stringify({ lockId: cds.utils.uuid(), tenantId, type, subType })
|
|
79
79
|
);
|
|
80
80
|
break;
|
|
81
81
|
}
|
package/src/redis/redisSub.js
CHANGED
|
@@ -21,7 +21,7 @@ const initEventQueueRedisSubscribe = () => {
|
|
|
21
21
|
const _messageHandlerProcessEvents = async (messageData) => {
|
|
22
22
|
const logger = cds.log(COMPONENT_NAME);
|
|
23
23
|
try {
|
|
24
|
-
const { tenantId, type, subType } = JSON.parse(messageData);
|
|
24
|
+
const { lockId, tenantId, type, subType } = JSON.parse(messageData);
|
|
25
25
|
logger.debug("received redis event", {
|
|
26
26
|
tenantId,
|
|
27
27
|
type,
|
|
@@ -65,7 +65,7 @@ const _messageHandlerProcessEvents = async (messageData) => {
|
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
return await cds.tx(tenantContext, async ({ context }) => {
|
|
68
|
-
return await runnerHelper.runEventCombinationForTenant(context, type, subType);
|
|
68
|
+
return await runnerHelper.runEventCombinationForTenant(context, type, subType, { lockId, shouldTrace: true });
|
|
69
69
|
});
|
|
70
70
|
} catch (err) {
|
|
71
71
|
logger.error("could not parse event information", {
|
package/src/runner/runner.js
CHANGED
|
@@ -15,6 +15,7 @@ const config = require("../config");
|
|
|
15
15
|
const redisPub = require("../redis/redisPub");
|
|
16
16
|
const openEvents = require("./openEvents");
|
|
17
17
|
const { runEventCombinationForTenant } = require("./runnerHelper");
|
|
18
|
+
const trace = require("../shared/openTelemetry");
|
|
18
19
|
|
|
19
20
|
const COMPONENT_NAME = "/eventQueue/runner";
|
|
20
21
|
const EVENT_QUEUE_RUN_ID = "EVENT_QUEUE_RUN_ID";
|
|
@@ -79,6 +80,13 @@ const _multiTenancyRedis = async () => {
|
|
|
79
80
|
};
|
|
80
81
|
|
|
81
82
|
const _checkPeriodicEventUpdate = async (tenantIds) => {
|
|
83
|
+
if (!eventQueueConfig.updatePeriodicEvents || !eventQueueConfig.periodicEvents.length) {
|
|
84
|
+
cds.log(COMPONENT_NAME).info("updating of periodic events is disabled or no periodic events configured", {
|
|
85
|
+
updateEnabled: eventQueueConfig.updatePeriodicEvents,
|
|
86
|
+
events: eventQueueConfig.periodicEvents.length,
|
|
87
|
+
});
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
82
90
|
const hash = common.hashStringTo32Bit(JSON.stringify(tenantIds));
|
|
83
91
|
if (!tenantIdHash) {
|
|
84
92
|
tenantIdHash = hash;
|
|
@@ -101,31 +109,48 @@ const _executeEventsAllTenantsRedis = async (tenantIds) => {
|
|
|
101
109
|
// NOTE: do checks for all tenants on the same app instance --> acquire lock tenant independent
|
|
102
110
|
// distribute from this instance to all others
|
|
103
111
|
const dummyContext = new cds.EventContext({});
|
|
104
|
-
const couldAcquireLock = await
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
112
|
+
const couldAcquireLock = await trace(
|
|
113
|
+
dummyContext,
|
|
114
|
+
"acquire-lock-master-runner",
|
|
115
|
+
async () => {
|
|
116
|
+
return await distributedLock.acquireLock(dummyContext, EVENT_QUEUE_RUN_REDIS_CHECK, {
|
|
117
|
+
expiryTime: eventQueueConfig.runInterval * 0.95,
|
|
118
|
+
tenantScoped: false,
|
|
119
|
+
});
|
|
120
|
+
},
|
|
121
|
+
{ newRootSpan: true }
|
|
122
|
+
);
|
|
108
123
|
if (!couldAcquireLock) {
|
|
109
124
|
return;
|
|
110
125
|
}
|
|
111
126
|
|
|
112
127
|
for (const tenantId of tenantIds) {
|
|
113
128
|
await cds.tx({ tenant: tenantId }, async (tx) => {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
+
await trace(
|
|
130
|
+
tx.context,
|
|
131
|
+
"get-openEvents-and-publish",
|
|
132
|
+
async () => {
|
|
133
|
+
tx.context.user = new cds.User.Privileged({
|
|
134
|
+
id: config.userId,
|
|
135
|
+
authInfo: await common.getAuthInfo(tenantId),
|
|
136
|
+
});
|
|
137
|
+
const entries = await openEvents.getOpenQueueEntries(tx);
|
|
138
|
+
logger.info("broadcasting events for run", {
|
|
139
|
+
tenantId,
|
|
140
|
+
entries: entries.length,
|
|
141
|
+
});
|
|
142
|
+
if (!entries.length) {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
await redisPub.broadcastEvent(tenantId, entries).catch((err) => {
|
|
146
|
+
logger.error("broadcasting event failed", err, {
|
|
147
|
+
tenantId,
|
|
148
|
+
entries: entries.length,
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
},
|
|
152
|
+
{ newRootSpan: true }
|
|
153
|
+
);
|
|
129
154
|
});
|
|
130
155
|
}
|
|
131
156
|
} catch (err) {
|
|
@@ -137,16 +162,25 @@ const _executeEventsAllTenants = async (tenantIds, runId) => {
|
|
|
137
162
|
const promises = [];
|
|
138
163
|
|
|
139
164
|
for (const tenantId of tenantIds) {
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
165
|
+
const id = cds.utils.uuid();
|
|
166
|
+
let tenantContext;
|
|
167
|
+
const events = await trace(
|
|
168
|
+
{ id, tenant: tenantId },
|
|
169
|
+
"fetch-openEvents-and-authInfo",
|
|
170
|
+
async () => {
|
|
171
|
+
const user = await cds.tx({ tenant: tenantId }, async () => {
|
|
172
|
+
return new cds.User.Privileged({ id: config.userId, authInfo: await common.getAuthInfo(tenantId) });
|
|
173
|
+
});
|
|
174
|
+
tenantContext = {
|
|
175
|
+
tenant: tenantId,
|
|
176
|
+
user,
|
|
177
|
+
};
|
|
178
|
+
return await cds.tx(tenantContext, async (tx) => {
|
|
179
|
+
return await openEvents.getOpenQueueEntries(tx);
|
|
180
|
+
});
|
|
181
|
+
},
|
|
182
|
+
{ newRootSpan: true }
|
|
183
|
+
);
|
|
150
184
|
|
|
151
185
|
if (!events.length) {
|
|
152
186
|
continue;
|
|
@@ -158,20 +192,29 @@ const _executeEventsAllTenants = async (tenantIds, runId) => {
|
|
|
158
192
|
const label = `${eventConfig.type}_${eventConfig.subType}`;
|
|
159
193
|
return await WorkerQueue.instance.addToQueue(eventConfig.load, label, eventConfig.priority, async () => {
|
|
160
194
|
return await cds.tx(tenantContext, async ({ context }) => {
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
195
|
+
await trace(
|
|
196
|
+
context,
|
|
197
|
+
label,
|
|
198
|
+
async () => {
|
|
199
|
+
try {
|
|
200
|
+
const lockId = `${runId}_${label}`;
|
|
201
|
+
const couldAcquireLock = await distributedLock.acquireLock(context, lockId, {
|
|
202
|
+
expiryTime: eventQueueConfig.runInterval * 0.95,
|
|
203
|
+
});
|
|
204
|
+
if (!couldAcquireLock) {
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
await runEventCombinationForTenant(context, eventConfig.type, eventConfig.subType, {
|
|
208
|
+
skipWorkerPool: true,
|
|
209
|
+
});
|
|
210
|
+
} catch (err) {
|
|
211
|
+
cds.log(COMPONENT_NAME).error("executing event-queue run for tenant failed", {
|
|
212
|
+
tenantId,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
},
|
|
216
|
+
{ newRootSpan: true }
|
|
217
|
+
);
|
|
175
218
|
});
|
|
176
219
|
});
|
|
177
220
|
})
|
|
@@ -191,15 +234,17 @@ const _executePeriodicEventsAllTenants = async (tenantIds) => {
|
|
|
191
234
|
user,
|
|
192
235
|
};
|
|
193
236
|
await cds.tx(tenantContext, async ({ context }) => {
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
237
|
+
await trace(tenantContext, "update-periodic-events-for-tenant", async () => {
|
|
238
|
+
if (!config.redisEnabled) {
|
|
239
|
+
const couldAcquireLock = await distributedLock.acquireLock(context, EVENT_QUEUE_UPDATE_PERIODIC_EVENTS, {
|
|
240
|
+
expiryTime: eventQueueConfig.runInterval * 0.95,
|
|
241
|
+
});
|
|
242
|
+
if (!couldAcquireLock) {
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
200
245
|
}
|
|
201
|
-
|
|
202
|
-
|
|
246
|
+
await _checkPeriodicEventsSingleTenant(context);
|
|
247
|
+
});
|
|
203
248
|
});
|
|
204
249
|
} catch (err) {
|
|
205
250
|
cds.log(COMPONENT_NAME).error("executing event-queue run for tenant failed", {
|
|
@@ -268,28 +313,30 @@ const _calculateOffsetForFirstRun = async () => {
|
|
|
268
313
|
const now = Date.now();
|
|
269
314
|
// NOTE: this is only supported with Redis, because this is a tenant agnostic information
|
|
270
315
|
// currently there is no proper place to store this information beside t0 schema
|
|
316
|
+
const dummyContext = new cds.EventContext({});
|
|
271
317
|
try {
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
tenantScoped: false,
|
|
276
|
-
});
|
|
277
|
-
if (!lastRunTs) {
|
|
278
|
-
const ts = new Date(now).toISOString();
|
|
279
|
-
const couldSetValue = await distributedLock.setValueWithExpire(dummyContext, EVENT_QUEUE_RUN_TS, ts, {
|
|
318
|
+
await trace(dummyContext, "calculateOffsetForFirstRun", async () => {
|
|
319
|
+
if (eventQueueConfig.redisEnabled) {
|
|
320
|
+
let lastRunTs = await distributedLock.checkLockExistsAndReturnValue(dummyContext, EVENT_QUEUE_RUN_TS, {
|
|
280
321
|
tenantScoped: false,
|
|
281
|
-
expiryTime: eventQueueConfig.runInterval,
|
|
282
322
|
});
|
|
283
|
-
if (
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
lastRunTs = await distributedLock.checkLockExistsAndReturnValue(dummyContext, EVENT_QUEUE_RUN_TS, {
|
|
323
|
+
if (!lastRunTs) {
|
|
324
|
+
const ts = new Date(now).toISOString();
|
|
325
|
+
const couldSetValue = await distributedLock.setValueWithExpire(dummyContext, EVENT_QUEUE_RUN_TS, ts, {
|
|
287
326
|
tenantScoped: false,
|
|
327
|
+
expiryTime: eventQueueConfig.runInterval,
|
|
288
328
|
});
|
|
329
|
+
if (couldSetValue) {
|
|
330
|
+
lastRunTs = ts;
|
|
331
|
+
} else {
|
|
332
|
+
lastRunTs = await distributedLock.checkLockExistsAndReturnValue(dummyContext, EVENT_QUEUE_RUN_TS, {
|
|
333
|
+
tenantScoped: false,
|
|
334
|
+
});
|
|
335
|
+
}
|
|
289
336
|
}
|
|
337
|
+
offsetDependingOnLastRun = new Date(lastRunTs).getTime() + eventQueueConfig.runInterval - now;
|
|
290
338
|
}
|
|
291
|
-
|
|
292
|
-
}
|
|
339
|
+
});
|
|
293
340
|
} catch (err) {
|
|
294
341
|
cds
|
|
295
342
|
.log(COMPONENT_NAME)
|
|
@@ -315,19 +362,26 @@ const _multiTenancyPeriodicEvents = async (tenantIds) => {
|
|
|
315
362
|
try {
|
|
316
363
|
logger.info("executing event queue update periodic events");
|
|
317
364
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
365
|
+
const dummyContext = new cds.EventContext({});
|
|
366
|
+
return await trace(
|
|
367
|
+
dummyContext,
|
|
368
|
+
"update-periodic-events",
|
|
369
|
+
async () => {
|
|
370
|
+
if (config.redisEnabled) {
|
|
371
|
+
const couldAcquireLock = await distributedLock.acquireLock(dummyContext, EVENT_QUEUE_UPDATE_PERIODIC_EVENTS, {
|
|
372
|
+
expiryTime: 60 * 1000, // short living lock --> assume we do not have 2 onboards within 1 minute
|
|
373
|
+
tenantScoped: false,
|
|
374
|
+
});
|
|
375
|
+
if (!couldAcquireLock) {
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
328
379
|
|
|
329
|
-
|
|
330
|
-
|
|
380
|
+
tenantIds = tenantIds ?? (await cdsHelper.getAllTenantIds());
|
|
381
|
+
return await _executePeriodicEventsAllTenants(tenantIds);
|
|
382
|
+
},
|
|
383
|
+
{ newRootSpan: true }
|
|
384
|
+
);
|
|
331
385
|
} catch (err) {
|
|
332
386
|
logger.error("Couldn't fetch tenant ids for updating periodic event processing!", err);
|
|
333
387
|
}
|
|
@@ -7,10 +7,12 @@ const cds = require("@sap/cds");
|
|
|
7
7
|
const { processEventQueue } = require("../processEventQueue");
|
|
8
8
|
const eventQueueConfig = require("../config");
|
|
9
9
|
const WorkerQueue = require("../shared/WorkerQueue");
|
|
10
|
+
const distributedLock = require("../shared/distributedLock");
|
|
11
|
+
const trace = require("../shared/openTelemetry");
|
|
10
12
|
|
|
11
13
|
const COMPONENT_NAME = "/eventQueue/runnerHelper";
|
|
12
14
|
|
|
13
|
-
const runEventCombinationForTenant = async (context, type, subType, skipWorkerPool) => {
|
|
15
|
+
const runEventCombinationForTenant = async (context, type, subType, { skipWorkerPool, lockId, shouldTrace } = {}) => {
|
|
14
16
|
try {
|
|
15
17
|
if (skipWorkerPool) {
|
|
16
18
|
return await processEventQueue(context, type, subType);
|
|
@@ -21,7 +23,23 @@ const runEventCombinationForTenant = async (context, type, subType, skipWorkerPo
|
|
|
21
23
|
eventConfig.load,
|
|
22
24
|
label,
|
|
23
25
|
eventConfig.priority,
|
|
24
|
-
AsyncResource.bind(async () =>
|
|
26
|
+
AsyncResource.bind(async () => {
|
|
27
|
+
const _exec = async () => {
|
|
28
|
+
if (lockId) {
|
|
29
|
+
const lockAvailable = await distributedLock.acquireLock(context, lockId);
|
|
30
|
+
if (!lockAvailable) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
await processEventQueue(context, type, subType);
|
|
36
|
+
};
|
|
37
|
+
if (shouldTrace) {
|
|
38
|
+
return await trace(context, label, _exec, { newRootSpan: true });
|
|
39
|
+
} else {
|
|
40
|
+
return await _exec();
|
|
41
|
+
}
|
|
42
|
+
})
|
|
25
43
|
);
|
|
26
44
|
}
|
|
27
45
|
} catch (err) {
|
|
@@ -27,7 +27,10 @@ class EventScheduler {
|
|
|
27
27
|
subType,
|
|
28
28
|
delaySeconds: (date.getTime() - Date.now()) / 1000,
|
|
29
29
|
});
|
|
30
|
-
|
|
30
|
+
this.#eventsByTenants[tenantId] ??= {};
|
|
31
|
+
let timeoutId;
|
|
32
|
+
const timeout = setTimeout(() => {
|
|
33
|
+
delete this.#eventsByTenants[tenantId][timeoutId];
|
|
31
34
|
delete this.#scheduledEvents[key];
|
|
32
35
|
redisPub.broadcastEvent(tenantId, { type, subType }).catch((err) => {
|
|
33
36
|
cds.log(COMPONENT_NAME).error("could not execute scheduled event", err, {
|
|
@@ -38,9 +41,16 @@ class EventScheduler {
|
|
|
38
41
|
});
|
|
39
42
|
});
|
|
40
43
|
}, relative).unref();
|
|
44
|
+
// Convert the timeout object to a primitive timeout id to avoid circular dependencies between the callback of setTimeout
|
|
45
|
+
// and the closure. The usage of the timeout object in the callback, leads to a deadlock for the garbage collector
|
|
46
|
+
// as the timeout object has a reference to the callback of setTimeout.
|
|
47
|
+
timeoutId = String(timeout);
|
|
48
|
+
this.#eventsByTenants[tenantId][timeoutId] = true;
|
|
41
49
|
}
|
|
42
50
|
|
|
43
|
-
clearForTenant() {
|
|
51
|
+
clearForTenant(tenantId) {
|
|
52
|
+
Object.values(this.#eventsByTenants[tenantId]).forEach((timeoutId) => clearTimeout(timeoutId));
|
|
53
|
+
}
|
|
44
54
|
|
|
45
55
|
calculateOffset(type, subType, startAfter) {
|
|
46
56
|
const eventConfig = config.getEventConfig(type, subType);
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const cds = require("@sap/cds");
|
|
4
|
+
let otel;
|
|
5
|
+
try {
|
|
6
|
+
otel = require("@opentelemetry/api");
|
|
7
|
+
} catch {
|
|
8
|
+
// ignore
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const config = require("../config");
|
|
12
|
+
|
|
13
|
+
const trace = async (context, label, fn, { attributes = {}, newRootSpan = false } = {}) => {
|
|
14
|
+
if (!config.enableCAPTelemetry || !otel || !cds._telemetry?.tracer) {
|
|
15
|
+
return fn();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const span = cds._telemetry.tracer.startSpan(`eventqueue-${label}`, {
|
|
19
|
+
kind: otel.SpanKind.INTERNAL,
|
|
20
|
+
root: newRootSpan,
|
|
21
|
+
});
|
|
22
|
+
_setAttributes(context, span, attributes);
|
|
23
|
+
const ctxWithSpan = otel.trace.setSpan(otel.context.active(), span);
|
|
24
|
+
return otel.context.with(ctxWithSpan, async () => {
|
|
25
|
+
const onSuccess = (res) => {
|
|
26
|
+
span.setStatus({ code: otel.SpanStatusCode.OK });
|
|
27
|
+
return res;
|
|
28
|
+
};
|
|
29
|
+
const onFailure = (e) => {
|
|
30
|
+
span.recordException(e);
|
|
31
|
+
span.setStatus(
|
|
32
|
+
Object.assign({ code: otel.SpanStatusCode.ERROR }, e.message ? { message: e.message } : undefined)
|
|
33
|
+
);
|
|
34
|
+
throw e;
|
|
35
|
+
};
|
|
36
|
+
const onDone = () => {
|
|
37
|
+
if (span.status.code !== otel.SpanStatusCode.UNSET && !span.ended) {
|
|
38
|
+
span.end();
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
const res = fn();
|
|
44
|
+
if (res instanceof Promise) {
|
|
45
|
+
return res.then(onSuccess).catch(onFailure).finally(onDone);
|
|
46
|
+
}
|
|
47
|
+
return onSuccess(res);
|
|
48
|
+
} catch (e) {
|
|
49
|
+
onFailure(e);
|
|
50
|
+
} finally {
|
|
51
|
+
onDone();
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const _setAttributes = (context, span, attributes) => {
|
|
57
|
+
span.setAttribute("sap.tenancy.tenant_id", context.tenant);
|
|
58
|
+
span.setAttribute("sap.correlation_id", context.id);
|
|
59
|
+
for (const attributeKey in attributes) {
|
|
60
|
+
span.setAttribute(attributeKey, attributes[attributeKey]);
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
module.exports = trace;
|