@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 +1 -1
- package/src/EventQueueProcessorBase.js +131 -97
- package/src/config.js +11 -3
- package/src/index.d.ts +0 -1
- package/src/processEventQueue.js +5 -1
- package/src/shared/SetIntervalDriftSafe.js +16 -1
- package/src/shared/distributedLock.js +20 -1
- package/src/shared/openTelemetry.js +6 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cap-js-community/event-queue",
|
|
3
|
-
"version": "1.
|
|
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, [
|
|
402
|
-
this.__commitedStatusMap[
|
|
404
|
+
(result, [queueEntryId, processingStatus]) => {
|
|
405
|
+
this.__commitedStatusMap[queueEntryId] = processingStatus;
|
|
406
|
+
delete this.__notCommitedStatusMap[queueEntryId];
|
|
403
407
|
if (processingStatus === EventProcessingStatus.Open) {
|
|
404
|
-
result.invalidAttempts.push(
|
|
408
|
+
result.invalidAttempts.push(queueEntryId);
|
|
405
409
|
} else if (processingStatus === EventProcessingStatus.Done) {
|
|
406
|
-
result.success.push(
|
|
410
|
+
result.success.push(queueEntryId);
|
|
407
411
|
} else if (processingStatus === EventProcessingStatus.Error) {
|
|
408
|
-
result.failed.push(
|
|
412
|
+
result.failed.push(queueEntryId);
|
|
409
413
|
} else if (processingStatus === EventProcessingStatus.Exceeded) {
|
|
410
|
-
result.exceeded.push(
|
|
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 (
|
|
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.#
|
|
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.#
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
847
|
-
|
|
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
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
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
|
-
|
|
896
|
-
newTs =
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
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
|
-
|
|
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
|
-
|
|
915
|
-
|
|
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.
|
|
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
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
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
|
|
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
|
-
|
|
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;
|
package/src/processEventQueue.js
CHANGED
|
@@ -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 =",
|
|
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 || !
|
|
17
|
+
if (!config.enableCAPTelemetry || !otel || !telemetry) {
|
|
17
18
|
return fn();
|
|
18
19
|
}
|
|
19
20
|
|
|
20
|
-
const
|
|
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
|
});
|