@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 +1 -1
- package/src/EventQueueProcessorBase.js +131 -97
- package/src/config.js +14 -4
- package/src/index.d.ts +7 -2
- package/src/processEventQueue.js +5 -1
- package/src/runner/runner.js +64 -52
- package/src/runner/runnerHelper.js +1 -0
- package/src/shared/SetIntervalDriftSafe.js +16 -1
- package/src/shared/WorkerQueue.js +10 -5
- package/src/shared/distributedLock.js +20 -1
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.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, [
|
|
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
|
@@ -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
|
|
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
|
-
|
|
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(
|
|
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,
|
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);
|
package/src/runner/runner.js
CHANGED
|
@@ -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(
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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(
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
|
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 =",
|
|
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
|
};
|