@cap-js-community/event-queue 0.1.49 → 0.1.51

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.
@@ -2,8 +2,8 @@
2
2
 
3
3
  const cds = require("@sap/cds");
4
4
 
5
- const { executeInNewTransaction } = require("./shared/cdsHelper");
6
- const { EventProcessingStatus } = require("./constants");
5
+ const { executeInNewTransaction, TriggerRollback } = require("./shared/cdsHelper");
6
+ const { EventProcessingStatus, TransactionMode } = require("./constants");
7
7
  const distributedLock = require("./shared/distributedLock");
8
8
  const EventQueueError = require("./EventQueueError");
9
9
  const { arrayToFlatMap } = require("./shared/common");
@@ -17,8 +17,15 @@ const DEFAULT_RETRY_ATTEMPTS = 3;
17
17
  const DEFAULT_PARALLEL_EVENT_PROCESSING = 1;
18
18
  const LIMIT_PARALLEL_EVENT_PROCESSING = 10;
19
19
  const SELECT_LIMIT_EVENTS_PER_TICK = 100;
20
+ const DEFAULT_DELETE_FINISHED_EVENTS_AFTER = 0;
21
+ const DAYS_TO_MS = 24 * 60 * 60 * 1000;
22
+ const TRIES_FOR_EXCEEDED_EVENTS = 3;
20
23
 
21
24
  class EventQueueProcessorBase {
25
+ #eventsWithExceededTries = [];
26
+ #exceededTriesExceeded = [];
27
+ #selectedEventMap = {};
28
+
22
29
  constructor(context, eventType, eventSubType, config) {
23
30
  this.__context = context;
24
31
  this.__baseContext = context;
@@ -32,30 +39,34 @@ class EventQueueProcessorBase {
32
39
  this.__eventSubType = eventSubType;
33
40
  this.__queueEntriesWithPayloadMap = {};
34
41
  this.__config = config ?? {};
35
- this.__parallelEventProcessing =
36
- this.__config.parallelEventProcessing ??
37
- DEFAULT_PARALLEL_EVENT_PROCESSING;
42
+ this.__parallelEventProcessing = this.__config.parallelEventProcessing ?? DEFAULT_PARALLEL_EVENT_PROCESSING;
38
43
  if (this.__parallelEventProcessing > LIMIT_PARALLEL_EVENT_PROCESSING) {
39
44
  this.__parallelEventProcessing = LIMIT_PARALLEL_EVENT_PROCESSING;
40
45
  }
41
46
  // NOTE: keep the feature, this might be needed again
42
47
  this.__concurrentEventProcessing = false;
43
48
  this.__startTime = this.__config.startTime ?? new Date();
44
- this.__retryAttempts =
45
- this.__config.retryAttempts ?? DEFAULT_RETRY_ATTEMPTS;
46
- this.__selectMaxChunkSize =
47
- this.__config.selectMaxChunkSize ?? SELECT_LIMIT_EVENTS_PER_TICK;
49
+ this.__retryAttempts = this.__config.retryAttempts ?? DEFAULT_RETRY_ATTEMPTS;
50
+ this.__selectMaxChunkSize = this.__config.selectMaxChunkSize ?? SELECT_LIMIT_EVENTS_PER_TICK;
48
51
  this.__selectNextChunk = !!this.__config.checkForNextChunk;
49
52
  this.__keepalivePromises = {};
50
53
  this.__outdatedCheckEnabled = this.__config.eventOutdatedCheck ?? true;
51
- this.__commitOnEventLevel = this.__config.commitOnEventLevel ?? true;
52
- this.__eventsWithExceededTries = [];
54
+ this.__transactionMode = this.__config.transactionMode ?? TransactionMode.isolated;
55
+ if (this.__config.deleteFinishedEventsAfterDays) {
56
+ this.__deleteFinishedEventsAfter =
57
+ Number.isInteger(this.__config.deleteFinishedEventsAfterDays) && this.__config.deleteFinishedEventsAfterDays > 0
58
+ ? this.__config.deleteFinishedEventsAfterDays
59
+ : DEFAULT_DELETE_FINISHED_EVENTS_AFTER;
60
+ } else {
61
+ this.__deleteFinishedEventsAfter = DEFAULT_DELETE_FINISHED_EVENTS_AFTER;
62
+ }
53
63
  this.__emptyChunkSelected = false;
54
64
  this.__lockAcquired = false;
55
65
  this.__txUsageAllowed = true;
56
66
  this.__txMap = {};
57
67
  this.__txRollback = {};
58
68
  this.__eventQueueConfig = eventQueueConfig.getConfigInstance();
69
+ this.__queueEntries = [];
59
70
  }
60
71
 
61
72
  /**
@@ -77,17 +88,11 @@ class EventQueueProcessorBase {
77
88
  }
78
89
 
79
90
  startPerformanceTracerEvents() {
80
- this.__performanceLoggerEvents = new PerformanceTracer(
81
- this.logger,
82
- "Processing events"
83
- );
91
+ this.__performanceLoggerEvents = new PerformanceTracer(this.logger, "Processing events");
84
92
  }
85
93
 
86
94
  startPerformanceTracerPreprocessing() {
87
- this.__performanceLoggerPreprocessing = new PerformanceTracer(
88
- this.logger,
89
- "Preprocessing events"
90
- );
95
+ this.__performanceLoggerPreprocessing = new PerformanceTracer(this.logger, "Preprocessing events");
91
96
  }
92
97
 
93
98
  endPerformanceTracerEvents() {
@@ -175,10 +180,7 @@ class EventQueueProcessorBase {
175
180
  eventType: this.__eventType,
176
181
  eventSubType: this.__eventSubType,
177
182
  });
178
- this._determineAndAddEventStatusToMap(
179
- queueEntry.ID,
180
- EventProcessingStatus.Done
181
- );
183
+ this.#determineAndAddEventStatusToMap(queueEntry.ID, EventProcessingStatus.Done);
182
184
  }
183
185
 
184
186
  /**
@@ -189,14 +191,19 @@ class EventQueueProcessorBase {
189
191
  * In this case the events should be clustered together and only one mail should be sent.
190
192
  */
191
193
  clusterQueueEntries() {
192
- Object.entries(this.__queueEntriesWithPayloadMap).forEach(
193
- ([key, { queueEntry, payload }]) => {
194
- this._addEntryToProcessingMap(key, queueEntry, payload);
195
- }
196
- );
194
+ Object.entries(this.__queueEntriesWithPayloadMap).forEach(([key, { queueEntry, payload }]) => {
195
+ this.addEntryToProcessingMap(key, queueEntry, payload);
196
+ });
197
197
  }
198
198
 
199
- _addEntryToProcessingMap(key, queueEntry, payload) {
199
+ /**
200
+ * This function allows to add entries to the process map. This function is needed if the function clusterQueueEntries
201
+ * is redefined. For each entry in the processing map the processEvent function will be called once.
202
+ * @param {String} key key for event
203
+ * @param {Object} queueEntry queueEntry which should be clustered with this key
204
+ * @param {Object} payload payload which should be clustered with this key
205
+ */
206
+ addEntryToProcessingMap(key, queueEntry, payload) {
200
207
  this.logger.debug("add entry to processing map", {
201
208
  key,
202
209
  queueEntry,
@@ -212,33 +219,28 @@ class EventQueueProcessorBase {
212
219
 
213
220
  /**
214
221
  * This function sets the status of multiple events to a given status. If the structure of queueEntryProcessingStatusTuple
215
- * is not as expected all events will be set to error. The function respects the config commitOnEventLevel. If
216
- * commitOnEventLevel is true the status will be written to a dedicated map and returned afterwards to handle concurrent
222
+ * is not as expected all events will be set to error. The function respects the config transactionMode. If
223
+ * transactionMode is isolated the status will be written to a dedicated map and returned afterwards to handle concurrent
217
224
  * event processing.
218
225
  * @param {Array} queueEntries which has been selected from event queue table and been modified by modifyQueueEntry
219
226
  * @param {Array<Object>} queueEntryProcessingStatusTuple Array of tuple <queueEntryId, processingStatus>
227
+ * @param {boolean} returnMap Allows the function to allow the result as map
220
228
  * @return {Object} statusMap Map which contains all events for which a status has been set so far
221
229
  */
222
- setEventStatus(queueEntries, queueEntryProcessingStatusTuple) {
230
+ setEventStatus(queueEntries, queueEntryProcessingStatusTuple, returnMap = false) {
223
231
  this.logger.debug("setting event status for entries", {
224
- queueEntryProcessingStatusTuple: JSON.stringify(
225
- queueEntryProcessingStatusTuple
226
- ),
232
+ queueEntryProcessingStatusTuple: JSON.stringify(queueEntryProcessingStatusTuple),
227
233
  eventType: this.__eventType,
228
234
  eventSubType: this.__eventSubType,
229
235
  });
230
- const statusMap = this.__commitOnEventLevel ? {} : this.__statusMap;
236
+ const statusMap = this.commitOnEventLevel || returnMap ? {} : this.__statusMap;
231
237
  try {
232
238
  queueEntryProcessingStatusTuple.forEach(([id, processingStatus]) =>
233
- this._determineAndAddEventStatusToMap(id, processingStatus, statusMap)
239
+ this.#determineAndAddEventStatusToMap(id, processingStatus, statusMap)
234
240
  );
235
241
  } catch (error) {
236
242
  queueEntries.forEach((queueEntry) =>
237
- this._determineAndAddEventStatusToMap(
238
- queueEntry.ID,
239
- EventProcessingStatus.Error,
240
- statusMap
241
- )
243
+ this.#determineAndAddEventStatusToMap(queueEntry.ID, EventProcessingStatus.Error, statusMap)
242
244
  );
243
245
  this.logger.error(
244
246
  `The supplied status tuple doesn't have the required structure. Setting all entries to error. Error: ${error.toString()}`,
@@ -261,24 +263,16 @@ class EventQueueProcessorBase {
261
263
  try {
262
264
  queueEntry.payload = JSON.parse(queueEntry.payload);
263
265
  } catch {
264
- return queueEntry.payload;
266
+ /* empty */
265
267
  }
266
268
  }
267
269
 
268
- _determineAndAddEventStatusToMap(
269
- id,
270
- processingStatus,
271
- statusMap = this.__statusMap
272
- ) {
270
+ #determineAndAddEventStatusToMap(id, processingStatus, statusMap = this.__statusMap) {
273
271
  if (!statusMap[id]) {
274
272
  statusMap[id] = processingStatus;
275
273
  return;
276
274
  }
277
- if (
278
- [EventProcessingStatus.Error, EventProcessingStatus.Exceeded].includes(
279
- statusMap[id]
280
- )
281
- ) {
275
+ if ([EventProcessingStatus.Error, EventProcessingStatus.Exceeded].includes(statusMap[id])) {
282
276
  // NOTE: worst aggregation --> if already error|exceeded keep this state
283
277
  return;
284
278
  }
@@ -298,17 +292,9 @@ class EventQueueProcessorBase {
298
292
  }
299
293
  );
300
294
  queueEntries.forEach((queueEntry) =>
301
- this._determineAndAddEventStatusToMap(
302
- queueEntry.ID,
303
- EventProcessingStatus.Error
304
- )
305
- );
306
- return Object.fromEntries(
307
- queueEntries.map((queueEntry) => [
308
- queueEntry.ID,
309
- EventProcessingStatus.Error,
310
- ])
295
+ this.#determineAndAddEventStatusToMap(queueEntry.ID, EventProcessingStatus.Error)
311
296
  );
297
+ return Object.fromEntries(queueEntries.map((queueEntry) => [queueEntry.ID, EventProcessingStatus.Error]));
312
298
  }
313
299
 
314
300
  /**
@@ -316,23 +302,18 @@ class EventQueueProcessorBase {
316
302
  * selected events a status has been submitted. Persisting the status of events is done in a dedicated database tx.
317
303
  * The function accepts no arguments as there are dedicated functions to set the status of events (e.g. setEventStatus)
318
304
  */
319
- async persistEventStatus(
320
- tx,
321
- { skipChecks, statusMap = this.__statusMap } = {}
322
- ) {
305
+ async persistEventStatus(tx, { skipChecks, statusMap = this.__statusMap } = {}) {
323
306
  this.logger.debug("entering persistEventStatus", {
324
307
  eventType: this.__eventType,
325
308
  eventSubType: this.__eventSubType,
326
309
  });
327
- this._ensureOnlySelectedQueueEntries(statusMap);
310
+ this.#ensureOnlySelectedQueueEntries(statusMap);
328
311
  if (!skipChecks) {
329
- this._ensureEveryQueueEntryHasStatus();
312
+ this.#ensureEveryQueueEntryHasStatus();
330
313
  }
331
- this._ensureEveryStatusIsAllowed(statusMap);
314
+ this.#ensureEveryStatusIsAllowed(statusMap);
332
315
 
333
- const { success, failed, exceeded, invalidAttempts } = Object.entries(
334
- statusMap
335
- ).reduce(
316
+ const { success, failed, exceeded, invalidAttempts } = Object.entries(statusMap).reduce(
336
317
  (result, [notificationEntityId, processingStatus]) => {
337
318
  this.__commitedStatusMap[notificationEntityId] = processingStatus;
338
319
  if (processingStatus === EventProcessingStatus.Open) {
@@ -372,34 +353,24 @@ class EventQueueProcessorBase {
372
353
  .where("ID IN", invalidAttempts)
373
354
  );
374
355
  }
375
- if (success.length) {
356
+ const ts = new Date().toISOString();
357
+ const updateTuples = [
358
+ [success, EventProcessingStatus.Done],
359
+ [failed, EventProcessingStatus.Error],
360
+ [exceeded, EventProcessingStatus.Exceeded],
361
+ ];
362
+
363
+ for (const [eventIds, status] of updateTuples) {
364
+ if (!eventIds.length) {
365
+ continue;
366
+ }
376
367
  await tx.run(
377
368
  UPDATE.entity(this.__eventQueueConfig.tableNameEventQueue)
378
369
  .set({
379
- status: EventProcessingStatus.Done,
380
- lastAttemptTimestamp: new Date().toISOString(),
381
- })
382
- .where("ID IN", success)
383
- );
384
- }
385
- if (failed.length) {
386
- await tx.run(
387
- UPDATE.entity(this.__eventQueueConfig.tableNameEventQueue)
388
- .where("ID IN", failed)
389
- .with({
390
- status: EventProcessingStatus.Error,
391
- lastAttemptTimestamp: new Date().toISOString(),
392
- })
393
- );
394
- }
395
- if (exceeded.length) {
396
- await tx.run(
397
- UPDATE.entity(this.__eventQueueConfig.tableNameEventQueue)
398
- .where("ID IN", exceeded)
399
- .with({
400
- status: EventProcessingStatus.Exceeded,
401
- lastAttemptTimestamp: new Date().toISOString(),
370
+ status: status,
371
+ lastAttemptTimestamp: ts,
402
372
  })
373
+ .where("ID IN", eventIds)
403
374
  );
404
375
  }
405
376
  this.logger.debug("exiting persistEventStatus", {
@@ -408,30 +379,43 @@ class EventQueueProcessorBase {
408
379
  });
409
380
  }
410
381
 
411
- _ensureEveryQueueEntryHasStatus() {
382
+ async deleteFinishedEvents(tx) {
383
+ if (!this.__deleteFinishedEventsAfter) {
384
+ return;
385
+ }
386
+ const deleteCount = await tx.run(
387
+ DELETE.from(this.__eventQueueConfig.tableNameEventQueue).where(
388
+ "type =",
389
+ this.eventType,
390
+ "AND subType=",
391
+ this.eventSubType,
392
+ "AND lastAttemptTimestamp <=",
393
+ new Date(Date.now() - this.__deleteFinishedEventsAfter * DAYS_TO_MS).toISOString()
394
+ )
395
+ );
396
+ this.logger.debug("Deleted finished events", {
397
+ eventType: this.eventType,
398
+ eventSubType: this.eventSubType,
399
+ deleteFinishedEventsAfter: this.__deleteFinishedEventsAfter,
400
+ deleteCount,
401
+ });
402
+ }
403
+
404
+ #ensureEveryQueueEntryHasStatus() {
412
405
  this.__queueEntries.forEach((queueEntry) => {
413
- if (
414
- queueEntry.ID in this.__statusMap ||
415
- queueEntry.ID in this.__commitedStatusMap
416
- ) {
406
+ if (queueEntry.ID in this.__statusMap || queueEntry.ID in this.__commitedStatusMap) {
417
407
  return;
418
408
  }
419
- this.logger.error(
420
- "Missing status for selected event entry. Setting status to error",
421
- {
422
- eventType: this.__eventType,
423
- eventSubType: this.__eventSubType,
424
- queueEntry,
425
- }
426
- );
427
- this._determineAndAddEventStatusToMap(
428
- queueEntry.ID,
429
- EventProcessingStatus.Error
430
- );
409
+ this.logger.error("Missing status for selected event entry. Setting status to error", {
410
+ eventType: this.__eventType,
411
+ eventSubType: this.__eventSubType,
412
+ queueEntry,
413
+ });
414
+ this.#determineAndAddEventStatusToMap(queueEntry.ID, EventProcessingStatus.Error);
431
415
  });
432
416
  }
433
417
 
434
- _ensureEveryStatusIsAllowed(statusMap) {
418
+ #ensureEveryStatusIsAllowed(statusMap) {
435
419
  Object.entries(statusMap).forEach(([queueEntryId, status]) => {
436
420
  if (
437
421
  [
@@ -444,22 +428,19 @@ class EventQueueProcessorBase {
444
428
  return;
445
429
  }
446
430
 
447
- this.logger.error(
448
- "Not allowed event status returned. Only Open, Done, Error is allowed!",
449
- {
450
- eventType: this.__eventType,
451
- eventSubType: this.__eventSubType,
452
- queueEntryId,
453
- status: statusMap[queueEntryId],
454
- }
455
- );
431
+ this.logger.error("Not allowed event status returned. Only Open, Done, Error is allowed!", {
432
+ eventType: this.__eventType,
433
+ eventSubType: this.__eventSubType,
434
+ queueEntryId,
435
+ status: statusMap[queueEntryId],
436
+ });
456
437
  delete statusMap[queueEntryId];
457
438
  });
458
439
  }
459
440
 
460
- _ensureOnlySelectedQueueEntries(statusMap) {
441
+ #ensureOnlySelectedQueueEntries(statusMap) {
461
442
  Object.keys(statusMap).forEach((queueEntryId) => {
462
- if (this.__queueEntriesMap[queueEntryId]) {
443
+ if (this.#selectedEventMap[queueEntryId]) {
463
444
  return;
464
445
  }
465
446
 
@@ -476,18 +457,12 @@ class EventQueueProcessorBase {
476
457
  }
477
458
 
478
459
  handleErrorDuringClustering(error) {
479
- this.logger.error(
480
- `Error during clustering of events - setting all queue entries to error. Error: ${error}`,
481
- {
482
- eventType: this.__eventType,
483
- eventSubType: this.__eventSubType,
484
- }
485
- );
460
+ this.logger.error(`Error during clustering of events - setting all queue entries to error. Error: ${error}`, {
461
+ eventType: this.__eventType,
462
+ eventSubType: this.__eventSubType,
463
+ });
486
464
  this.__queueEntries.forEach((queueEntry) => {
487
- this._determineAndAddEventStatusToMap(
488
- queueEntry.ID,
489
- EventProcessingStatus.Error
490
- );
465
+ this.#determineAndAddEventStatusToMap(queueEntry.ID, EventProcessingStatus.Error);
491
466
  });
492
467
  }
493
468
 
@@ -500,29 +475,15 @@ class EventQueueProcessorBase {
500
475
  eventSubType: this.__eventSubType,
501
476
  }
502
477
  );
503
- this._determineAndAddEventStatusToMap(
504
- queueEntry.ID,
505
- EventProcessingStatus.Error
506
- );
478
+ this.#determineAndAddEventStatusToMap(queueEntry.ID, EventProcessingStatus.Error);
507
479
  }
508
480
 
509
- static async handleMissingTypeImplementation(
510
- context,
511
- eventType,
512
- eventSubType
513
- ) {
514
- const baseInstance = new EventQueueProcessorBase(
515
- context,
481
+ static async handleMissingTypeImplementation(context, eventType, eventSubType) {
482
+ const baseInstance = new EventQueueProcessorBase(context, eventType, eventSubType);
483
+ baseInstance.logger.error("No Implementation found in the provided configuration file.", {
516
484
  eventType,
517
- eventSubType
518
- );
519
- baseInstance.logger.error(
520
- "No Implementation found in the provided configuration file.",
521
- {
522
- eventType,
523
- eventSubType,
524
- }
525
- );
485
+ eventSubType,
486
+ });
526
487
  }
527
488
 
528
489
  /**
@@ -534,137 +495,179 @@ class EventQueueProcessorBase {
534
495
  */
535
496
  async getQueueEntriesAndSetToInProgress() {
536
497
  let result = [];
537
- await executeInNewTransaction(
538
- this.__baseContext,
539
- "eventQueue-getQueueEntriesAndSetToInProgress",
540
- async (tx) => {
541
- const entries = await tx.run(
542
- SELECT.from(this.__eventQueueConfig.tableNameEventQueue)
543
- .forUpdate({ wait: this.__eventQueueConfig.forUpdateTimeout })
544
- .limit(this.getSelectMaxChunkSize())
545
- .where(
546
- "type =",
547
- this.__eventType,
548
- "AND subType=",
549
- this.__eventSubType,
550
- "AND ( status =",
551
- EventProcessingStatus.Open,
552
- "OR ( status =",
553
- EventProcessingStatus.Error,
554
- "AND lastAttemptTimestamp <=",
555
- this.__startTime.toISOString(),
556
- ") OR ( status =",
557
- EventProcessingStatus.InProgress,
558
- "AND lastAttemptTimestamp <=",
559
- new Date(
560
- new Date().getTime() - this.__eventQueueConfig.globalTxTimeout
561
- ).toISOString(),
562
- ") )"
563
- )
564
- .orderBy("createdAt", "ID")
565
- );
566
-
567
- if (!entries.length) {
568
- this.logger.debug("no entries available for processing", {
569
- eventType: this.__eventType,
570
- eventSubType: this.__eventSubType,
571
- });
572
- this.__emptyChunkSelected = true;
573
- return;
574
- }
575
-
576
- const { exceededTries, openEvents } =
577
- this._filterExceededEvents(entries);
578
- if (exceededTries.length) {
579
- this.__eventsWithExceededTries = exceededTries;
580
- }
581
- result = openEvents;
582
-
583
- if (!result.length) {
584
- this.__emptyChunkSelected = true;
585
- return;
586
- }
498
+ await executeInNewTransaction(this.__baseContext, "eventQueue-getQueueEntriesAndSetToInProgress", async (tx) => {
499
+ const entries = await tx.run(
500
+ SELECT.from(this.__eventQueueConfig.tableNameEventQueue)
501
+ .forUpdate({ wait: this.__eventQueueConfig.forUpdateTimeout })
502
+ .limit(this.selectMaxChunkSize)
503
+ .where(
504
+ "type =",
505
+ this.__eventType,
506
+ "AND subType=",
507
+ this.__eventSubType,
508
+ "AND ( status =",
509
+ EventProcessingStatus.Open,
510
+ "OR ( status =",
511
+ EventProcessingStatus.Error,
512
+ "AND lastAttemptTimestamp <=",
513
+ this.__startTime.toISOString(),
514
+ ") OR ( status =",
515
+ EventProcessingStatus.InProgress,
516
+ "AND lastAttemptTimestamp <=",
517
+ new Date(new Date().getTime() - this.__eventQueueConfig.globalTxTimeout).toISOString(),
518
+ ") )"
519
+ )
520
+ .orderBy("createdAt", "ID")
521
+ );
587
522
 
588
- this.logger.info("Selected event queue entries for processing", {
589
- queueEntriesCount: result.length,
523
+ if (!entries.length) {
524
+ this.logger.debug("no entries available for processing", {
590
525
  eventType: this.__eventType,
591
526
  eventSubType: this.__eventSubType,
592
527
  });
528
+ this.__emptyChunkSelected = true;
529
+ return;
530
+ }
593
531
 
594
- const isoTimestamp = new Date().toISOString();
595
- await tx.run(
596
- UPDATE.entity(this.__eventQueueConfig.tableNameEventQueue)
597
- .with({
598
- status: EventProcessingStatus.InProgress,
599
- lastAttemptTimestamp: isoTimestamp,
600
- attempts: { "+=": 1 },
601
- })
602
- .where(
603
- "ID IN",
604
- result.map(({ ID }) => ID)
605
- )
606
- );
607
- result.forEach((entry) => {
608
- entry.lastAttemptTimestamp = isoTimestamp;
609
- // NOTE: empty payloads are supported on DB-Level.
610
- // Behaviour of event queue is: null as payload is treated as obsolete/done
611
- // For supporting this convert null to empty string --> "" as payload will be processed normally
612
- if (entry.payload === null) {
613
- entry.payload = "";
614
- }
615
- });
532
+ this.#selectedEventMap = arrayToFlatMap(entries);
533
+ const { exceededTries, openEvents, exceededTriesExceeded } = this.#filterExceededEvents(entries);
534
+ if (exceededTries.length) {
535
+ this.#eventsWithExceededTries = exceededTries;
616
536
  }
617
- );
618
- this.__queueEntries = result;
619
- this.__queueEntriesMap = arrayToFlatMap(result);
537
+ if (exceededTriesExceeded.length) {
538
+ this.#exceededTriesExceeded = exceededTriesExceeded;
539
+ }
540
+
541
+ result = openEvents;
542
+
543
+ if (!result.length) {
544
+ this.__emptyChunkSelected = true;
545
+ }
546
+
547
+ this.logger.info("Selected event queue entries for processing", {
548
+ queueEntriesCount: result.length,
549
+ eventType: this.__eventType,
550
+ eventSubType: this.__eventSubType,
551
+ });
552
+
553
+ const isoTimestamp = new Date().toISOString();
554
+ await tx.run(
555
+ UPDATE.entity(this.__eventQueueConfig.tableNameEventQueue)
556
+ .with({
557
+ status: EventProcessingStatus.InProgress,
558
+ lastAttemptTimestamp: isoTimestamp,
559
+ attempts: { "+=": 1 },
560
+ })
561
+ .where(
562
+ "ID IN",
563
+ entries.map(({ ID }) => ID)
564
+ )
565
+ );
566
+ entries.forEach((entry) => {
567
+ entry.lastAttemptTimestamp = isoTimestamp;
568
+ // NOTE: empty payloads are supported on DB-Level.
569
+ // Behaviour of event queue is: null as payload is treated as obsolete/done
570
+ // For supporting this convert null to empty string --> "" as payload will be processed normally
571
+ if (entry.payload === null) {
572
+ entry.payload = "";
573
+ }
574
+ });
575
+ this.__queueEntries = result;
576
+ this.__queueEntriesMap = arrayToFlatMap(result);
577
+ });
620
578
  return result;
621
579
  }
622
580
 
623
- _filterExceededEvents(events) {
581
+ #filterExceededEvents(events) {
624
582
  return events.reduce(
625
583
  (result, event) => {
626
- if (event.attempts === this.__retryAttempts) {
584
+ if (event.attempts === this.__retryAttempts + TRIES_FOR_EXCEEDED_EVENTS) {
585
+ result.exceededTriesExceeded.push(event);
586
+ } else if (event.attempts >= this.__retryAttempts) {
627
587
  result.exceededTries.push(event);
628
588
  } else {
629
589
  result.openEvents.push(event);
630
590
  }
631
591
  return result;
632
592
  },
633
- { exceededTries: [], openEvents: [] }
593
+ { exceededTries: [], openEvents: [], exceededTriesExceeded: [] }
634
594
  );
635
595
  }
636
596
 
637
- async handleExceededEvents(exceededEvents) {
638
- await this.tx.run(
639
- UPDATE.entity(this.__eventQueueConfig.tableNameEventQueue)
640
- .with({
641
- status: EventProcessingStatus.Exceeded,
642
- })
643
- .where(
644
- "ID IN",
645
- exceededEvents.map(({ ID }) => ID)
646
- )
647
- );
648
- this.logger.error(
649
- "The retry attempts for the following events are exceeded",
650
- {
597
+ async handleExceededEvents() {
598
+ await this.#handleExceededTriesExceeded();
599
+ if (!this.#eventsWithExceededTries.length) {
600
+ return;
601
+ }
602
+
603
+ for (const exceededEvent of this.#eventsWithExceededTries) {
604
+ await executeInNewTransaction(
605
+ this.context,
606
+ `eventQueue-handleExceededEvents-${this.eventType}##${this.eventSubType}`,
607
+ async (tx) => {
608
+ try {
609
+ this.processEventContext = tx.context;
610
+ this.modifyQueueEntry(exceededEvent);
611
+ await this.hookForExceededEvents({ ...exceededEvent });
612
+ this.logger.warn("The retry attempts for the following events are exceeded", {
613
+ eventType: this.__eventType,
614
+ eventSubType: this.__eventSubType,
615
+ retryAttempts: this.__retryAttempts,
616
+ queueEntriesId: exceededEvent.ID,
617
+ currentAttempt: exceededEvent.attempts,
618
+ });
619
+ await this.#persistEventQueueStatusForExceeded(this.tx, [exceededEvent], EventProcessingStatus.Exceeded);
620
+ } catch (err) {
621
+ this.logger.error(
622
+ `Caught error during hook for exceeded events - setting queue entry to error. Please catch your promises/exceptions. Error: ${err}`,
623
+ {
624
+ eventType: this.__eventType,
625
+ eventSubType: this.__eventSubType,
626
+ retryAttempts: this.__retryAttempts,
627
+ queueEntriesId: exceededEvent.ID,
628
+ currentAttempt: exceededEvent.attempts,
629
+ }
630
+ );
631
+ await executeInNewTransaction(this.context, "error-hookForExceededEvents", async (tx) =>
632
+ this.#persistEventQueueStatusForExceeded(tx, [exceededEvent], EventProcessingStatus.Error)
633
+ );
634
+ throw new TriggerRollback();
635
+ }
636
+ }
637
+ );
638
+ }
639
+ }
640
+
641
+ async #handleExceededTriesExceeded() {
642
+ if (this.#exceededTriesExceeded.length) {
643
+ this.logger.error("Event hook failure exceeded, status set to 'exceeded' without invoking hook again!", {
651
644
  eventType: this.__eventType,
652
645
  eventSubType: this.__eventSubType,
653
- retryAttempts: this.__retryAttempts,
654
- queueEntriesIds: exceededEvents.map(({ ID }) => ID),
655
- }
646
+ queueEntriesIds: this.#eventsWithExceededTries.map(({ ID }) => ID),
647
+ });
648
+ await executeInNewTransaction(this.context, "exceededTriesExceeded", async (tx) => {
649
+ await this.#persistEventQueueStatusForExceeded(tx, this.#exceededTriesExceeded, EventProcessingStatus.Exceeded);
650
+ });
651
+ }
652
+ }
653
+
654
+ async #persistEventQueueStatusForExceeded(tx, events, status) {
655
+ const statusMap = this.setEventStatus(
656
+ events,
657
+ events.map((e) => [e.ID, status]),
658
+ true
656
659
  );
657
- await this.hookForExceededEvents(exceededEvents);
660
+ await this.persistEventStatus(tx, { statusMap, skipChecks: true });
658
661
  }
659
662
 
660
663
  /**
661
664
  * This function enables the possibility to execute custom actions for events for which the retry attempts have been
662
665
  * exceeded. As always a valid transaction is available with this.tx. This transaction will be committed after the
663
666
  * execution of this function.
664
- * @param {Object} exceededEvents exceeded event queue entries
667
+ * @param {Object} exceededEvent exceeded event queue entry
665
668
  */
666
669
  // eslint-disable-next-line no-unused-vars
667
- async hookForExceededEvents(exceededEvents) {}
670
+ async hookForExceededEvents(exceededEvent) {}
668
671
 
669
672
  /**
670
673
  * This function serves the purpose of mass enabled preloading data for processing the events which are added with
@@ -687,9 +690,7 @@ class EventQueueProcessorBase {
687
690
  return false;
688
691
  }
689
692
  let eventOutdated;
690
- const runningChecks = queueEntries
691
- .map((queueEntry) => this.__keepalivePromises[queueEntry.ID])
692
- .filter((p) => p);
693
+ const runningChecks = queueEntries.map((queueEntry) => this.__keepalivePromises[queueEntry.ID]).filter((p) => p);
693
694
  if (runningChecks.length === queueEntries.length) {
694
695
  const results = await Promise.allSettled(runningChecks);
695
696
  for (const { value } of results) {
@@ -702,70 +703,54 @@ class EventQueueProcessorBase {
702
703
  await Promise.allSettled(runningChecks);
703
704
  }
704
705
  const checkAndUpdatePromise = new Promise((resolve) => {
705
- executeInNewTransaction(
706
- this.__baseContext,
707
- "eventProcessing-isOutdatedAndKeepalive",
708
- async (tx) => {
709
- const queueEntriesFresh = await tx.run(
710
- SELECT.from(this.__eventQueueConfig.tableNameEventQueue)
711
- .forUpdate({ wait: this.__eventQueueConfig.forUpdateTimeout })
706
+ executeInNewTransaction(this.__baseContext, "eventProcessing-isOutdatedAndKeepalive", async (tx) => {
707
+ const queueEntriesFresh = await tx.run(
708
+ SELECT.from(this.__eventQueueConfig.tableNameEventQueue)
709
+ .forUpdate({ wait: this.__eventQueueConfig.forUpdateTimeout })
710
+ .where(
711
+ "ID IN",
712
+ queueEntries.map(({ ID }) => ID)
713
+ )
714
+ .columns("ID", "lastAttemptTimestamp")
715
+ );
716
+ eventOutdated = queueEntriesFresh.some((queueEntryFresh) => {
717
+ const queueEntry = this.__queueEntriesMap[queueEntryFresh.ID];
718
+ return queueEntry?.lastAttemptTimestamp !== queueEntryFresh.lastAttemptTimestamp;
719
+ });
720
+ let newTs = new Date().toISOString();
721
+ if (!eventOutdated) {
722
+ await tx.run(
723
+ UPDATE.entity(this.__eventQueueConfig.tableNameEventQueue)
724
+ .set("lastAttemptTimestamp =", newTs)
712
725
  .where(
713
726
  "ID IN",
714
727
  queueEntries.map(({ ID }) => ID)
715
728
  )
716
- .columns("ID", "lastAttemptTimestamp")
717
729
  );
718
- eventOutdated = queueEntriesFresh.some((queueEntryFresh) => {
719
- const queueEntry = this.__queueEntriesMap[queueEntryFresh.ID];
720
- return (
721
- queueEntry?.lastAttemptTimestamp !==
722
- queueEntryFresh.lastAttemptTimestamp
723
- );
724
- });
725
- let newTs = new Date().toISOString();
726
- if (!eventOutdated) {
727
- await tx.run(
728
- UPDATE.entity(this.__eventQueueConfig.tableNameEventQueue)
729
- .set("lastAttemptTimestamp =", newTs)
730
- .where(
731
- "ID IN",
732
- queueEntries.map(({ ID }) => ID)
733
- )
734
- );
735
- } else {
736
- newTs = null;
737
- this.logger.warn(
738
- "event data has been modified. Processing skipped.",
739
- {
740
- eventType: this.__eventType,
741
- eventSubType: this.__eventSubType,
742
- queueEntriesIds: queueEntries.map(({ ID }) => ID),
743
- }
744
- );
745
- queueEntries.forEach(
746
- ({ ID: queueEntryId }) =>
747
- delete this.__queueEntriesMap[queueEntryId]
748
- );
749
- }
750
- this.__queueEntries = Object.values(this.__queueEntriesMap);
751
- queueEntriesFresh.forEach((queueEntryFresh) => {
752
- if (this.__queueEntriesMap[queueEntryFresh.ID]) {
753
- const queueEntry = this.__queueEntriesMap[queueEntryFresh.ID];
754
- if (newTs) {
755
- queueEntry.lastAttemptTimestamp = newTs;
756
- }
757
- }
758
- delete this.__keepalivePromises[queueEntryFresh.ID];
730
+ } else {
731
+ newTs = null;
732
+ this.logger.warn("event data has been modified. Processing skipped.", {
733
+ eventType: this.__eventType,
734
+ eventSubType: this.__eventSubType,
735
+ queueEntriesIds: queueEntries.map(({ ID }) => ID),
759
736
  });
760
- resolve(eventOutdated);
737
+ queueEntries.forEach(({ ID: queueEntryId }) => delete this.__queueEntriesMap[queueEntryId]);
761
738
  }
762
- );
739
+ this.__queueEntries = Object.values(this.__queueEntriesMap);
740
+ queueEntriesFresh.forEach((queueEntryFresh) => {
741
+ if (this.__queueEntriesMap[queueEntryFresh.ID]) {
742
+ const queueEntry = this.__queueEntriesMap[queueEntryFresh.ID];
743
+ if (newTs) {
744
+ queueEntry.lastAttemptTimestamp = newTs;
745
+ }
746
+ }
747
+ delete this.__keepalivePromises[queueEntryFresh.ID];
748
+ });
749
+ resolve(eventOutdated);
750
+ });
763
751
  });
764
752
 
765
- queueEntries.forEach(
766
- (queueEntry) =>
767
- (this.__keepalivePromises[queueEntry.ID] = checkAndUpdatePromise)
768
- );
753
+ queueEntries.forEach((queueEntry) => (this.__keepalivePromises[queueEntry.ID] = checkAndUpdatePromise));
769
754
  return await checkAndUpdatePromise;
770
755
  }
771
756
 
@@ -790,15 +775,9 @@ class EventQueueProcessorBase {
790
775
  return;
791
776
  }
792
777
  try {
793
- await distributedLock.releaseLock(
794
- this.context,
795
- [this.eventType, this.eventSubType].join("##")
796
- );
778
+ await distributedLock.releaseLock(this.context, [this.eventType, this.eventSubType].join("##"));
797
779
  } catch (err) {
798
- this.logger.error(
799
- "Releasing distributed lock failed. Error:",
800
- err.toString()
801
- );
780
+ this.logger.error("Releasing distributed lock failed. Error:", err.toString());
802
781
  }
803
782
  }
804
783
 
@@ -806,26 +785,11 @@ class EventQueueProcessorBase {
806
785
  return Object.values(statusMap).includes(EventProcessingStatus.Error);
807
786
  }
808
787
 
809
- getSelectNextChunk() {
810
- return this.__selectNextChunk;
811
- }
812
-
813
- getSelectMaxChunkSize() {
814
- return this.__selectMaxChunkSize;
815
- }
816
-
817
788
  clearEventProcessingContext() {
818
789
  this.__processContext = null;
819
790
  this.__processTx = null;
820
791
  }
821
792
 
822
- get shouldTriggerRollback() {
823
- return (
824
- this.statusMapContainsError(this.__statusMap) ||
825
- this.statusMapContainsError(this.__commitedStatusMap)
826
- );
827
- }
828
-
829
793
  get logger() {
830
794
  return this.__logger ?? this.__baseLogger;
831
795
  }
@@ -875,7 +839,11 @@ class EventQueueProcessorBase {
875
839
  }
876
840
 
877
841
  get commitOnEventLevel() {
878
- return this.__commitOnEventLevel;
842
+ return this.__transactionMode === TransactionMode.isolated;
843
+ }
844
+
845
+ get transactionMode() {
846
+ return this.__transactionMode;
879
847
  }
880
848
 
881
849
  get eventType() {
@@ -886,14 +854,18 @@ class EventQueueProcessorBase {
886
854
  return this.__eventSubType;
887
855
  }
888
856
 
889
- get exceededEvents() {
890
- return this.__eventsWithExceededTries;
891
- }
892
-
893
857
  get emptyChunkSelected() {
894
858
  return this.__emptyChunkSelected;
895
859
  }
896
860
 
861
+ get selectNextChunk() {
862
+ return this.__selectNextChunk;
863
+ }
864
+
865
+ get selectMaxChunkSize() {
866
+ return this.__selectMaxChunkSize;
867
+ }
868
+
897
869
  set txUsageAllowed(value) {
898
870
  this.__txUsageAllowed = value;
899
871
  }