@cap-js-community/event-queue 1.9.0-beta.1 → 1.9.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 +12 -1
- package/src/EventQueueError.js +30 -0
- package/src/EventQueueProcessorBase.js +1 -1
- package/src/config.js +44 -17
- package/src/initialize.js +8 -3
- package/src/outbox/EventQueueGenericOutboxHandler.js +26 -4
- package/src/processEventQueue.js +5 -1
- package/src/shared/openTelemetry.js +4 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cap-js-community/event-queue",
|
|
3
|
-
"version": "1.9.0
|
|
3
|
+
"version": "1.9.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",
|
|
@@ -79,6 +79,17 @@
|
|
|
79
79
|
"registerAsEventProcessor": false,
|
|
80
80
|
"updatePeriodicEvents": false,
|
|
81
81
|
"insertEventsBeforeCommit": false
|
|
82
|
+
},
|
|
83
|
+
"periodicEvents": {
|
|
84
|
+
"[production]": {
|
|
85
|
+
"EVENT_QUEUE_BASE/DELETE_EVENTS": {
|
|
86
|
+
"priority": "low",
|
|
87
|
+
"impl": "./housekeeping/EventQueueDeleteEvents",
|
|
88
|
+
"load": 20,
|
|
89
|
+
"interval": 86400,
|
|
90
|
+
"internalEvent": true
|
|
91
|
+
}
|
|
92
|
+
}
|
|
82
93
|
}
|
|
83
94
|
},
|
|
84
95
|
"requires": {
|
package/src/EventQueueError.js
CHANGED
|
@@ -18,6 +18,8 @@ const ERROR_CODES = {
|
|
|
18
18
|
NO_INTERVAL_OR_CRON: "NO_INTERVAL_OR_CRON",
|
|
19
19
|
INTERVAL_AND_CRON: "INTERVAL_AND_CRON",
|
|
20
20
|
MISSING_IMPL: "MISSING_IMPL",
|
|
21
|
+
MISSING_TYPE: "MISSING_TYPE",
|
|
22
|
+
MISSING_SUBTYPE: "MISSING_SUBTYPE",
|
|
21
23
|
DUPLICATE_EVENT_REGISTRATION: "DUPLICATE_EVENT_REGISTRATION",
|
|
22
24
|
NO_MANUEL_INSERT_OF_PERIODIC: "NO_MANUEL_INSERT_OF_PERIODIC",
|
|
23
25
|
LOAD_HIGHER_THAN_LIMIT: "LOAD_HIGHER_THAN_LIMIT",
|
|
@@ -62,6 +64,12 @@ const ERROR_CODES_META = {
|
|
|
62
64
|
[ERROR_CODES.MISSING_IMPL]: {
|
|
63
65
|
message: "Missing path to event class implementation.",
|
|
64
66
|
},
|
|
67
|
+
[ERROR_CODES.MISSING_TYPE]: {
|
|
68
|
+
message: "Missing type event implementation.",
|
|
69
|
+
},
|
|
70
|
+
[ERROR_CODES.MISSING_SUBTYPE]: {
|
|
71
|
+
message: "Missing subtype event implementation.",
|
|
72
|
+
},
|
|
65
73
|
[ERROR_CODES.DUPLICATE_EVENT_REGISTRATION]: {
|
|
66
74
|
message: "Duplicate event registration, check the uniqueness of type and subType.",
|
|
67
75
|
},
|
|
@@ -265,6 +273,28 @@ class EventQueueError extends VError {
|
|
|
265
273
|
);
|
|
266
274
|
}
|
|
267
275
|
|
|
276
|
+
static missingType(config) {
|
|
277
|
+
const { message } = ERROR_CODES_META[ERROR_CODES.MISSING_TYPE];
|
|
278
|
+
return new EventQueueError(
|
|
279
|
+
{
|
|
280
|
+
name: ERROR_CODES.MISSING_TYPE,
|
|
281
|
+
info: { config },
|
|
282
|
+
},
|
|
283
|
+
message
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
static missingSubType(config) {
|
|
288
|
+
const { message } = ERROR_CODES_META[ERROR_CODES.MISSING_SUBTYPE];
|
|
289
|
+
return new EventQueueError(
|
|
290
|
+
{
|
|
291
|
+
name: ERROR_CODES.MISSING_SUBTYPE,
|
|
292
|
+
info: { config },
|
|
293
|
+
},
|
|
294
|
+
message
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
|
|
268
298
|
static duplicateEventRegistration(type, subType) {
|
|
269
299
|
const { message } = ERROR_CODES_META[ERROR_CODES.DUPLICATE_EVENT_REGISTRATION];
|
|
270
300
|
return new EventQueueError(
|
|
@@ -852,7 +852,7 @@ class EventQueueProcessorBase {
|
|
|
852
852
|
// eslint-disable-next-line no-unused-vars
|
|
853
853
|
async beforeProcessingEvents() {}
|
|
854
854
|
|
|
855
|
-
async
|
|
855
|
+
async isOutdatedAndKeepAlive() {
|
|
856
856
|
if (this.__keepAliveViolated) {
|
|
857
857
|
return true;
|
|
858
858
|
}
|
package/src/config.js
CHANGED
|
@@ -30,18 +30,6 @@ const PRIORITIES = Object.values(Priorities);
|
|
|
30
30
|
const UTC_DEFAULT = false;
|
|
31
31
|
const USE_CRON_TZ_DEFAULT = true;
|
|
32
32
|
|
|
33
|
-
const BASE_PERIODIC_EVENTS = [
|
|
34
|
-
{
|
|
35
|
-
type: "EVENT_QUEUE_BASE",
|
|
36
|
-
subType: "DELETE_EVENTS",
|
|
37
|
-
priority: Priorities.Low,
|
|
38
|
-
impl: "./housekeeping/EventQueueDeleteEvents",
|
|
39
|
-
load: 20,
|
|
40
|
-
interval: 86400, // 1 day,
|
|
41
|
-
internalEvent: true,
|
|
42
|
-
},
|
|
43
|
-
];
|
|
44
|
-
|
|
45
33
|
const BASE_TABLES = {
|
|
46
34
|
EVENT: "sap.eventqueue.Event",
|
|
47
35
|
LOCK: "sap.eventqueue.Lock",
|
|
@@ -82,6 +70,8 @@ class Config {
|
|
|
82
70
|
#crashOnRedisUnavailable;
|
|
83
71
|
#tenantIdFilterTokenInfoCb;
|
|
84
72
|
#tenantIdFilterEventProcessingCb;
|
|
73
|
+
#configEvents;
|
|
74
|
+
#configPeriodicEvents;
|
|
85
75
|
static #instance;
|
|
86
76
|
constructor() {
|
|
87
77
|
this.#logger = cds.log(COMPONENT_NAME);
|
|
@@ -357,13 +347,33 @@ class Config {
|
|
|
357
347
|
this.#isEventQueueActive = value;
|
|
358
348
|
}
|
|
359
349
|
|
|
350
|
+
mixFileContentWithEnv(fileContent) {
|
|
351
|
+
fileContent.events ??= [];
|
|
352
|
+
fileContent.periodicEvents ??= [];
|
|
353
|
+
const events = this.#configEvents ?? {};
|
|
354
|
+
const periodicEvents = this.#configPeriodicEvents ?? {};
|
|
355
|
+
fileContent.events = fileContent.events.concat(this.#mapEnvEvents(events));
|
|
356
|
+
fileContent.periodicEvents = fileContent.periodicEvents.concat(this.#mapEnvEvents(periodicEvents));
|
|
357
|
+
this.fileContent = fileContent;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
#mapEnvEvents(events) {
|
|
361
|
+
return Object.entries(events)
|
|
362
|
+
.map(([key, event]) => {
|
|
363
|
+
if (!event) {
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
const [type, subType] = key.split("/");
|
|
367
|
+
event.type ??= type;
|
|
368
|
+
event.subType ??= subType;
|
|
369
|
+
return { ...event };
|
|
370
|
+
})
|
|
371
|
+
.filter((a) => a);
|
|
372
|
+
}
|
|
373
|
+
|
|
360
374
|
set fileContent(config) {
|
|
361
|
-
this.#config = config;
|
|
362
375
|
config.events = config.events ?? [];
|
|
363
|
-
|
|
364
|
-
config.periodicEvents = (config.periodicEvents ?? []).concat(
|
|
365
|
-
(shouldIncludeBaseEvents ? BASE_PERIODIC_EVENTS : []).map((event) => ({ ...event }))
|
|
366
|
-
);
|
|
376
|
+
config.periodicEvents = config.periodicEvents ?? [];
|
|
367
377
|
this.#eventMap = config.events.reduce((result, event) => {
|
|
368
378
|
this.#basicEventTransformation(event);
|
|
369
379
|
this.#validateAdHocEvents(result, event);
|
|
@@ -380,6 +390,7 @@ class Config {
|
|
|
380
390
|
result[this.generateKey(event.type, event.subType)] = event;
|
|
381
391
|
return result;
|
|
382
392
|
}, this.#eventMap);
|
|
393
|
+
this.#config = config;
|
|
383
394
|
}
|
|
384
395
|
|
|
385
396
|
#basicEventTransformation(event) {
|
|
@@ -402,6 +413,14 @@ class Config {
|
|
|
402
413
|
throw EventQueueError.missingImpl(event.type, event.subType);
|
|
403
414
|
}
|
|
404
415
|
|
|
416
|
+
if (!event.type) {
|
|
417
|
+
throw EventQueueError.missingType(event);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
if (!event.subType) {
|
|
421
|
+
throw EventQueueError.missingSubType(event);
|
|
422
|
+
}
|
|
423
|
+
|
|
405
424
|
if (event.appNames) {
|
|
406
425
|
if (!Array.isArray(event.appNames) || event.appNames.some((appName) => typeof appName !== "string")) {
|
|
407
426
|
throw EventQueueError.appNamesFormat(event.type, event.subType, event.appNames);
|
|
@@ -506,6 +525,14 @@ class Config {
|
|
|
506
525
|
return this.#config.events;
|
|
507
526
|
}
|
|
508
527
|
|
|
528
|
+
set configEvents(value) {
|
|
529
|
+
this.#configEvents = JSON.parse(JSON.stringify(value));
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
set configPeriodicEvents(value) {
|
|
533
|
+
this.#configPeriodicEvents = JSON.parse(JSON.stringify(value));
|
|
534
|
+
}
|
|
535
|
+
|
|
509
536
|
get periodicEvents() {
|
|
510
537
|
return this.#config.periodicEvents;
|
|
511
538
|
}
|
package/src/initialize.js
CHANGED
|
@@ -26,6 +26,8 @@ const TIMEOUT_SHUTDOWN = 2500;
|
|
|
26
26
|
|
|
27
27
|
const CONFIG_VARS = [
|
|
28
28
|
["configFilePath", null],
|
|
29
|
+
["events", null, "configEvents"],
|
|
30
|
+
["periodicEvents", null, "configPeriodicEvents"],
|
|
29
31
|
["registerAsEventProcessor", true],
|
|
30
32
|
["processEventsAfterPublish", true],
|
|
31
33
|
["isEventQueueActive", true],
|
|
@@ -49,6 +51,8 @@ const CONFIG_VARS = [
|
|
|
49
51
|
*
|
|
50
52
|
* @param {Object} options - The configuration options.
|
|
51
53
|
* @param {string} [options.configFilePath=null] - Path to the configuration file.
|
|
54
|
+
* @param {string} [options.events={}] - Options to allow events in the configuration.
|
|
55
|
+
* @param {string} [options.periodicEvents={}] - Options to allow periodicEvents in the configuration.
|
|
52
56
|
* @param {boolean} [options.registerAsEventProcessor=true] - Register the instance as an event processor.
|
|
53
57
|
* @param {boolean} [options.processEventsAfterPublish=true] - Process events immediately after publishing.
|
|
54
58
|
* @param {boolean} [options.isEventQueueActive=true] - Flag to activate/deactivate the event queue.
|
|
@@ -97,7 +101,8 @@ const initialize = async (options = {}) => {
|
|
|
97
101
|
throw EventQueueError.redisConnectionFailure();
|
|
98
102
|
}
|
|
99
103
|
}
|
|
100
|
-
|
|
104
|
+
const fileContent = await readConfigFromFile(config.configFilePath);
|
|
105
|
+
config.mixFileContentWithEnv(fileContent);
|
|
101
106
|
|
|
102
107
|
monkeyPatchCAPOutbox();
|
|
103
108
|
registerCdsShutdown();
|
|
@@ -178,9 +183,9 @@ const monkeyPatchCAPOutbox = () => {
|
|
|
178
183
|
};
|
|
179
184
|
|
|
180
185
|
const mixConfigVarsWithEnv = (options) => {
|
|
181
|
-
CONFIG_VARS.forEach(([configName, defaultValue]) => {
|
|
186
|
+
CONFIG_VARS.forEach(([configName, defaultValue, mappingName]) => {
|
|
182
187
|
const configValue = options[configName];
|
|
183
|
-
config[configName] = configValue ?? cds.env.eventQueue?.[configName] ?? defaultValue;
|
|
188
|
+
config[mappingName ?? configName] = configValue ?? cds.env.eventQueue?.[configName] ?? defaultValue;
|
|
184
189
|
});
|
|
185
190
|
};
|
|
186
191
|
|
|
@@ -16,7 +16,6 @@ class EventQueueGenericOutboxHandler extends EventQueueBaseClass {
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
async processEvent(processContext, key, queueEntries, payload) {
|
|
19
|
-
let status = EventProcessingStatus.Done;
|
|
20
19
|
try {
|
|
21
20
|
const service = await cds.connect.to(this.eventSubType);
|
|
22
21
|
const { useEventQueueUser } = this.eventConfig;
|
|
@@ -30,14 +29,37 @@ class EventQueueGenericOutboxHandler extends EventQueueBaseClass {
|
|
|
30
29
|
authInfo: await common.getTokenInfo(processContext.tenant),
|
|
31
30
|
});
|
|
32
31
|
processContext._eventQueue = { processor: this, key, queueEntries, payload };
|
|
33
|
-
await cds.unboxed(service).tx(processContext)[invocationFn](msg);
|
|
32
|
+
const result = await cds.unboxed(service).tx(processContext)[invocationFn](msg);
|
|
33
|
+
return this.#determineResultStatus(result, queueEntries);
|
|
34
34
|
} catch (err) {
|
|
35
|
-
status = EventProcessingStatus.Error;
|
|
36
35
|
this.logger.error("error processing outboxed service call", err, {
|
|
37
36
|
serviceName: this.eventSubType,
|
|
38
37
|
});
|
|
38
|
+
return queueEntries.map((queueEntry) => [queueEntry.ID, EventProcessingStatus.Error]);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
#determineResultStatus(result, queueEntries) {
|
|
43
|
+
const validStatusValues = Object.values(EventProcessingStatus);
|
|
44
|
+
const validStatus = validStatusValues.includes(result);
|
|
45
|
+
if (validStatus) {
|
|
46
|
+
return queueEntries.map((queueEntry) => [queueEntry.ID, result]);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (!Array.isArray(result)) {
|
|
50
|
+
return queueEntries.map((queueEntry) => [queueEntry.ID, EventProcessingStatus.Done]);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const valid = !result.some((entry) => {
|
|
54
|
+
const [, status] = entry;
|
|
55
|
+
return !validStatusValues.includes(status);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
if (valid) {
|
|
59
|
+
return result;
|
|
60
|
+
} else {
|
|
61
|
+
return queueEntries.map((queueEntry) => [queueEntry.ID, EventProcessingStatus.Done]);
|
|
39
62
|
}
|
|
40
|
-
return queueEntries.map((queueEntry) => [queueEntry.ID, status]);
|
|
41
63
|
}
|
|
42
64
|
}
|
|
43
65
|
|
package/src/processEventQueue.js
CHANGED
|
@@ -159,6 +159,7 @@ const processPeriodicEvent = async (context, eventTypeInstance) => {
|
|
|
159
159
|
`eventQueue-periodic-process-${eventTypeInstance.eventType}##${eventTypeInstance.eventSubType}`,
|
|
160
160
|
async (tx) => {
|
|
161
161
|
await trace(eventTypeInstance.context, "process-periodic-event", async () => {
|
|
162
|
+
eventTypeInstance.continuesKeepAlive();
|
|
162
163
|
eventTypeInstance.processEventContext = tx.context;
|
|
163
164
|
eventTypeInstance.setTxForEventProcessing(queueEntry.ID, cds.tx(tx.context));
|
|
164
165
|
try {
|
|
@@ -170,6 +171,7 @@ const processPeriodicEvent = async (context, eventTypeInstance) => {
|
|
|
170
171
|
await tx.rollback();
|
|
171
172
|
return;
|
|
172
173
|
} finally {
|
|
174
|
+
eventTypeInstance.stopKeepAlive();
|
|
173
175
|
eventTypeInstance.endPerformanceTracerPeriodicEvents();
|
|
174
176
|
}
|
|
175
177
|
if (
|
|
@@ -198,6 +200,8 @@ const processPeriodicEvent = async (context, eventTypeInstance) => {
|
|
|
198
200
|
eventType: eventTypeInstance?.eventType,
|
|
199
201
|
eventSubType: eventTypeInstance?.eventSubType,
|
|
200
202
|
});
|
|
203
|
+
} finally {
|
|
204
|
+
await eventTypeInstance.keepAlivePromise;
|
|
201
205
|
}
|
|
202
206
|
};
|
|
203
207
|
|
|
@@ -314,7 +318,7 @@ const _checkEventIsBlocked = async (baseInstance) => {
|
|
|
314
318
|
|
|
315
319
|
const _processEvent = async (eventTypeInstance, processContext, key, queueEntries, payload) => {
|
|
316
320
|
try {
|
|
317
|
-
const eventOutdated = await eventTypeInstance.
|
|
321
|
+
const eventOutdated = await eventTypeInstance.isOutdatedAndKeepAlive(queueEntries);
|
|
318
322
|
if (eventOutdated) {
|
|
319
323
|
// NOTE: return empty status map to comply with the interface
|
|
320
324
|
return {};
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
const cds = require("@sap/cds");
|
|
4
|
-
let otel
|
|
4
|
+
let otel;
|
|
5
5
|
try {
|
|
6
|
-
telemetry = require("@cap-js/telemetry");
|
|
7
6
|
otel = require("@opentelemetry/api");
|
|
8
7
|
} catch {
|
|
9
8
|
// ignore
|
|
@@ -14,12 +13,13 @@ const config = require("../config");
|
|
|
14
13
|
const COMPONENT_NAME = "/shared/openTelemetry";
|
|
15
14
|
|
|
16
15
|
const trace = async (context, label, fn, { attributes = {}, newRootSpan = false } = {}) => {
|
|
17
|
-
|
|
16
|
+
const tracerProvider = otel?.trace.getTracerProvider();
|
|
17
|
+
// Check if a real provider is registered
|
|
18
|
+
if (!config.enableCAPTelemetry || !tracerProvider || tracerProvider === otel.trace.NOOP_TRACER_PROVIDER) {
|
|
18
19
|
return fn();
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
const tracer = otel.trace.getTracer("eventqueue");
|
|
22
|
-
|
|
23
23
|
const span = tracer.startSpan(`eventqueue-${label}`, {
|
|
24
24
|
kind: otel.SpanKind.INTERNAL,
|
|
25
25
|
root: newRootSpan,
|