@cap-js-community/event-queue 1.8.7 → 1.9.0-beta.1

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.7",
3
+ "version": "1.9.0-beta.1",
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
@@ -18,6 +18,8 @@ const MIN_INTERVAL_SEC = 10;
18
18
  const DEFAULT_LOAD = 1;
19
19
  const DEFAULT_PRIORITY = Priorities.Medium;
20
20
  const DEFAULT_INCREASE_PRIORITY = true;
21
+ const DEFAULT_KEEP_ALIVE_INTERVAL = 60;
22
+ const DEFAULT_MAX_FACTOR_STUCK_2_KEEP_ALIVE_INTERVAL = 3.5;
21
23
  const SUFFIX_PERIODIC = "_PERIODIC";
22
24
  const COMMAND_BLOCK = "EVENT_QUEUE_EVENT_BLOCK";
23
25
  const COMMAND_UNBLOCK = "EVENT_QUEUE_EVENT_UNBLOCK";
@@ -302,14 +304,13 @@ class Config {
302
304
  const eventConfig = {
303
305
  type: CAP_EVENT_TYPE,
304
306
  subType: serviceName,
305
- load: config.load ?? DEFAULT_LOAD,
307
+ load: config.load,
306
308
  impl: "./outbox/EventQueueGenericOutboxHandler",
307
309
  selectMaxChunkSize: config.chunkSize,
308
310
  parallelEventProcessing: config.parallelEventProcessing ?? (config.parallel && CAP_PARALLEL_DEFAULT),
309
311
  retryAttempts: config.maxAttempts,
310
312
  transactionMode: config.transactionMode,
311
313
  processAfterCommit: config.processAfterCommit,
312
- eventOutdatedCheck: config.eventOutdatedCheck,
313
314
  checkForNextChunk: config.checkForNextChunk,
314
315
  deleteFinishedEventsAfterDays: config.deleteFinishedEventsAfterDays,
315
316
  appNames: config.appNames,
@@ -319,9 +320,11 @@ class Config {
319
320
  priority: config.priority,
320
321
  multiInstanceProcessing: config.multiInstanceProcessing,
321
322
  increasePriorityOverTime: config.increasePriorityOverTime,
323
+ keepAliveInterval: config.keepAliveInterval,
322
324
  internalEvent: true,
323
325
  };
324
326
 
327
+ this.#basicEventTransformation(eventConfig);
325
328
  this.#basicEventTransformationAfterValidate(eventConfig);
326
329
  this.#config.events.push(eventConfig);
327
330
  this.#eventMap[this.generateKey(CAP_EVENT_TYPE, serviceName)] = eventConfig;
@@ -357,7 +360,10 @@ class Config {
357
360
  set fileContent(config) {
358
361
  this.#config = config;
359
362
  config.events = config.events ?? [];
360
- 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
+ );
361
367
  this.#eventMap = config.events.reduce((result, event) => {
362
368
  this.#basicEventTransformation(event);
363
369
  this.#validateAdHocEvents(result, event);
@@ -380,6 +386,8 @@ class Config {
380
386
  event.load = event.load ?? DEFAULT_LOAD;
381
387
  event.priority = event.priority ?? DEFAULT_PRIORITY;
382
388
  event.increasePriorityOverTime = event.increasePriorityOverTime ?? DEFAULT_INCREASE_PRIORITY;
389
+ event.keepAliveInterval = (event.keepAliveInterval ?? DEFAULT_KEEP_ALIVE_INTERVAL) * 1000;
390
+ event.keepAliveMaxInProgressTime = event.keepAliveInterval * DEFAULT_MAX_FACTOR_STUCK_2_KEEP_ALIVE_INTERVAL;
383
391
  }
384
392
 
385
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;
@@ -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);
@@ -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
 
@@ -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
  };
@@ -1,8 +1,9 @@
1
1
  "use strict";
2
2
 
3
3
  const cds = require("@sap/cds");
4
- let otel;
4
+ let otel, telemetry;
5
5
  try {
6
+ telemetry = require("@cap-js/telemetry");
6
7
  otel = require("@opentelemetry/api");
7
8
  } catch {
8
9
  // ignore
@@ -13,11 +14,13 @@ const config = require("../config");
13
14
  const COMPONENT_NAME = "/shared/openTelemetry";
14
15
 
15
16
  const trace = async (context, label, fn, { attributes = {}, newRootSpan = false } = {}) => {
16
- if (!config.enableCAPTelemetry || !otel || !cds._telemetry?.tracer) {
17
+ if (!config.enableCAPTelemetry || !otel || !telemetry) {
17
18
  return fn();
18
19
  }
19
20
 
20
- const span = cds._telemetry.tracer.startSpan(`eventqueue-${label}`, {
21
+ const tracer = otel.trace.getTracer("eventqueue");
22
+
23
+ const span = tracer.startSpan(`eventqueue-${label}`, {
21
24
  kind: otel.SpanKind.INTERNAL,
22
25
  root: newRootSpan,
23
26
  });