@cap-js-community/event-queue 1.9.1 → 1.9.3
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 +34 -9
- package/src/config.js +31 -9
- package/src/index.d.ts +7 -8
- package/src/initialize.js +2 -2
- package/src/periodicEvents.js +7 -11
- package/src/processEventQueue.js +43 -31
- package/src/publishEvent.js +10 -1
- package/src/redis/redisPub.js +1 -1
- package/src/runner/runner.js +14 -6
- package/src/runner/runnerHelper.js +1 -1
- package/src/shared/SetIntervalDriftSafe.js +5 -0
- package/src/shared/openTelemetry.js +44 -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.3",
|
|
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 () => {
|
|
@@ -978,6 +987,7 @@ class EventQueueProcessorBase {
|
|
|
978
987
|
});
|
|
979
988
|
return false;
|
|
980
989
|
}
|
|
990
|
+
this.lockAcquiredTime = new Date();
|
|
981
991
|
return true;
|
|
982
992
|
}
|
|
983
993
|
|
|
@@ -1000,9 +1010,8 @@ class EventQueueProcessorBase {
|
|
|
1000
1010
|
}
|
|
1001
1011
|
|
|
1002
1012
|
// NOTE: do not pass current date as we always want to calc. a future date
|
|
1003
|
-
const cronExpression =
|
|
1004
|
-
|
|
1005
|
-
...(this.#eventConfig.useCronTimezone && { tz: this.#config.cronTimezone }),
|
|
1013
|
+
const cronExpression = CronExpressionParser.parse(this.#eventConfig.cron, {
|
|
1014
|
+
tz: eventConfig.tz,
|
|
1006
1015
|
});
|
|
1007
1016
|
return cronExpression.next();
|
|
1008
1017
|
}
|
|
@@ -1247,6 +1256,22 @@ class EventQueueProcessorBase {
|
|
|
1247
1256
|
get eventConfig() {
|
|
1248
1257
|
return this.#eventConfig;
|
|
1249
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
|
+
}
|
|
1250
1275
|
}
|
|
1251
1276
|
|
|
1252
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
|
}
|
|
@@ -744,18 +762,22 @@ class Config {
|
|
|
744
762
|
return this.#insertEventsBeforeCommit;
|
|
745
763
|
}
|
|
746
764
|
|
|
747
|
-
set
|
|
748
|
-
this.#
|
|
765
|
+
set enableTelemetry(value) {
|
|
766
|
+
this.#enableTelemetry = value;
|
|
749
767
|
}
|
|
750
768
|
|
|
751
|
-
get
|
|
752
|
-
return this.#
|
|
769
|
+
get enableTelemetry() {
|
|
770
|
+
return this.#enableTelemetry;
|
|
753
771
|
}
|
|
754
772
|
|
|
755
773
|
get isMultiTenancy() {
|
|
756
774
|
return !!cds.requires.multitenancy;
|
|
757
775
|
}
|
|
758
776
|
|
|
777
|
+
get _rawEventMap() {
|
|
778
|
+
return this.#eventMap;
|
|
779
|
+
}
|
|
780
|
+
|
|
759
781
|
/**
|
|
760
782
|
@return { Config }
|
|
761
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
|
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;
|
package/src/processEventQueue.js
CHANGED
|
@@ -9,14 +9,15 @@ 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);
|
|
@@ -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,18 +317,30 @@ 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
346
|
const resilientRequire = async (eventConfig) => {
|
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";
|
|
@@ -77,20 +77,24 @@ const _multiTenancyRedis = async () => {
|
|
|
77
77
|
try {
|
|
78
78
|
logger.info("executing event queue run for multi instance and tenant");
|
|
79
79
|
const tenantIds = await cdsHelper.getAllTenantIds();
|
|
80
|
-
await _checkPeriodicEventUpdate(tenantIds);
|
|
81
|
-
|
|
80
|
+
const shouldContinue = await _checkPeriodicEventUpdate(tenantIds);
|
|
81
|
+
shouldContinue && (await _executeEventsAllTenantsRedis(tenantIds));
|
|
82
82
|
} catch (err) {
|
|
83
83
|
logger.info("executing event queue run for multi instance and tenant failed", err);
|
|
84
84
|
}
|
|
85
85
|
};
|
|
86
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
|
|
87
91
|
const _checkPeriodicEventUpdate = async (tenantIds) => {
|
|
88
92
|
if (!eventQueueConfig.updatePeriodicEvents || !eventQueueConfig.periodicEvents.length) {
|
|
89
93
|
cds.log(COMPONENT_NAME).info("updating of periodic events is disabled or no periodic events configured", {
|
|
90
94
|
updateEnabled: eventQueueConfig.updatePeriodicEvents,
|
|
91
95
|
events: eventQueueConfig.periodicEvents.length,
|
|
92
96
|
});
|
|
93
|
-
return;
|
|
97
|
+
return true;
|
|
94
98
|
}
|
|
95
99
|
const hash = common.hashStringTo32Bit(JSON.stringify(tenantIds));
|
|
96
100
|
if (!tenantIdHash) {
|
|
@@ -102,9 +106,12 @@ const _checkPeriodicEventUpdate = async (tenantIds) => {
|
|
|
102
106
|
if (tenantIdHash && tenantIdHash !== hash) {
|
|
103
107
|
tenantIdHash = hash;
|
|
104
108
|
cds.log(COMPONENT_NAME).info("tenant id hash changed, triggering updating periodic events!");
|
|
105
|
-
|
|
109
|
+
await _multiTenancyPeriodicEvents(tenantIds).catch((err) => {
|
|
106
110
|
cds.log(COMPONENT_NAME).error("Error during triggering updating periodic events!", err);
|
|
107
111
|
});
|
|
112
|
+
return true;
|
|
113
|
+
} else {
|
|
114
|
+
return true;
|
|
108
115
|
}
|
|
109
116
|
};
|
|
110
117
|
|
|
@@ -461,7 +468,8 @@ const _multiTenancyPeriodicEvents = async (tenantIds) => {
|
|
|
461
468
|
}
|
|
462
469
|
|
|
463
470
|
tenantIds = tenantIds ?? (await cdsHelper.getAllTenantIds());
|
|
464
|
-
|
|
471
|
+
await _executePeriodicEventsAllTenants(tenantIds);
|
|
472
|
+
return true;
|
|
465
473
|
},
|
|
466
474
|
{ newRootSpan: true }
|
|
467
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
|
|
|
@@ -1,31 +1,49 @@
|
|
|
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 () => {
|
|
30
48
|
const onSuccess = (res) => {
|
|
31
49
|
span.setStatus({ code: otel.SpanStatusCode.OK });
|
|
@@ -72,4 +90,13 @@ const _setAttributes = (context, span, attributes) => {
|
|
|
72
90
|
}
|
|
73
91
|
};
|
|
74
92
|
|
|
75
|
-
|
|
93
|
+
const getCurrentTraceContext = () => {
|
|
94
|
+
if (!otel) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
const carrier = {};
|
|
98
|
+
otel.propagation.inject(otel.context.active(), carrier);
|
|
99
|
+
return carrier;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
module.exports = { trace, getCurrentTraceContext };
|