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

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.3",
3
+ "version": "0.2.4",
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": [
@@ -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
  });
@@ -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,7 +853,7 @@ 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
 
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;
@@ -27,6 +28,7 @@ class Config {
27
28
  #configFilePath;
28
29
  #processEventsAfterPublish;
29
30
  #skipCsnCheck;
31
+ #registerAsEventProcessor;
30
32
  #disableRedis;
31
33
  #env;
32
34
  #eventMap;
@@ -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;
@@ -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
@@ -88,6 +88,7 @@ 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,
@@ -63,12 +63,15 @@ const checkAndInsertPeriodicEvents = async (context) => {
63
63
  cds.log(COMPONENT_NAME).info("deleting periodic events because they have changed", {
64
64
  changedEvents: exitingWithNotMatchingInterval.map(({ type, subType }) => ({ type, subType })),
65
65
  });
66
- await tx.run(
67
- DELETE.from(eventConfig.tableNameEventQueue).where(
68
- "ID IN",
69
- exitingWithNotMatchingInterval.map(({ ID }) => ID)
70
- )
71
- );
66
+
67
+ if (exitingWithNotMatchingInterval.length) {
68
+ await tx.run(
69
+ DELETE.from(eventConfig.tableNameEventQueue).where(
70
+ "ID IN",
71
+ exitingWithNotMatchingInterval.map(({ ID }) => ID)
72
+ )
73
+ );
74
+ }
72
75
 
73
76
  const newOrChangedEvents = newEvents.concat(exitingWithNotMatchingInterval);
74
77
 
@@ -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.parallelTenantProcessing);
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 = {