@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 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.register && cds.env.eventQueue) {
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.4.7",
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.logger.debug("entering persistEventStatus", {
386
- eventType: this.#eventType,
387
- eventSubType: this.#eventSubType,
388
- });
389
- this.#ensureOnlySelectedQueueEntries(statusMap);
390
- if (!skipChecks) {
391
- this.#ensureEveryQueueEntryHasStatus();
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
- this.logger.debug("persistEventStatus for entries", {
417
- eventType: this.#eventType,
418
- eventSubType: this.#eventSubType,
419
- invalidAttempts,
420
- failed,
421
- exceeded,
422
- success,
423
- });
424
- if (invalidAttempts.length) {
425
- await tx.run(
426
- UPDATE.entity(this.#config.tableNameEventQueue)
427
- .set({
428
- status: EventProcessingStatus.Open,
429
- lastAttemptTimestamp: new Date().toISOString(),
430
- attempts: { "-=": 1 },
431
- })
432
- .where("ID IN", invalidAttempts)
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
- const ts = new Date().toISOString();
436
- const updateTuples = [
437
- [success, EventProcessingStatus.Done],
438
- [failed, EventProcessingStatus.Error],
439
- [exceeded, EventProcessingStatus.Exceeded],
440
- ];
441
-
442
- for (const [eventIds, status] of updateTuples) {
443
- if (!eventIds.length) {
444
- continue;
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
- await tx.run(
447
- UPDATE.entity(this.#config.tableNameEventQueue)
448
- .set({
449
- status: status,
450
- lastAttemptTimestamp: ts,
451
- })
452
- .where("ID IN", eventIds)
453
- );
454
- }
455
- this.logger.debug("exiting persistEventStatus", {
456
- eventType: this.#eventType,
457
- eventSubType: this.#eventSubType,
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
- for (const exceededEvent of this.#eventsWithExceededTries) {
732
- await executeInNewTransaction(
733
- this.context,
734
- `eventQueue-handleExceededEvents-${this.#eventType}##${this.#eventSubType}`,
735
- async (tx) => {
736
- try {
737
- this.processEventContext = tx.context;
738
- this.modifyQueueEntry(exceededEvent);
739
- await this.hookForExceededEvents({ ...exceededEvent });
740
- this.logger.warn("The retry attempts for the following events are exceeded", {
741
- eventType: this.#eventType,
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
- await executeInNewTransaction(this.context, "error-hookForExceededEvents", async (tx) =>
761
- this.#persistEventQueueStatusForExceeded(tx, [exceededEvent], EventProcessingStatus.Error)
762
- );
763
- throw new TriggerRollback();
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
- const lockAcquired = await distributedLock.acquireLock(
892
- this.context,
893
- [this.#eventType, this.#eventSubType].join("##")
894
- );
895
- if (!lockAcquired) {
896
- this.logger.debug("no lock available, exit processing", {
897
- type: this.#eventType,
898
- subType: this.#eventSubType,
899
- });
900
- return false;
901
- }
902
- this.__lockAcquired = true;
903
- return true;
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 distributedLock.releaseLock(this.context, [this.#eventType, this.#eventSubType].join("##"));
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
- skipBroadcast?: boolean
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);
@@ -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
- const queueEntries = await eventTypeInstance.getQueueEntriesAndSetToInProgress();
48
- eventTypeInstance.startPerformanceTracerPreprocessing();
49
- for (const queueEntry of queueEntries) {
50
- try {
51
- eventTypeInstance.modifyQueueEntry(queueEntry);
52
- const payload = await eventTypeInstance.checkEventAndGeneratePayload(queueEntry);
53
- if (payload === null) {
54
- eventTypeInstance.setStatusToDone(queueEntry);
55
- continue;
56
- }
57
- if (payload === undefined) {
58
- eventTypeInstance.handleInvalidPayloadReturned(queueEntry);
59
- continue;
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
- throw new TriggerRollback();
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
- try {
77
- eventTypeInstance.clusterQueueEntries(eventTypeInstance.queueEntriesWithPayloadMap);
78
- await processEventMap(eventTypeInstance);
79
- } catch (err) {
80
- eventTypeInstance.handleErrorDuringClustering(err);
81
- }
82
- if (
83
- eventTypeInstance.transactionMode !== TransactionMode.alwaysCommit ||
84
- Object.entries(eventTypeInstance.eventProcessingMap).some(([key]) =>
85
- eventTypeInstance.shouldRollbackTransaction(key)
86
- )
87
- ) {
88
- throw new TriggerRollback();
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.processEventContext = tx.context;
132
- const queueEntries = await eventTypeInstance.getQueueEntriesAndSetToInProgress();
133
- if (!queueEntries.length) {
134
- return;
135
- }
136
- if (queueEntries.length > 1) {
137
- queueEntry = await eventTypeInstance.handleDuplicatedPeriodicEventEntry(queueEntries);
138
- } else {
139
- queueEntry = queueEntries[0];
140
- }
141
- processNext = await eventTypeInstance.scheduleNextPeriodEvent(queueEntry);
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.processEventContext = tx.context;
155
- eventTypeInstance.setTxForEventProcessing(queueEntry.ID, cds.tx(tx.context));
156
- try {
157
- eventTypeInstance.startPerformanceTracerPeriodicEvents();
158
- await eventTypeInstance.processPeriodicEvent(tx.context, queueEntry.ID, queueEntry);
159
- } catch (err) {
160
- status = EventProcessingStatus.Error;
161
- eventTypeInstance.handleErrorDuringPeriodicEventProcessing(err, queueEntry);
162
- throw new TriggerRollback();
163
- } finally {
164
- eventTypeInstance.endPerformanceTracerPeriodicEvents();
165
- }
166
- if (
167
- eventTypeInstance.transactionMode === TransactionMode.alwaysRollback ||
168
- eventTypeInstance.shouldRollbackTransaction(queueEntry.ID)
169
- ) {
170
- throw new TriggerRollback();
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.processEventContext = tx.context;
180
- await eventTypeInstance.setPeriodicEventStatus(queueEntry.ID, status);
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
 
@@ -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 {Boolean} skipBroadcast - (Optional) If set to true, event broadcasting will be skipped. Defaults to false.
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
- const result = await tx.run(INSERT.into(config.tableNameEventQueue).entries(eventsForProcessing));
56
+ result = await tx.run(INSERT.into(config.tableNameEventQueue).entries(events));
54
57
  tx._skipEventQueueBroadcase = false;
55
58
  return result;
56
59
  }
@@ -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
  }
@@ -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", {
@@ -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 distributedLock.acquireLock(dummyContext, EVENT_QUEUE_RUN_REDIS_CHECK, {
105
- expiryTime: eventQueueConfig.runInterval * 0.95,
106
- tenantScoped: false,
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
- tx.context.user = new cds.User.Privileged({ id: config.userId, authInfo: await common.getAuthInfo(tenantId) });
115
- const entries = await openEvents.getOpenQueueEntries(tx);
116
- logger.info("broadcasting events for run", {
117
- tenantId,
118
- entries: entries.length,
119
- });
120
- if (!entries.length) {
121
- return;
122
- }
123
- await redisPub.broadcastEvent(tenantId, entries).catch((err) => {
124
- logger.error("broadcasting event failed", err, {
125
- tenantId,
126
- entries: entries.length,
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 user = await cds.tx({ tenant: tenantId }, async () => {
141
- return new cds.User.Privileged({ id: config.userId, authInfo: await common.getAuthInfo(tenantId) });
142
- });
143
- const tenantContext = {
144
- tenant: tenantId,
145
- user,
146
- };
147
- const events = await cds.tx(tenantContext, async (tx) => {
148
- return await openEvents.getOpenQueueEntries(tx);
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
- try {
162
- const lockId = `${runId}_${label}`;
163
- const couldAcquireLock = await distributedLock.acquireLock(context, lockId, {
164
- expiryTime: eventQueueConfig.runInterval * 0.95,
165
- });
166
- if (!couldAcquireLock) {
167
- return;
168
- }
169
- await runEventCombinationForTenant(context, eventConfig.type, eventConfig.subType, true);
170
- } catch (err) {
171
- cds.log(COMPONENT_NAME).error("executing event-queue run for tenant failed", {
172
- tenantId,
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
- if (!config.redisEnabled) {
195
- const couldAcquireLock = await distributedLock.acquireLock(context, EVENT_QUEUE_UPDATE_PERIODIC_EVENTS, {
196
- expiryTime: eventQueueConfig.runInterval * 0.95,
197
- });
198
- if (!couldAcquireLock) {
199
- return;
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
- await _checkPeriodicEventsSingleTenant(context);
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
- if (eventQueueConfig.redisEnabled) {
273
- const dummyContext = new cds.EventContext({});
274
- let lastRunTs = await distributedLock.checkLockExistsAndReturnValue(dummyContext, EVENT_QUEUE_RUN_TS, {
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 (couldSetValue) {
284
- lastRunTs = ts;
285
- } else {
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
- offsetDependingOnLastRun = new Date(lastRunTs).getTime() + eventQueueConfig.runInterval - now;
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
- if (config.redisEnabled) {
319
- const dummyContext = new cds.EventContext({});
320
- const couldAcquireLock = await distributedLock.acquireLock(dummyContext, EVENT_QUEUE_UPDATE_PERIODIC_EVENTS, {
321
- expiryTime: 60 * 1000, // short living lock --> assume we do not have 2 onboards within 1 minute
322
- tenantScoped: false,
323
- });
324
- if (!couldAcquireLock) {
325
- return;
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
- tenantIds = tenantIds ?? (await cdsHelper.getAllTenantIds());
330
- return await _executePeriodicEventsAllTenants(tenantIds);
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 () => await processEventQueue(context, type, subType))
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
- setTimeout(() => {
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;