@cap-js-community/event-queue 1.9.1 → 1.9.2

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/db/Event.cds CHANGED
@@ -23,4 +23,6 @@ entity Event: cuid {
23
23
  lastAttemptTimestamp: Timestamp;
24
24
  createdAt: Timestamp @cds.on.insert : $now;
25
25
  startAfter: Timestamp;
26
+ context: LargeString;
27
+ error: String;
26
28
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cap-js-community/event-queue",
3
- "version": "1.9.1",
3
+ "version": "1.9.2",
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",
@@ -44,16 +44,17 @@
44
44
  },
45
45
  "dependencies": {
46
46
  "@sap/xssec": "^4.2.4",
47
- "cron-parser": "^4.9.0",
47
+ "cron-parser": "^5.0.0",
48
48
  "redis": "^4.7.0",
49
49
  "verror": "^1.10.1",
50
50
  "yaml": "^2.6.1"
51
51
  },
52
52
  "devDependencies": {
53
- "@cap-js/hana": "^1.5.2",
54
- "@cap-js/sqlite": "^1.7.8",
55
- "@sap/cds": "^8.6.0",
56
- "@sap/cds-dk": "^8.6.1",
53
+ "@cap-js/cds-test": "^0.2.0",
54
+ "@cap-js/hana": "^1.7.0",
55
+ "@cap-js/sqlite": "^1.9.0",
56
+ "@sap/cds": "^8.8.0",
57
+ "@sap/cds-dk": "^8.8.0",
57
58
  "eslint": "^8.57.0",
58
59
  "eslint-config-prettier": "^9.1.0",
59
60
  "eslint-plugin-jest": "^28.6.0",
@@ -62,7 +63,8 @@
62
63
  "hdb": "^0.19.10",
63
64
  "jest": "^29.7.0",
64
65
  "prettier": "^2.8.8",
65
- "sqlite3": "^5.1.7"
66
+ "sqlite3": "^5.1.7",
67
+ "@opentelemetry/api": "^1.9.0"
66
68
  },
67
69
  "homepage": "https://cap-js-community.github.io/event-queue/",
68
70
  "repository": {
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
 
3
3
  const cds = require("@sap/cds");
4
- const cronParser = require("cron-parser");
4
+ const { CronExpressionParser } = require("cron-parser");
5
5
 
6
6
  const { executeInNewTransaction } = require("./shared/cdsHelper");
7
7
  const { EventProcessingStatus, TransactionMode } = require("./constants");
@@ -11,7 +11,7 @@ const { arrayToFlatMap } = require("./shared/common");
11
11
  const eventScheduler = require("./shared/eventScheduler");
12
12
  const eventConfig = require("./config");
13
13
  const PerformanceTracer = require("./shared/PerformanceTracer");
14
- const trace = require("./shared/openTelemetry");
14
+ const { trace } = require("./shared/openTelemetry");
15
15
  const SetIntervalDriftSafe = require("./shared/SetIntervalDriftSafe");
16
16
 
17
17
  const IMPLEMENT_ERROR_MESSAGE = "needs to be reimplemented";
@@ -66,7 +66,6 @@ class EventQueueProcessorBase {
66
66
  }
67
67
  this.#retryFailedAfter = this.#eventConfig.retryFailedAfter ?? DEFAULT_RETRY_AFTER;
68
68
  this.__concurrentEventProcessing = this.#eventConfig.multiInstanceProcessing;
69
- this.__startTime = this.#eventConfig.startTime ?? new Date();
70
69
  this.__retryAttempts = this.#isPeriodic ? 1 : this.#eventConfig.retryAttempts ?? DEFAULT_RETRY_ATTEMPTS;
71
70
  this.__selectMaxChunkSize = this.#eventConfig.selectMaxChunkSize ?? SELECT_LIMIT_EVENTS_PER_TICK;
72
71
  this.__selectNextChunk = !!this.#eventConfig.checkForNextChunk;
@@ -331,6 +330,11 @@ class EventQueueProcessorBase {
331
330
  } catch {
332
331
  /* empty */
333
332
  }
333
+ try {
334
+ queueEntry.context = JSON.parse(queueEntry.context);
335
+ } catch {
336
+ /* empty */
337
+ }
334
338
  }
335
339
 
336
340
  #determineAndAddEventStatusToMap(id, processingStatus, statusMap = this.__statusMap) {
@@ -602,7 +606,7 @@ class EventQueueProcessorBase {
602
606
  " ) AND ( status =",
603
607
  EventProcessingStatus.Open,
604
608
  "AND ( lastAttemptTimestamp <=",
605
- this.__startTime.toISOString(),
609
+ this.startTime.toISOString(),
606
610
  ...(this.isPeriodicEvent
607
611
  ? [
608
612
  "OR lastAttemptTimestamp IS NULL ) OR ( status =",
@@ -615,7 +619,7 @@ class EventQueueProcessorBase {
615
619
  "OR lastAttemptTimestamp IS NULL ) OR ( status =",
616
620
  EventProcessingStatus.Error,
617
621
  "AND lastAttemptTimestamp <=",
618
- this.__startTime.toISOString(),
622
+ this.startTime.toISOString(),
619
623
  ") OR ( status =",
620
624
  EventProcessingStatus.InProgress,
621
625
  "AND lastAttemptTimestamp <=",
@@ -859,7 +863,12 @@ class EventQueueProcessorBase {
859
863
  }
860
864
 
861
865
  continuesKeepAlive() {
862
- this.#keepAliveRunner.run(async () => {
866
+ if (Date.now() - this.lockAcquiredTime.getTime() >= this.#eventConfig.keepAliveInterval) {
867
+ trace(this.baseContext, "keepAlive-between-iterations", async () => {
868
+ await this.#renewDistributedLock();
869
+ }).catch((err) => this.logger.error("renewing lock between intervals failed!", err));
870
+ }
871
+ this.#keepAliveRunner.start(async () => {
863
872
  await this.#currentKeepAlivePromise;
864
873
  this.#currentKeepAlivePromise = executeInNewTransaction(this.__baseContext, "keepAlive", async (tx) => {
865
874
  await trace(tx.context, "keepAlive", async () => {
@@ -978,6 +987,7 @@ class EventQueueProcessorBase {
978
987
  });
979
988
  return false;
980
989
  }
990
+ this.lockAcquiredTime = new Date();
981
991
  return true;
982
992
  }
983
993
 
@@ -1000,9 +1010,8 @@ class EventQueueProcessorBase {
1000
1010
  }
1001
1011
 
1002
1012
  // NOTE: do not pass current date as we always want to calc. a future date
1003
- const cronExpression = cronParser.parseExpression(this.#eventConfig.cron, {
1004
- utc: this.#eventConfig.utc,
1005
- ...(this.#eventConfig.useCronTimezone && { tz: this.#config.cronTimezone }),
1013
+ const cronExpression = CronExpressionParser.parse(this.#eventConfig.cron, {
1014
+ tz: eventConfig.tz,
1006
1015
  });
1007
1016
  return cronExpression.next();
1008
1017
  }
@@ -1247,6 +1256,22 @@ class EventQueueProcessorBase {
1247
1256
  get eventConfig() {
1248
1257
  return this.#eventConfig;
1249
1258
  }
1259
+
1260
+ get lockAcquiredTime() {
1261
+ return this.#eventConfig.lockAcquiredTime;
1262
+ }
1263
+
1264
+ get startTime() {
1265
+ return this.#eventConfig.startTime;
1266
+ }
1267
+
1268
+ set lockAcquiredTime(value) {
1269
+ this.#eventConfig.lockAcquiredTime = value;
1270
+ }
1271
+
1272
+ get inheritTraceContext() {
1273
+ return this.#eventConfig.inheritTraceContext;
1274
+ }
1250
1275
  }
1251
1276
 
1252
1277
  module.exports = EventQueueProcessorBase;
package/src/config.js CHANGED
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
 
3
3
  const cds = require("@sap/cds");
4
- const cronParser = require("cron-parser");
4
+ const { CronExpressionParser } = require("cron-parser");
5
5
 
6
6
  const { getEnvInstance } = require("./shared/env");
7
7
  const redis = require("./shared/redis");
@@ -20,6 +20,7 @@ const DEFAULT_PRIORITY = Priorities.Medium;
20
20
  const DEFAULT_INCREASE_PRIORITY = true;
21
21
  const DEFAULT_KEEP_ALIVE_INTERVAL = 60;
22
22
  const DEFAULT_MAX_FACTOR_STUCK_2_KEEP_ALIVE_INTERVAL = 3.5;
23
+ const DEFAULT_INHERIT_TRACE_CONTEXT = true;
23
24
  const SUFFIX_PERIODIC = "_PERIODIC";
24
25
  const COMMAND_BLOCK = "EVENT_QUEUE_EVENT_BLOCK";
25
26
  const COMMAND_UNBLOCK = "EVENT_QUEUE_EVENT_UNBLOCK";
@@ -62,7 +63,7 @@ class Config {
62
63
  #cleanupLocksAndEventsForDev;
63
64
  #redisOptions;
64
65
  #insertEventsBeforeCommit;
65
- #enableCAPTelemetry;
66
+ #enableTelemetry;
66
67
  #unsubscribeHandlers = [];
67
68
  #unsubscribedTenants = {};
68
69
  #cronTimezone;
@@ -93,7 +94,9 @@ class Config {
93
94
  }
94
95
 
95
96
  getEventConfig(type, subType) {
96
- return this.#eventMap[this.generateKey(type, subType)];
97
+ return this.#eventMap[this.generateKey(type, subType)]
98
+ ? { ...this.#eventMap[this.generateKey(type, subType)] }
99
+ : undefined;
97
100
  }
98
101
 
99
102
  isCapOutboxEvent(type) {
@@ -311,6 +314,7 @@ class Config {
311
314
  multiInstanceProcessing: config.multiInstanceProcessing,
312
315
  increasePriorityOverTime: config.increasePriorityOverTime,
313
316
  keepAliveInterval: config.keepAliveInterval,
317
+ inheritTraceContext: true,
314
318
  internalEvent: true,
315
319
  };
316
320
 
@@ -461,10 +465,23 @@ class Config {
461
465
 
462
466
  if (event.cron) {
463
467
  let cron;
468
+
469
+ // NOTE: logic is as follows:
470
+ // - if event.utc is true --> always use UTC
471
+ // - if event.useCronTimezone is false OR event.cronTimezone is not defined --> use UTC as well
472
+ // - if event.utc is not true AND event.cronTimezone is set AND event.useCronTimezone is NOT set to false use event.cronTimezone
464
473
  event.utc = event.utc ?? UTC_DEFAULT;
465
- event.useCronTimezone = event.useCronTimezone ?? USE_CRON_TZ_DEFAULT;
474
+
475
+ if (!event.cronTimezone) {
476
+ event.useCronTimezone = false;
477
+ } else {
478
+ event.useCronTimezone = event.useCronTimezone ?? USE_CRON_TZ_DEFAULT;
479
+ }
480
+
481
+ event.tz = event.utc || !event.useCronTimezone ? "UTC" : event.cronTimezone;
482
+
466
483
  try {
467
- cron = cronParser.parseExpression(event.cron);
484
+ cron = CronExpressionParser.parse(event.cron);
468
485
  } catch {
469
486
  throw EventQueueError.cantParseCronExpression(event.type, event.subType, event.cron);
470
487
  }
@@ -497,6 +514,7 @@ class Config {
497
514
  if (this.isMultiTenancy && event.multiInstanceProcessing) {
498
515
  throw EventQueueError.multiInstanceProcessingNotAllowed(event.type, event.subType);
499
516
  }
517
+ event.inheritTraceContext = event.inheritTraceContext ?? DEFAULT_INHERIT_TRACE_CONTEXT;
500
518
 
501
519
  this.#basicEventValidation(event);
502
520
  }
@@ -744,18 +762,22 @@ class Config {
744
762
  return this.#insertEventsBeforeCommit;
745
763
  }
746
764
 
747
- set enableCAPTelemetry(value) {
748
- this.#enableCAPTelemetry = value;
765
+ set enableTelemetry(value) {
766
+ this.#enableTelemetry = value;
749
767
  }
750
768
 
751
- get enableCAPTelemetry() {
752
- return this.#enableCAPTelemetry;
769
+ get enableTelemetry() {
770
+ return this.#enableTelemetry;
753
771
  }
754
772
 
755
773
  get isMultiTenancy() {
756
774
  return !!cds.requires.multitenancy;
757
775
  }
758
776
 
777
+ get _rawEventMap() {
778
+ return this.#eventMap;
779
+ }
780
+
759
781
  /**
760
782
  @return { Config }
761
783
  **/
package/src/index.d.ts CHANGED
@@ -6,6 +6,7 @@ export declare const EventProcessingStatus: {
6
6
  Done: 2;
7
7
  Error: 3;
8
8
  Exceeded: 4;
9
+ Suspended: 5;
9
10
  };
10
11
 
11
12
  declare type EventProcessingStatusKeysType = keyof typeof EventProcessingStatus;
@@ -108,6 +109,7 @@ interface EventEntityPublish {
108
109
  referenceEntity?: string;
109
110
  referenceEntityKey?: string;
110
111
  payload?: string;
112
+ startAfter?: string;
111
113
  }
112
114
 
113
115
  interface EventTriggerProcessing {
@@ -145,6 +147,7 @@ export declare class EventQueueProcessorBase {
145
147
  shouldRollbackTransaction(key: string): boolean;
146
148
  beforeProcessingEvents(): Promise<void>;
147
149
  addEntryToProcessingMap(key: string, queueEntry: EventEntity, payload: Object): void;
150
+ getTxForEventProcessing(key: string): cds.Transaction;
148
151
 
149
152
  set logger(value: CdsLogger);
150
153
  get logger(): CdsLogger;
@@ -162,15 +165,11 @@ export function publishEvent(
162
165
  options?: {
163
166
  skipBroadcast?: boolean;
164
167
  skipInsertEventsBeforeCommit?: boolean;
168
+ addTraceContext?: boolean;
165
169
  }
166
170
  ): Promise<any>;
167
171
 
168
- export function processEventQueue(
169
- context: cds.EventContext,
170
- eventType: string,
171
- eventSubType: string,
172
- startTime: Date
173
- ): Promise<any>;
172
+ export function processEventQueue(context: cds.EventContext, eventType: string, eventSubType: string): Promise<any>;
174
173
 
175
174
  export function triggerEventProcessingRedis(
176
175
  tenantId: string,
@@ -256,8 +255,8 @@ declare class Config {
256
255
  get redisOptions(): any;
257
256
  set insertEventsBeforeCommit(value: any);
258
257
  get insertEventsBeforeCommit(): any;
259
- set enableCAPTelemetry(value: any);
260
- get enableCAPTelemetry(): any;
258
+ set enableTelemetry(value: any);
259
+ get enableTelemetry(): any;
261
260
  get isMultiTenancy(): boolean;
262
261
  }
263
262
 
package/src/initialize.js CHANGED
@@ -40,7 +40,7 @@ const CONFIG_VARS = [
40
40
  ["cleanupLocksAndEventsForDev", false],
41
41
  ["redisOptions", {}],
42
42
  ["insertEventsBeforeCommit", true],
43
- ["enableCAPTelemetry", false],
43
+ ["enableTelemetry", true],
44
44
  ["cronTimezone", null],
45
45
  ["publishEventBlockList", true],
46
46
  ["crashOnRedisUnavailable", false],
@@ -65,7 +65,7 @@ const CONFIG_VARS = [
65
65
  * @param {boolean} [options.cleanupLocksAndEventsForDev=false] - Cleanup locks and events for development environments.
66
66
  * @param {Object} [options.redisOptions={}] - Configuration options for Redis.
67
67
  * @param {boolean} [options.insertEventsBeforeCommit=true] - Insert events into the queue before committing the transaction.
68
- * @param {boolean} [options.enableCAPTelemetry=false] - Enable telemetry for CAP.
68
+ * @param {boolean} [options.enableTelemetry=false] - Enable telemetry for CAP.
69
69
  * @param {string} [options.cronTimezone=null] - Default timezone for cron jobs.
70
70
  * @param {string} [options.publishEventBlockList=true] - If redis is available event blocklist is distributed to all application instances
71
71
  * @param {string} [options.crashOnRedisUnavailable=true] - If enabled an error is thrown if the redis connection check is not successful
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
 
3
3
  const cds = require("@sap/cds");
4
- const cronParser = require("cron-parser");
4
+ const { CronExpressionParser } = require("cron-parser");
5
5
 
6
6
  const { EventProcessingStatus } = require("./constants");
7
7
  const { processChunkedSync } = require("./shared/common");
@@ -117,10 +117,9 @@ const _determineChangedCron = (existingEventsCron) => {
117
117
  const config = eventConfig.getEventConfig(event.type, event.subType);
118
118
  const eventStartAfter = new Date(event.startAfter);
119
119
  const eventCreatedAt = new Date(event.createdAt);
120
- const cronExpression = cronParser.parseExpression(config.cron, {
120
+ const cronExpression = CronExpressionParser.parse(config.cron, {
121
121
  currentDate: eventCreatedAt,
122
- utc: config.utc,
123
- ...(config.useCronTimezone && { tz: eventConfig.cronTimezone }),
122
+ tz: config.tz,
124
123
  });
125
124
  return Math.abs(cronExpression.next().getTime() - eventStartAfter.getTime()) > 30 * 1000; // report as changed if diff created than 30 seconds
126
125
  });
@@ -135,13 +134,10 @@ const _insertPeriodEvents = async (tx, events, now) => {
135
134
  let startTime = now;
136
135
  const config = eventConfig.getEventConfig(event.type, event.subType);
137
136
  if (config.cron) {
138
- startTime = cronParser
139
- .parseExpression(config.cron, {
140
- currentDate: now,
141
- utc: config.utc,
142
- ...(config.useCronTimezone && { tz: eventConfig.cronTimezone }),
143
- })
144
- .next();
137
+ startTime = CronExpressionParser.parse(config.cron, {
138
+ currentDate: now,
139
+ tz: config.tz,
140
+ }).next();
145
141
  }
146
142
  base.startAfter = startTime.toISOString();
147
143
  return base;
@@ -9,14 +9,15 @@ const { TransactionMode, EventProcessingStatus } = require("./constants");
9
9
  const { limiter } = require("./shared/common");
10
10
 
11
11
  const { executeInNewTransaction } = require("./shared/cdsHelper");
12
- const trace = require("./shared/openTelemetry");
12
+ const { trace } = require("./shared/openTelemetry");
13
13
 
14
14
  const COMPONENT_NAME = "/eventQueue/processEventQueue";
15
15
 
16
- const processEventQueue = async (context, eventType, eventSubType, startTime = new Date()) => {
16
+ const processEventQueue = async (context, eventType, eventSubType) => {
17
17
  let iterationCounter = 0;
18
18
  let shouldContinue = true;
19
19
  let baseInstance;
20
+ let startTime = new Date();
20
21
  try {
21
22
  let eventTypeInstance;
22
23
  const eventConfig = config.getEventConfig(eventType, eventSubType);
@@ -37,10 +38,11 @@ const processEventQueue = async (context, eventType, eventSubType, startTime = n
37
38
  if (!continueProcessing) {
38
39
  return;
39
40
  }
41
+ eventConfig.startTime = startTime;
42
+ eventConfig.lockAcquiredTime = new Date();
40
43
  if (baseInstance.isPeriodicEvent) {
41
44
  return await processPeriodicEvent(context, baseInstance);
42
45
  }
43
- eventConfig.startTime = startTime;
44
46
  while (shouldContinue) {
45
47
  iterationCounter++;
46
48
  await executeInNewTransaction(context, `eventQueue-pre-processing-${eventType}##${eventSubType}`, async (tx) => {
@@ -76,28 +78,26 @@ const processEventQueue = async (context, eventType, eventSubType, startTime = n
76
78
  if (Object.keys(eventTypeInstance.queueEntriesWithPayloadMap).length) {
77
79
  await executeInNewTransaction(context, `eventQueue-processing-${eventType}##${eventSubType}`, async (tx) => {
78
80
  eventTypeInstance.processEventContext = tx.context;
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
- await tx.rollback();
93
- }
94
- });
81
+ try {
82
+ eventTypeInstance.clusterQueueEntries(eventTypeInstance.queueEntriesWithPayloadMap);
83
+ await processEventMap(eventTypeInstance);
84
+ } catch (err) {
85
+ eventTypeInstance.handleErrorDuringClustering(err);
86
+ }
87
+ if (
88
+ eventTypeInstance.transactionMode !== TransactionMode.alwaysCommit ||
89
+ Object.entries(eventTypeInstance.eventProcessingMap).some(([key]) =>
90
+ eventTypeInstance.shouldRollbackTransaction(key)
91
+ )
92
+ ) {
93
+ await tx.rollback();
94
+ }
95
95
  });
96
96
  }
97
97
  await executeInNewTransaction(context, `eventQueue-persistStatus-${eventType}##${eventSubType}`, async (tx) => {
98
98
  await eventTypeInstance.persistEventStatus(tx);
99
99
  });
100
- shouldContinue = reevaluateShouldContinue(eventTypeInstance, iterationCounter, startTime);
100
+ shouldContinue = reevaluateShouldContinue(eventTypeInstance, iterationCounter, eventConfig.startTime);
101
101
  }
102
102
  } catch (err) {
103
103
  cds.log(COMPONENT_NAME).error("Processing event queue failed with unexpected error.", err, {
@@ -317,18 +317,30 @@ const _checkEventIsBlocked = async (baseInstance) => {
317
317
  };
318
318
 
319
319
  const _processEvent = async (eventTypeInstance, processContext, key, queueEntries, payload) => {
320
- try {
321
- const eventOutdated = await eventTypeInstance.isOutdatedAndKeepAlive(queueEntries);
322
- if (eventOutdated) {
323
- // NOTE: return empty status map to comply with the interface
324
- return {};
325
- }
326
- eventTypeInstance.setTxForEventProcessing(key, cds.tx(processContext));
327
- const statusTuple = await eventTypeInstance.processEvent(processContext, key, queueEntries, payload);
328
- return eventTypeInstance.setEventStatus(queueEntries, statusTuple);
329
- } catch (err) {
330
- return eventTypeInstance.handleErrorDuringProcessing(err, queueEntries);
320
+ let traceContext;
321
+ if (queueEntries.length === 1 && eventTypeInstance.inheritTraceContext) {
322
+ traceContext = queueEntries[0].context?.traceContext;
331
323
  }
324
+
325
+ return await trace(
326
+ eventTypeInstance.baseContext,
327
+ `process-event-${eventTypeInstance.eventType}-${eventTypeInstance.eventSubType}`,
328
+ async () => {
329
+ try {
330
+ const eventOutdated = await eventTypeInstance.isOutdatedAndKeepAlive(queueEntries);
331
+ if (eventOutdated) {
332
+ // NOTE: return empty status map to comply with the interface
333
+ return {};
334
+ }
335
+ eventTypeInstance.setTxForEventProcessing(key, cds.tx(processContext));
336
+ const statusTuple = await eventTypeInstance.processEvent(processContext, key, queueEntries, payload);
337
+ return eventTypeInstance.setEventStatus(queueEntries, statusTuple);
338
+ } catch (err) {
339
+ return eventTypeInstance.handleErrorDuringProcessing(err, queueEntries);
340
+ }
341
+ },
342
+ { traceContext }
343
+ );
332
344
  };
333
345
 
334
346
  const resilientRequire = async (eventConfig) => {
@@ -3,6 +3,7 @@
3
3
  const config = require("./config");
4
4
  const common = require("./shared/common");
5
5
  const EventQueueError = require("./EventQueueError");
6
+ const openTelemetry = require("./shared/openTelemetry");
6
7
 
7
8
  /**
8
9
  * Asynchronously publishes a series of events to the event queue.
@@ -29,7 +30,11 @@ const EventQueueError = require("./EventQueueError");
29
30
  * @throws {EventQueueError} Throws an error if the startAfter field is not a valid date.
30
31
  * @returns {Promise<*>} Returns a promise which resolves to the result of the database insert operation.
31
32
  */
32
- const publishEvent = async (tx, events, { skipBroadcast = false, skipInsertEventsBeforeCommit = false } = {}) => {
33
+ const publishEvent = async (
34
+ tx,
35
+ events,
36
+ { skipBroadcast = false, skipInsertEventsBeforeCommit = false, addTraceContext = true } = {}
37
+ ) => {
33
38
  if (!config.initialized) {
34
39
  throw EventQueueError.notInitialized();
35
40
  }
@@ -51,6 +56,10 @@ const publishEvent = async (tx, events, { skipBroadcast = false, skipInsertEvent
51
56
  if (typeof event.payload !== "string") {
52
57
  event.payload = JSON.stringify(event.payload);
53
58
  }
59
+
60
+ if (addTraceContext) {
61
+ event.context = JSON.stringify({ traceContext: openTelemetry.getCurrentTraceContext() });
62
+ }
54
63
  }
55
64
  if (config.insertEventsBeforeCommit && !skipInsertEventsBeforeCommit) {
56
65
  _registerHandlerAndAddEvents(tx, events);
@@ -9,7 +9,7 @@ const distributedLock = require("../shared/distributedLock");
9
9
  const config = require("../config");
10
10
  const common = require("../shared/common");
11
11
  const { runEventCombinationForTenant } = require("../runner/runnerHelper");
12
- const trace = require("../shared/openTelemetry");
12
+ const { trace } = require("../shared/openTelemetry");
13
13
  const { TenantIdCheckTypes } = require("../constants");
14
14
 
15
15
  const EVENT_MESSAGE_CHANNEL = "EVENT_QUEUE_MESSAGE_CHANNEL";
@@ -15,7 +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
+ const { trace } = require("../shared/openTelemetry");
19
19
 
20
20
  const COMPONENT_NAME = "/eventQueue/runner";
21
21
  const EVENT_QUEUE_RUN_ID = "EVENT_QUEUE_RUN_ID";
@@ -77,20 +77,24 @@ const _multiTenancyRedis = async () => {
77
77
  try {
78
78
  logger.info("executing event queue run for multi instance and tenant");
79
79
  const tenantIds = await cdsHelper.getAllTenantIds();
80
- await _checkPeriodicEventUpdate(tenantIds);
81
- return await _executeEventsAllTenantsRedis(tenantIds);
80
+ const shouldContinue = await _checkPeriodicEventUpdate(tenantIds);
81
+ shouldContinue && (await _executeEventsAllTenantsRedis(tenantIds));
82
82
  } catch (err) {
83
83
  logger.info("executing event queue run for multi instance and tenant failed", err);
84
84
  }
85
85
  };
86
86
 
87
+ // NOTE: _checkPeriodicEventUpdate the function must return truthy if _executeEventsAllTenantsRedis should continue
88
+ // processing open events. The idea is: if _multiTenancyPeriodicEvents is executed after the deployment we want
89
+ // to wait for all instances that periodic events are up-to-date and the updating of periodic events does not
90
+ // interfere with the processing of events
87
91
  const _checkPeriodicEventUpdate = async (tenantIds) => {
88
92
  if (!eventQueueConfig.updatePeriodicEvents || !eventQueueConfig.periodicEvents.length) {
89
93
  cds.log(COMPONENT_NAME).info("updating of periodic events is disabled or no periodic events configured", {
90
94
  updateEnabled: eventQueueConfig.updatePeriodicEvents,
91
95
  events: eventQueueConfig.periodicEvents.length,
92
96
  });
93
- return;
97
+ return true;
94
98
  }
95
99
  const hash = common.hashStringTo32Bit(JSON.stringify(tenantIds));
96
100
  if (!tenantIdHash) {
@@ -102,9 +106,12 @@ const _checkPeriodicEventUpdate = async (tenantIds) => {
102
106
  if (tenantIdHash && tenantIdHash !== hash) {
103
107
  tenantIdHash = hash;
104
108
  cds.log(COMPONENT_NAME).info("tenant id hash changed, triggering updating periodic events!");
105
- return await _multiTenancyPeriodicEvents(tenantIds).catch((err) => {
109
+ await _multiTenancyPeriodicEvents(tenantIds).catch((err) => {
106
110
  cds.log(COMPONENT_NAME).error("Error during triggering updating periodic events!", err);
107
111
  });
112
+ return true;
113
+ } else {
114
+ return true;
108
115
  }
109
116
  };
110
117
 
@@ -461,7 +468,8 @@ const _multiTenancyPeriodicEvents = async (tenantIds) => {
461
468
  }
462
469
 
463
470
  tenantIds = tenantIds ?? (await cdsHelper.getAllTenantIds());
464
- return await _executePeriodicEventsAllTenants(tenantIds);
471
+ await _executePeriodicEventsAllTenants(tenantIds);
472
+ return true;
465
473
  },
466
474
  { newRootSpan: true }
467
475
  );
@@ -8,7 +8,7 @@ const { processEventQueue } = require("../processEventQueue");
8
8
  const eventQueueConfig = require("../config");
9
9
  const WorkerQueue = require("../shared/WorkerQueue");
10
10
  const distributedLock = require("../shared/distributedLock");
11
- const trace = require("../shared/openTelemetry");
11
+ const { trace } = require("../shared/openTelemetry");
12
12
 
13
13
  const COMPONENT_NAME = "/eventQueue/runnerHelper";
14
14
 
@@ -17,6 +17,11 @@ class SetIntervalDriftSafe {
17
17
  this.#logger = cds.log(COMPONENT);
18
18
  }
19
19
 
20
+ start(fn) {
21
+ this.#shouldRun = true;
22
+ this.run(fn);
23
+ }
24
+
20
25
  run(fn) {
21
26
  if (!this.#shouldRun) {
22
27
  return;
@@ -1,32 +1,56 @@
1
1
  "use strict";
2
2
 
3
+ const _resilientRequire = (module) => {
4
+ try {
5
+ return require(module);
6
+ } catch {
7
+ // ignore
8
+ }
9
+ };
10
+
3
11
  const cds = require("@sap/cds");
4
- let otel;
5
- try {
6
- otel = require("@opentelemetry/api");
7
- } catch {
8
- // ignore
9
- }
12
+ const otel = _resilientRequire("@opentelemetry/api");
10
13
 
11
14
  const config = require("../config");
12
15
 
13
16
  const COMPONENT_NAME = "/shared/openTelemetry";
14
17
 
15
- const trace = async (context, label, fn, { attributes = {}, newRootSpan = false } = {}) => {
16
- const tracerProvider = otel?.trace.getTracerProvider();
17
- // Check if a real provider is registered
18
- if (!config.enableCAPTelemetry || !tracerProvider || tracerProvider === otel.trace.NOOP_TRACER_PROVIDER) {
18
+ const trace = async (context, label, fn, { attributes = {}, newRootSpan = false, traceContext } = {}) => {
19
+ if (!config.enableTelemetry || !otel) {
19
20
  return fn();
20
21
  }
21
22
 
22
- const tracer = otel.trace.getTracer("eventqueue");
23
- const span = tracer.startSpan(`eventqueue-${label}`, {
24
- kind: otel.SpanKind.INTERNAL,
25
- root: newRootSpan,
26
- });
23
+ const tracerProvider = otel.trace.getTracerProvider();
24
+ if ((!tracerProvider || tracerProvider === otel.trace.NOOP_TRACER_PROVIDER) && !process.env.DT_NODE_PRELOAD_OPTIONS) {
25
+ return fn();
26
+ }
27
+
28
+ const tracer = otel.trace.getTracer("@cap-js-community/event-queue");
29
+ const extractedContext = traceContext
30
+ ? otel.propagation.extract(otel.context.active(), traceContext)
31
+ : otel.context.active();
32
+ const span = tracer.startSpan(
33
+ `eventqueue-${label}`,
34
+ {
35
+ kind: otel.SpanKind.INTERNAL,
36
+ root: newRootSpan,
37
+ },
38
+ extractedContext
39
+ );
27
40
  _setAttributes(context, span, attributes);
28
- const ctxWithSpan = otel.trace.setSpan(otel.context.active(), span);
41
+ const ctxWithSpan = otel.trace.setSpan(extractedContext, span);
42
+
43
+ return await _startOtelTrace(ctxWithSpan, traceContext, span, fn);
44
+ };
45
+
46
+ const _startOtelTrace = async (ctxWithSpan, traceContext, span, fn) => {
29
47
  return otel.context.with(ctxWithSpan, async () => {
48
+ if (traceContext) {
49
+ cds.log("/eventQueue/telemetry").info("Linked span:", span.spanContext());
50
+ const carrier = {};
51
+ otel.propagation.inject(ctxWithSpan, carrier);
52
+ cds.log("/eventQueue/telemetry").info("Extracted trace context by inject", carrier);
53
+ }
30
54
  const onSuccess = (res) => {
31
55
  span.setStatus({ code: otel.SpanStatusCode.OK });
32
56
  return res;
@@ -72,4 +96,13 @@ const _setAttributes = (context, span, attributes) => {
72
96
  }
73
97
  };
74
98
 
75
- module.exports = trace;
99
+ const getCurrentTraceContext = () => {
100
+ if (!otel) {
101
+ return null;
102
+ }
103
+ const carrier = {};
104
+ otel.propagation.inject(otel.context.active(), carrier);
105
+ return carrier;
106
+ };
107
+
108
+ module.exports = { trace, getCurrentTraceContext };