@cap-js-community/event-queue 1.9.0 → 1.9.2
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/db/Event.cds +2 -0
- package/package.json +9 -7
- package/src/EventQueueProcessorBase.js +36 -10
- package/src/config.js +35 -9
- package/src/index.d.ts +7 -8
- package/src/initialize.js +3 -3
- package/src/periodicEvents.js +15 -13
- package/src/processEventQueue.js +59 -35
- package/src/publishEvent.js +10 -1
- package/src/redis/redisPub.js +1 -1
- package/src/runner/runner.js +18 -7
- package/src/runner/runnerHelper.js +1 -1
- package/src/shared/SetIntervalDriftSafe.js +5 -0
- package/src/shared/common.js +0 -1
- package/src/shared/distributedLock.js +11 -1
- package/src/shared/openTelemetry.js +50 -17
package/db/Event.cds
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cap-js-community/event-queue",
|
|
3
|
-
"version": "1.9.
|
|
3
|
+
"version": "1.9.2",
|
|
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",
|
|
@@ -44,16 +44,17 @@
|
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
46
|
"@sap/xssec": "^4.2.4",
|
|
47
|
-
"cron-parser": "^
|
|
47
|
+
"cron-parser": "^5.0.0",
|
|
48
48
|
"redis": "^4.7.0",
|
|
49
49
|
"verror": "^1.10.1",
|
|
50
50
|
"yaml": "^2.6.1"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
|
-
"@cap-js/
|
|
54
|
-
"@cap-js/
|
|
55
|
-
"@
|
|
56
|
-
"@sap/cds
|
|
53
|
+
"@cap-js/cds-test": "^0.2.0",
|
|
54
|
+
"@cap-js/hana": "^1.7.0",
|
|
55
|
+
"@cap-js/sqlite": "^1.9.0",
|
|
56
|
+
"@sap/cds": "^8.8.0",
|
|
57
|
+
"@sap/cds-dk": "^8.8.0",
|
|
57
58
|
"eslint": "^8.57.0",
|
|
58
59
|
"eslint-config-prettier": "^9.1.0",
|
|
59
60
|
"eslint-plugin-jest": "^28.6.0",
|
|
@@ -62,7 +63,8 @@
|
|
|
62
63
|
"hdb": "^0.19.10",
|
|
63
64
|
"jest": "^29.7.0",
|
|
64
65
|
"prettier": "^2.8.8",
|
|
65
|
-
"sqlite3": "^5.1.7"
|
|
66
|
+
"sqlite3": "^5.1.7",
|
|
67
|
+
"@opentelemetry/api": "^1.9.0"
|
|
66
68
|
},
|
|
67
69
|
"homepage": "https://cap-js-community.github.io/event-queue/",
|
|
68
70
|
"repository": {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
const cds = require("@sap/cds");
|
|
4
|
-
const
|
|
4
|
+
const { CronExpressionParser } = require("cron-parser");
|
|
5
5
|
|
|
6
6
|
const { executeInNewTransaction } = require("./shared/cdsHelper");
|
|
7
7
|
const { EventProcessingStatus, TransactionMode } = require("./constants");
|
|
@@ -11,7 +11,7 @@ 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 trace = require("./shared/openTelemetry");
|
|
14
|
+
const { trace } = require("./shared/openTelemetry");
|
|
15
15
|
const SetIntervalDriftSafe = require("./shared/SetIntervalDriftSafe");
|
|
16
16
|
|
|
17
17
|
const IMPLEMENT_ERROR_MESSAGE = "needs to be reimplemented";
|
|
@@ -66,7 +66,6 @@ class EventQueueProcessorBase {
|
|
|
66
66
|
}
|
|
67
67
|
this.#retryFailedAfter = this.#eventConfig.retryFailedAfter ?? DEFAULT_RETRY_AFTER;
|
|
68
68
|
this.__concurrentEventProcessing = this.#eventConfig.multiInstanceProcessing;
|
|
69
|
-
this.__startTime = this.#eventConfig.startTime ?? new Date();
|
|
70
69
|
this.__retryAttempts = this.#isPeriodic ? 1 : this.#eventConfig.retryAttempts ?? DEFAULT_RETRY_ATTEMPTS;
|
|
71
70
|
this.__selectMaxChunkSize = this.#eventConfig.selectMaxChunkSize ?? SELECT_LIMIT_EVENTS_PER_TICK;
|
|
72
71
|
this.__selectNextChunk = !!this.#eventConfig.checkForNextChunk;
|
|
@@ -331,6 +330,11 @@ class EventQueueProcessorBase {
|
|
|
331
330
|
} catch {
|
|
332
331
|
/* empty */
|
|
333
332
|
}
|
|
333
|
+
try {
|
|
334
|
+
queueEntry.context = JSON.parse(queueEntry.context);
|
|
335
|
+
} catch {
|
|
336
|
+
/* empty */
|
|
337
|
+
}
|
|
334
338
|
}
|
|
335
339
|
|
|
336
340
|
#determineAndAddEventStatusToMap(id, processingStatus, statusMap = this.__statusMap) {
|
|
@@ -602,7 +606,7 @@ class EventQueueProcessorBase {
|
|
|
602
606
|
" ) AND ( status =",
|
|
603
607
|
EventProcessingStatus.Open,
|
|
604
608
|
"AND ( lastAttemptTimestamp <=",
|
|
605
|
-
this.
|
|
609
|
+
this.startTime.toISOString(),
|
|
606
610
|
...(this.isPeriodicEvent
|
|
607
611
|
? [
|
|
608
612
|
"OR lastAttemptTimestamp IS NULL ) OR ( status =",
|
|
@@ -615,7 +619,7 @@ class EventQueueProcessorBase {
|
|
|
615
619
|
"OR lastAttemptTimestamp IS NULL ) OR ( status =",
|
|
616
620
|
EventProcessingStatus.Error,
|
|
617
621
|
"AND lastAttemptTimestamp <=",
|
|
618
|
-
this.
|
|
622
|
+
this.startTime.toISOString(),
|
|
619
623
|
") OR ( status =",
|
|
620
624
|
EventProcessingStatus.InProgress,
|
|
621
625
|
"AND lastAttemptTimestamp <=",
|
|
@@ -859,7 +863,12 @@ class EventQueueProcessorBase {
|
|
|
859
863
|
}
|
|
860
864
|
|
|
861
865
|
continuesKeepAlive() {
|
|
862
|
-
|
|
866
|
+
if (Date.now() - this.lockAcquiredTime.getTime() >= this.#eventConfig.keepAliveInterval) {
|
|
867
|
+
trace(this.baseContext, "keepAlive-between-iterations", async () => {
|
|
868
|
+
await this.#renewDistributedLock();
|
|
869
|
+
}).catch((err) => this.logger.error("renewing lock between intervals failed!", err));
|
|
870
|
+
}
|
|
871
|
+
this.#keepAliveRunner.start(async () => {
|
|
863
872
|
await this.#currentKeepAlivePromise;
|
|
864
873
|
this.#currentKeepAlivePromise = executeInNewTransaction(this.__baseContext, "keepAlive", async (tx) => {
|
|
865
874
|
await trace(tx.context, "keepAlive", async () => {
|
|
@@ -929,6 +938,7 @@ class EventQueueProcessorBase {
|
|
|
929
938
|
}
|
|
930
939
|
});
|
|
931
940
|
}
|
|
941
|
+
this.logger.info("keep alive finished!", { numberOfEvents: ids.length });
|
|
932
942
|
});
|
|
933
943
|
}).catch((err) => {
|
|
934
944
|
this.logger.error("keep alive handling failed!", err);
|
|
@@ -971,12 +981,13 @@ class EventQueueProcessorBase {
|
|
|
971
981
|
{ expiryTime: this.#eventConfig.keepAliveMaxInProgressTime }
|
|
972
982
|
);
|
|
973
983
|
if (!lockAcquired) {
|
|
974
|
-
this.logger.error("renewing
|
|
984
|
+
this.logger.error("renewing distributed lock failed!", {
|
|
975
985
|
type: this.#eventType,
|
|
976
986
|
subType: this.#eventSubType,
|
|
977
987
|
});
|
|
978
988
|
return false;
|
|
979
989
|
}
|
|
990
|
+
this.lockAcquiredTime = new Date();
|
|
980
991
|
return true;
|
|
981
992
|
}
|
|
982
993
|
|
|
@@ -999,9 +1010,8 @@ class EventQueueProcessorBase {
|
|
|
999
1010
|
}
|
|
1000
1011
|
|
|
1001
1012
|
// NOTE: do not pass current date as we always want to calc. a future date
|
|
1002
|
-
const cronExpression =
|
|
1003
|
-
|
|
1004
|
-
...(this.#eventConfig.useCronTimezone && { tz: this.#config.cronTimezone }),
|
|
1013
|
+
const cronExpression = CronExpressionParser.parse(this.#eventConfig.cron, {
|
|
1014
|
+
tz: eventConfig.tz,
|
|
1005
1015
|
});
|
|
1006
1016
|
return cronExpression.next();
|
|
1007
1017
|
}
|
|
@@ -1246,6 +1256,22 @@ class EventQueueProcessorBase {
|
|
|
1246
1256
|
get eventConfig() {
|
|
1247
1257
|
return this.#eventConfig;
|
|
1248
1258
|
}
|
|
1259
|
+
|
|
1260
|
+
get lockAcquiredTime() {
|
|
1261
|
+
return this.#eventConfig.lockAcquiredTime;
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
get startTime() {
|
|
1265
|
+
return this.#eventConfig.startTime;
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
set lockAcquiredTime(value) {
|
|
1269
|
+
this.#eventConfig.lockAcquiredTime = value;
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
get inheritTraceContext() {
|
|
1273
|
+
return this.#eventConfig.inheritTraceContext;
|
|
1274
|
+
}
|
|
1249
1275
|
}
|
|
1250
1276
|
|
|
1251
1277
|
module.exports = EventQueueProcessorBase;
|
package/src/config.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
const cds = require("@sap/cds");
|
|
4
|
-
const
|
|
4
|
+
const { CronExpressionParser } = require("cron-parser");
|
|
5
5
|
|
|
6
6
|
const { getEnvInstance } = require("./shared/env");
|
|
7
7
|
const redis = require("./shared/redis");
|
|
@@ -20,6 +20,7 @@ const DEFAULT_PRIORITY = Priorities.Medium;
|
|
|
20
20
|
const DEFAULT_INCREASE_PRIORITY = true;
|
|
21
21
|
const DEFAULT_KEEP_ALIVE_INTERVAL = 60;
|
|
22
22
|
const DEFAULT_MAX_FACTOR_STUCK_2_KEEP_ALIVE_INTERVAL = 3.5;
|
|
23
|
+
const DEFAULT_INHERIT_TRACE_CONTEXT = true;
|
|
23
24
|
const SUFFIX_PERIODIC = "_PERIODIC";
|
|
24
25
|
const COMMAND_BLOCK = "EVENT_QUEUE_EVENT_BLOCK";
|
|
25
26
|
const COMMAND_UNBLOCK = "EVENT_QUEUE_EVENT_UNBLOCK";
|
|
@@ -62,7 +63,7 @@ class Config {
|
|
|
62
63
|
#cleanupLocksAndEventsForDev;
|
|
63
64
|
#redisOptions;
|
|
64
65
|
#insertEventsBeforeCommit;
|
|
65
|
-
#
|
|
66
|
+
#enableTelemetry;
|
|
66
67
|
#unsubscribeHandlers = [];
|
|
67
68
|
#unsubscribedTenants = {};
|
|
68
69
|
#cronTimezone;
|
|
@@ -93,7 +94,9 @@ class Config {
|
|
|
93
94
|
}
|
|
94
95
|
|
|
95
96
|
getEventConfig(type, subType) {
|
|
96
|
-
return this.#eventMap[this.generateKey(type, subType)]
|
|
97
|
+
return this.#eventMap[this.generateKey(type, subType)]
|
|
98
|
+
? { ...this.#eventMap[this.generateKey(type, subType)] }
|
|
99
|
+
: undefined;
|
|
97
100
|
}
|
|
98
101
|
|
|
99
102
|
isCapOutboxEvent(type) {
|
|
@@ -311,6 +314,7 @@ class Config {
|
|
|
311
314
|
multiInstanceProcessing: config.multiInstanceProcessing,
|
|
312
315
|
increasePriorityOverTime: config.increasePriorityOverTime,
|
|
313
316
|
keepAliveInterval: config.keepAliveInterval,
|
|
317
|
+
inheritTraceContext: true,
|
|
314
318
|
internalEvent: true,
|
|
315
319
|
};
|
|
316
320
|
|
|
@@ -461,10 +465,23 @@ class Config {
|
|
|
461
465
|
|
|
462
466
|
if (event.cron) {
|
|
463
467
|
let cron;
|
|
468
|
+
|
|
469
|
+
// NOTE: logic is as follows:
|
|
470
|
+
// - if event.utc is true --> always use UTC
|
|
471
|
+
// - if event.useCronTimezone is false OR event.cronTimezone is not defined --> use UTC as well
|
|
472
|
+
// - if event.utc is not true AND event.cronTimezone is set AND event.useCronTimezone is NOT set to false use event.cronTimezone
|
|
464
473
|
event.utc = event.utc ?? UTC_DEFAULT;
|
|
465
|
-
|
|
474
|
+
|
|
475
|
+
if (!event.cronTimezone) {
|
|
476
|
+
event.useCronTimezone = false;
|
|
477
|
+
} else {
|
|
478
|
+
event.useCronTimezone = event.useCronTimezone ?? USE_CRON_TZ_DEFAULT;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
event.tz = event.utc || !event.useCronTimezone ? "UTC" : event.cronTimezone;
|
|
482
|
+
|
|
466
483
|
try {
|
|
467
|
-
cron =
|
|
484
|
+
cron = CronExpressionParser.parse(event.cron);
|
|
468
485
|
} catch {
|
|
469
486
|
throw EventQueueError.cantParseCronExpression(event.type, event.subType, event.cron);
|
|
470
487
|
}
|
|
@@ -497,6 +514,7 @@ class Config {
|
|
|
497
514
|
if (this.isMultiTenancy && event.multiInstanceProcessing) {
|
|
498
515
|
throw EventQueueError.multiInstanceProcessingNotAllowed(event.type, event.subType);
|
|
499
516
|
}
|
|
517
|
+
event.inheritTraceContext = event.inheritTraceContext ?? DEFAULT_INHERIT_TRACE_CONTEXT;
|
|
500
518
|
|
|
501
519
|
this.#basicEventValidation(event);
|
|
502
520
|
}
|
|
@@ -529,6 +547,10 @@ class Config {
|
|
|
529
547
|
this.#configEvents = JSON.parse(JSON.stringify(value));
|
|
530
548
|
}
|
|
531
549
|
|
|
550
|
+
get hasConfigEvents() {
|
|
551
|
+
return !!(Object.keys(this.#configEvents ?? {}).length || Object.keys(this.#configPeriodicEvents ?? {}).length);
|
|
552
|
+
}
|
|
553
|
+
|
|
532
554
|
set configPeriodicEvents(value) {
|
|
533
555
|
this.#configPeriodicEvents = JSON.parse(JSON.stringify(value));
|
|
534
556
|
}
|
|
@@ -740,18 +762,22 @@ class Config {
|
|
|
740
762
|
return this.#insertEventsBeforeCommit;
|
|
741
763
|
}
|
|
742
764
|
|
|
743
|
-
set
|
|
744
|
-
this.#
|
|
765
|
+
set enableTelemetry(value) {
|
|
766
|
+
this.#enableTelemetry = value;
|
|
745
767
|
}
|
|
746
768
|
|
|
747
|
-
get
|
|
748
|
-
return this.#
|
|
769
|
+
get enableTelemetry() {
|
|
770
|
+
return this.#enableTelemetry;
|
|
749
771
|
}
|
|
750
772
|
|
|
751
773
|
get isMultiTenancy() {
|
|
752
774
|
return !!cds.requires.multitenancy;
|
|
753
775
|
}
|
|
754
776
|
|
|
777
|
+
get _rawEventMap() {
|
|
778
|
+
return this.#eventMap;
|
|
779
|
+
}
|
|
780
|
+
|
|
755
781
|
/**
|
|
756
782
|
@return { Config }
|
|
757
783
|
**/
|
package/src/index.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ export declare const EventProcessingStatus: {
|
|
|
6
6
|
Done: 2;
|
|
7
7
|
Error: 3;
|
|
8
8
|
Exceeded: 4;
|
|
9
|
+
Suspended: 5;
|
|
9
10
|
};
|
|
10
11
|
|
|
11
12
|
declare type EventProcessingStatusKeysType = keyof typeof EventProcessingStatus;
|
|
@@ -108,6 +109,7 @@ interface EventEntityPublish {
|
|
|
108
109
|
referenceEntity?: string;
|
|
109
110
|
referenceEntityKey?: string;
|
|
110
111
|
payload?: string;
|
|
112
|
+
startAfter?: string;
|
|
111
113
|
}
|
|
112
114
|
|
|
113
115
|
interface EventTriggerProcessing {
|
|
@@ -145,6 +147,7 @@ export declare class EventQueueProcessorBase {
|
|
|
145
147
|
shouldRollbackTransaction(key: string): boolean;
|
|
146
148
|
beforeProcessingEvents(): Promise<void>;
|
|
147
149
|
addEntryToProcessingMap(key: string, queueEntry: EventEntity, payload: Object): void;
|
|
150
|
+
getTxForEventProcessing(key: string): cds.Transaction;
|
|
148
151
|
|
|
149
152
|
set logger(value: CdsLogger);
|
|
150
153
|
get logger(): CdsLogger;
|
|
@@ -162,15 +165,11 @@ export function publishEvent(
|
|
|
162
165
|
options?: {
|
|
163
166
|
skipBroadcast?: boolean;
|
|
164
167
|
skipInsertEventsBeforeCommit?: boolean;
|
|
168
|
+
addTraceContext?: boolean;
|
|
165
169
|
}
|
|
166
170
|
): Promise<any>;
|
|
167
171
|
|
|
168
|
-
export function processEventQueue(
|
|
169
|
-
context: cds.EventContext,
|
|
170
|
-
eventType: string,
|
|
171
|
-
eventSubType: string,
|
|
172
|
-
startTime: Date
|
|
173
|
-
): Promise<any>;
|
|
172
|
+
export function processEventQueue(context: cds.EventContext, eventType: string, eventSubType: string): Promise<any>;
|
|
174
173
|
|
|
175
174
|
export function triggerEventProcessingRedis(
|
|
176
175
|
tenantId: string,
|
|
@@ -256,8 +255,8 @@ declare class Config {
|
|
|
256
255
|
get redisOptions(): any;
|
|
257
256
|
set insertEventsBeforeCommit(value: any);
|
|
258
257
|
get insertEventsBeforeCommit(): any;
|
|
259
|
-
set
|
|
260
|
-
get
|
|
258
|
+
set enableTelemetry(value: any);
|
|
259
|
+
get enableTelemetry(): any;
|
|
261
260
|
get isMultiTenancy(): boolean;
|
|
262
261
|
}
|
|
263
262
|
|
package/src/initialize.js
CHANGED
|
@@ -40,7 +40,7 @@ const CONFIG_VARS = [
|
|
|
40
40
|
["cleanupLocksAndEventsForDev", false],
|
|
41
41
|
["redisOptions", {}],
|
|
42
42
|
["insertEventsBeforeCommit", true],
|
|
43
|
-
["
|
|
43
|
+
["enableTelemetry", true],
|
|
44
44
|
["cronTimezone", null],
|
|
45
45
|
["publishEventBlockList", true],
|
|
46
46
|
["crashOnRedisUnavailable", false],
|
|
@@ -65,7 +65,7 @@ const CONFIG_VARS = [
|
|
|
65
65
|
* @param {boolean} [options.cleanupLocksAndEventsForDev=false] - Cleanup locks and events for development environments.
|
|
66
66
|
* @param {Object} [options.redisOptions={}] - Configuration options for Redis.
|
|
67
67
|
* @param {boolean} [options.insertEventsBeforeCommit=true] - Insert events into the queue before committing the transaction.
|
|
68
|
-
* @param {boolean} [options.
|
|
68
|
+
* @param {boolean} [options.enableTelemetry=false] - Enable telemetry for CAP.
|
|
69
69
|
* @param {string} [options.cronTimezone=null] - Default timezone for cron jobs.
|
|
70
70
|
* @param {string} [options.publishEventBlockList=true] - If redis is available event blocklist is distributed to all application instances
|
|
71
71
|
* @param {string} [options.crashOnRedisUnavailable=true] - If enabled an error is thrown if the redis connection check is not successful
|
|
@@ -134,7 +134,7 @@ const readConfigFromFile = async (configFilepath) => {
|
|
|
134
134
|
"configFilepath with unsupported extension, allowed extensions are .yaml and .json"
|
|
135
135
|
);
|
|
136
136
|
} catch (err) {
|
|
137
|
-
if (config.useAsCAPOutbox) {
|
|
137
|
+
if (!configFilepath && (config.useAsCAPOutbox || config.hasConfigEvents)) {
|
|
138
138
|
return {};
|
|
139
139
|
}
|
|
140
140
|
throw err;
|
package/src/periodicEvents.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
const cds = require("@sap/cds");
|
|
4
|
-
const
|
|
4
|
+
const { CronExpressionParser } = require("cron-parser");
|
|
5
5
|
|
|
6
6
|
const { EventProcessingStatus } = require("./constants");
|
|
7
7
|
const { processChunkedSync } = require("./shared/common");
|
|
@@ -117,10 +117,9 @@ const _determineChangedCron = (existingEventsCron) => {
|
|
|
117
117
|
const config = eventConfig.getEventConfig(event.type, event.subType);
|
|
118
118
|
const eventStartAfter = new Date(event.startAfter);
|
|
119
119
|
const eventCreatedAt = new Date(event.createdAt);
|
|
120
|
-
const cronExpression =
|
|
120
|
+
const cronExpression = CronExpressionParser.parse(config.cron, {
|
|
121
121
|
currentDate: eventCreatedAt,
|
|
122
|
-
|
|
123
|
-
...(config.useCronTimezone && { tz: eventConfig.cronTimezone }),
|
|
122
|
+
tz: config.tz,
|
|
124
123
|
});
|
|
125
124
|
return Math.abs(cronExpression.next().getTime() - eventStartAfter.getTime()) > 30 * 1000; // report as changed if diff created than 30 seconds
|
|
126
125
|
});
|
|
@@ -135,13 +134,10 @@ const _insertPeriodEvents = async (tx, events, now) => {
|
|
|
135
134
|
let startTime = now;
|
|
136
135
|
const config = eventConfig.getEventConfig(event.type, event.subType);
|
|
137
136
|
if (config.cron) {
|
|
138
|
-
startTime =
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
...(config.useCronTimezone && { tz: eventConfig.cronTimezone }),
|
|
143
|
-
})
|
|
144
|
-
.next();
|
|
137
|
+
startTime = CronExpressionParser.parse(config.cron, {
|
|
138
|
+
currentDate: now,
|
|
139
|
+
tz: config.tz,
|
|
140
|
+
}).next();
|
|
145
141
|
}
|
|
146
142
|
base.startAfter = startTime.toISOString();
|
|
147
143
|
return base;
|
|
@@ -150,8 +146,14 @@ const _insertPeriodEvents = async (tx, events, now) => {
|
|
|
150
146
|
processChunkedSync(eventsToBeInserted, CHUNK_SIZE_INSERT_PERIODIC_EVENTS, (chunk) => {
|
|
151
147
|
logger.info(`${counter}/${chunks} | inserting chunk of changed or new periodic events`, {
|
|
152
148
|
events: chunk.map(({ type, subType, startAfter }) => {
|
|
153
|
-
const { interval } = eventConfig.getEventConfig(type, subType);
|
|
154
|
-
return {
|
|
149
|
+
const { interval, cron } = eventConfig.getEventConfig(type, subType);
|
|
150
|
+
return {
|
|
151
|
+
type,
|
|
152
|
+
subType,
|
|
153
|
+
...(startAfter && { startAfter }),
|
|
154
|
+
...(interval && { interval }),
|
|
155
|
+
...(cron && { cron }),
|
|
156
|
+
};
|
|
155
157
|
}),
|
|
156
158
|
});
|
|
157
159
|
counter++;
|
package/src/processEventQueue.js
CHANGED
|
@@ -9,18 +9,19 @@ const { TransactionMode, EventProcessingStatus } = require("./constants");
|
|
|
9
9
|
const { limiter } = require("./shared/common");
|
|
10
10
|
|
|
11
11
|
const { executeInNewTransaction } = require("./shared/cdsHelper");
|
|
12
|
-
const trace = require("./shared/openTelemetry");
|
|
12
|
+
const { trace } = require("./shared/openTelemetry");
|
|
13
13
|
|
|
14
14
|
const COMPONENT_NAME = "/eventQueue/processEventQueue";
|
|
15
15
|
|
|
16
|
-
const processEventQueue = async (context, eventType, eventSubType
|
|
16
|
+
const processEventQueue = async (context, eventType, eventSubType) => {
|
|
17
17
|
let iterationCounter = 0;
|
|
18
18
|
let shouldContinue = true;
|
|
19
19
|
let baseInstance;
|
|
20
|
+
let startTime = new Date();
|
|
20
21
|
try {
|
|
21
22
|
let eventTypeInstance;
|
|
22
23
|
const eventConfig = config.getEventConfig(eventType, eventSubType);
|
|
23
|
-
const [err, EventTypeClass] = resilientRequire(eventConfig);
|
|
24
|
+
const [err, EventTypeClass] = await resilientRequire(eventConfig);
|
|
24
25
|
if (!eventConfig || err || !(typeof EventTypeClass.constructor === "function")) {
|
|
25
26
|
cds.log(COMPONENT_NAME).error("No Implementation found in the provided configuration file.", {
|
|
26
27
|
eventType,
|
|
@@ -37,10 +38,11 @@ const processEventQueue = async (context, eventType, eventSubType, startTime = n
|
|
|
37
38
|
if (!continueProcessing) {
|
|
38
39
|
return;
|
|
39
40
|
}
|
|
41
|
+
eventConfig.startTime = startTime;
|
|
42
|
+
eventConfig.lockAcquiredTime = new Date();
|
|
40
43
|
if (baseInstance.isPeriodicEvent) {
|
|
41
44
|
return await processPeriodicEvent(context, baseInstance);
|
|
42
45
|
}
|
|
43
|
-
eventConfig.startTime = startTime;
|
|
44
46
|
while (shouldContinue) {
|
|
45
47
|
iterationCounter++;
|
|
46
48
|
await executeInNewTransaction(context, `eventQueue-pre-processing-${eventType}##${eventSubType}`, async (tx) => {
|
|
@@ -76,28 +78,26 @@ const processEventQueue = async (context, eventType, eventSubType, startTime = n
|
|
|
76
78
|
if (Object.keys(eventTypeInstance.queueEntriesWithPayloadMap).length) {
|
|
77
79
|
await executeInNewTransaction(context, `eventQueue-processing-${eventType}##${eventSubType}`, async (tx) => {
|
|
78
80
|
eventTypeInstance.processEventContext = tx.context;
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
)
|
|
92
|
-
|
|
93
|
-
}
|
|
94
|
-
});
|
|
81
|
+
try {
|
|
82
|
+
eventTypeInstance.clusterQueueEntries(eventTypeInstance.queueEntriesWithPayloadMap);
|
|
83
|
+
await processEventMap(eventTypeInstance);
|
|
84
|
+
} catch (err) {
|
|
85
|
+
eventTypeInstance.handleErrorDuringClustering(err);
|
|
86
|
+
}
|
|
87
|
+
if (
|
|
88
|
+
eventTypeInstance.transactionMode !== TransactionMode.alwaysCommit ||
|
|
89
|
+
Object.entries(eventTypeInstance.eventProcessingMap).some(([key]) =>
|
|
90
|
+
eventTypeInstance.shouldRollbackTransaction(key)
|
|
91
|
+
)
|
|
92
|
+
) {
|
|
93
|
+
await tx.rollback();
|
|
94
|
+
}
|
|
95
95
|
});
|
|
96
96
|
}
|
|
97
97
|
await executeInNewTransaction(context, `eventQueue-persistStatus-${eventType}##${eventSubType}`, async (tx) => {
|
|
98
98
|
await eventTypeInstance.persistEventStatus(tx);
|
|
99
99
|
});
|
|
100
|
-
shouldContinue = reevaluateShouldContinue(eventTypeInstance, iterationCounter, startTime);
|
|
100
|
+
shouldContinue = reevaluateShouldContinue(eventTypeInstance, iterationCounter, eventConfig.startTime);
|
|
101
101
|
}
|
|
102
102
|
} catch (err) {
|
|
103
103
|
cds.log(COMPONENT_NAME).error("Processing event queue failed with unexpected error.", err, {
|
|
@@ -317,26 +317,50 @@ const _checkEventIsBlocked = async (baseInstance) => {
|
|
|
317
317
|
};
|
|
318
318
|
|
|
319
319
|
const _processEvent = async (eventTypeInstance, processContext, key, queueEntries, payload) => {
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
// NOTE: return empty status map to comply with the interface
|
|
324
|
-
return {};
|
|
325
|
-
}
|
|
326
|
-
eventTypeInstance.setTxForEventProcessing(key, cds.tx(processContext));
|
|
327
|
-
const statusTuple = await eventTypeInstance.processEvent(processContext, key, queueEntries, payload);
|
|
328
|
-
return eventTypeInstance.setEventStatus(queueEntries, statusTuple);
|
|
329
|
-
} catch (err) {
|
|
330
|
-
return eventTypeInstance.handleErrorDuringProcessing(err, queueEntries);
|
|
320
|
+
let traceContext;
|
|
321
|
+
if (queueEntries.length === 1 && eventTypeInstance.inheritTraceContext) {
|
|
322
|
+
traceContext = queueEntries[0].context?.traceContext;
|
|
331
323
|
}
|
|
324
|
+
|
|
325
|
+
return await trace(
|
|
326
|
+
eventTypeInstance.baseContext,
|
|
327
|
+
`process-event-${eventTypeInstance.eventType}-${eventTypeInstance.eventSubType}`,
|
|
328
|
+
async () => {
|
|
329
|
+
try {
|
|
330
|
+
const eventOutdated = await eventTypeInstance.isOutdatedAndKeepAlive(queueEntries);
|
|
331
|
+
if (eventOutdated) {
|
|
332
|
+
// NOTE: return empty status map to comply with the interface
|
|
333
|
+
return {};
|
|
334
|
+
}
|
|
335
|
+
eventTypeInstance.setTxForEventProcessing(key, cds.tx(processContext));
|
|
336
|
+
const statusTuple = await eventTypeInstance.processEvent(processContext, key, queueEntries, payload);
|
|
337
|
+
return eventTypeInstance.setEventStatus(queueEntries, statusTuple);
|
|
338
|
+
} catch (err) {
|
|
339
|
+
return eventTypeInstance.handleErrorDuringProcessing(err, queueEntries);
|
|
340
|
+
}
|
|
341
|
+
},
|
|
342
|
+
{ traceContext }
|
|
343
|
+
);
|
|
332
344
|
};
|
|
333
345
|
|
|
334
|
-
const resilientRequire = (eventConfig) => {
|
|
346
|
+
const resilientRequire = async (eventConfig) => {
|
|
335
347
|
try {
|
|
336
348
|
const path = eventConfig?.impl;
|
|
337
349
|
const internal = eventConfig?.internalEvent;
|
|
338
|
-
const
|
|
339
|
-
|
|
350
|
+
const filePath = pathLib.join(internal ? __dirname : process.cwd(), path);
|
|
351
|
+
const fileExtension = pathLib.extname(filePath);
|
|
352
|
+
switch (fileExtension) {
|
|
353
|
+
case ".js":
|
|
354
|
+
return [null, require(filePath)];
|
|
355
|
+
case ".mjs":
|
|
356
|
+
return [null, (await import(`file://${filePath}`)).default];
|
|
357
|
+
case "":
|
|
358
|
+
try {
|
|
359
|
+
return [null, require(filePath)];
|
|
360
|
+
} catch {
|
|
361
|
+
return [null, (await import(`file://${filePath}`)).default];
|
|
362
|
+
}
|
|
363
|
+
}
|
|
340
364
|
} catch (err) {
|
|
341
365
|
return [err, null];
|
|
342
366
|
}
|
package/src/publishEvent.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const config = require("./config");
|
|
4
4
|
const common = require("./shared/common");
|
|
5
5
|
const EventQueueError = require("./EventQueueError");
|
|
6
|
+
const openTelemetry = require("./shared/openTelemetry");
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Asynchronously publishes a series of events to the event queue.
|
|
@@ -29,7 +30,11 @@ const EventQueueError = require("./EventQueueError");
|
|
|
29
30
|
* @throws {EventQueueError} Throws an error if the startAfter field is not a valid date.
|
|
30
31
|
* @returns {Promise<*>} Returns a promise which resolves to the result of the database insert operation.
|
|
31
32
|
*/
|
|
32
|
-
const publishEvent = async (
|
|
33
|
+
const publishEvent = async (
|
|
34
|
+
tx,
|
|
35
|
+
events,
|
|
36
|
+
{ skipBroadcast = false, skipInsertEventsBeforeCommit = false, addTraceContext = true } = {}
|
|
37
|
+
) => {
|
|
33
38
|
if (!config.initialized) {
|
|
34
39
|
throw EventQueueError.notInitialized();
|
|
35
40
|
}
|
|
@@ -51,6 +56,10 @@ const publishEvent = async (tx, events, { skipBroadcast = false, skipInsertEvent
|
|
|
51
56
|
if (typeof event.payload !== "string") {
|
|
52
57
|
event.payload = JSON.stringify(event.payload);
|
|
53
58
|
}
|
|
59
|
+
|
|
60
|
+
if (addTraceContext) {
|
|
61
|
+
event.context = JSON.stringify({ traceContext: openTelemetry.getCurrentTraceContext() });
|
|
62
|
+
}
|
|
54
63
|
}
|
|
55
64
|
if (config.insertEventsBeforeCommit && !skipInsertEventsBeforeCommit) {
|
|
56
65
|
_registerHandlerAndAddEvents(tx, events);
|
package/src/redis/redisPub.js
CHANGED
|
@@ -9,7 +9,7 @@ const distributedLock = require("../shared/distributedLock");
|
|
|
9
9
|
const config = require("../config");
|
|
10
10
|
const common = require("../shared/common");
|
|
11
11
|
const { runEventCombinationForTenant } = require("../runner/runnerHelper");
|
|
12
|
-
const trace = require("../shared/openTelemetry");
|
|
12
|
+
const { trace } = require("../shared/openTelemetry");
|
|
13
13
|
const { TenantIdCheckTypes } = require("../constants");
|
|
14
14
|
|
|
15
15
|
const EVENT_MESSAGE_CHANNEL = "EVENT_QUEUE_MESSAGE_CHANNEL";
|
package/src/runner/runner.js
CHANGED
|
@@ -15,7 +15,7 @@ const config = require("../config");
|
|
|
15
15
|
const redisPub = require("../redis/redisPub");
|
|
16
16
|
const openEvents = require("./openEvents");
|
|
17
17
|
const { runEventCombinationForTenant } = require("./runnerHelper");
|
|
18
|
-
const trace = require("../shared/openTelemetry");
|
|
18
|
+
const { trace } = require("../shared/openTelemetry");
|
|
19
19
|
|
|
20
20
|
const COMPONENT_NAME = "/eventQueue/runner";
|
|
21
21
|
const EVENT_QUEUE_RUN_ID = "EVENT_QUEUE_RUN_ID";
|
|
@@ -51,7 +51,10 @@ const _scheduleFunction = async (singleRunFn, periodicFn) => {
|
|
|
51
51
|
}
|
|
52
52
|
if (!singleRunDone) {
|
|
53
53
|
singleRunDone = true;
|
|
54
|
-
singleRunFn()
|
|
54
|
+
singleRunFn()
|
|
55
|
+
.then(periodicFn)
|
|
56
|
+
.catch(() => (singleRunDone = false));
|
|
57
|
+
return;
|
|
55
58
|
}
|
|
56
59
|
return periodicFn();
|
|
57
60
|
};
|
|
@@ -74,20 +77,24 @@ const _multiTenancyRedis = async () => {
|
|
|
74
77
|
try {
|
|
75
78
|
logger.info("executing event queue run for multi instance and tenant");
|
|
76
79
|
const tenantIds = await cdsHelper.getAllTenantIds();
|
|
77
|
-
await _checkPeriodicEventUpdate(tenantIds);
|
|
78
|
-
|
|
80
|
+
const shouldContinue = await _checkPeriodicEventUpdate(tenantIds);
|
|
81
|
+
shouldContinue && (await _executeEventsAllTenantsRedis(tenantIds));
|
|
79
82
|
} catch (err) {
|
|
80
83
|
logger.info("executing event queue run for multi instance and tenant failed", err);
|
|
81
84
|
}
|
|
82
85
|
};
|
|
83
86
|
|
|
87
|
+
// NOTE: _checkPeriodicEventUpdate the function must return truthy if _executeEventsAllTenantsRedis should continue
|
|
88
|
+
// processing open events. The idea is: if _multiTenancyPeriodicEvents is executed after the deployment we want
|
|
89
|
+
// to wait for all instances that periodic events are up-to-date and the updating of periodic events does not
|
|
90
|
+
// interfere with the processing of events
|
|
84
91
|
const _checkPeriodicEventUpdate = async (tenantIds) => {
|
|
85
92
|
if (!eventQueueConfig.updatePeriodicEvents || !eventQueueConfig.periodicEvents.length) {
|
|
86
93
|
cds.log(COMPONENT_NAME).info("updating of periodic events is disabled or no periodic events configured", {
|
|
87
94
|
updateEnabled: eventQueueConfig.updatePeriodicEvents,
|
|
88
95
|
events: eventQueueConfig.periodicEvents.length,
|
|
89
96
|
});
|
|
90
|
-
return;
|
|
97
|
+
return true;
|
|
91
98
|
}
|
|
92
99
|
const hash = common.hashStringTo32Bit(JSON.stringify(tenantIds));
|
|
93
100
|
if (!tenantIdHash) {
|
|
@@ -99,9 +106,12 @@ const _checkPeriodicEventUpdate = async (tenantIds) => {
|
|
|
99
106
|
if (tenantIdHash && tenantIdHash !== hash) {
|
|
100
107
|
tenantIdHash = hash;
|
|
101
108
|
cds.log(COMPONENT_NAME).info("tenant id hash changed, triggering updating periodic events!");
|
|
102
|
-
|
|
109
|
+
await _multiTenancyPeriodicEvents(tenantIds).catch((err) => {
|
|
103
110
|
cds.log(COMPONENT_NAME).error("Error during triggering updating periodic events!", err);
|
|
104
111
|
});
|
|
112
|
+
return true;
|
|
113
|
+
} else {
|
|
114
|
+
return true;
|
|
105
115
|
}
|
|
106
116
|
};
|
|
107
117
|
|
|
@@ -458,7 +468,8 @@ const _multiTenancyPeriodicEvents = async (tenantIds) => {
|
|
|
458
468
|
}
|
|
459
469
|
|
|
460
470
|
tenantIds = tenantIds ?? (await cdsHelper.getAllTenantIds());
|
|
461
|
-
|
|
471
|
+
await _executePeriodicEventsAllTenants(tenantIds);
|
|
472
|
+
return true;
|
|
462
473
|
},
|
|
463
474
|
{ newRootSpan: true }
|
|
464
475
|
);
|
|
@@ -8,7 +8,7 @@ const { processEventQueue } = require("../processEventQueue");
|
|
|
8
8
|
const eventQueueConfig = require("../config");
|
|
9
9
|
const WorkerQueue = require("../shared/WorkerQueue");
|
|
10
10
|
const distributedLock = require("../shared/distributedLock");
|
|
11
|
-
const trace = require("../shared/openTelemetry");
|
|
11
|
+
const { trace } = require("../shared/openTelemetry");
|
|
12
12
|
|
|
13
13
|
const COMPONENT_NAME = "/eventQueue/runnerHelper";
|
|
14
14
|
|
package/src/shared/common.js
CHANGED
|
@@ -90,10 +90,20 @@ const _acquireLockRedis = async (
|
|
|
90
90
|
|
|
91
91
|
const _renewLockRedis = async (context, fullKey, expiryTime, { value = "true" } = {}) => {
|
|
92
92
|
const client = await redis.createMainClientAndConnect(config.redisOptions);
|
|
93
|
-
|
|
93
|
+
let result = await client.set(fullKey, value, {
|
|
94
94
|
PX: Math.round(expiryTime),
|
|
95
95
|
XX: true,
|
|
96
96
|
});
|
|
97
|
+
|
|
98
|
+
if (result !== REDIS_COMMAND_OK) {
|
|
99
|
+
const readResult = await client.get(fullKey);
|
|
100
|
+
if (!readResult) {
|
|
101
|
+
result = await client.set(fullKey, value, {
|
|
102
|
+
PX: Math.round(expiryTime),
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
97
107
|
return result === REDIS_COMMAND_OK;
|
|
98
108
|
};
|
|
99
109
|
|
|
@@ -1,32 +1,56 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
+
const _resilientRequire = (module) => {
|
|
4
|
+
try {
|
|
5
|
+
return require(module);
|
|
6
|
+
} catch {
|
|
7
|
+
// ignore
|
|
8
|
+
}
|
|
9
|
+
};
|
|
10
|
+
|
|
3
11
|
const cds = require("@sap/cds");
|
|
4
|
-
|
|
5
|
-
try {
|
|
6
|
-
otel = require("@opentelemetry/api");
|
|
7
|
-
} catch {
|
|
8
|
-
// ignore
|
|
9
|
-
}
|
|
12
|
+
const otel = _resilientRequire("@opentelemetry/api");
|
|
10
13
|
|
|
11
14
|
const config = require("../config");
|
|
12
15
|
|
|
13
16
|
const COMPONENT_NAME = "/shared/openTelemetry";
|
|
14
17
|
|
|
15
|
-
const trace = async (context, label, fn, { attributes = {}, newRootSpan = false } = {}) => {
|
|
16
|
-
|
|
17
|
-
// Check if a real provider is registered
|
|
18
|
-
if (!config.enableCAPTelemetry || !tracerProvider || tracerProvider === otel.trace.NOOP_TRACER_PROVIDER) {
|
|
18
|
+
const trace = async (context, label, fn, { attributes = {}, newRootSpan = false, traceContext } = {}) => {
|
|
19
|
+
if (!config.enableTelemetry || !otel) {
|
|
19
20
|
return fn();
|
|
20
21
|
}
|
|
21
22
|
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
const tracerProvider = otel.trace.getTracerProvider();
|
|
24
|
+
if ((!tracerProvider || tracerProvider === otel.trace.NOOP_TRACER_PROVIDER) && !process.env.DT_NODE_PRELOAD_OPTIONS) {
|
|
25
|
+
return fn();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const tracer = otel.trace.getTracer("@cap-js-community/event-queue");
|
|
29
|
+
const extractedContext = traceContext
|
|
30
|
+
? otel.propagation.extract(otel.context.active(), traceContext)
|
|
31
|
+
: otel.context.active();
|
|
32
|
+
const span = tracer.startSpan(
|
|
33
|
+
`eventqueue-${label}`,
|
|
34
|
+
{
|
|
35
|
+
kind: otel.SpanKind.INTERNAL,
|
|
36
|
+
root: newRootSpan,
|
|
37
|
+
},
|
|
38
|
+
extractedContext
|
|
39
|
+
);
|
|
27
40
|
_setAttributes(context, span, attributes);
|
|
28
|
-
const ctxWithSpan = otel.trace.setSpan(
|
|
41
|
+
const ctxWithSpan = otel.trace.setSpan(extractedContext, span);
|
|
42
|
+
|
|
43
|
+
return await _startOtelTrace(ctxWithSpan, traceContext, span, fn);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const _startOtelTrace = async (ctxWithSpan, traceContext, span, fn) => {
|
|
29
47
|
return otel.context.with(ctxWithSpan, async () => {
|
|
48
|
+
if (traceContext) {
|
|
49
|
+
cds.log("/eventQueue/telemetry").info("Linked span:", span.spanContext());
|
|
50
|
+
const carrier = {};
|
|
51
|
+
otel.propagation.inject(ctxWithSpan, carrier);
|
|
52
|
+
cds.log("/eventQueue/telemetry").info("Extracted trace context by inject", carrier);
|
|
53
|
+
}
|
|
30
54
|
const onSuccess = (res) => {
|
|
31
55
|
span.setStatus({ code: otel.SpanStatusCode.OK });
|
|
32
56
|
return res;
|
|
@@ -72,4 +96,13 @@ const _setAttributes = (context, span, attributes) => {
|
|
|
72
96
|
}
|
|
73
97
|
};
|
|
74
98
|
|
|
75
|
-
|
|
99
|
+
const getCurrentTraceContext = () => {
|
|
100
|
+
if (!otel) {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
const carrier = {};
|
|
104
|
+
otel.propagation.inject(otel.context.active(), carrier);
|
|
105
|
+
return carrier;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
module.exports = { trace, getCurrentTraceContext };
|