@cap-js-community/event-queue 1.8.6 → 1.9.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cap-js-community/event-queue",
3
- "version": "1.8.6",
3
+ "version": "1.9.0-beta.0",
4
4
  "description": "An event queue that enables secure transactional processing of asynchronous and periodic events, featuring instant event processing with Redis Pub/Sub and load distribution across all application instances.",
5
5
  "main": "src/index.js",
6
6
  "types": "src/index.d.ts",
@@ -11,8 +11,8 @@ const { arrayToFlatMap } = require("./shared/common");
11
11
  const eventScheduler = require("./shared/eventScheduler");
12
12
  const eventConfig = require("./config");
13
13
  const PerformanceTracer = require("./shared/PerformanceTracer");
14
- const { broadcastEvent } = require("./redis/redisPub");
15
14
  const trace = require("./shared/openTelemetry");
15
+ const SetIntervalDriftSafe = require("./shared/SetIntervalDriftSafe");
16
16
 
17
17
  const IMPLEMENT_ERROR_MESSAGE = "needs to be reimplemented";
18
18
  const COMPONENT_NAME = "/eventQueue/EventQueueProcessorBase";
@@ -23,7 +23,6 @@ const LIMIT_PARALLEL_EVENT_PROCESSING = 10;
23
23
  const SELECT_LIMIT_EVENTS_PER_TICK = 100;
24
24
  const TRIES_FOR_EXCEEDED_EVENTS = 3;
25
25
  const EVENT_START_AFTER_HEADROOM = 3 * 1000;
26
- const ETAG_CHECK_AFTER_MIN = 10;
27
26
  const SUFFIX_PERIODIC = "_PERIODIC";
28
27
  const DEFAULT_RETRY_AFTER = 5 * 60 * 1000;
29
28
 
@@ -40,6 +39,9 @@ class EventQueueProcessorBase {
40
39
  #isPeriodic;
41
40
  #lastSuccessfulRunTimestamp;
42
41
  #retryFailedAfter;
42
+ #keepAliveRunner;
43
+ #currentKeepAlivePromise = Promise.resolve();
44
+ #etagMap;
43
45
 
44
46
  constructor(context, eventType, eventSubType, config) {
45
47
  this.__context = context;
@@ -53,6 +55,8 @@ class EventQueueProcessorBase {
53
55
  this.__eventProcessingMap = {};
54
56
  this.__statusMap = {};
55
57
  this.__commitedStatusMap = {};
58
+ this.__notCommitedStatusMap = {};
59
+ this.__outdatedEventMap = {};
56
60
  this.#eventType = eventType;
57
61
  this.#eventSubType = eventSubType;
58
62
  this.#eventConfig = config ?? {};
@@ -66,8 +70,6 @@ class EventQueueProcessorBase {
66
70
  this.__retryAttempts = this.#isPeriodic ? 1 : this.#eventConfig.retryAttempts ?? DEFAULT_RETRY_ATTEMPTS;
67
71
  this.__selectMaxChunkSize = this.#eventConfig.selectMaxChunkSize ?? SELECT_LIMIT_EVENTS_PER_TICK;
68
72
  this.__selectNextChunk = !!this.#eventConfig.checkForNextChunk;
69
- this.__keepalivePromises = {};
70
- this.__outdatedCheckEnabled = this.#eventConfig.eventOutdatedCheck ?? true;
71
73
  this.__transactionMode = this.#eventConfig.transactionMode ?? TransactionMode.isolated;
72
74
  this.__emptyChunkSelected = false;
73
75
  this.__lockAcquired = false;
@@ -75,6 +77,7 @@ class EventQueueProcessorBase {
75
77
  this.__txMap = {};
76
78
  this.__txRollback = {};
77
79
  this.__queueEntries = [];
80
+ this.#keepAliveRunner = new SetIntervalDriftSafe(this.#eventConfig.keepAliveInterval);
78
81
  }
79
82
 
80
83
  /**
@@ -398,16 +401,17 @@ class EventQueueProcessorBase {
398
401
  this.#ensureEveryStatusIsAllowed(statusMap);
399
402
 
400
403
  const { success, failed, exceeded, invalidAttempts } = Object.entries(statusMap).reduce(
401
- (result, [notificationEntityId, processingStatus]) => {
402
- this.__commitedStatusMap[notificationEntityId] = processingStatus;
404
+ (result, [queueEntryId, processingStatus]) => {
405
+ this.__commitedStatusMap[queueEntryId] = processingStatus;
406
+ delete this.__notCommitedStatusMap[queueEntryId];
403
407
  if (processingStatus === EventProcessingStatus.Open) {
404
- result.invalidAttempts.push(notificationEntityId);
408
+ result.invalidAttempts.push(queueEntryId);
405
409
  } else if (processingStatus === EventProcessingStatus.Done) {
406
- result.success.push(notificationEntityId);
410
+ result.success.push(queueEntryId);
407
411
  } else if (processingStatus === EventProcessingStatus.Error) {
408
- result.failed.push(notificationEntityId);
412
+ result.failed.push(queueEntryId);
409
413
  } else if (processingStatus === EventProcessingStatus.Exceeded) {
410
- result.exceeded.push(notificationEntityId);
414
+ result.exceeded.push(queueEntryId);
411
415
  }
412
416
  return result;
413
417
  },
@@ -483,7 +487,11 @@ class EventQueueProcessorBase {
483
487
 
484
488
  #ensureEveryQueueEntryHasStatus() {
485
489
  this.__queueEntries.forEach((queueEntry) => {
486
- if (queueEntry.ID in this.__statusMap || queueEntry.ID in this.__commitedStatusMap) {
490
+ if (
491
+ queueEntry.ID in this.__statusMap ||
492
+ queueEntry.ID in this.__commitedStatusMap ||
493
+ queueEntry.ID in this.__outdatedEventMap
494
+ ) {
487
495
  return;
488
496
  }
489
497
  this.logger.error("Missing status for selected event entry. Setting status to error", {
@@ -600,7 +608,7 @@ class EventQueueProcessorBase {
600
608
  "OR lastAttemptTimestamp IS NULL ) OR ( status =",
601
609
  EventProcessingStatus.InProgress,
602
610
  "AND lastAttemptTimestamp <=",
603
- new Date(baseDate - this.#config.globalTxTimeout).toISOString(),
611
+ new Date(baseDate - this.#eventConfig.keepAliveMaxInProgressTime).toISOString(),
604
612
  ") )",
605
613
  ]
606
614
  : [
@@ -611,7 +619,7 @@ class EventQueueProcessorBase {
611
619
  ") OR ( status =",
612
620
  EventProcessingStatus.InProgress,
613
621
  "AND lastAttemptTimestamp <=",
614
- new Date(baseDate - this.#config.globalTxTimeout).toISOString(),
622
+ new Date(baseDate - this.#eventConfig.keepAliveMaxInProgressTime).toISOString(),
615
623
  ") )",
616
624
  ])
617
625
  )
@@ -698,6 +706,8 @@ class EventQueueProcessorBase {
698
706
  });
699
707
  this.__queueEntries = result;
700
708
  this.__queueEntriesMap = arrayToFlatMap(result);
709
+ this.__notCommitedStatusMap = arrayToFlatMap(result);
710
+ this.#etagMap = Object.fromEntries(result.map((event) => [event.ID, event.lastAttemptTimestamp]));
701
711
 
702
712
  if (this.#isPeriodic && this.#eventConfig.lastSuccessfulRunTimestamp) {
703
713
  this.#lastSuccessfulRunTimestamp = await this.#selectLastSuccessfulPeriodicTimestamp(tx);
@@ -765,7 +775,7 @@ class EventQueueProcessorBase {
765
775
  return await trace(this.baseContext, "handle-exceeded-events", async () => {
766
776
  for (const exceededEvent of this.#eventsWithExceededTries) {
767
777
  await executeInNewTransaction(
768
- this.context,
778
+ this.__baseContext,
769
779
  `eventQueue-handleExceededEvents-${this.#eventType}##${this.#eventSubType}`,
770
780
  async (tx) => {
771
781
  try {
@@ -792,7 +802,7 @@ class EventQueueProcessorBase {
792
802
  currentAttempt: exceededEvent.attempts,
793
803
  }
794
804
  );
795
- await executeInNewTransaction(this.context, "error-hookForExceededEvents", async (tx) =>
805
+ await executeInNewTransaction(this.__baseContext, "error-hookForExceededEvents", async (tx) =>
796
806
  this.#persistEventQueueStatusForExceeded(tx, [exceededEvent], EventProcessingStatus.Error)
797
807
  );
798
808
  await tx.rollback();
@@ -810,7 +820,7 @@ class EventQueueProcessorBase {
810
820
  eventSubType: this.#eventSubType,
811
821
  queueEntriesIds: this.#eventsWithExceededTries.map(({ ID }) => ID),
812
822
  });
813
- await executeInNewTransaction(this.context, "exceededTriesExceeded", async (tx) => {
823
+ await executeInNewTransaction(this.__baseContext, "exceededTriesExceeded", async (tx) => {
814
824
  await this.#persistEventQueueStatusForExceeded(tx, this.#exceededTriesExceeded, EventProcessingStatus.Exceeded);
815
825
  });
816
826
  }
@@ -842,81 +852,89 @@ class EventQueueProcessorBase {
842
852
  // eslint-disable-next-line no-unused-vars
843
853
  async beforeProcessingEvents() {}
844
854
 
845
- /**
846
- * This function checks if the db records of events have been modified since the selection (beginning of processing)
847
- * If the db records are unmodified the field lastAttemptTimestamp of the records is updated to
848
- * "send a keep alive signal". This extends the allowed processing time of the events as events which are in progress
849
- * for more than 30 minutes (global tx timeout) are selected with the next tick.
850
- * If events are outdated/modified these events are not being processed and no status will be persisted.
851
- * @return {Promise<boolean>} true if the db record of the event has been modified since selection
852
- */
853
- async isOutdatedAndKeepalive(queueEntries) {
854
- if (!this.__outdatedCheckEnabled || new Date() - this.__startTime <= ETAG_CHECK_AFTER_MIN * 60 * 1000) {
855
- return false;
856
- }
857
- let eventOutdated;
858
- const runningChecks = queueEntries.map((queueEntry) => this.__keepalivePromises[queueEntry.ID]).filter((p) => p);
859
- if (runningChecks.length === queueEntries.length) {
860
- const results = await Promise.allSettled(runningChecks);
861
- for (const { value } of results) {
862
- if (value) {
863
- return true;
864
- }
865
- }
866
- return false;
867
- } else if (runningChecks.length) {
868
- await Promise.allSettled(runningChecks);
855
+ async isOutdatedAndKeepalive() {
856
+ if (this.__keepAliveViolated) {
857
+ return true;
869
858
  }
870
- const checkAndUpdatePromise = new Promise((resolve, reject) => {
871
- executeInNewTransaction(this.__baseContext, "eventProcessing-isOutdatedAndKeepalive", async (tx) => {
872
- const queueEntriesFresh = await tx.run(
873
- SELECT.from(this.#config.tableNameEventQueue)
874
- .forUpdate({ wait: this.#config.forUpdateTimeout })
875
- .where(
876
- "ID IN",
877
- queueEntries.map(({ ID }) => ID)
878
- )
879
- .columns("ID", "lastAttemptTimestamp")
880
- );
881
- eventOutdated = queueEntriesFresh.some((queueEntryFresh) => {
882
- const queueEntry = this.__queueEntriesMap[queueEntryFresh.ID];
883
- return queueEntry?.lastAttemptTimestamp !== queueEntryFresh.lastAttemptTimestamp;
884
- });
885
- let newTs = new Date().toISOString();
886
- if (!eventOutdated) {
887
- await tx.run(
888
- UPDATE.entity(this.#config.tableNameEventQueue)
889
- .set("lastAttemptTimestamp =", newTs)
890
- .where(
891
- "ID IN",
892
- queueEntries.map(({ ID }) => ID)
893
- )
859
+ }
860
+
861
+ continuesKeepAlive() {
862
+ this.#keepAliveRunner.run(async () => {
863
+ await this.#currentKeepAlivePromise;
864
+ this.#currentKeepAlivePromise = executeInNewTransaction(this.__baseContext, "keepAlive", async (tx) => {
865
+ await trace(tx.context, "keepAlive", async () => {
866
+ const ids = Object.values(this.__notCommitedStatusMap).map(({ ID }) => ID);
867
+ if (!ids.length) {
868
+ return;
869
+ }
870
+ this.logger.info("keep alive triggered for events", { numberOfEvents: ids.length });
871
+ await this.#renewDistributedLock();
872
+
873
+ // we make sure to always keep alive the global event lock; but we do not modify the events itself anymore
874
+ if (this.__keepAliveViolated) {
875
+ return;
876
+ }
877
+
878
+ const events = await tx.run(
879
+ SELECT.from(this.#config.tableNameEventQueue)
880
+ .forUpdate({ wait: this.#config.forUpdateTimeout })
881
+ .where("ID IN", ids, "AND status =", EventProcessingStatus.InProgress)
882
+ .columns("ID", "lastAttemptTimestamp")
894
883
  );
895
- } else {
896
- newTs = null;
897
- this.logger.warn("event data has been modified. Processing skipped.", {
898
- eventType: this.#eventType,
899
- eventSubType: this.#eventSubType,
900
- queueEntriesIds: queueEntries.map(({ ID }) => ID),
901
- });
902
- queueEntries.forEach(({ ID: queueEntryId }) => delete this.__queueEntriesMap[queueEntryId]);
903
- }
904
- this.__queueEntries = Object.values(this.__queueEntriesMap);
905
- queueEntriesFresh.forEach((queueEntryFresh) => {
906
- if (this.__queueEntriesMap[queueEntryFresh.ID]) {
907
- const queueEntry = this.__queueEntriesMap[queueEntryFresh.ID];
908
- if (newTs) {
909
- queueEntry.lastAttemptTimestamp = newTs;
884
+
885
+ const newTs = new Date().toISOString();
886
+ const outdatedEvents = [];
887
+ const validEventIds = [];
888
+ for (const event of events) {
889
+ const etag = this.#etagMap[event.ID];
890
+ if (etag !== event.lastAttemptTimestamp) {
891
+ outdatedEvents.push(event);
892
+ this.__keepAliveViolated = true;
893
+ } else {
894
+ validEventIds.push(event.ID);
895
+ this.#etagMap[event.ID] = newTs;
910
896
  }
911
897
  }
912
- delete this.__keepalivePromises[queueEntryFresh.ID];
898
+
899
+ if (outdatedEvents.length) {
900
+ // NOTE: we stop right here something is really off
901
+ outdatedEvents.forEach(({ ID: queueEntryId }) => {
902
+ delete this.__queueEntriesMap[queueEntryId];
903
+ this.__outdatedEventMap[queueEntryId] = 1;
904
+ });
905
+
906
+ this.logger.warn(
907
+ "Event data has been modified on the database. Further processing skipped. Parallel running events might have already commited status!",
908
+ {
909
+ eventType: this.#eventType,
910
+ eventSubType: this.#eventSubType,
911
+ queueEntriesIds: outdatedEvents.map(({ ID }) => ID),
912
+ }
913
+ );
914
+ }
915
+
916
+ if (validEventIds.length) {
917
+ await tx.run(
918
+ UPDATE.entity(this.#config.tableNameEventQueue)
919
+ .set("lastAttemptTimestamp =", newTs)
920
+ .where("ID IN", validEventIds)
921
+ );
922
+ // NOTE: update internal map after tx is successfully commited!
923
+ tx.context.on("succeeded", () => {
924
+ for (const event of events) {
925
+ const etag = this.#etagMap[event.ID];
926
+ if (etag === event.lastAttemptTimestamp) {
927
+ this.#etagMap[event.ID] = newTs;
928
+ }
929
+ }
930
+ });
931
+ }
913
932
  });
914
- resolve(eventOutdated);
915
- }).catch(reject);
933
+ }).catch((err) => {
934
+ this.logger.error("keep alive handling failed!", err);
935
+ });
936
+ await this.#currentKeepAlivePromise;
916
937
  });
917
-
918
- queueEntries.forEach((queueEntry) => (this.__keepalivePromises[queueEntry.ID] = checkAndUpdatePromise));
919
- return await checkAndUpdatePromise;
920
938
  }
921
939
 
922
940
  async acquireDistributedLock() {
@@ -926,9 +944,9 @@ class EventQueueProcessorBase {
926
944
 
927
945
  return await trace(this.baseContext, "acquire-lock", async () => {
928
946
  const lockAcquired = await distributedLock.acquireLock(
929
- this.context,
947
+ this.__context,
930
948
  [this.#eventType, this.#eventSubType].join("##"),
931
- { keepTrackOfLock: true }
949
+ { keepTrackOfLock: true, expiryTime: this.#eventConfig.keepAliveMaxInProgressTime }
932
950
  );
933
951
  if (!lockAcquired) {
934
952
  this.logger.debug("no lock available, exit processing", {
@@ -942,6 +960,26 @@ class EventQueueProcessorBase {
942
960
  });
943
961
  }
944
962
 
963
+ async #renewDistributedLock() {
964
+ if (this.concurrentEventProcessing) {
965
+ return true;
966
+ }
967
+
968
+ const lockAcquired = await distributedLock.renewLock(
969
+ this.__context,
970
+ [this.#eventType, this.#eventSubType].join("##"),
971
+ { expiryTime: this.#eventConfig.keepAliveMaxInProgressTime }
972
+ );
973
+ if (!lockAcquired) {
974
+ this.logger.error("renewing redis lock failed!", {
975
+ type: this.#eventType,
976
+ subType: this.#eventSubType,
977
+ });
978
+ return false;
979
+ }
980
+ return true;
981
+ }
982
+
945
983
  async handleReleaseLock() {
946
984
  if (!this.__lockAcquired) {
947
985
  return;
@@ -1085,16 +1123,12 @@ class EventQueueProcessorBase {
1085
1123
  this.__processTx = null;
1086
1124
  }
1087
1125
 
1088
- broadCastEvent() {
1089
- setTimeout(() => {
1090
- broadcastEvent(this.__baseContext.tenant, { type: this.#eventType, subType: this.#eventSubType }).catch((err) => {
1091
- this.logger.error("could not execute scheduled event", err, {
1092
- tenantId: this.__baseContext.tenant,
1093
- type: this.#eventType,
1094
- subType: this.#eventSubType,
1095
- });
1096
- });
1097
- }, 1000).unref();
1126
+ stopKeepAlive() {
1127
+ this.#keepAliveRunner.stop();
1128
+ }
1129
+
1130
+ get keepAlivePromise() {
1131
+ return this.#currentKeepAlivePromise;
1098
1132
  }
1099
1133
 
1100
1134
  get logger() {
package/src/config.js CHANGED
@@ -17,6 +17,9 @@ const COMPONENT_NAME = "/eventQueue/config";
17
17
  const MIN_INTERVAL_SEC = 10;
18
18
  const DEFAULT_LOAD = 1;
19
19
  const DEFAULT_PRIORITY = Priorities.Medium;
20
+ const DEFAULT_INCREASE_PRIORITY = true;
21
+ const DEFAULT_KEEP_ALIVE_INTERVAL_MIN = 1;
22
+ const DEFAULT_MAX_FACTOR_STUCK_2_KEEP_ALIVE_INTERVAL = 3.5;
20
23
  const SUFFIX_PERIODIC = "_PERIODIC";
21
24
  const COMMAND_BLOCK = "EVENT_QUEUE_EVENT_BLOCK";
22
25
  const COMMAND_UNBLOCK = "EVENT_QUEUE_EVENT_UNBLOCK";
@@ -301,14 +304,13 @@ class Config {
301
304
  const eventConfig = {
302
305
  type: CAP_EVENT_TYPE,
303
306
  subType: serviceName,
304
- load: config.load ?? DEFAULT_LOAD,
307
+ load: config.load,
305
308
  impl: "./outbox/EventQueueGenericOutboxHandler",
306
309
  selectMaxChunkSize: config.chunkSize,
307
310
  parallelEventProcessing: config.parallelEventProcessing ?? (config.parallel && CAP_PARALLEL_DEFAULT),
308
311
  retryAttempts: config.maxAttempts,
309
312
  transactionMode: config.transactionMode,
310
313
  processAfterCommit: config.processAfterCommit,
311
- eventOutdatedCheck: config.eventOutdatedCheck,
312
314
  checkForNextChunk: config.checkForNextChunk,
313
315
  deleteFinishedEventsAfterDays: config.deleteFinishedEventsAfterDays,
314
316
  appNames: config.appNames,
@@ -317,9 +319,12 @@ class Config {
317
319
  retryFailedAfter: config.retryFailedAfter,
318
320
  priority: config.priority,
319
321
  multiInstanceProcessing: config.multiInstanceProcessing,
322
+ increasePriorityOverTime: config.increasePriorityOverTime,
323
+ keepAliveInterval: config.keepAliveInterval,
320
324
  internalEvent: true,
321
325
  };
322
326
 
327
+ this.#basicEventTransformation(eventConfig);
323
328
  this.#basicEventTransformationAfterValidate(eventConfig);
324
329
  this.#config.events.push(eventConfig);
325
330
  this.#eventMap[this.generateKey(CAP_EVENT_TYPE, serviceName)] = eventConfig;
@@ -355,7 +360,10 @@ class Config {
355
360
  set fileContent(config) {
356
361
  this.#config = config;
357
362
  config.events = config.events ?? [];
358
- config.periodicEvents = (config.periodicEvents ?? []).concat(BASE_PERIODIC_EVENTS.map((event) => ({ ...event })));
363
+ const shouldIncludeBaseEvents = cds.env.profiles.includes("production") || cds.env.profiles.includes("test");
364
+ config.periodicEvents = (config.periodicEvents ?? []).concat(
365
+ (shouldIncludeBaseEvents ? BASE_PERIODIC_EVENTS : []).map((event) => ({ ...event }))
366
+ );
359
367
  this.#eventMap = config.events.reduce((result, event) => {
360
368
  this.#basicEventTransformation(event);
361
369
  this.#validateAdHocEvents(result, event);
@@ -364,7 +372,6 @@ class Config {
364
372
  return result;
365
373
  }, {});
366
374
  this.#eventMap = config.periodicEvents.reduce((result, event) => {
367
- event.priority = event.priority ?? DEFAULT_PRIORITY;
368
375
  event.type = `${event.type}${SUFFIX_PERIODIC}`;
369
376
  event.isPeriodic = true;
370
377
  this.#basicEventTransformation(event);
@@ -378,6 +385,9 @@ class Config {
378
385
  #basicEventTransformation(event) {
379
386
  event.load = event.load ?? DEFAULT_LOAD;
380
387
  event.priority = event.priority ?? DEFAULT_PRIORITY;
388
+ event.increasePriorityOverTime = event.increasePriorityOverTime ?? DEFAULT_INCREASE_PRIORITY;
389
+ event.keepAliveInterval = (event.keepAliveInterval ?? DEFAULT_KEEP_ALIVE_INTERVAL_MIN) * 60 * 1000;
390
+ event.keepAliveMaxInProgressTime = event.keepAliveInterval * DEFAULT_MAX_FACTOR_STUCK_2_KEEP_ALIVE_INTERVAL;
381
391
  }
382
392
 
383
393
  #basicEventTransformationAfterValidate(event) {
package/src/index.d.ts CHANGED
@@ -78,7 +78,6 @@ export type EventConfig = {
78
78
  retryAttempts: number | undefined;
79
79
  transactionMode: string | undefined;
80
80
  processAfterCommit: boolean | undefined;
81
- eventOutdatedCheck: boolean | undefined;
82
81
  checkForNextChunk: boolean | undefined;
83
82
  deleteFinishedEventsAfterDays: number | undefined;
84
83
  appNames: string[] | undefined;
@@ -269,7 +268,13 @@ export const workerQueue: WorkerQueue;
269
268
  declare class WorkerQueue {
270
269
  constructor(concurrency: number);
271
270
 
272
- addToQueue(load: number, label: string, priority?: Priorities, cb?: () => any): Promise<any>;
271
+ addToQueue(
272
+ load: number,
273
+ label: string,
274
+ priority: Priorities,
275
+ increasePriorityOverTime: boolean,
276
+ cb: () => any
277
+ ): Promise<any>;
273
278
 
274
279
  _executeFunction(
275
280
  load: number,
@@ -208,6 +208,7 @@ const processEventMap = async (instance) => {
208
208
  if (instance.commitOnEventLevel) {
209
209
  instance.txUsageAllowed = false;
210
210
  }
211
+ instance.continuesKeepAlive();
211
212
  await limiter(
212
213
  instance.parallelEventProcessing,
213
214
  Object.entries(instance.eventProcessingMap),
@@ -241,10 +242,12 @@ const processEventMap = async (instance) => {
241
242
  instance.handleErrorTx(err);
242
243
  })
243
244
  .finally(() => {
245
+ instance.stopKeepAlive();
244
246
  instance.clearEventProcessingContext();
245
247
  if (instance.commitOnEventLevel) {
246
248
  instance.txUsageAllowed = true;
247
249
  }
250
+ return instance.keepAlivePromise;
248
251
  });
249
252
  instance.endPerformanceTracerEvents();
250
253
  };
@@ -313,7 +316,8 @@ const _processEvent = async (eventTypeInstance, processContext, key, queueEntrie
313
316
  try {
314
317
  const eventOutdated = await eventTypeInstance.isOutdatedAndKeepalive(queueEntries);
315
318
  if (eventOutdated) {
316
- return;
319
+ // NOTE: return empty status map to comply with the interface
320
+ return {};
317
321
  }
318
322
  eventTypeInstance.setTxForEventProcessing(key, cds.tx(processContext));
319
323
  const statusTuple = await eventTypeInstance.processEvent(processContext, key, queueEntries, payload);
@@ -194,33 +194,39 @@ const _executeEventsAllTenants = async (tenantIds, runId) => {
194
194
  events.map(async (openEvent) => {
195
195
  const eventConfig = config.getEventConfig(openEvent.type, openEvent.subType);
196
196
  const label = `${eventConfig.type}_${eventConfig.subType}`;
197
- return await WorkerQueue.instance.addToQueue(eventConfig.load, label, eventConfig.priority, async () => {
198
- return await cds.tx(tenantContext, async ({ context }) => {
199
- await trace(
200
- context,
201
- label,
202
- async () => {
203
- try {
204
- const lockId = `${runId}_${label}`;
205
- const couldAcquireLock = await distributedLock.acquireLock(context, lockId, {
206
- expiryTime: eventQueueConfig.runInterval * 0.95,
207
- });
208
- if (!couldAcquireLock) {
209
- return;
197
+ return await WorkerQueue.instance.addToQueue(
198
+ eventConfig.load,
199
+ label,
200
+ eventConfig.priority,
201
+ eventConfig.increasePriorityOverTime,
202
+ async () => {
203
+ return await cds.tx(tenantContext, async ({ context }) => {
204
+ await trace(
205
+ context,
206
+ label,
207
+ async () => {
208
+ try {
209
+ const lockId = `${runId}_${label}`;
210
+ const couldAcquireLock = await distributedLock.acquireLock(context, lockId, {
211
+ expiryTime: eventQueueConfig.runInterval * 0.95,
212
+ });
213
+ if (!couldAcquireLock) {
214
+ return;
215
+ }
216
+ await runEventCombinationForTenant(context, eventConfig.type, eventConfig.subType, {
217
+ skipWorkerPool: true,
218
+ });
219
+ } catch (err) {
220
+ cds.log(COMPONENT_NAME).error("executing event-queue run for tenant failed", {
221
+ tenantId,
222
+ });
210
223
  }
211
- await runEventCombinationForTenant(context, eventConfig.type, eventConfig.subType, {
212
- skipWorkerPool: true,
213
- });
214
- } catch (err) {
215
- cds.log(COMPONENT_NAME).error("executing event-queue run for tenant failed", {
216
- tenantId,
217
- });
218
- }
219
- },
220
- { newRootSpan: true }
221
- );
222
- });
223
- });
224
+ },
225
+ { newRootSpan: true }
226
+ );
227
+ });
228
+ }
229
+ );
224
230
  })
225
231
  );
226
232
  }
@@ -275,33 +281,39 @@ const _singleTenantDb = async () => {
275
281
  events.map(async (openEvent) => {
276
282
  const eventConfig = config.getEventConfig(openEvent.type, openEvent.subType);
277
283
  const label = `${eventConfig.type}_${eventConfig.subType}`;
278
- return await WorkerQueue.instance.addToQueue(eventConfig.load, label, eventConfig.priority, async () => {
279
- return await cds.tx({}, async ({ context }) => {
280
- await trace(
281
- context,
282
- label,
283
- async () => {
284
- try {
285
- const lockId = `${label}`;
286
- const couldAcquireLock = eventConfig.multiInstanceProcessing
287
- ? true
288
- : await distributedLock.acquireLock(context, lockId, {
289
- expiryTime: eventQueueConfig.runInterval * 0.95,
290
- });
291
- if (!couldAcquireLock) {
292
- return;
284
+ return await WorkerQueue.instance.addToQueue(
285
+ eventConfig.load,
286
+ label,
287
+ eventConfig.priority,
288
+ eventConfig.increasePriorityOverTime,
289
+ async () => {
290
+ return await cds.tx({}, async ({ context }) => {
291
+ await trace(
292
+ context,
293
+ label,
294
+ async () => {
295
+ try {
296
+ const lockId = `${label}`;
297
+ const couldAcquireLock = eventConfig.multiInstanceProcessing
298
+ ? true
299
+ : await distributedLock.acquireLock(context, lockId, {
300
+ expiryTime: eventQueueConfig.runInterval * 0.95,
301
+ });
302
+ if (!couldAcquireLock) {
303
+ return;
304
+ }
305
+ await runEventCombinationForTenant(context, eventConfig.type, eventConfig.subType, {
306
+ skipWorkerPool: true,
307
+ });
308
+ } catch (err) {
309
+ cds.log(COMPONENT_NAME).error("executing event-queue run for tenant failed");
293
310
  }
294
- await runEventCombinationForTenant(context, eventConfig.type, eventConfig.subType, {
295
- skipWorkerPool: true,
296
- });
297
- } catch (err) {
298
- cds.log(COMPONENT_NAME).error("executing event-queue run for tenant failed");
299
- }
300
- },
301
- { newRootSpan: true }
302
- );
303
- });
304
- });
311
+ },
312
+ { newRootSpan: true }
313
+ );
314
+ });
315
+ }
316
+ );
305
317
  })
306
318
  );
307
319
  };
@@ -23,6 +23,7 @@ const runEventCombinationForTenant = async (context, type, subType, { skipWorker
23
23
  eventConfig.load,
24
24
  label,
25
25
  eventConfig.priority,
26
+ eventConfig.increasePriorityOverTime,
26
27
  AsyncResource.bind(async () => {
27
28
  const _exec = async () => {
28
29
  if (!eventConfig.multiInstanceProcessing && lockId) {
@@ -8,6 +8,8 @@ class SetIntervalDriftSafe {
8
8
  #expectedCycleTime = 0;
9
9
  #nextTickScheduledFor;
10
10
  #logger;
11
+ #shouldRun = true;
12
+ #lastTimeoutId;
11
13
 
12
14
  constructor(interval) {
13
15
  this.#interval = interval;
@@ -16,6 +18,10 @@ class SetIntervalDriftSafe {
16
18
  }
17
19
 
18
20
  run(fn) {
21
+ if (!this.#shouldRun) {
22
+ return;
23
+ }
24
+
19
25
  const now = Date.now();
20
26
  if (this.#expectedCycleTime === 0) {
21
27
  this.#expectedCycleTime = now + this.#interval;
@@ -24,10 +30,19 @@ class SetIntervalDriftSafe {
24
30
  this.#expectedCycleTime += this.#interval;
25
31
  }
26
32
  this.#nextTickScheduledFor = now + this.#adjustedInterval;
27
- setTimeout(() => {
33
+ const timeoutId = setTimeout(() => {
34
+ if (!this.#shouldRun) {
35
+ return;
36
+ }
28
37
  this.run(fn);
29
38
  fn();
30
39
  }, this.#adjustedInterval).unref();
40
+ this.#lastTimeoutId = Number(timeoutId);
41
+ }
42
+
43
+ stop() {
44
+ this.#shouldRun = false;
45
+ clearTimeout(this.#lastTimeoutId);
31
46
  }
32
47
  }
33
48
 
@@ -52,7 +52,7 @@ class WorkerQueue {
52
52
  runner.run(this.#adjustPriority.bind(this));
53
53
  }
54
54
 
55
- addToQueue(load, label, priority = Priorities.Medium, cb) {
55
+ addToQueue(load, label, priority = Priorities.Medium, increasePriorityOverTime, cb) {
56
56
  if (load > this.#concurrencyLimit) {
57
57
  throw EventQueueError.loadHigherThanLimit(load, label);
58
58
  }
@@ -63,7 +63,7 @@ class WorkerQueue {
63
63
 
64
64
  const startTime = process.hrtime.bigint();
65
65
  const p = new Promise((resolve, reject) => {
66
- this.#queue[priority].push([load, label, cb, resolve, reject, startTime]);
66
+ this.#queue[priority].push([load, label, cb, resolve, reject, increasePriorityOverTime, startTime]);
67
67
  });
68
68
  this.#checkForNext();
69
69
  return p;
@@ -78,7 +78,12 @@ class WorkerQueue {
78
78
  const nextPriority = priorityValues[i + 1];
79
79
  for (let i = 0; i < this.queue[priority].length; i++) {
80
80
  const queueEntry = this.queue[priority][i];
81
- const startTime = queueEntry[6] ?? queueEntry[5];
81
+ // NOTE: index 5 - increasingPrioOverTime
82
+ if (!queueEntry[5]) {
83
+ continue;
84
+ }
85
+ // NOTE: index 6 original time; index 7 shifted time
86
+ const startTime = queueEntry[7] ?? queueEntry[6];
82
87
  if (Math.round(Number(checkTime - startTime) / NANO_TO_MS) > INCREASE_PRIORITY_AFTER * MIN_TO_MS) {
83
88
  const [entry] = this.queue[priority].splice(i, 1);
84
89
  entry.push(checkTime);
@@ -88,7 +93,7 @@ class WorkerQueue {
88
93
  }
89
94
  }
90
95
 
91
- _executeFunction(load, label, cb, resolve, reject, startTime, priority) {
96
+ _executeFunction(priority, load, label, cb, resolve, reject, skipIncreasingPrioOverTime, startTime) {
92
97
  this.#checkAndLogWaitingTime(startTime, label, priority);
93
98
  const promise = Promise.resolve().then(() => cb());
94
99
  this.#runningPromises.push(promise);
@@ -123,7 +128,7 @@ class WorkerQueue {
123
128
  const [load] = this.#queue[priority][i];
124
129
  if (this.#runningLoad + load <= this.#concurrencyLimit) {
125
130
  const [args] = this.#queue[priority].splice(i, 1);
126
- this._executeFunction(...args, priority);
131
+ this._executeFunction(priority, ...args);
127
132
  entryFound = true;
128
133
  break;
129
134
  }
@@ -22,6 +22,15 @@ const acquireLock = async (
22
22
  }
23
23
  };
24
24
 
25
+ const renewLock = async (context, key, { tenantScoped = true, expiryTime = config.globalTxTimeout } = {}) => {
26
+ const fullKey = _generateKey(context, tenantScoped, key);
27
+ if (config.redisEnabled) {
28
+ return await _renewLockRedis(context, fullKey, expiryTime);
29
+ } else {
30
+ return await _acquireLockDB(context, fullKey, expiryTime, { overrideValue: true });
31
+ }
32
+ };
33
+
25
34
  const setValueWithExpire = async (
26
35
  context,
27
36
  key,
@@ -79,6 +88,15 @@ const _acquireLockRedis = async (
79
88
  return isOk;
80
89
  };
81
90
 
91
+ const _renewLockRedis = async (context, fullKey, expiryTime, { value = "true" } = {}) => {
92
+ const client = await redis.createMainClientAndConnect(config.redisOptions);
93
+ const result = await client.set(fullKey, value, {
94
+ PX: Math.round(expiryTime),
95
+ XX: true,
96
+ });
97
+ return result === REDIS_COMMAND_OK;
98
+ };
99
+
82
100
  const _checkLockExistsRedis = async (context, fullKey) => {
83
101
  const client = await redis.createMainClientAndConnect(config.redisOptions);
84
102
  return await client.get(fullKey);
@@ -136,7 +154,7 @@ const _acquireLockDB = async (context, fullKey, expiryTime, { value = "true", ov
136
154
  createdAt: new Date().toISOString(),
137
155
  value,
138
156
  })
139
- .where("code =", currentEntry.code)
157
+ .where("code =", fullKey)
140
158
  );
141
159
  result = true;
142
160
  } else {
@@ -178,4 +196,5 @@ module.exports = {
178
196
  checkLockExistsAndReturnValue,
179
197
  setValueWithExpire,
180
198
  shutdownHandler,
199
+ renewLock,
181
200
  };