@cap-js-community/event-queue 0.2.5 → 1.0.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cap-js-community/event-queue",
3
- "version": "0.2.5",
3
+ "version": "1.0.0",
4
4
  "description": "An event queue that enables secure transactional processing of asynchronous events, featuring instant event processing with Redis Pub/Sub and load distribution across all application instances.",
5
5
  "main": "src/index.js",
6
6
  "files": [
@@ -22,7 +22,7 @@
22
22
  "scripts": {
23
23
  "test:unit": "jest --testPathIgnorePatterns=\"/test-integration/\"",
24
24
  "test:integration": "jest --testPathIgnorePatterns=\"/test/\" --runInBand --forceExit",
25
- "test:all": "npm run test:unit && npm run test:integration",
25
+ "test": "npm run test:unit && npm run test:integration",
26
26
  "test:all:coverage": "jest --runInBand --forceExit --collect-coverage",
27
27
  "test:prepare": "npm run build:ci --prefix=./test-integration/_env",
28
28
  "test:deploySchema": "node test-integration/_env/srv/hana/deploy.js",
@@ -39,22 +39,22 @@
39
39
  "upgrade-lock": "npx shx rm -rf package-lock.json node_modules && npm i --package-lock"
40
40
  },
41
41
  "engines": {
42
- "node": ">=16"
42
+ "node": ">=18"
43
43
  },
44
44
  "dependencies": {
45
- "redis": "4.6.11",
45
+ "redis": "4.6.12",
46
46
  "verror": "1.10.1",
47
47
  "yaml": "2.3.4"
48
48
  },
49
49
  "devDependencies": {
50
- "@sap/cds": "7.4.0",
51
- "@sap/cds-dk": "7.4.0",
52
- "eslint": "8.54.0",
53
- "eslint-config-prettier": "9.0.0",
50
+ "@sap/cds": "7.5.0",
51
+ "@sap/cds-dk": "7.5.0",
52
+ "eslint": "8.56.0",
53
+ "eslint-config-prettier": "9.1.0",
54
54
  "eslint-plugin-jest": "27.6.0",
55
55
  "eslint-plugin-node": "11.1.0",
56
56
  "express": "4.18.2",
57
- "hdb": "0.19.6",
57
+ "hdb": "0.19.7",
58
58
  "jest": "29.7.0",
59
59
  "prettier": "2.8.8",
60
60
  "sqlite3": "5.1.6"
@@ -210,12 +210,12 @@ class EventQueueError extends VError {
210
210
  message
211
211
  );
212
212
  }
213
- static loadHigherThanLimit(load) {
213
+ static loadHigherThanLimit(load, label) {
214
214
  const { message } = ERROR_CODES_META[ERROR_CODES.LOAD_HIGHER_THAN_LIMIT];
215
215
  return new EventQueueError(
216
216
  {
217
217
  name: ERROR_CODES.LOAD_HIGHER_THAN_LIMIT,
218
- info: { load },
218
+ info: { load, label },
219
219
  },
220
220
  message
221
221
  );
@@ -18,8 +18,6 @@ const DEFAULT_RETRY_ATTEMPTS = 3;
18
18
  const DEFAULT_PARALLEL_EVENT_PROCESSING = 1;
19
19
  const LIMIT_PARALLEL_EVENT_PROCESSING = 10;
20
20
  const SELECT_LIMIT_EVENTS_PER_TICK = 100;
21
- const DEFAULT_DELETE_FINISHED_EVENTS_AFTER = 0;
22
- const DAYS_TO_MS = 24 * 60 * 60 * 1000;
23
21
  const TRIES_FOR_EXCEEDED_EVENTS = 3;
24
22
  const EVENT_START_AFTER_HEADROOM = 3 * 1000;
25
23
 
@@ -34,6 +32,7 @@ class EventQueueProcessorBase {
34
32
  #eventSchedulerInstance = null;
35
33
  #eventConfig;
36
34
  #isPeriodic;
35
+ #lastSuccessfulRunTimestamp;
37
36
 
38
37
  constructor(context, eventType, eventSubType, config) {
39
38
  this.__context = context;
@@ -57,21 +56,12 @@ class EventQueueProcessorBase {
57
56
  // NOTE: keep the feature, this might be needed again
58
57
  this.__concurrentEventProcessing = false;
59
58
  this.__startTime = this.#eventConfig.startTime ?? new Date();
60
- this.__retryAttempts = this.#eventConfig.retryAttempts ?? DEFAULT_RETRY_ATTEMPTS;
59
+ this.__retryAttempts = this.#isPeriodic ? 1 : this.#eventConfig.retryAttempts ?? DEFAULT_RETRY_ATTEMPTS;
61
60
  this.__selectMaxChunkSize = this.#eventConfig.selectMaxChunkSize ?? SELECT_LIMIT_EVENTS_PER_TICK;
62
61
  this.__selectNextChunk = !!this.#eventConfig.checkForNextChunk;
63
62
  this.__keepalivePromises = {};
64
63
  this.__outdatedCheckEnabled = this.#eventConfig.eventOutdatedCheck ?? true;
65
64
  this.__transactionMode = this.#eventConfig.transactionMode ?? TransactionMode.isolated;
66
- if (this.#eventConfig.deleteFinishedEventsAfterDays) {
67
- this.__deleteFinishedEventsAfter =
68
- Number.isInteger(this.#eventConfig.deleteFinishedEventsAfterDays) &&
69
- this.#eventConfig.deleteFinishedEventsAfterDays > 0
70
- ? this.#eventConfig.deleteFinishedEventsAfterDays
71
- : DEFAULT_DELETE_FINISHED_EVENTS_AFTER;
72
- } else {
73
- this.__deleteFinishedEventsAfter = DEFAULT_DELETE_FINISHED_EVENTS_AFTER;
74
- }
75
65
  this.__emptyChunkSelected = false;
76
66
  this.__lockAcquired = false;
77
67
  this.__txUsageAllowed = true;
@@ -113,11 +103,30 @@ class EventQueueProcessorBase {
113
103
  }
114
104
 
115
105
  startPerformanceTracerEvents() {
116
- this.__performanceLoggerEvents = new PerformanceTracer(this.logger, "Processing events");
106
+ this.__performanceLoggerEvents = new PerformanceTracer(this.logger, "Processing events", {
107
+ properties: {
108
+ type: this.eventType,
109
+ subType: this.eventSubType,
110
+ },
111
+ });
112
+ }
113
+
114
+ startPerformanceTracerPeriodicEvents() {
115
+ this.__performanceLoggerPeriodicEvents = new PerformanceTracer(this.logger, "Processing periodic event", {
116
+ properties: {
117
+ type: this.eventType,
118
+ subType: this.eventSubType,
119
+ },
120
+ });
117
121
  }
118
122
 
119
123
  startPerformanceTracerPreprocessing() {
120
- this.__performanceLoggerPreprocessing = new PerformanceTracer(this.logger, "Preprocessing events");
124
+ this.__performanceLoggerPreprocessing = new PerformanceTracer(this.logger, "Preprocessing events", {
125
+ properties: {
126
+ type: this.eventType,
127
+ subType: this.eventSubType,
128
+ },
129
+ });
121
130
  }
122
131
 
123
132
  endPerformanceTracerEvents() {
@@ -130,6 +139,16 @@ class EventQueueProcessorBase {
130
139
  );
131
140
  }
132
141
 
142
+ endPerformanceTracerPeriodicEvents() {
143
+ this.__performanceLoggerPeriodicEvents?.endPerformanceTrace(
144
+ { threshold: 50 },
145
+ {
146
+ eventType: this.#eventType,
147
+ eventSubType: this.#eventSubType,
148
+ }
149
+ );
150
+ }
151
+
133
152
  endPerformanceTracerPreprocessing() {
134
153
  this.__performanceLoggerPreprocessing?.endPerformanceTrace(
135
154
  { threshold: 50 },
@@ -148,10 +167,9 @@ class EventQueueProcessorBase {
148
167
  });
149
168
  }
150
169
 
151
- logStartMessage(queueEntries) {
152
- // TODO: how to handle custom fields
170
+ logStartMessage() {
153
171
  this.logger.info("Processing queue event", {
154
- numberQueueEntries: queueEntries.length,
172
+ numberClusterEntries: Object.keys(this.eventProcessingMap).length,
155
173
  eventType: this.#eventType,
156
174
  eventSubType: this.#eventSubType,
157
175
  });
@@ -332,11 +350,11 @@ class EventQueueProcessorBase {
332
350
  });
333
351
  }
334
352
 
335
- async setPeriodicEventStatus(queueEntryIds) {
353
+ async setPeriodicEventStatus(queueEntryIds, status) {
336
354
  await this.tx.run(
337
355
  UPDATE.entity(this.#config.tableNameEventQueue)
338
356
  .set({
339
- status: EventProcessingStatus.Done,
357
+ status: status,
340
358
  })
341
359
  .where({
342
360
  ID: queueEntryIds,
@@ -426,28 +444,6 @@ class EventQueueProcessorBase {
426
444
  });
427
445
  }
428
446
 
429
- async deleteFinishedEvents(tx) {
430
- if (!this.__deleteFinishedEventsAfter) {
431
- return;
432
- }
433
- const deleteCount = await tx.run(
434
- DELETE.from(this.#config.tableNameEventQueue).where(
435
- "type =",
436
- this.#eventType,
437
- "AND subType=",
438
- this.#eventSubType,
439
- "AND lastAttemptTimestamp <=",
440
- new Date(Date.now() - this.__deleteFinishedEventsAfter * DAYS_TO_MS).toISOString()
441
- )
442
- );
443
- this.logger.debug("Deleted finished events", {
444
- eventType: this.#eventType,
445
- eventSubType: this.#eventSubType,
446
- deleteFinishedEventsAfter: this.__deleteFinishedEventsAfter,
447
- deleteCount,
448
- });
449
- }
450
-
451
447
  #ensureEveryQueueEntryHasStatus() {
452
448
  this.__queueEntries.forEach((queueEntry) => {
453
449
  if (queueEntry.ID in this.__statusMap || queueEntry.ID in this.__commitedStatusMap) {
@@ -549,15 +545,27 @@ class EventQueueProcessorBase {
549
545
  refDateStartAfter.toISOString(),
550
546
  " ) AND ( status =",
551
547
  EventProcessingStatus.Open,
552
- "OR ( status =",
553
- EventProcessingStatus.Error,
554
- "AND lastAttemptTimestamp <=",
548
+ "AND ( lastAttemptTimestamp <=",
555
549
  this.__startTime.toISOString(),
556
- ") OR ( status =",
557
- EventProcessingStatus.InProgress,
558
- "AND lastAttemptTimestamp <=",
559
- new Date(new Date().getTime() - this.#config.globalTxTimeout).toISOString(),
560
- ") )"
550
+ ...(this.isPeriodicEvent
551
+ ? [
552
+ "OR lastAttemptTimestamp IS NULL ) OR ( status =",
553
+ EventProcessingStatus.InProgress,
554
+ "AND lastAttemptTimestamp <=",
555
+ new Date(new Date().getTime() - this.#config.globalTxTimeout).toISOString(),
556
+ ") )",
557
+ ]
558
+ : [
559
+ "OR lastAttemptTimestamp IS NULL ) OR ( status =",
560
+ EventProcessingStatus.Error,
561
+ "AND lastAttemptTimestamp <=",
562
+ this.__startTime.toISOString(),
563
+ ") OR ( status =",
564
+ EventProcessingStatus.InProgress,
565
+ "AND lastAttemptTimestamp <=",
566
+ new Date(new Date().getTime() - this.#config.globalTxTimeout).toISOString(),
567
+ ") )",
568
+ ])
561
569
  )
562
570
  .orderBy("createdAt", "ID")
563
571
  );
@@ -575,9 +583,11 @@ class EventQueueProcessorBase {
575
583
  entries,
576
584
  refDateStartAfter
577
585
  );
578
- const eventsForProcessing = exceededTries.concat(openEvents).concat(exceededTriesExceeded);
586
+ const eventsForProcessing = openEvents
587
+ .concat(exceededTriesExceeded)
588
+ .concat(this.#isPeriodic ? [] : exceededTries);
579
589
  this.#selectedEventMap = arrayToFlatMap(eventsForProcessing);
580
- if (exceededTries.length) {
590
+ if (!this.#isPeriodic && exceededTries.length) {
581
591
  this.#eventsWithExceededTries = exceededTries;
582
592
  }
583
593
  if (exceededTriesExceeded.length) {
@@ -586,13 +596,30 @@ class EventQueueProcessorBase {
586
596
  this.#handleDelayedEvents(delayedEvents);
587
597
 
588
598
  result = openEvents;
589
- this.logger[eventsForProcessing.length ? "info" : "debug"]("Selected event queue entries for processing", {
590
- openEvents: openEvents.length,
591
- ...(delayedEvents.length && { delayedEvents: delayedEvents.length }),
592
- ...(exceededTries.length && { exceededTries: exceededTries.length }),
593
- eventType: this.#eventType,
594
- eventSubType: this.#eventSubType,
595
- });
599
+ this.logger[eventsForProcessing.length && !this.isPeriodicEvent ? "info" : "debug"](
600
+ "Selected event queue entries for processing",
601
+ {
602
+ openEvents: openEvents.length,
603
+ ...(delayedEvents.length && { delayedEvents: delayedEvents.length }),
604
+ ...(exceededTries.length && { exceededTries: exceededTries.length }),
605
+ eventType: this.#eventType,
606
+ eventSubType: this.#eventSubType,
607
+ }
608
+ );
609
+
610
+ if (this.#isPeriodic && exceededTries.length) {
611
+ await tx.run(
612
+ UPDATE.entity(this.#config.tableNameEventQueue)
613
+ .set({
614
+ status: EventProcessingStatus.Error,
615
+ lastAttemptTimestamp: new Date(),
616
+ })
617
+ .where(
618
+ "ID IN",
619
+ exceededTries.map(({ ID }) => ID)
620
+ )
621
+ );
622
+ }
596
623
 
597
624
  if (!eventsForProcessing.length) {
598
625
  this.__emptyChunkSelected = true;
@@ -623,10 +650,26 @@ class EventQueueProcessorBase {
623
650
  });
624
651
  this.__queueEntries = result;
625
652
  this.__queueEntriesMap = arrayToFlatMap(result);
653
+
654
+ if (this.#isPeriodic && this.#eventConfig.lastSuccessfulRunTimestamp) {
655
+ this.#lastSuccessfulRunTimestamp = await this.#selectLastSuccessfulPeriodicTimestamp(tx);
656
+ }
626
657
  });
627
658
  return result;
628
659
  }
629
660
 
661
+ async #selectLastSuccessfulPeriodicTimestamp() {
662
+ const entry = await SELECT.one
663
+ .from(this.#config.tableNameEventQueue)
664
+ .where({
665
+ type: this.#eventType,
666
+ subType: this.#eventSubType,
667
+ status: EventProcessingStatus.Done,
668
+ })
669
+ .columns("max (lastAttemptTimestamp) as lastAttemptsTs");
670
+ return entry.lastAttemptsTs;
671
+ }
672
+
630
673
  #handleDelayedEvents(delayedEvents) {
631
674
  for (const delayedEvent of delayedEvents) {
632
675
  this.#eventSchedulerInstance.scheduleEvent(
@@ -924,10 +967,37 @@ class EventQueueProcessorBase {
924
967
  obsoleteEntries.push(queueEntry);
925
968
  }
926
969
  }
927
- await this.setPeriodicEventStatus(obsoleteEntries.map(({ ID }) => ID));
970
+ await this.setPeriodicEventStatus(
971
+ obsoleteEntries.map(({ ID }) => ID),
972
+ EventProcessingStatus.Done
973
+ );
928
974
  return queueEntryToUse;
929
975
  }
930
976
 
977
+ /**
978
+ * Asynchronously gets the timestamp of the last successful run.
979
+ *
980
+ * @returns {Promise<string|null>} A Promise that resolves to a string representation of the timestamp
981
+ * of the last successful run (in ISO 8601 format: YYYY-MM-DDTHH:mm:ss.sss),
982
+ * or null if there has been no successful run yet.
983
+ *
984
+ * @example
985
+ * const timestamp = await instance.getLastSuccessfulRunTimestamp();
986
+ * console.log(timestamp); // Outputs: 2023-12-07T09:15:44.237
987
+ *
988
+ * @throws {Error} If an error occurs while fetching the timestamp.
989
+ */
990
+ async getLastSuccessfulRunTimestamp() {
991
+ if (!this.#isPeriodic) {
992
+ return null;
993
+ }
994
+ if (this.#lastSuccessfulRunTimestamp === undefined) {
995
+ this.#lastSuccessfulRunTimestamp = await this.#selectLastSuccessfulPeriodicTimestamp();
996
+ }
997
+
998
+ return this.#lastSuccessfulRunTimestamp;
999
+ }
1000
+
931
1001
  statusMapContainsError(statusMap) {
932
1002
  return Object.values(statusMap).includes(EventProcessingStatus.Error);
933
1003
  }
package/src/config.js CHANGED
@@ -9,9 +9,24 @@ const EventQueueError = require("./EventQueueError");
9
9
  const FOR_UPDATE_TIMEOUT = 10;
10
10
  const GLOBAL_TX_TIMEOUT = 30 * 60 * 1000;
11
11
  const REDIS_CONFIG_CHANNEL = "EVENT_QUEUE_CONFIG_CHANNEL";
12
+ const REDIS_CONFIG_BLOCKLIST_CHANNEL = "REDIS_CONFIG_BLOCKLIST_CHANNEL";
12
13
  const COMPONENT_NAME = "eventQueue/config";
13
14
  const MIN_INTERVAL_SEC = 10;
14
15
  const DEFAULT_LOAD = 1;
16
+ const SUFFIX_PERIODIC = "_PERIODIC";
17
+ const COMMAND_BLOCK = "EVENT_QUEUE_EVENT_BLOCK";
18
+ const COMMAND_UNBLOCK = "EVENT_QUEUE_EVENT_UNBLOCK";
19
+
20
+ const BASE_PERIODIC_EVENTS = [
21
+ {
22
+ type: "EVENT_QUEUE_BASE",
23
+ subType: "DELETE_EVENTS",
24
+ impl: "./housekeeping/EventQueueDeleteEvents",
25
+ load: 1,
26
+ interval: 86400, // 1 day,
27
+ internalEvent: true,
28
+ },
29
+ ];
15
30
 
16
31
  class Config {
17
32
  #logger;
@@ -24,7 +39,7 @@ class Config {
24
39
  #instanceLoadLimit;
25
40
  #tableNameEventQueue;
26
41
  #tableNameEventLock;
27
- #isRunnerDeactivated;
42
+ #isEventQueueActive;
28
43
  #configFilePath;
29
44
  #processEventsAfterPublish;
30
45
  #skipCsnCheck;
@@ -33,6 +48,8 @@ class Config {
33
48
  #env;
34
49
  #eventMap;
35
50
  #updatePeriodicEvents;
51
+ #blockedPeriodicEvents;
52
+ #isPeriodicEventBlockedCb;
36
53
  static #instance;
37
54
  constructor() {
38
55
  this.#logger = cds.log(COMPONENT_NAME);
@@ -42,15 +59,16 @@ class Config {
42
59
  this.#runInterval = null;
43
60
  this.#redisEnabled = null;
44
61
  this.#initialized = false;
45
- this.#instanceLoadLimit = null;
62
+ this.#instanceLoadLimit = 100;
46
63
  this.#tableNameEventQueue = null;
47
64
  this.#tableNameEventLock = null;
48
- this.#isRunnerDeactivated = false;
65
+ this.#isEventQueueActive = true;
49
66
  this.#configFilePath = null;
50
67
  this.#processEventsAfterPublish = null;
51
68
  this.#skipCsnCheck = null;
52
69
  this.#disableRedis = null;
53
70
  this.#env = getEnvInstance();
71
+ this.#blockedPeriodicEvents = {};
54
72
  }
55
73
 
56
74
  getEventConfig(type, subType) {
@@ -70,6 +88,7 @@ class Config {
70
88
  }
71
89
 
72
90
  attachConfigChangeHandler() {
91
+ this.#attachBlocklistChangeHandler();
73
92
  redis.subscribeRedisChannel(REDIS_CONFIG_CHANNEL, (messageData) => {
74
93
  try {
75
94
  const { key, value } = JSON.parse(messageData);
@@ -78,7 +97,7 @@ class Config {
78
97
  this[key] = value;
79
98
  }
80
99
  } catch (err) {
81
- this.#logger.error("could not parse event config change", {
100
+ this.#logger.error("could not parse event config change", err, {
82
101
  messageData,
83
102
  });
84
103
  }
@@ -95,18 +114,102 @@ class Config {
95
114
  });
96
115
  }
97
116
 
98
- get isRunnerDeactivated() {
99
- return this.#isRunnerDeactivated;
117
+ #attachBlocklistChangeHandler() {
118
+ redis.subscribeRedisChannel(REDIS_CONFIG_BLOCKLIST_CHANNEL, (messageData) => {
119
+ try {
120
+ const { command, key, tenant } = JSON.parse(messageData);
121
+ if (command === COMMAND_BLOCK) {
122
+ this.#blockPeriodicEventLocalState(key, tenant);
123
+ } else {
124
+ this.#unblockPeriodicEventLocalState(key, tenant);
125
+ }
126
+ } catch (err) {
127
+ this.#logger.error("could not parse event blocklist change", err, {
128
+ messageData,
129
+ });
130
+ }
131
+ });
132
+ }
133
+
134
+ blockPeriodicEvent(type, subType, tenant = "*") {
135
+ const typeWithSuffix = `${type}${SUFFIX_PERIODIC}`;
136
+ const config = this.getEventConfig(typeWithSuffix, subType);
137
+ if (!config) {
138
+ return;
139
+ }
140
+ const key = this.generateKey(typeWithSuffix, subType);
141
+ this.#blockPeriodicEventLocalState(key, tenant);
142
+ if (!this.redisEnabled) {
143
+ return;
144
+ }
145
+
146
+ redis
147
+ .publishMessage(REDIS_CONFIG_BLOCKLIST_CHANNEL, JSON.stringify({ command: COMMAND_BLOCK, key, tenant }))
148
+ .catch((error) => {
149
+ this.#logger.error(`publishing config block failed key: ${key}`, error);
150
+ });
151
+ }
152
+
153
+ #blockPeriodicEventLocalState(key, tenant) {
154
+ this.#blockedPeriodicEvents[key] ??= {};
155
+ this.#blockedPeriodicEvents[key][tenant] = true;
156
+ return key;
157
+ }
158
+
159
+ clearPeriodicEventBlockList() {
160
+ this.#blockedPeriodicEvents = {};
161
+ }
162
+
163
+ unblockPeriodicEvent(type, subType, tenant = "*") {
164
+ const typeWithSuffix = `${type}${SUFFIX_PERIODIC}`;
165
+ const key = this.generateKey(typeWithSuffix, subType);
166
+ const config = this.getEventConfig(typeWithSuffix, subType);
167
+ if (!config) {
168
+ return;
169
+ }
170
+ this.#unblockPeriodicEventLocalState(key, tenant);
171
+ if (!this.redisEnabled) {
172
+ return;
173
+ }
174
+
175
+ redis
176
+ .publishMessage(REDIS_CONFIG_BLOCKLIST_CHANNEL, JSON.stringify({ command: COMMAND_UNBLOCK, key, tenant }))
177
+ .catch((error) => {
178
+ this.#logger.error(`publishing config block failed key: ${key}`, error);
179
+ });
180
+ }
181
+
182
+ #unblockPeriodicEventLocalState(key, tenant) {
183
+ const map = this.#blockedPeriodicEvents[key];
184
+ if (!map) {
185
+ return;
186
+ }
187
+ this.#blockedPeriodicEvents[key][tenant] = false;
188
+ return key;
189
+ }
190
+
191
+ isPeriodicEventBlocked(type, subType, tenant) {
192
+ const map = this.#blockedPeriodicEvents[this.generateKey(type, subType)];
193
+ if (!map) {
194
+ return false;
195
+ }
196
+ const tenantSpecific = map[tenant];
197
+ const allTenants = map["*"];
198
+ return tenantSpecific ?? allTenants;
199
+ }
200
+
201
+ get isEventQueueActive() {
202
+ return this.#isEventQueueActive;
100
203
  }
101
204
 
102
- set isRunnerDeactivated(value) {
103
- this.#isRunnerDeactivated = value;
205
+ set isEventQueueActive(value) {
206
+ this.#isEventQueueActive = value;
104
207
  }
105
208
 
106
209
  set fileContent(config) {
107
210
  this.#config = config;
108
211
  config.events = config.events ?? [];
109
- config.periodicEvents = config.periodicEvents ?? [];
212
+ config.periodicEvents = (config.periodicEvents ?? []).concat(BASE_PERIODIC_EVENTS.map((event) => ({ ...event })));
110
213
  this.#eventMap = config.events.reduce((result, event) => {
111
214
  event.load = event.load ?? DEFAULT_LOAD;
112
215
  this.validateAdHocEvents(result, event);
@@ -115,7 +218,6 @@ class Config {
115
218
  }, {});
116
219
  this.#eventMap = config.periodicEvents.reduce((result, event) => {
117
220
  event.load = event.load ?? DEFAULT_LOAD;
118
- const SUFFIX_PERIODIC = "_PERIODIC";
119
221
  event.type = `${event.type}${SUFFIX_PERIODIC}`;
120
222
  event.isPeriodic = true;
121
223
  this.validatePeriodicConfig(result, event);
@@ -222,6 +324,14 @@ class Config {
222
324
  this.#instanceLoadLimit = value;
223
325
  }
224
326
 
327
+ get isPeriodicEventBlockedCb() {
328
+ return this.#isPeriodicEventBlockedCb;
329
+ }
330
+
331
+ set isPeriodicEventBlockedCb(value) {
332
+ this.#isPeriodicEventBlockedCb = value;
333
+ }
334
+
225
335
  get tableNameEventQueue() {
226
336
  return this.#tableNameEventQueue;
227
337
  }
@@ -0,0 +1,51 @@
1
+ "use strict";
2
+
3
+ const EventQueueBaseClass = require("../EventQueueProcessorBase");
4
+ const config = require("../config");
5
+ const eventConfig = require("../config");
6
+
7
+ const DELETE_DEFAULT_PERIOD_IN_DAYS = 7;
8
+ const DAY_IN_MS = 24 * 60 * 60 * 1000;
9
+
10
+ class EventQueueDeleteEvents extends EventQueueBaseClass {
11
+ constructor(context, eventType, eventSubType, config) {
12
+ super(context, eventType, eventSubType, config);
13
+ }
14
+
15
+ async processPeriodicEvent(processContext, key) {
16
+ const tx = this.getTxForEventProcessing(key);
17
+ const deleteByDaysMap = config.events.concat(config.periodicEvents).reduce((result, event) => {
18
+ if (!event.deleteFinishedEventsAfterDays) {
19
+ event.deleteFinishedEventsAfterDays = DELETE_DEFAULT_PERIOD_IN_DAYS;
20
+ }
21
+ result[event.deleteFinishedEventsAfterDays] ??= [];
22
+ result[event.deleteFinishedEventsAfterDays].push(event);
23
+ return result;
24
+ }, {});
25
+
26
+ for (const [deleteAfter, events] of Object.entries(deleteByDaysMap)) {
27
+ const deleteCount = await tx.run(
28
+ DELETE.from(eventConfig.tableNameEventQueue).where([
29
+ { list: [{ ref: ["type"] }, { ref: ["subType"] }] },
30
+ "IN",
31
+ {
32
+ list: events.map((event) => ({
33
+ list: [{ val: event.type }, { val: event.subType }],
34
+ })),
35
+ },
36
+ "AND",
37
+ { ref: ["lastAttemptTimestamp"] },
38
+ "<=",
39
+ { val: new Date(processContext.timestamp.getTime() - deleteAfter * DAY_IN_MS).toISOString() },
40
+ ])
41
+ );
42
+ this.logger.info("deleted eligible events", {
43
+ deleteAfterDays: deleteAfter,
44
+ deleteCount,
45
+ events: events.map((event) => `${event.type}_${event.subType}`),
46
+ });
47
+ }
48
+ }
49
+ }
50
+
51
+ module.exports = EventQueueDeleteEvents;
package/src/initialize.js CHANGED
@@ -27,9 +27,8 @@ const CONFIG_VARS = [
27
27
  ["configFilePath", null],
28
28
  ["registerAsEventProcessor", true],
29
29
  ["processEventsAfterPublish", true],
30
- ["isRunnerDeactivated", false],
30
+ ["isEventQueueActive", true],
31
31
  ["runInterval", 5 * 60 * 1000],
32
- ["instanceLoadLimit", 20],
33
32
  ["tableNameEventQueue", BASE_TABLES.EVENT],
34
33
  ["tableNameEventLock", BASE_TABLES.LOCK],
35
34
  ["disableRedis", false],
@@ -41,9 +40,8 @@ const initialize = async ({
41
40
  configFilePath,
42
41
  registerAsEventProcessor,
43
42
  processEventsAfterPublish,
44
- isRunnerDeactivated,
43
+ isEventQueueActive,
45
44
  runInterval,
46
- instanceLoadLimit,
47
45
  tableNameEventQueue,
48
46
  tableNameEventLock,
49
47
  disableRedis,
@@ -52,7 +50,7 @@ const initialize = async ({
52
50
  } = {}) => {
53
51
  // TODO: initialize check:
54
52
  // - content of yaml check
55
- // - betweenRuns and instanceLoadLimit
53
+ // - betweenRuns
56
54
 
57
55
  if (config.initialized) {
58
56
  return;
@@ -63,9 +61,8 @@ const initialize = async ({
63
61
  configFilePath,
64
62
  registerAsEventProcessor,
65
63
  processEventsAfterPublish,
66
- isRunnerDeactivated,
64
+ isEventQueueActive,
67
65
  runInterval,
68
- instanceLoadLimit,
69
66
  tableNameEventQueue,
70
67
  tableNameEventLock,
71
68
  disableRedis,
@@ -92,7 +89,6 @@ const initialize = async ({
92
89
  multiTenancyEnabled: config.isMultiTenancy,
93
90
  redisEnabled: config.redisEnabled,
94
91
  runInterval: config.runInterval,
95
- config: config.instanceLoadLimit,
96
92
  });
97
93
  };
98
94
 
@@ -5,7 +5,7 @@ const pathLib = require("path");
5
5
  const cds = require("@sap/cds");
6
6
 
7
7
  const config = require("./config");
8
- const { TransactionMode } = require("./constants");
8
+ const { TransactionMode, EventProcessingStatus } = require("./constants");
9
9
  const { limiter } = require("./shared/common");
10
10
 
11
11
  const { executeInNewTransaction, TriggerRollback } = require("./shared/cdsHelper");
@@ -20,7 +20,7 @@ const processEventQueue = async (context, eventType, eventSubType, startTime = n
20
20
  try {
21
21
  let eventTypeInstance;
22
22
  const eventConfig = config.getEventConfig(eventType, eventSubType);
23
- const [err, EventTypeClass] = resilientRequire(eventConfig?.impl);
23
+ const [err, EventTypeClass] = resilientRequire(eventConfig);
24
24
  if (!eventConfig || err || !(typeof EventTypeClass.constructor === "function")) {
25
25
  cds.log(COMPONENT_NAME).error("No Implementation found in the provided configuration file.", {
26
26
  eventType,
@@ -90,15 +90,6 @@ const processEventQueue = async (context, eventType, eventSubType, startTime = n
90
90
  await eventTypeInstance.persistEventStatus(tx);
91
91
  });
92
92
  shouldContinue = reevaluateShouldContinue(eventTypeInstance, iterationCounter, startTime);
93
- if (!shouldContinue) {
94
- await executeInNewTransaction(
95
- context,
96
- `eventQueue-deleteFinishedEvents-${eventType}##${eventSubType}`,
97
- async (tx) => {
98
- await eventTypeInstance.deleteFinishedEvents(tx);
99
- }
100
- );
101
- }
102
93
  }
103
94
  } catch (err) {
104
95
  cds.log(COMPONENT_NAME).error("Processing event queue failed with unexpected error.", err, {
@@ -124,11 +115,35 @@ const reevaluateShouldContinue = (eventTypeInstance, iterationCounter, startTime
124
115
  return false;
125
116
  };
126
117
 
127
- // TODO: don't forget to release lock
128
118
  const processPeriodicEvent = async (eventTypeInstance) => {
129
119
  let queueEntry;
130
120
  let processNext = true;
131
121
 
122
+ const isPeriodicEventBlockedCb = config.isPeriodicEventBlockedCb;
123
+ const params = [eventTypeInstance.eventType, eventTypeInstance.eventSubType, eventTypeInstance.context.tenant];
124
+ let eventBlocked = false;
125
+ if (isPeriodicEventBlockedCb) {
126
+ try {
127
+ eventBlocked = await isPeriodicEventBlockedCb(...params);
128
+ } catch (err) {
129
+ eventBlocked = true;
130
+ eventTypeInstance.logger.error("skipping run because periodic event blocked check failed!", err, {
131
+ type: eventTypeInstance.eventType,
132
+ subType: eventTypeInstance.eventSubType,
133
+ });
134
+ }
135
+ } else {
136
+ eventBlocked = config.isPeriodicEventBlocked(...params);
137
+ }
138
+
139
+ if (eventBlocked) {
140
+ eventTypeInstance.logger.info("skipping run because periodic event is blocked by configuration", {
141
+ type: eventTypeInstance.eventType,
142
+ subType: eventTypeInstance.eventSubType,
143
+ });
144
+ return;
145
+ }
146
+
132
147
  try {
133
148
  while (processNext) {
134
149
  await executeInNewTransaction(
@@ -153,6 +168,7 @@ const processPeriodicEvent = async (eventTypeInstance) => {
153
168
  return;
154
169
  }
155
170
 
171
+ let status = EventProcessingStatus.Done;
156
172
  await executeInNewTransaction(
157
173
  eventTypeInstance.context,
158
174
  `eventQueue-periodic-process-${eventTypeInstance.eventType}##${eventTypeInstance.eventSubType}`,
@@ -160,13 +176,17 @@ const processPeriodicEvent = async (eventTypeInstance) => {
160
176
  eventTypeInstance.processEventContext = tx.context;
161
177
  eventTypeInstance.setTxForEventProcessing(queueEntry.ID, cds.tx(tx.context));
162
178
  try {
179
+ eventTypeInstance.startPerformanceTracerPeriodicEvents();
163
180
  await eventTypeInstance.processPeriodicEvent(tx.context, queueEntry.ID, queueEntry);
164
181
  } catch (err) {
182
+ status = EventProcessingStatus.Error;
165
183
  eventTypeInstance.handleErrorDuringPeriodicEventProcessing(err, queueEntry);
166
184
  throw new TriggerRollback();
185
+ } finally {
186
+ eventTypeInstance.endPerformanceTracerPeriodicEvents();
167
187
  }
168
188
  if (
169
- eventTypeInstance.transactionMode !== TransactionMode.alwaysCommit ||
189
+ eventTypeInstance.transactionMode === TransactionMode.alwaysRollback ||
170
190
  eventTypeInstance.shouldRollbackTransaction(queueEntry.ID)
171
191
  ) {
172
192
  throw new TriggerRollback();
@@ -179,7 +199,7 @@ const processPeriodicEvent = async (eventTypeInstance) => {
179
199
  `eventQueue-periodic-setStatus-${eventTypeInstance.eventType}##${eventTypeInstance.eventSubType}`,
180
200
  async (tx) => {
181
201
  eventTypeInstance.processEventContext = tx.context;
182
- await eventTypeInstance.setPeriodicEventStatus(queueEntry.ID);
202
+ await eventTypeInstance.setPeriodicEventStatus(queueEntry.ID, status);
183
203
  }
184
204
  );
185
205
  }
@@ -196,6 +216,7 @@ const processPeriodicEvent = async (eventTypeInstance) => {
196
216
  const processEventMap = async (eventTypeInstance) => {
197
217
  eventTypeInstance.startPerformanceTracerEvents();
198
218
  await eventTypeInstance.beforeProcessingEvents();
219
+ eventTypeInstance.logStartMessage();
199
220
  if (eventTypeInstance.commitOnEventLevel) {
200
221
  eventTypeInstance.txUsageAllowed = false;
201
222
  }
@@ -244,7 +265,6 @@ const processEventMap = async (eventTypeInstance) => {
244
265
 
245
266
  const _processEvent = async (eventTypeInstance, processContext, key, queueEntries, payload) => {
246
267
  try {
247
- eventTypeInstance.logStartMessage(queueEntries);
248
268
  const eventOutdated = await eventTypeInstance.isOutdatedAndKeepalive(queueEntries);
249
269
  if (eventOutdated) {
250
270
  return;
@@ -257,9 +277,11 @@ const _processEvent = async (eventTypeInstance, processContext, key, queueEntrie
257
277
  }
258
278
  };
259
279
 
260
- const resilientRequire = (path) => {
280
+ const resilientRequire = (eventConfig) => {
261
281
  try {
262
- const module = require(pathLib.join(process.cwd(), path));
282
+ const path = eventConfig?.impl;
283
+ const internal = eventConfig?.internalEvent;
284
+ const module = require(pathLib.join(internal ? __dirname : process.cwd(), path));
263
285
  return [null, module];
264
286
  } catch (err) {
265
287
  return [err, null];
@@ -1,9 +1,12 @@
1
1
  "use strict";
2
2
 
3
+ const cds = require("@sap/cds");
4
+
3
5
  const redis = require("./shared/redis");
4
6
  const { checkLockExistsAndReturnValue } = require("./shared/distributedLock");
5
7
  const config = require("./config");
6
8
  const { runEventCombinationForTenant } = require("./runner");
9
+ const { getSubdomainForTenantId } = require("./shared/cdsHelper");
7
10
 
8
11
  const EVENT_MESSAGE_CHANNEL = "EVENT_QUEUE_MESSAGE_CHANNEL";
9
12
  const COMPONENT_NAME = "eventQueue/redisPubSub";
@@ -26,7 +29,23 @@ const messageHandlerProcessEvents = async (messageData) => {
26
29
  type,
27
30
  subType,
28
31
  });
29
- await runEventCombinationForTenant(tenantId, type, subType);
32
+ if (!config.isEventQueueActive) {
33
+ cds.log(COMPONENT_NAME).info("Skipping processing because runner is deactivated!", {
34
+ type,
35
+ subType,
36
+ });
37
+ return;
38
+ }
39
+
40
+ const subdomain = await getSubdomainForTenantId(tenantId);
41
+ const tenantContext = {
42
+ tenant: tenantId,
43
+ // NOTE: we need this because of logging otherwise logs would not contain the subdomain
44
+ http: { req: { authInfo: { getSubdomain: () => subdomain } } },
45
+ };
46
+ return await cds.tx(tenantContext, async ({ context }) => {
47
+ return await runEventCombinationForTenant(context, type, subType);
48
+ });
30
49
  } catch (err) {
31
50
  logger.error("could not parse event information", {
32
51
  messageData,
@@ -36,13 +55,32 @@ const messageHandlerProcessEvents = async (messageData) => {
36
55
 
37
56
  const broadcastEvent = async (tenantId, type, subType) => {
38
57
  const logger = cds.log(COMPONENT_NAME);
39
- if (!config.redisEnabled) {
40
- if (config.registerAsEventProcessor) {
41
- await runEventCombinationForTenant(tenantId, type, subType);
42
- }
43
- return;
44
- }
45
58
  try {
59
+ if (!config.isEventQueueActive) {
60
+ cds.log(COMPONENT_NAME).info("Skipping processing because runner is deactivated!", {
61
+ type,
62
+ subType,
63
+ });
64
+ return;
65
+ }
66
+ if (!config.redisEnabled) {
67
+ if (config.registerAsEventProcessor) {
68
+ let context = {};
69
+ if (tenantId) {
70
+ const subdomain = await getSubdomainForTenantId(tenantId);
71
+ context = {
72
+ // NOTE: we need this because of logging otherwise logs would not contain the subdomain
73
+ tenant: tenantId,
74
+ http: { req: { authInfo: { getSubdomain: () => subdomain } } },
75
+ };
76
+ }
77
+
78
+ return await cds.tx(context, async ({ context }) => {
79
+ return await runEventCombinationForTenant(context, type, subType);
80
+ });
81
+ }
82
+ return;
83
+ }
46
84
  const result = await checkLockExistsAndReturnValue(
47
85
  new cds.EventContext({ tenant: tenantId }),
48
86
  [type, subType].join("##")
package/src/runner.js CHANGED
@@ -37,7 +37,7 @@ const _scheduleFunction = async (singleRunFn, periodicFn) => {
37
37
 
38
38
  const fnWithRunningCheck = () => {
39
39
  const logger = cds.log(COMPONENT_NAME);
40
- if (eventQueueConfig.isRunnerDeactivated) {
40
+ if (!eventQueueConfig.isEventQueueActive) {
41
41
  logger.info("runner is deactivated via config variable. Skipping this run.");
42
42
  return;
43
43
  }
@@ -82,6 +82,9 @@ const _checkAndTriggerPeriodicEventUpdate = (tenantIds) => {
82
82
  const hash = hashStringTo32Bit(JSON.stringify(tenantIds));
83
83
  if (!tenantIdHash) {
84
84
  tenantIdHash = hash;
85
+ _multiTenancyPeriodicEvents().catch((err) => {
86
+ cds.log(COMPONENT_NAME).error("Error during triggering updating periodic events!", err);
87
+ });
85
88
  return;
86
89
  }
87
90
  if (tenantIdHash && tenantIdHash !== hash) {
@@ -94,45 +97,63 @@ const _checkAndTriggerPeriodicEventUpdate = (tenantIds) => {
94
97
 
95
98
  const _executeEventsAllTenants = (tenantIds, runId) => {
96
99
  const events = eventQueueConfig.allEvents;
97
- const promises = [];
98
- tenantIds.forEach((tenantId) => {
100
+ const product = tenantIds.reduce((result, tenantId) => {
99
101
  events.forEach((event) => {
100
- promises.push(
101
- WorkerQueue.instance.addToQueue(event.load, async () => {
102
- try {
103
- const lockId = `${runId}_${event.type}_${event.subType}`;
104
- const tenantContext = new cds.EventContext({ tenant: tenantId });
105
- const couldAcquireLock = await distributedLock.acquireLock(tenantContext, lockId, {
106
- expiryTime: eventQueueConfig.runInterval * 0.95,
107
- });
108
- if (!couldAcquireLock) {
109
- return;
110
- }
111
- await runEventCombinationForTenant(tenantId, event.type, event.subType, true);
112
- } catch (err) {
113
- cds.log(COMPONENT_NAME).error("executing event-queue run for tenant failed", {
114
- tenantId,
115
- });
102
+ result.push([tenantId, event]);
103
+ });
104
+ return result;
105
+ }, []);
106
+
107
+ return product.map(async ([tenantId, event]) => {
108
+ const subdomain = await getSubdomainForTenantId(tenantId);
109
+ const tenantContext = {
110
+ tenant: tenantId,
111
+ // NOTE: we need this because of logging otherwise logs would not contain the subdomain
112
+ http: { req: { authInfo: { getSubdomain: () => subdomain } } },
113
+ };
114
+ return await cds.tx(tenantContext, async ({ context }) => {
115
+ const label = `${event.type}_${event.subType}`;
116
+ return await WorkerQueue.instance.addToQueue(event.load, label, async () => {
117
+ try {
118
+ const lockId = `${runId}_${label}`;
119
+ const couldAcquireLock = await distributedLock.acquireLock(context, lockId, {
120
+ expiryTime: eventQueueConfig.runInterval * 0.95,
121
+ });
122
+ if (!couldAcquireLock) {
123
+ return;
116
124
  }
117
- })
118
- );
125
+ await runEventCombinationForTenant(context, event.type, event.subType, true);
126
+ } catch (err) {
127
+ cds.log(COMPONENT_NAME).error("executing event-queue run for tenant failed", {
128
+ tenantId,
129
+ });
130
+ }
131
+ });
119
132
  });
120
133
  });
121
- return promises;
122
134
  };
123
135
 
124
136
  const _executePeriodicEventsAllTenants = (tenantIds, runId) => {
125
137
  tenantIds.forEach((tenantId) => {
126
- WorkerQueue.instance.addToQueue(1, async () => {
138
+ const label = `UPDATE_PERIODIC_EVENTS_${tenantId}`;
139
+ WorkerQueue.instance.addToQueue(1, label, async () => {
127
140
  try {
128
- const tenantContext = new cds.EventContext({ tenant: tenantId });
129
- const couldAcquireLock = await distributedLock.acquireLock(tenantContext, runId, {
130
- expiryTime: eventQueueConfig.runInterval * 0.95,
141
+ const subdomain = await getSubdomainForTenantId(tenantId);
142
+ const tenantContext = {
143
+ tenant: tenantId,
144
+ // NOTE: we need this because of logging otherwise logs would not contain the subdomain
145
+ http: { req: { authInfo: { getSubdomain: () => subdomain } } },
146
+ };
147
+
148
+ return await cds.tx(tenantContext, async ({ context }) => {
149
+ const couldAcquireLock = await distributedLock.acquireLock(context, runId, {
150
+ expiryTime: eventQueueConfig.runInterval * 0.95,
151
+ });
152
+ if (!couldAcquireLock) {
153
+ return;
154
+ }
155
+ await _checkPeriodicEventsSingleTenant(context);
131
156
  });
132
- if (!couldAcquireLock) {
133
- return;
134
- }
135
- await _checkPeriodicEventsSingleTenant(tenantId);
136
157
  } catch (err) {
137
158
  cds.log(COMPONENT_NAME).error("executing event-queue run for tenant failed", {
138
159
  tenantId,
@@ -143,11 +164,19 @@ const _executePeriodicEventsAllTenants = (tenantIds, runId) => {
143
164
  };
144
165
 
145
166
  const _singleTenantDb = async (tenantId) => {
146
- const events = eventQueueConfig.allEvents;
147
- events.forEach((event) => {
148
- WorkerQueue.instance.addToQueue(event.load, async () => {
167
+ return eventQueueConfig.allEvents.map((event) => {
168
+ const label = `${event.type}_${event.subType}`;
169
+ return WorkerQueue.instance.addToQueue(event.load, label, async () => {
149
170
  try {
150
- await runEventCombinationForTenant(tenantId, event.type, event.subType, true);
171
+ const context = new cds.EventContext({ tenant: tenantId });
172
+ const lockId = `${EVENT_QUEUE_RUN_ID}_${label}`;
173
+ const couldAcquireLock = await distributedLock.acquireLock(context, lockId, {
174
+ expiryTime: eventQueueConfig.runInterval * 0.95,
175
+ });
176
+ if (!couldAcquireLock) {
177
+ return;
178
+ }
179
+ await runEventCombinationForTenant(context, event.type, event.subType, true);
151
180
  } catch (err) {
152
181
  cds.log(COMPONENT_NAME).error("executing event-queue run for tenant failed", {
153
182
  tenantId,
@@ -215,28 +244,23 @@ const _calculateOffsetForFirstRun = async () => {
215
244
  return offsetDependingOnLastRun;
216
245
  };
217
246
 
218
- const runEventCombinationForTenant = async (tenantId, type, subType, skipWorkerPool) => {
247
+ const runEventCombinationForTenant = async (context, type, subType, skipWorkerPool) => {
219
248
  try {
220
- const subdomain = await getSubdomainForTenantId(tenantId);
221
- const context = new cds.EventContext({
222
- tenant: tenantId,
223
- // NOTE: we need this because of logging otherwise logs would not contain the subdomain
224
- http: { req: { authInfo: { getSubdomain: () => subdomain } } },
225
- });
226
- cds.context = context;
227
249
  if (skipWorkerPool) {
228
250
  return await processEventQueue(context, type, subType);
229
251
  } else {
230
252
  const config = eventQueueConfig.getEventConfig(type, subType);
253
+ const label = `${type}_${subType}`;
231
254
  return await WorkerQueue.instance.addToQueue(
232
255
  config.load,
256
+ label,
233
257
  async () => await processEventQueue(context, type, subType)
234
258
  );
235
259
  }
236
260
  } catch (err) {
237
261
  const logger = cds.log(COMPONENT_NAME);
238
262
  logger.error("error executing event combination for tenant", err, {
239
- tenantId,
263
+ tenantId: context.tenant,
240
264
  type,
241
265
  subType,
242
266
  });
@@ -266,7 +290,7 @@ const _multiTenancyPeriodicEvents = async () => {
266
290
  }
267
291
  };
268
292
 
269
- const _checkPeriodicEventsSingleTenant = async (tenantId) => {
293
+ const _checkPeriodicEventsSingleTenant = async (context = {}) => {
270
294
  const logger = cds.log(COMPONENT_NAME);
271
295
  if (!eventQueueConfig.updatePeriodicEvents || !eventQueueConfig.periodicEvents.length) {
272
296
  logger.info("updating of periodic events is disabled or no periodic events configured", {
@@ -276,23 +300,16 @@ const _checkPeriodicEventsSingleTenant = async (tenantId) => {
276
300
  return;
277
301
  }
278
302
  try {
279
- const subdomain = await cdsHelper.getSubdomainForTenantId(tenantId);
280
- const context = new cds.EventContext({
281
- tenant: tenantId,
282
- // NOTE: we need this because of logging otherwise logs would not contain the subdomain
283
- http: { req: { authInfo: { getSubdomain: () => subdomain } } },
284
- });
285
- cds.context = context;
286
303
  logger.info("executing updating periotic events", {
287
- tenantId,
288
- subdomain,
304
+ tenantId: context.tenant,
305
+ subdomain: context.http?.req.authInfo.getSubdomain(),
289
306
  });
290
307
  await cdsHelper.executeInNewTransaction(context, "update-periodic-events", async (tx) => {
291
308
  await periodicEvents.checkAndInsertPeriodicEvents(tx.context);
292
309
  });
293
310
  } catch (err) {
294
311
  logger.error("Couldn't update periodic events for tenant! Next try after defined interval.", err, {
295
- tenantId,
312
+ tenantId: context.tenant,
296
313
  redisEnabled: eventQueueConfig.redisEnabled,
297
314
  });
298
315
  }
@@ -304,10 +321,12 @@ module.exports = {
304
321
  multiTenancyRedis,
305
322
  runEventCombinationForTenant,
306
323
  _: {
324
+ _singleTenantDb,
307
325
  _multiTenancyRedis,
308
326
  _multiTenancyDb,
309
327
  _calculateOffsetForFirstRun,
310
328
  _acquireRunId,
311
329
  EVENT_QUEUE_RUN_TS,
330
+ clearHash: () => (tenantIdHash = null),
312
331
  },
313
332
  };
@@ -17,6 +17,7 @@ class PerformanceTracer {
17
17
  this.__start = new Date();
18
18
  this.__logger = logger;
19
19
  this.__name = name;
20
+ this.__properties = options.properties ??= {};
20
21
  options.startMessage &&
21
22
  logger.info("Performance measurement started", {
22
23
  name: name,
@@ -64,6 +65,7 @@ class PerformanceTracer {
64
65
  this.__logger.info("Performance measurement executed", {
65
66
  name: this.__name,
66
67
  milliseconds: executionTime,
68
+ ...this.__properties,
67
69
  customFields,
68
70
  });
69
71
  }
@@ -8,9 +8,9 @@ const EventQueueError = require("../EventQueueError");
8
8
  const COMPONENT_NAME = "eventQueue/WorkerQueue";
9
9
  const NANO_TO_MS = 1e6;
10
10
  const THRESHOLD = {
11
- INFO: 5 * 1000,
12
- WARN: 10 * 1000,
13
- ERROR: 15 * 1000,
11
+ INFO: 35 * 1000,
12
+ WARN: 55 * 1000,
13
+ ERROR: 75 * 1000,
14
14
  };
15
15
 
16
16
  class WorkerQueue {
@@ -31,21 +31,21 @@ class WorkerQueue {
31
31
  this.#queue = [];
32
32
  }
33
33
 
34
- addToQueue(load, cb) {
34
+ addToQueue(load, label, cb) {
35
35
  if (load > this.#concurrencyLimit) {
36
- throw EventQueueError.loadHigherThanLimit(load);
36
+ throw EventQueueError.loadHigherThanLimit(load, label);
37
37
  }
38
38
 
39
39
  const startTime = process.hrtime.bigint();
40
40
  const p = new Promise((resolve, reject) => {
41
- this.#queue.push([load, cb, resolve, reject, startTime]);
41
+ this.#queue.push([load, label, cb, resolve, reject, startTime]);
42
42
  });
43
43
  this._checkForNext();
44
44
  return p;
45
45
  }
46
46
 
47
- _executeFunction(load, cb, resolve, reject, startTime) {
48
- this.checkAndLogWaitingTime(startTime);
47
+ _executeFunction(load, label, cb, resolve, reject, startTime) {
48
+ this.checkAndLogWaitingTime(startTime, label);
49
49
  const promise = Promise.resolve().then(() => cb());
50
50
  this.#runningPromises.push(promise);
51
51
  this.#runningLoad = this.#runningLoad + load;
@@ -59,7 +59,7 @@ class WorkerQueue {
59
59
  resolve(...results);
60
60
  })
61
61
  .catch((err) => {
62
- cds.log(COMPONENT_NAME).error("Error happened in WorkQueue. Errors should be caught before!", err);
62
+ cds.log(COMPONENT_NAME).error("Error happened in WorkQueue. Errors should be caught before!", err, { label });
63
63
  reject(err);
64
64
  });
65
65
  }
@@ -87,7 +87,7 @@ class WorkerQueue {
87
87
  return WorkerQueue.#instance;
88
88
  }
89
89
 
90
- checkAndLogWaitingTime(startTime) {
90
+ checkAndLogWaitingTime(startTime, label) {
91
91
  const diffMs = Math.round(Number(process.hrtime.bigint() - startTime) / NANO_TO_MS);
92
92
  let logLevel;
93
93
  if (diffMs >= THRESHOLD.ERROR) {
@@ -101,6 +101,7 @@ class WorkerQueue {
101
101
  }
102
102
  cds.log(COMPONENT_NAME)[logLevel]("Waiting time in worker queue", {
103
103
  diffMs,
104
+ label,
104
105
  });
105
106
  }
106
107
  }
@@ -80,7 +80,7 @@ const subscribeRedisChannel = (channel, subscribeCb) => {
80
80
  subscriberChannelClientPromise[channel]
81
81
  .then((client) => {
82
82
  cds.log(COMPONENT_NAME).info("subscribe redis client connected channel", { channel });
83
- client.subscribe(channel, subscribeCb);
83
+ client.subscribe(channel, subscribeCb).catch(errorHandlerCreateClient);
84
84
  })
85
85
  .catch((err) => {
86
86
  cds