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

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/README.md CHANGED
@@ -5,22 +5,24 @@
5
5
  [![REUSE status](https://api.reuse.software/badge/github.com/cap-js-community/event-queue)](https://api.reuse.software/info/github.com/cap-js-community/event-queue)
6
6
  [![CI Main](https://github.com/cap-js-community/event-queue/actions/workflows/main-ci.yml/badge.svg)](https://github.com/cap-js-community/event-queue/commits/main)
7
7
 
8
- The Event-Queue is a framework built on top of CAP Node.js, specifically designed to enable efficient and streamlined
9
- asynchronous event processing in a multi-tenancy environment. With a strong emphasis on load balancing, this package
10
- ensures optimal distribution of workload across all available application instances. By offering managed tenant-specific
11
- transactions, similar to CAP handlers, the Event-Queue framework simplifies event and asynchronous processing, thereby
12
- enhancing the overall performance of your application.
8
+ The Event-Queue is a framework built on top of CAP Node.js, designed specifically for efficient and
9
+ streamlined asynchronous event processing. With a focus on load balancing, this package ensures optimal
10
+ event distribution across all available application instances. By providing managed transactions similar to CAP
11
+ handlers, the Event-Queue framework simplifies event processing, enhancing the overall performance of your application.
12
+
13
+ Additionally, Event-Queue provides support for [periodic events](https://cap-js-community.github.io/event-queue/configure-event/#periodic-events),
14
+ allowing for processing at defined intervals. This feature further extends its capabilities in load balancing and
15
+ transaction management, ensuring that even regularly occurring tasks are handled efficiently and effectively without
16
+ overloading any single instance. This makes it an ideal solution for applications needing consistent, reliable event processing.
13
17
 
14
18
  ## Getting started
15
19
 
16
20
  - Run `npm add @cap-js-community/event-queue` in `@sap/cds` project
17
- - Activate the cds-plugin in the cds section of the package.sjon
21
+ - Activate the cds-plugin in the cds section of the package.json.
18
22
 
19
23
  ### As cds-plugin
20
24
 
21
- Extend the cds section of your package.json. Reference to the cds-plugin section in the capire documentation about the
22
- cds-plugin concept.
23
- https://cap.cloud.sap/docs/releases/march23#new-cds-plugin-technique
25
+ For detailed information check out the [documentation](https://cap-js-community.github.io/event-queue/setup).
24
26
 
25
27
  ```json
26
28
  {
@@ -35,13 +37,16 @@ https://cap.cloud.sap/docs/releases/march23#new-cds-plugin-technique
35
37
 
36
38
  ## Features
37
39
 
38
- - load balancing of event processing throughout app instances
39
- - control concurrency in app instances
40
- - managed transactions for event processing
40
+ Learn more about features in the [documentation](https://cap-js-community.github.io/event-queue/#functionality-and-problem-solutions). To compare the
41
+ event-queue with other SAP products head over to [Distinction from other solutions](https://cap-js-community.github.io/event-queue/diff-to-outbox/).
42
+
43
+ - [load balancing](https://cap-js-community.github.io/event-queue/load-balancing) and concurrency control of event processing across app instances
44
+ - [periodic events](https://cap-js-community.github.io/event-queue/configure-event/#periodic-events) similar to running cron jobs for business processes
45
+ - [managed transactions](https://cap-js-community.github.io/event-queue/transaction-handling) for event processing
41
46
  - async processing of processing intensive tasks for better UI responsiveness
42
47
  - push/pull mechanism for reducing delay between publish an event and processing
43
48
  - cluster published events during processing (e.g. for combining multiple E-Mail events to one E-Mail)
44
- - plug and play via cds-plugin
49
+ - [plug and play](https://cap-js-community.github.io/event-queue/setup) via cds-plugin
45
50
 
46
51
  ## Documentation
47
52
 
@@ -50,7 +55,7 @@ Head over to our [Documentation](https://cap-js-community.github.io/event-queue/
50
55
  ## Support, Feedback, Contributing
51
56
 
52
57
  This project is open to feature requests/suggestions, bug reports etc.
53
- via [GitHub issues](https://github.com/cap-js-community/<your-project>/issues). Contribution and feedback are encouraged
58
+ via [GitHub issues](https://github.com/cap-js-communityevent-queue/issues). Contribution and feedback are encouraged
54
59
  and always welcome. For more information about how to contribute, the project structure, as well as additional
55
60
  contribution information, see our [Contribution Guidelines](CONTRIBUTING.md).
56
61
 
@@ -62,7 +67,7 @@ times.
62
67
 
63
68
  ## Licensing
64
69
 
65
- Copyright 2023 SAP SE or an SAP affiliate company and `@cap-js-community/event-queue contributors`. Please see
70
+ Copyright 2023 SAP SE or an SAP affiliate company and `@cap-js-community/event-queue` contributors. Please see
66
71
  our [LICENSE](LICENSE) for copyright and license information. Detailed information including third-party components and
67
72
  their licensing/copyright information is
68
73
  available [via the REUSE tool](https://api.reuse.software/info/github.com/cap-js-community/<your-project>).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cap-js-community/event-queue",
3
- "version": "0.2.3",
3
+ "version": "0.2.5",
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": [
@@ -31,7 +31,7 @@
31
31
  "lint:ci": "npm run eslint:ci && npm run prettier:ci",
32
32
  "eslint": "eslint --fix .",
33
33
  "eslint:ci": "eslint .",
34
- "prettier": "prettier --write --log-level error .",
34
+ "prettier": "prettier --write --loglevel error .",
35
35
  "prettier:ci": "prettier --check .",
36
36
  "prepareRelease": "npm prune --production",
37
37
  "docs": "cd docs && bundle exec jekyll serve",
@@ -42,14 +42,14 @@
42
42
  "node": ">=16"
43
43
  },
44
44
  "dependencies": {
45
- "redis": "4.6.10",
45
+ "redis": "4.6.11",
46
46
  "verror": "1.10.1",
47
47
  "yaml": "2.3.4"
48
48
  },
49
49
  "devDependencies": {
50
- "@sap/cds": "7.3.1",
51
- "@sap/cds-dk": "7.3.1",
52
- "eslint": "8.52.0",
50
+ "@sap/cds": "7.4.0",
51
+ "@sap/cds-dk": "7.4.0",
52
+ "eslint": "8.54.0",
53
53
  "eslint-config-prettier": "9.0.0",
54
54
  "eslint-plugin-jest": "27.6.0",
55
55
  "eslint-plugin-node": "11.1.0",
@@ -16,6 +16,7 @@ const ERROR_CODES = {
16
16
  MISSING_IMPL: "MISSING_IMPL",
17
17
  DUPLICATE_EVENT_REGISTRATION: "DUPLICATE_EVENT_REGISTRATION",
18
18
  NO_MANUEL_INSERT_OF_PERIODIC: "NO_MANUEL_INSERT_OF_PERIODIC",
19
+ LOAD_HIGHER_THAN_LIMIT: "LOAD_HIGHER_THAN_LIMIT",
19
20
  };
20
21
 
21
22
  const ERROR_CODES_META = {
@@ -59,6 +60,9 @@ const ERROR_CODES_META = {
59
60
  [ERROR_CODES.NO_MANUEL_INSERT_OF_PERIODIC]: {
60
61
  message: "Periodic events are managed by the framework and are not allowed to insert manually.",
61
62
  },
63
+ [ERROR_CODES.LOAD_HIGHER_THAN_LIMIT]: {
64
+ message: "The defined load of an event is higher than the maximum defined limit. Check your configuration!",
65
+ },
62
66
  };
63
67
 
64
68
  class EventQueueError extends VError {
@@ -206,6 +210,16 @@ class EventQueueError extends VError {
206
210
  message
207
211
  );
208
212
  }
213
+ static loadHigherThanLimit(load) {
214
+ const { message } = ERROR_CODES_META[ERROR_CODES.LOAD_HIGHER_THAN_LIMIT];
215
+ return new EventQueueError(
216
+ {
217
+ name: ERROR_CODES.LOAD_HIGHER_THAN_LIMIT,
218
+ info: { load },
219
+ },
220
+ message
221
+ );
222
+ }
209
223
  }
210
224
 
211
225
  module.exports = EventQueueError;
@@ -98,6 +98,20 @@ class EventQueueProcessorBase {
98
98
  throw new Error(IMPLEMENT_ERROR_MESSAGE);
99
99
  }
100
100
 
101
+ /**
102
+ * Process one periodic event
103
+ * @param processContext the context valid for the event processing. This context is associated with a valid transaction
104
+ * Access to the context is also possible with this.getContextForEventProcessing(key).
105
+ * The associated tx can be accessed with this.getTxForEventProcessing(key).
106
+ * @param {string} key cluster key generated during the clustering step. By default, this is ID of the event queue entry
107
+ * @param {Object} queueEntry this is the queueEntry which should be processed
108
+ * @returns {Promise<undefined>}
109
+ */
110
+ // eslint-disable-next-line no-unused-vars
111
+ async processPeriodicEvent(processContext, key, queueEntry) {
112
+ throw new Error(IMPLEMENT_ERROR_MESSAGE);
113
+ }
114
+
101
115
  startPerformanceTracerEvents() {
102
116
  this.__performanceLoggerEvents = new PerformanceTracer(this.logger, "Processing events");
103
117
  }
@@ -254,7 +268,8 @@ class EventQueueProcessorBase {
254
268
  this.#determineAndAddEventStatusToMap(queueEntry.ID, EventProcessingStatus.Error, statusMap)
255
269
  );
256
270
  this.logger.error(
257
- `The supplied status tuple doesn't have the required structure. Setting all entries to error. Error: ${error.toString()}`,
271
+ "The supplied status tuple doesn't have the required structure. Setting all entries to error.",
272
+ error,
258
273
  {
259
274
  eventType: this.#eventType,
260
275
  eventSubType: this.#eventSubType,
@@ -295,7 +310,8 @@ class EventQueueProcessorBase {
295
310
  handleErrorDuringProcessing(error, queueEntries) {
296
311
  queueEntries = Array.isArray(queueEntries) ? queueEntries : [queueEntries];
297
312
  this.logger.error(
298
- `Caught error during event processing - setting queue entry to error. Please catch your promises/exceptions. Error: ${error}`,
313
+ "Caught error during event processing - setting queue entry to error. Please catch your promises/exceptions",
314
+ error,
299
315
  {
300
316
  eventType: this.#eventType,
301
317
  eventSubType: this.#eventSubType,
@@ -309,14 +325,11 @@ class EventQueueProcessorBase {
309
325
  }
310
326
 
311
327
  handleErrorDuringPeriodicEventProcessing(error, queueEntry) {
312
- this.logger.error(
313
- `Caught error during event periodic processing. Please catch your promises/exceptions. Error: ${error}`,
314
- {
315
- eventType: this.#eventType,
316
- eventSubType: this.#eventSubType,
317
- queueEntryId: queueEntry.ID,
318
- }
319
- );
328
+ this.logger.error("Caught error during event periodic processing. Please catch your promises/exceptions.", error, {
329
+ eventType: this.#eventType,
330
+ eventSubType: this.#eventSubType,
331
+ queueEntryId: queueEntry.ID,
332
+ });
320
333
  }
321
334
 
322
335
  async setPeriodicEventStatus(queueEntryIds) {
@@ -491,7 +504,7 @@ class EventQueueProcessorBase {
491
504
  }
492
505
 
493
506
  handleErrorDuringClustering(error) {
494
- this.logger.error(`Error during clustering of events - setting all queue entries to error. Error: ${error}`, {
507
+ this.logger.error("Error during clustering of events - setting all queue entries to error.", error, {
495
508
  eventType: this.#eventType,
496
509
  eventSubType: this.#eventSubType,
497
510
  });
@@ -521,7 +534,7 @@ class EventQueueProcessorBase {
521
534
  */
522
535
  async getQueueEntriesAndSetToInProgress() {
523
536
  let result = [];
524
- const refDateStartAfter = new Date(Date.now() + this.#config.runInterval);
537
+ const refDateStartAfter = new Date(Date.now() + this.#config.runInterval * 1.2);
525
538
  await executeInNewTransaction(this.__baseContext, "eventQueue-getQueueEntriesAndSetToInProgress", async (tx) => {
526
539
  const entries = await tx.run(
527
540
  SELECT.from(this.#config.tableNameEventQueue)
@@ -573,7 +586,7 @@ class EventQueueProcessorBase {
573
586
  this.#handleDelayedEvents(delayedEvents);
574
587
 
575
588
  result = openEvents;
576
- this.logger.info("Selected event queue entries for processing", {
589
+ this.logger[eventsForProcessing.length ? "info" : "debug"]("Selected event queue entries for processing", {
577
590
  openEvents: openEvents.length,
578
591
  ...(delayedEvents.length && { delayedEvents: delayedEvents.length }),
579
592
  ...(exceededTries.length && { exceededTries: exceededTries.length }),
@@ -626,7 +639,7 @@ class EventQueueProcessorBase {
626
639
  }
627
640
 
628
641
  #clusterEvents(events, refDateStartAfter) {
629
- const refDate = new Date(refDateStartAfter.getTime() - this.#config.runInterval + EVENT_START_AFTER_HEADROOM);
642
+ const refDate = new Date(refDateStartAfter.getTime() - this.#config.runInterval * 1.2 + EVENT_START_AFTER_HEADROOM);
630
643
  return events.reduce(
631
644
  (result, event) => {
632
645
  if (event.attempts === this.__retryAttempts + TRIES_FOR_EXCEEDED_EVENTS) {
@@ -677,7 +690,8 @@ class EventQueueProcessorBase {
677
690
  await this.#persistEventQueueStatusForExceeded(this.tx, [exceededEvent], EventProcessingStatus.Exceeded);
678
691
  } catch (err) {
679
692
  this.logger.error(
680
- `Caught error during hook for exceeded events - setting queue entry to error. Please catch your promises/exceptions. Error: ${err}`,
693
+ "Caught error during hook for exceeded events - setting queue entry to error. Please catch your promises/exceptions.",
694
+ err,
681
695
  {
682
696
  eventType: this.#eventType,
683
697
  eventSubType: this.#eventSubType,
@@ -822,6 +836,10 @@ class EventQueueProcessorBase {
822
836
  [this.#eventType, this.#eventSubType].join("##")
823
837
  );
824
838
  if (!lockAcquired) {
839
+ this.logger.debug("no lock available, exit processing", {
840
+ type: this.#eventType,
841
+ subType: this.#eventSubType,
842
+ });
825
843
  return false;
826
844
  }
827
845
  this.__lockAcquired = true;
@@ -835,16 +853,16 @@ class EventQueueProcessorBase {
835
853
  try {
836
854
  await distributedLock.releaseLock(this.context, [this.#eventType, this.#eventSubType].join("##"));
837
855
  } catch (err) {
838
- this.logger.error("Releasing distributed lock failed. Error:", err.toString());
856
+ this.logger.error("Releasing distributed lock failed.", err);
839
857
  }
840
858
  }
841
859
 
842
860
  async scheduleNextPeriodEvent(queueEntry) {
843
- const interval = this.#eventConfig.interval;
861
+ const intervalInSec = this.#eventConfig.interval * 1000;
844
862
  const newEvent = {
845
863
  type: this.#eventType,
846
864
  subType: this.#eventSubType,
847
- startAfter: new Date(new Date(queueEntry.startAfter).getTime() + interval * 1000),
865
+ startAfter: new Date(new Date(queueEntry.startAfter).getTime() + intervalInSec),
848
866
  };
849
867
  const { relative } = this.#eventSchedulerInstance.calculateOffset(
850
868
  this.#eventType,
@@ -853,7 +871,7 @@ class EventQueueProcessorBase {
853
871
  );
854
872
 
855
873
  // more than one interval behind - shift tick to keep up
856
- if (relative < 0 && Math.abs(relative) >= this.#eventConfig.interval * 1000) {
874
+ if (relative < 0 && Math.abs(relative) >= intervalInSec) {
857
875
  newEvent.startAfter = new Date(Date.now() + 5 * 1000);
858
876
  this.logger.info("interval adjusted because shifted more than one interval", {
859
877
  eventType: this.#eventType,
@@ -865,7 +883,7 @@ class EventQueueProcessorBase {
865
883
  this.tx._skipEventQueueBroadcase = true;
866
884
  await this.tx.run(INSERT.into(this.#config.tableNameEventQueue).entries({ ...newEvent }));
867
885
  this.tx._skipEventQueueBroadcase = false;
868
- if (interval < this.#config.runInterval) {
886
+ if (intervalInSec < this.#config.runInterval * 1.5) {
869
887
  this.#handleDelayedEvents([newEvent]);
870
888
  const { relative: relativeAfterSchedule } = this.#eventSchedulerInstance.calculateOffset(
871
889
  this.#eventType,
@@ -923,6 +941,10 @@ class EventQueueProcessorBase {
923
941
  return this.__logger ?? this.__baseLogger;
924
942
  }
925
943
 
944
+ set logger(value) {
945
+ this.__logger = value;
946
+ }
947
+
926
948
  get queueEntriesWithPayloadMap() {
927
949
  return this.#queueEntriesWithPayloadMap;
928
950
  }
package/src/config.js CHANGED
@@ -11,6 +11,7 @@ const GLOBAL_TX_TIMEOUT = 30 * 60 * 1000;
11
11
  const REDIS_CONFIG_CHANNEL = "EVENT_QUEUE_CONFIG_CHANNEL";
12
12
  const COMPONENT_NAME = "eventQueue/config";
13
13
  const MIN_INTERVAL_SEC = 10;
14
+ const DEFAULT_LOAD = 1;
14
15
 
15
16
  class Config {
16
17
  #logger;
@@ -20,13 +21,14 @@ class Config {
20
21
  #runInterval;
21
22
  #redisEnabled;
22
23
  #initialized;
23
- #parallelTenantProcessing;
24
+ #instanceLoadLimit;
24
25
  #tableNameEventQueue;
25
26
  #tableNameEventLock;
26
27
  #isRunnerDeactivated;
27
28
  #configFilePath;
28
29
  #processEventsAfterPublish;
29
30
  #skipCsnCheck;
31
+ #registerAsEventProcessor;
30
32
  #disableRedis;
31
33
  #env;
32
34
  #eventMap;
@@ -40,7 +42,7 @@ class Config {
40
42
  this.#runInterval = null;
41
43
  this.#redisEnabled = null;
42
44
  this.#initialized = false;
43
- this.#parallelTenantProcessing = null;
45
+ this.#instanceLoadLimit = null;
44
46
  this.#tableNameEventQueue = null;
45
47
  this.#tableNameEventLock = null;
46
48
  this.#isRunnerDeactivated = false;
@@ -106,11 +108,13 @@ class Config {
106
108
  config.events = config.events ?? [];
107
109
  config.periodicEvents = config.periodicEvents ?? [];
108
110
  this.#eventMap = config.events.reduce((result, event) => {
111
+ event.load = event.load ?? DEFAULT_LOAD;
109
112
  this.validateAdHocEvents(result, event);
110
113
  result[[event.type, event.subType].join("##")] = event;
111
114
  return result;
112
115
  }, {});
113
116
  this.#eventMap = config.periodicEvents.reduce((result, event) => {
117
+ event.load = event.load ?? DEFAULT_LOAD;
114
118
  const SUFFIX_PERIODIC = "_PERIODIC";
115
119
  event.type = `${event.type}${SUFFIX_PERIODIC}`;
116
120
  event.isPeriodic = true;
@@ -210,12 +214,12 @@ class Config {
210
214
  this.#initialized = value;
211
215
  }
212
216
 
213
- get parallelTenantProcessing() {
214
- return this.#parallelTenantProcessing;
217
+ get instanceLoadLimit() {
218
+ return this.#instanceLoadLimit;
215
219
  }
216
220
 
217
- set parallelTenantProcessing(value) {
218
- this.#parallelTenantProcessing = value;
221
+ set instanceLoadLimit(value) {
222
+ this.#instanceLoadLimit = value;
219
223
  }
220
224
 
221
225
  get tableNameEventQueue() {
@@ -274,6 +278,14 @@ class Config {
274
278
  return this.#updatePeriodicEvents;
275
279
  }
276
280
 
281
+ set registerAsEventProcessor(value) {
282
+ this.#registerAsEventProcessor = value;
283
+ }
284
+
285
+ get registerAsEventProcessor() {
286
+ return this.#registerAsEventProcessor;
287
+ }
288
+
277
289
  get isMultiTenancy() {
278
290
  return !!cds.requires.multitenancy;
279
291
  }
package/src/initialize.js CHANGED
@@ -29,7 +29,7 @@ const CONFIG_VARS = [
29
29
  ["processEventsAfterPublish", true],
30
30
  ["isRunnerDeactivated", false],
31
31
  ["runInterval", 5 * 60 * 1000],
32
- ["parallelTenantProcessing", 5],
32
+ ["instanceLoadLimit", 20],
33
33
  ["tableNameEventQueue", BASE_TABLES.EVENT],
34
34
  ["tableNameEventLock", BASE_TABLES.LOCK],
35
35
  ["disableRedis", false],
@@ -43,7 +43,7 @@ const initialize = async ({
43
43
  processEventsAfterPublish,
44
44
  isRunnerDeactivated,
45
45
  runInterval,
46
- parallelTenantProcessing,
46
+ instanceLoadLimit,
47
47
  tableNameEventQueue,
48
48
  tableNameEventLock,
49
49
  disableRedis,
@@ -52,7 +52,7 @@ const initialize = async ({
52
52
  } = {}) => {
53
53
  // TODO: initialize check:
54
54
  // - content of yaml check
55
- // - betweenRuns and parallelTenantProcessing
55
+ // - betweenRuns and instanceLoadLimit
56
56
 
57
57
  if (config.initialized) {
58
58
  return;
@@ -65,7 +65,7 @@ const initialize = async ({
65
65
  processEventsAfterPublish,
66
66
  isRunnerDeactivated,
67
67
  runInterval,
68
- parallelTenantProcessing,
68
+ instanceLoadLimit,
69
69
  tableNameEventQueue,
70
70
  tableNameEventLock,
71
71
  disableRedis,
@@ -88,10 +88,11 @@ const initialize = async ({
88
88
  registerCdsShutdown();
89
89
  logger.info("event queue initialized", {
90
90
  registerAsEventProcessor: config.registerAsEventProcessor,
91
+ processEventsAfterPublish: config.processEventsAfterPublish,
91
92
  multiTenancyEnabled: config.isMultiTenancy,
92
93
  redisEnabled: config.redisEnabled,
93
94
  runInterval: config.runInterval,
94
- config: config.parallelTenantProcessing,
95
+ config: config.instanceLoadLimit,
95
96
  });
96
97
  };
97
98
 
@@ -7,6 +7,7 @@ const { processChunkedSync } = require("./shared/common");
7
7
  const eventConfig = require("./config");
8
8
 
9
9
  const COMPONENT_NAME = "eventQueue/periodicEvents";
10
+ const CHUNK_SIZE_INSERT_PERIODIC_EVENTS = 4;
10
11
 
11
12
  const checkAndInsertPeriodicEvents = async (context) => {
12
13
  const tx = cds.tx(context);
@@ -63,12 +64,15 @@ const checkAndInsertPeriodicEvents = async (context) => {
63
64
  cds.log(COMPONENT_NAME).info("deleting periodic events because they have changed", {
64
65
  changedEvents: exitingWithNotMatchingInterval.map(({ type, subType }) => ({ type, subType })),
65
66
  });
66
- await tx.run(
67
- DELETE.from(eventConfig.tableNameEventQueue).where(
68
- "ID IN",
69
- exitingWithNotMatchingInterval.map(({ ID }) => ID)
70
- )
71
- );
67
+
68
+ if (exitingWithNotMatchingInterval.length) {
69
+ await tx.run(
70
+ DELETE.from(eventConfig.tableNameEventQueue).where(
71
+ "ID IN",
72
+ exitingWithNotMatchingInterval.map(({ ID }) => ID)
73
+ )
74
+ );
75
+ }
72
76
 
73
77
  const newOrChangedEvents = newEvents.concat(exitingWithNotMatchingInterval);
74
78
 
@@ -81,13 +85,17 @@ const checkAndInsertPeriodicEvents = async (context) => {
81
85
 
82
86
  const insertPeriodEvents = async (tx, events) => {
83
87
  const startAfter = new Date();
84
- processChunkedSync(events, 4, (chunk) => {
85
- cds.log(COMPONENT_NAME).info("inserting changed or new periodic events", {
88
+ let counter = 1;
89
+ const chunks = Math.ceil(events.length / CHUNK_SIZE_INSERT_PERIODIC_EVENTS);
90
+ const logger = cds.log(COMPONENT_NAME);
91
+ processChunkedSync(events, CHUNK_SIZE_INSERT_PERIODIC_EVENTS, (chunk) => {
92
+ logger.info(`${counter}/${chunks} | inserting chunk of changed or new periodic events`, {
86
93
  events: chunk.map(({ type, subType }) => {
87
94
  const { interval } = eventConfig.getEventConfig(type, subType);
88
95
  return { type, subType, interval };
89
96
  }),
90
97
  });
98
+ counter++;
91
99
  });
92
100
  const periodEventsInsert = events.map((periodicEvent) => ({
93
101
  type: periodicEvent.type,
@@ -6,23 +6,13 @@ const cds = require("@sap/cds");
6
6
 
7
7
  const config = require("./config");
8
8
  const { TransactionMode } = require("./constants");
9
- const { limiter, Funnel } = require("./shared/common");
9
+ const { limiter } = require("./shared/common");
10
10
 
11
11
  const { executeInNewTransaction, TriggerRollback } = require("./shared/cdsHelper");
12
12
 
13
13
  const COMPONENT_NAME = "eventQueue/processEventQueue";
14
14
  const MAX_EXECUTION_TIME = 5 * 60 * 1000;
15
15
 
16
- const eventQueueRunner = async (context, events) => {
17
- const startTime = new Date();
18
- const funnel = new Funnel();
19
- await Promise.allSettled(
20
- events.map((event) =>
21
- funnel.run(event.load, async () => processEventQueue(context, event.type, event.subType, startTime))
22
- )
23
- );
24
- };
25
-
26
16
  const processEventQueue = async (context, eventType, eventSubType, startTime = new Date()) => {
27
17
  let iterationCounter = 0;
28
18
  let shouldContinue = true;
@@ -111,7 +101,7 @@ const processEventQueue = async (context, eventType, eventSubType, startTime = n
111
101
  }
112
102
  }
113
103
  } catch (err) {
114
- cds.log(COMPONENT_NAME).error("Processing event queue failed with unexpected error. Error:", err, {
104
+ cds.log(COMPONENT_NAME).error("Processing event queue failed with unexpected error.", err, {
115
105
  eventType,
116
106
  eventSubType,
117
107
  });
@@ -170,7 +160,7 @@ const processPeriodicEvent = async (eventTypeInstance) => {
170
160
  eventTypeInstance.processEventContext = tx.context;
171
161
  eventTypeInstance.setTxForEventProcessing(queueEntry.ID, cds.tx(tx.context));
172
162
  try {
173
- await eventTypeInstance.processEvent(tx.context, queueEntry.ID, [queueEntry]);
163
+ await eventTypeInstance.processPeriodicEvent(tx.context, queueEntry.ID, queueEntry);
174
164
  } catch (err) {
175
165
  eventTypeInstance.handleErrorDuringPeriodicEventProcessing(err, queueEntry);
176
166
  throw new TriggerRollback();
@@ -194,7 +184,7 @@ const processPeriodicEvent = async (eventTypeInstance) => {
194
184
  );
195
185
  }
196
186
  } catch (err) {
197
- cds.log(COMPONENT_NAME).error("Processing periodic events failed with unexpected error. Error:", err, {
187
+ cds.log(COMPONENT_NAME).error("Processing periodic events failed with unexpected error.", err, {
198
188
  eventType: eventTypeInstance?.eventType,
199
189
  eventSubType: eventTypeInstance?.eventSubType,
200
190
  });
@@ -278,5 +268,4 @@ const resilientRequire = (path) => {
278
268
 
279
269
  module.exports = {
280
270
  processEventQueue,
281
- eventQueueRunner,
282
271
  };
@@ -48,7 +48,10 @@ const broadcastEvent = async (tenantId, type, subType) => {
48
48
  [type, subType].join("##")
49
49
  );
50
50
  if (result) {
51
- logger.info("skip publish redis event as no lock is available");
51
+ logger.info("skip publish redis event as no lock is available", {
52
+ type,
53
+ subType,
54
+ });
52
55
  return;
53
56
  }
54
57
  logger.debug("publishing redis event", {
@@ -58,7 +61,7 @@ const broadcastEvent = async (tenantId, type, subType) => {
58
61
  });
59
62
  await redis.publishMessage(EVENT_MESSAGE_CHANNEL, JSON.stringify({ tenantId, type, subType }));
60
63
  } catch (err) {
61
- logger.error(`publish event failed with error: ${err.toString()}`, {
64
+ logger.error("publish event failed!", err, {
62
65
  tenantId,
63
66
  type,
64
67
  subType,
package/src/runner.js CHANGED
@@ -3,8 +3,8 @@
3
3
  const { randomUUID } = require("crypto");
4
4
 
5
5
  const eventQueueConfig = require("./config");
6
- const { eventQueueRunner, processEventQueue } = require("./processEventQueue");
7
- const { getWorkerPoolInstance } = require("./shared/WorkerQueue");
6
+ const { processEventQueue } = require("./processEventQueue");
7
+ const WorkerQueue = require("./shared/WorkerQueue");
8
8
  const cdsHelper = require("./shared/cdsHelper");
9
9
  const distributedLock = require("./shared/distributedLock");
10
10
  const SetIntervalDriftSafe = require("./shared/SetIntervalDriftSafe");
@@ -21,7 +21,7 @@ const OFFSET_FIRST_RUN = 10 * 1000;
21
21
  let tenantIdHash;
22
22
  let singleRunDone;
23
23
 
24
- const singleTenant = () => _scheduleFunction(_checkPeriodicEventsSingleTenant, _executeRunForTenant);
24
+ const singleTenant = () => _scheduleFunction(_checkPeriodicEventsSingleTenant, _singleTenantDb);
25
25
 
26
26
  const multiTenancyDb = () => _scheduleFunction(_multiTenancyPeriodicEvents, _multiTenancyDb);
27
27
 
@@ -75,7 +75,7 @@ const _multiTenancyRedis = async () => {
75
75
  return;
76
76
  }
77
77
 
78
- _executeAllTenants(tenantIds, runId);
78
+ return _executeEventsAllTenants(tenantIds, runId);
79
79
  };
80
80
 
81
81
  const _checkAndTriggerPeriodicEventUpdate = (tenantIds) => {
@@ -87,15 +87,43 @@ const _checkAndTriggerPeriodicEventUpdate = (tenantIds) => {
87
87
  if (tenantIdHash && tenantIdHash !== hash) {
88
88
  cds.log(COMPONENT_NAME).info("tenant id hash changed, triggering updating periodic events!");
89
89
  _multiTenancyPeriodicEvents().catch((err) => {
90
- cds.log(COMPONENT_NAME).error("Error during triggering updating periodic events! Error:", err);
90
+ cds.log(COMPONENT_NAME).error("Error during triggering updating periodic events!", err);
91
91
  });
92
92
  }
93
93
  };
94
94
 
95
- const _executeAllTenantsGeneric = (tenantIds, runId, fn) => {
96
- const workerQueueInstance = getWorkerPoolInstance();
95
+ const _executeEventsAllTenants = (tenantIds, runId) => {
96
+ const events = eventQueueConfig.allEvents;
97
+ const promises = [];
97
98
  tenantIds.forEach((tenantId) => {
98
- workerQueueInstance.addToQueue(async () => {
99
+ 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
+ });
116
+ }
117
+ })
118
+ );
119
+ });
120
+ });
121
+ return promises;
122
+ };
123
+
124
+ const _executePeriodicEventsAllTenants = (tenantIds, runId) => {
125
+ tenantIds.forEach((tenantId) => {
126
+ WorkerQueue.instance.addToQueue(1, async () => {
99
127
  try {
100
128
  const tenantContext = new cds.EventContext({ tenant: tenantId });
101
129
  const couldAcquireLock = await distributedLock.acquireLock(tenantContext, runId, {
@@ -104,7 +132,7 @@ const _executeAllTenantsGeneric = (tenantIds, runId, fn) => {
104
132
  if (!couldAcquireLock) {
105
133
  return;
106
134
  }
107
- await fn(tenantId, runId);
135
+ await _checkPeriodicEventsSingleTenant(tenantId);
108
136
  } catch (err) {
109
137
  cds.log(COMPONENT_NAME).error("executing event-queue run for tenant failed", {
110
138
  tenantId,
@@ -114,34 +142,20 @@ const _executeAllTenantsGeneric = (tenantIds, runId, fn) => {
114
142
  });
115
143
  };
116
144
 
117
- const _executeAllTenants = (tenantIds, runId) => _executeAllTenantsGeneric(tenantIds, runId, _executeRunForTenant);
118
-
119
- const _executePeriodicEventsAllTenants = (tenantIds, runId) =>
120
- _executeAllTenantsGeneric(tenantIds, runId, _checkPeriodicEventsSingleTenant);
121
-
122
- const _executeRunForTenant = async (tenantId, runId) => {
123
- const logger = cds.log(COMPONENT_NAME);
124
- try {
125
- const eventsForAutomaticRun = eventQueueConfig.allEvents;
126
- const subdomain = await cdsHelper.getSubdomainForTenantId(tenantId);
127
- const context = new cds.EventContext({
128
- tenant: tenantId,
129
- // NOTE: we need this because of logging otherwise logs would not contain the subdomain
130
- http: { req: { authInfo: { getSubdomain: () => subdomain } } },
131
- });
132
- cds.context = context;
133
- logger.info("executing eventQueue run", {
134
- tenantId,
135
- subdomain,
136
- ...(runId ? { runId } : null),
137
- });
138
- await eventQueueRunner(context, eventsForAutomaticRun);
139
- } catch (err) {
140
- logger.error(`Couldn't process eventQueue for tenant! Next try after defined interval. Error: ${err}`, {
141
- tenantId,
142
- redisEnabled: eventQueueConfig.redisEnabled,
145
+ const _singleTenantDb = async (tenantId) => {
146
+ const events = eventQueueConfig.allEvents;
147
+ events.forEach((event) => {
148
+ WorkerQueue.instance.addToQueue(event.load, async () => {
149
+ try {
150
+ await runEventCombinationForTenant(tenantId, event.type, event.subType, true);
151
+ } catch (err) {
152
+ cds.log(COMPONENT_NAME).error("executing event-queue run for tenant failed", {
153
+ tenantId,
154
+ redisEnabled: eventQueueConfig.redisEnabled,
155
+ });
156
+ }
143
157
  });
144
- }
158
+ });
145
159
  };
146
160
 
147
161
  const _acquireRunId = async (context) => {
@@ -196,15 +210,12 @@ const _calculateOffsetForFirstRun = async () => {
196
210
  } catch (err) {
197
211
  cds
198
212
  .log(COMPONENT_NAME)
199
- .error(
200
- "calculating offset for first run failed, falling back to default. Runs might be out-of-sync. Error:",
201
- err
202
- );
213
+ .error("calculating offset for first run failed, falling back to default. Runs might be out-of-sync.", err);
203
214
  }
204
215
  return offsetDependingOnLastRun;
205
216
  };
206
217
 
207
- const runEventCombinationForTenant = async (tenantId, type, subType) => {
218
+ const runEventCombinationForTenant = async (tenantId, type, subType, skipWorkerPool) => {
208
219
  try {
209
220
  const subdomain = await getSubdomainForTenantId(tenantId);
210
221
  const context = new cds.EventContext({
@@ -213,7 +224,15 @@ const runEventCombinationForTenant = async (tenantId, type, subType) => {
213
224
  http: { req: { authInfo: { getSubdomain: () => subdomain } } },
214
225
  });
215
226
  cds.context = context;
216
- getWorkerPoolInstance().addToQueue(async () => await processEventQueue(context, type, subType));
227
+ if (skipWorkerPool) {
228
+ return await processEventQueue(context, type, subType);
229
+ } else {
230
+ const config = eventQueueConfig.getEventConfig(type, subType);
231
+ return await WorkerQueue.instance.addToQueue(
232
+ config.load,
233
+ async () => await processEventQueue(context, type, subType)
234
+ );
235
+ }
217
236
  } catch (err) {
218
237
  const logger = cds.log(COMPONENT_NAME);
219
238
  logger.error("error executing event combination for tenant", err, {
@@ -230,11 +249,9 @@ const _multiTenancyDb = async () => {
230
249
  logger.info("executing event queue run for single instance and multi tenant");
231
250
  const tenantIds = await cdsHelper.getAllTenantIds();
232
251
  _checkAndTriggerPeriodicEventUpdate(tenantIds);
233
- _executeAllTenants(tenantIds, EVENT_QUEUE_RUN_ID);
252
+ return _executeEventsAllTenants(tenantIds, EVENT_QUEUE_RUN_ID);
234
253
  } catch (err) {
235
- logger.error(
236
- `Couldn't fetch tenant ids for event queue processing! Next try after defined interval. Error: ${err}`
237
- );
254
+ logger.error("Couldn't fetch tenant ids for event queue processing! Next try after defined interval.", err);
238
255
  }
239
256
  };
240
257
 
@@ -245,14 +262,18 @@ const _multiTenancyPeriodicEvents = async () => {
245
262
  const tenantIds = await cdsHelper.getAllTenantIds();
246
263
  _executePeriodicEventsAllTenants(tenantIds, EVENT_QUEUE_RUN_PERIODIC_EVENT);
247
264
  } catch (err) {
248
- logger.error(`Couldn't fetch tenant ids for updating periodic event processing! Error: ${err}`);
265
+ logger.error("Couldn't fetch tenant ids for updating periodic event processing!", err);
249
266
  }
250
267
  };
251
268
 
252
269
  const _checkPeriodicEventsSingleTenant = async (tenantId) => {
253
270
  const logger = cds.log(COMPONENT_NAME);
254
- if (!eventQueueConfig.updatePeriodicEvents) {
255
- logger.info("updating of periodic events is disabled");
271
+ if (!eventQueueConfig.updatePeriodicEvents || !eventQueueConfig.periodicEvents.length) {
272
+ logger.info("updating of periodic events is disabled or no periodic events configured", {
273
+ updateEnabled: eventQueueConfig.updatePeriodicEvents,
274
+ events: eventQueueConfig.periodicEvents.length,
275
+ });
276
+ return;
256
277
  }
257
278
  try {
258
279
  const subdomain = await cdsHelper.getSubdomainForTenantId(tenantId);
@@ -270,7 +291,7 @@ const _checkPeriodicEventsSingleTenant = async (tenantId) => {
270
291
  await periodicEvents.checkAndInsertPeriodicEvents(tx.context);
271
292
  });
272
293
  } catch (err) {
273
- logger.error(`Couldn't process eventQueue for tenant! Next try after defined interval. Error: ${err}`, {
294
+ logger.error("Couldn't update periodic events for tenant! Next try after defined interval.", err, {
274
295
  tenantId,
275
296
  redisEnabled: eventQueueConfig.redisEnabled,
276
297
  });
@@ -3,64 +3,106 @@
3
3
  const cds = require("@sap/cds");
4
4
 
5
5
  const config = require("../config");
6
+ const EventQueueError = require("../EventQueueError");
6
7
 
7
8
  const COMPONENT_NAME = "eventQueue/WorkerQueue";
8
-
9
- let instance = null;
9
+ const NANO_TO_MS = 1e6;
10
+ const THRESHOLD = {
11
+ INFO: 5 * 1000,
12
+ WARN: 10 * 1000,
13
+ ERROR: 15 * 1000,
14
+ };
10
15
 
11
16
  class WorkerQueue {
17
+ #concurrencyLimit;
18
+ #runningPromises;
19
+ #runningLoad;
20
+ #queue;
21
+ static #instance;
22
+
12
23
  constructor(concurrency) {
13
24
  if (Number.isNaN(concurrency) || concurrency <= 0) {
14
- this.__concurrencyLimit = 1;
25
+ this.#concurrencyLimit = 1;
15
26
  } else {
16
- this.__concurrencyLimit = concurrency;
27
+ this.#concurrencyLimit = concurrency;
17
28
  }
18
- this.__runningPromises = [];
19
- this.__queue = [];
29
+ this.#runningPromises = [];
30
+ this.#runningLoad = 0;
31
+ this.#queue = [];
20
32
  }
21
33
 
22
- addToQueue(cb) {
34
+ addToQueue(load, cb) {
35
+ if (load > this.#concurrencyLimit) {
36
+ throw EventQueueError.loadHigherThanLimit(load);
37
+ }
38
+
39
+ const startTime = process.hrtime.bigint();
23
40
  const p = new Promise((resolve, reject) => {
24
- this.__queue.push([cb, resolve, reject]);
41
+ this.#queue.push([load, cb, resolve, reject, startTime]);
25
42
  });
26
43
  this._checkForNext();
27
44
  return p;
28
45
  }
29
46
 
30
- _executeFunction(cb, resolve, reject) {
47
+ _executeFunction(load, cb, resolve, reject, startTime) {
48
+ this.checkAndLogWaitingTime(startTime);
31
49
  const promise = Promise.resolve().then(() => cb());
32
- this.__runningPromises.push(promise);
50
+ this.#runningPromises.push(promise);
51
+ this.#runningLoad = this.#runningLoad + load;
33
52
  promise
34
53
  .finally(() => {
35
- this.__runningPromises.splice(this.__runningPromises.indexOf(promise), 1);
54
+ this.#runningLoad = this.#runningLoad - load;
55
+ this.#runningPromises.splice(this.#runningPromises.indexOf(promise), 1);
36
56
  this._checkForNext();
37
57
  })
38
58
  .then((...results) => {
39
59
  resolve(...results);
40
60
  })
41
61
  .catch((err) => {
42
- cds.log(COMPONENT_NAME).error("Error happened in WorkQueue. Errors should be caught before! Error:", err);
62
+ cds.log(COMPONENT_NAME).error("Error happened in WorkQueue. Errors should be caught before!", err);
43
63
  reject(err);
44
64
  });
45
65
  }
46
66
 
47
67
  _checkForNext() {
48
- if (!this.__queue.length || this.__runningPromises.length >= this.__concurrencyLimit) {
68
+ const load = this.#queue[0]?.[0];
69
+ if (!this.#queue.length || this.#runningLoad + load > this.#concurrencyLimit) {
49
70
  return;
50
71
  }
51
- const [cb, resolve, reject] = this.__queue.shift();
52
- this._executeFunction(cb, resolve, reject);
72
+ const args = this.#queue.shift();
73
+ this._executeFunction(...args);
74
+ }
75
+
76
+ get runningPromises() {
77
+ return this.#runningPromises;
53
78
  }
54
- }
55
79
 
56
- module.exports = {
57
- getWorkerPoolInstance: () => {
58
- if (!instance) {
59
- instance = new WorkerQueue(config.parallelTenantProcessing);
80
+ /**
81
+ @return { WorkerQueue }
82
+ **/
83
+ static get instance() {
84
+ if (!WorkerQueue.#instance) {
85
+ WorkerQueue.#instance = new WorkerQueue(config.instanceLoadLimit);
60
86
  }
61
- return instance;
62
- },
63
- _: {
64
- WorkerQueue,
65
- },
66
- };
87
+ return WorkerQueue.#instance;
88
+ }
89
+
90
+ checkAndLogWaitingTime(startTime) {
91
+ const diffMs = Math.round(Number(process.hrtime.bigint() - startTime) / NANO_TO_MS);
92
+ let logLevel;
93
+ if (diffMs >= THRESHOLD.ERROR) {
94
+ logLevel = "error";
95
+ } else if (diffMs >= THRESHOLD.WARN) {
96
+ logLevel = "warn";
97
+ } else if (diffMs >= THRESHOLD.INFO) {
98
+ logLevel = "info";
99
+ } else {
100
+ logLevel = "debug";
101
+ }
102
+ cds.log(COMPONENT_NAME)[logLevel]("Waiting time in worker queue", {
103
+ diffMs,
104
+ });
105
+ }
106
+ }
107
+
108
+ module.exports = WorkerQueue;
@@ -1,9 +1,6 @@
1
1
  "use strict";
2
2
 
3
3
  const crypto = require("crypto");
4
-
5
- const { floor, abs, min } = Math;
6
-
7
4
  const arrayToFlatMap = (array, key = "ID") => {
8
5
  return array.reduce((result, element) => {
9
6
  result[element[key]] = element;
@@ -11,74 +8,6 @@ const arrayToFlatMap = (array, key = "ID") => {
11
8
  }, {});
12
9
  };
13
10
 
14
- /**
15
- * Establish a "Funnel" instance to limit how much
16
- * load can be processed in parallel. This is somewhat
17
- * similar to the limiter function however it has some
18
- * distinctintly different features. The Funnel will
19
- * not know in advance which functions and how many
20
- * loads it will have to process.
21
- */
22
- class Funnel {
23
- /**
24
- * Create a funnel with specified capacity
25
- * @param capacity - the capacity of the funnel (integer, sign will be ignored)
26
- */
27
- constructor(capacity = 100) {
28
- this.runningPromises = [];
29
- this.capacity = floor(abs(capacity));
30
- }
31
-
32
- /**
33
- * Asynchronously run a function that will put a specified load to the funnel.
34
- * The total amount of load of all running functions shall not
35
- * exceed the capacity of the funnel. If the desired load exceeds the capacity
36
- * the funnel will wait until sufficient capacity is available.
37
- * If a function requires a load >= capacity, then it will run
38
- * exclusively.
39
- * @param load - the load (integer, sign will be ignored)
40
- * @param f
41
- * @param args
42
- * @return {Promise<unknown>}
43
- */
44
- async run(load, f, ...args) {
45
- load = min(floor(abs(load)), Number.MAX_SAFE_INTEGER);
46
-
47
- // wait for sufficient capacity
48
- while (this.capacity < load && this.runningPromises.length > 0) {
49
- try {
50
- await Promise.race(this.runningPromises);
51
- } catch {
52
- // Yes, we must ignore exceptions here. The
53
- // caller expects exceptions from f and no
54
- // exceptions from other workloads.
55
- // Other exceptions must be handled by the
56
- // other callers. See (*) below.
57
- }
58
- }
59
-
60
- // map function call to promise
61
- const p = f.constructor.name === "AsyncFunction" ? f(...args) : Promise.resolve().then(() => f(...args));
62
-
63
- // create promise for book keeping
64
- const workload = p.finally(() => {
65
- // remove workload
66
- this.runningPromises.splice(this.runningPromises.indexOf(workload), 1);
67
- // and reclaim its capacity
68
- this.capacity += load;
69
- });
70
-
71
- // claim the capacity and schedule workload
72
- this.capacity -= load;
73
- this.runningPromises.push(workload);
74
-
75
- // make the caller wait for the workload
76
- // this also establish the seemingly missing
77
- // exception handling. See (*) above.
78
- return workload;
79
- }
80
- }
81
-
82
11
  /**
83
12
  * Defines a promise that resolves when all payloads are processed by the iterator, but limits
84
13
  * the number concurrent executions.
@@ -132,4 +61,4 @@ const processChunkedSync = (inputs, chunkSize, chunkHandler) => {
132
61
 
133
62
  const hashStringTo32Bit = (value) => crypto.createHash("sha256").update(String(value)).digest("base64").slice(0, 32);
134
63
 
135
- module.exports = { arrayToFlatMap, Funnel, limiter, isValidDate, processChunkedSync, hashStringTo32Bit };
64
+ module.exports = { arrayToFlatMap, limiter, isValidDate, processChunkedSync, hashStringTo32Bit };
@@ -4,6 +4,8 @@ const redis = require("./redis");
4
4
  const config = require("../config");
5
5
  const cdsHelper = require("./cdsHelper");
6
6
 
7
+ const KEY_PREFIX = "EVENT_QUEUE";
8
+
7
9
  const acquireLock = async (context, key, { tenantScoped = true, expiryTime = config.globalTxTimeout } = {}) => {
8
10
  const fullKey = _generateKey(context, tenantScoped, key);
9
11
  if (config.redisEnabled) {
@@ -128,7 +130,7 @@ const _generateKey = (context, tenantScoped, key) => {
128
130
  const keyParts = [];
129
131
  tenantScoped && keyParts.push(context.tenant);
130
132
  keyParts.push(key);
131
- return keyParts.join("##");
133
+ return `${KEY_PREFIX}_${keyParts.join("##")}`;
132
134
  };
133
135
 
134
136
  module.exports = {
@@ -19,7 +19,7 @@ class EventScheduler {
19
19
  return; // event combination already scheduled
20
20
  }
21
21
  this.#scheduledEvents[key] = true;
22
- cds.log(COMPONENT_NAME).info("scheduling event queue run for delayed event", {
22
+ cds.log(COMPONENT_NAME).debug("scheduling event queue run for delayed event", {
23
23
  type,
24
24
  subType,
25
25
  delaySeconds: (date.getTime() - Date.now()) / 1000,