@cap-js-community/event-queue 1.9.0 → 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.0",
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 () => {
@@ -929,6 +938,7 @@ class EventQueueProcessorBase {
929
938
  }
930
939
  });
931
940
  }
941
+ this.logger.info("keep alive finished!", { numberOfEvents: ids.length });
932
942
  });
933
943
  }).catch((err) => {
934
944
  this.logger.error("keep alive handling failed!", err);
@@ -971,12 +981,13 @@ class EventQueueProcessorBase {
971
981
  { expiryTime: this.#eventConfig.keepAliveMaxInProgressTime }
972
982
  );
973
983
  if (!lockAcquired) {
974
- this.logger.error("renewing redis lock failed!", {
984
+ this.logger.error("renewing distributed lock failed!", {
975
985
  type: this.#eventType,
976
986
  subType: this.#eventSubType,
977
987
  });
978
988
  return false;
979
989
  }
990
+ this.lockAcquiredTime = new Date();
980
991
  return true;
981
992
  }
982
993
 
@@ -999,9 +1010,8 @@ class EventQueueProcessorBase {
999
1010
  }
1000
1011
 
1001
1012
  // NOTE: do not pass current date as we always want to calc. a future date
1002
- const cronExpression = cronParser.parseExpression(this.#eventConfig.cron, {
1003
- utc: this.#eventConfig.utc,
1004
- ...(this.#eventConfig.useCronTimezone && { tz: this.#config.cronTimezone }),
1013
+ const cronExpression = CronExpressionParser.parse(this.#eventConfig.cron, {
1014
+ tz: eventConfig.tz,
1005
1015
  });
1006
1016
  return cronExpression.next();
1007
1017
  }
@@ -1246,6 +1256,22 @@ class EventQueueProcessorBase {
1246
1256
  get eventConfig() {
1247
1257
  return this.#eventConfig;
1248
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
+ }
1249
1275
  }
1250
1276
 
1251
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
  }
@@ -529,6 +547,10 @@ class Config {
529
547
  this.#configEvents = JSON.parse(JSON.stringify(value));
530
548
  }
531
549
 
550
+ get hasConfigEvents() {
551
+ return !!(Object.keys(this.#configEvents ?? {}).length || Object.keys(this.#configPeriodicEvents ?? {}).length);
552
+ }
553
+
532
554
  set configPeriodicEvents(value) {
533
555
  this.#configPeriodicEvents = JSON.parse(JSON.stringify(value));
534
556
  }
@@ -740,18 +762,22 @@ class Config {
740
762
  return this.#insertEventsBeforeCommit;
741
763
  }
742
764
 
743
- set enableCAPTelemetry(value) {
744
- this.#enableCAPTelemetry = value;
765
+ set enableTelemetry(value) {
766
+ this.#enableTelemetry = value;
745
767
  }
746
768
 
747
- get enableCAPTelemetry() {
748
- return this.#enableCAPTelemetry;
769
+ get enableTelemetry() {
770
+ return this.#enableTelemetry;
749
771
  }
750
772
 
751
773
  get isMultiTenancy() {
752
774
  return !!cds.requires.multitenancy;
753
775
  }
754
776
 
777
+ get _rawEventMap() {
778
+ return this.#eventMap;
779
+ }
780
+
755
781
  /**
756
782
  @return { Config }
757
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
@@ -134,7 +134,7 @@ const readConfigFromFile = async (configFilepath) => {
134
134
  "configFilepath with unsupported extension, allowed extensions are .yaml and .json"
135
135
  );
136
136
  } catch (err) {
137
- if (config.useAsCAPOutbox) {
137
+ if (!configFilepath && (config.useAsCAPOutbox || config.hasConfigEvents)) {
138
138
  return {};
139
139
  }
140
140
  throw err;
@@ -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;
@@ -150,8 +146,14 @@ const _insertPeriodEvents = async (tx, events, now) => {
150
146
  processChunkedSync(eventsToBeInserted, CHUNK_SIZE_INSERT_PERIODIC_EVENTS, (chunk) => {
151
147
  logger.info(`${counter}/${chunks} | inserting chunk of changed or new periodic events`, {
152
148
  events: chunk.map(({ type, subType, startAfter }) => {
153
- const { interval } = eventConfig.getEventConfig(type, subType);
154
- return { type, subType, interval, ...(startAfter && { startAfter }) };
149
+ const { interval, cron } = eventConfig.getEventConfig(type, subType);
150
+ return {
151
+ type,
152
+ subType,
153
+ ...(startAfter && { startAfter }),
154
+ ...(interval && { interval }),
155
+ ...(cron && { cron }),
156
+ };
155
157
  }),
156
158
  });
157
159
  counter++;
@@ -9,18 +9,19 @@ 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);
23
- const [err, EventTypeClass] = resilientRequire(eventConfig);
24
+ const [err, EventTypeClass] = await resilientRequire(eventConfig);
24
25
  if (!eventConfig || err || !(typeof EventTypeClass.constructor === "function")) {
25
26
  cds.log(COMPONENT_NAME).error("No Implementation found in the provided configuration file.", {
26
27
  eventType,
@@ -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,26 +317,50 @@ 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
- const resilientRequire = (eventConfig) => {
346
+ const resilientRequire = async (eventConfig) => {
335
347
  try {
336
348
  const path = eventConfig?.impl;
337
349
  const internal = eventConfig?.internalEvent;
338
- const module = require(pathLib.join(internal ? __dirname : process.cwd(), path));
339
- return [null, module];
350
+ const filePath = pathLib.join(internal ? __dirname : process.cwd(), path);
351
+ const fileExtension = pathLib.extname(filePath);
352
+ switch (fileExtension) {
353
+ case ".js":
354
+ return [null, require(filePath)];
355
+ case ".mjs":
356
+ return [null, (await import(`file://${filePath}`)).default];
357
+ case "":
358
+ try {
359
+ return [null, require(filePath)];
360
+ } catch {
361
+ return [null, (await import(`file://${filePath}`)).default];
362
+ }
363
+ }
340
364
  } catch (err) {
341
365
  return [err, null];
342
366
  }
@@ -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";
@@ -51,7 +51,10 @@ const _scheduleFunction = async (singleRunFn, periodicFn) => {
51
51
  }
52
52
  if (!singleRunDone) {
53
53
  singleRunDone = true;
54
- singleRunFn().catch(() => (singleRunDone = false));
54
+ singleRunFn()
55
+ .then(periodicFn)
56
+ .catch(() => (singleRunDone = false));
57
+ return;
55
58
  }
56
59
  return periodicFn();
57
60
  };
@@ -74,20 +77,24 @@ const _multiTenancyRedis = async () => {
74
77
  try {
75
78
  logger.info("executing event queue run for multi instance and tenant");
76
79
  const tenantIds = await cdsHelper.getAllTenantIds();
77
- await _checkPeriodicEventUpdate(tenantIds);
78
- return await _executeEventsAllTenantsRedis(tenantIds);
80
+ const shouldContinue = await _checkPeriodicEventUpdate(tenantIds);
81
+ shouldContinue && (await _executeEventsAllTenantsRedis(tenantIds));
79
82
  } catch (err) {
80
83
  logger.info("executing event queue run for multi instance and tenant failed", err);
81
84
  }
82
85
  };
83
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
84
91
  const _checkPeriodicEventUpdate = async (tenantIds) => {
85
92
  if (!eventQueueConfig.updatePeriodicEvents || !eventQueueConfig.periodicEvents.length) {
86
93
  cds.log(COMPONENT_NAME).info("updating of periodic events is disabled or no periodic events configured", {
87
94
  updateEnabled: eventQueueConfig.updatePeriodicEvents,
88
95
  events: eventQueueConfig.periodicEvents.length,
89
96
  });
90
- return;
97
+ return true;
91
98
  }
92
99
  const hash = common.hashStringTo32Bit(JSON.stringify(tenantIds));
93
100
  if (!tenantIdHash) {
@@ -99,9 +106,12 @@ const _checkPeriodicEventUpdate = async (tenantIds) => {
99
106
  if (tenantIdHash && tenantIdHash !== hash) {
100
107
  tenantIdHash = hash;
101
108
  cds.log(COMPONENT_NAME).info("tenant id hash changed, triggering updating periodic events!");
102
- return await _multiTenancyPeriodicEvents(tenantIds).catch((err) => {
109
+ await _multiTenancyPeriodicEvents(tenantIds).catch((err) => {
103
110
  cds.log(COMPONENT_NAME).error("Error during triggering updating periodic events!", err);
104
111
  });
112
+ return true;
113
+ } else {
114
+ return true;
105
115
  }
106
116
  };
107
117
 
@@ -458,7 +468,8 @@ const _multiTenancyPeriodicEvents = async (tenantIds) => {
458
468
  }
459
469
 
460
470
  tenantIds = tenantIds ?? (await cdsHelper.getAllTenantIds());
461
- return await _executePeriodicEventsAllTenants(tenantIds);
471
+ await _executePeriodicEventsAllTenants(tenantIds);
472
+ return true;
462
473
  },
463
474
  { newRootSpan: true }
464
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;
@@ -123,7 +123,6 @@ const getTokenInfo = async (tenantId) => {
123
123
  }
124
124
 
125
125
  if (!cds.requires?.auth.kind.match(/jwt|xsuaa/i)) {
126
- cds.log(COMPONENT_NAME).warn("Only 'jwt' or 'xsuaa' are supported as values for auth.kind.");
127
126
  return null;
128
127
  }
129
128
 
@@ -90,10 +90,20 @@ const _acquireLockRedis = async (
90
90
 
91
91
  const _renewLockRedis = async (context, fullKey, expiryTime, { value = "true" } = {}) => {
92
92
  const client = await redis.createMainClientAndConnect(config.redisOptions);
93
- const result = await client.set(fullKey, value, {
93
+ let result = await client.set(fullKey, value, {
94
94
  PX: Math.round(expiryTime),
95
95
  XX: true,
96
96
  });
97
+
98
+ if (result !== REDIS_COMMAND_OK) {
99
+ const readResult = await client.get(fullKey);
100
+ if (!readResult) {
101
+ result = await client.set(fullKey, value, {
102
+ PX: Math.round(expiryTime),
103
+ });
104
+ }
105
+ }
106
+
97
107
  return result === REDIS_COMMAND_OK;
98
108
  };
99
109
 
@@ -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 };