@cap-js-community/event-queue 1.9.0-beta.2 → 1.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +12 -1
- package/src/EventQueueError.js +30 -0
- package/src/EventQueueProcessorBase.js +2 -1
- package/src/config.js +48 -17
- package/src/initialize.js +9 -4
- package/src/outbox/EventQueueGenericOutboxHandler.js +26 -4
- package/src/periodicEvents.js +8 -2
- package/src/processEventQueue.js +16 -4
- package/src/runner/runner.js +4 -1
- package/src/shared/common.js +0 -1
- package/src/shared/distributedLock.js +11 -1
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.1",
|
|
4
4
|
"description": "An event queue that enables secure transactional processing of asynchronous and periodic events, featuring instant event processing with Redis Pub/Sub and load distribution across all application instances.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"types": "src/index.d.ts",
|
|
@@ -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(
|
|
@@ -929,6 +929,7 @@ class EventQueueProcessorBase {
|
|
|
929
929
|
}
|
|
930
930
|
});
|
|
931
931
|
}
|
|
932
|
+
this.logger.info("keep alive finished!", { numberOfEvents: ids.length });
|
|
932
933
|
});
|
|
933
934
|
}).catch((err) => {
|
|
934
935
|
this.logger.error("keep alive handling failed!", err);
|
|
@@ -971,7 +972,7 @@ class EventQueueProcessorBase {
|
|
|
971
972
|
{ expiryTime: this.#eventConfig.keepAliveMaxInProgressTime }
|
|
972
973
|
);
|
|
973
974
|
if (!lockAcquired) {
|
|
974
|
-
this.logger.error("renewing
|
|
975
|
+
this.logger.error("renewing distributed lock failed!", {
|
|
975
976
|
type: this.#eventType,
|
|
976
977
|
subType: this.#eventSubType,
|
|
977
978
|
});
|
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,18 @@ 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
|
+
get hasConfigEvents() {
|
|
533
|
+
return !!(Object.keys(this.#configEvents ?? {}).length || Object.keys(this.#configPeriodicEvents ?? {}).length);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
set configPeriodicEvents(value) {
|
|
537
|
+
this.#configPeriodicEvents = JSON.parse(JSON.stringify(value));
|
|
538
|
+
}
|
|
539
|
+
|
|
509
540
|
get periodicEvents() {
|
|
510
541
|
return this.#config.periodicEvents;
|
|
511
542
|
}
|
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();
|
|
@@ -129,7 +134,7 @@ const readConfigFromFile = async (configFilepath) => {
|
|
|
129
134
|
"configFilepath with unsupported extension, allowed extensions are .yaml and .json"
|
|
130
135
|
);
|
|
131
136
|
} catch (err) {
|
|
132
|
-
if (config.useAsCAPOutbox) {
|
|
137
|
+
if (!configFilepath && (config.useAsCAPOutbox || config.hasConfigEvents)) {
|
|
133
138
|
return {};
|
|
134
139
|
}
|
|
135
140
|
throw err;
|
|
@@ -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/periodicEvents.js
CHANGED
|
@@ -150,8 +150,14 @@ const _insertPeriodEvents = async (tx, events, now) => {
|
|
|
150
150
|
processChunkedSync(eventsToBeInserted, CHUNK_SIZE_INSERT_PERIODIC_EVENTS, (chunk) => {
|
|
151
151
|
logger.info(`${counter}/${chunks} | inserting chunk of changed or new periodic events`, {
|
|
152
152
|
events: chunk.map(({ type, subType, startAfter }) => {
|
|
153
|
-
const { interval } = eventConfig.getEventConfig(type, subType);
|
|
154
|
-
return {
|
|
153
|
+
const { interval, cron } = eventConfig.getEventConfig(type, subType);
|
|
154
|
+
return {
|
|
155
|
+
type,
|
|
156
|
+
subType,
|
|
157
|
+
...(startAfter && { startAfter }),
|
|
158
|
+
...(interval && { interval }),
|
|
159
|
+
...(cron && { cron }),
|
|
160
|
+
};
|
|
155
161
|
}),
|
|
156
162
|
});
|
|
157
163
|
counter++;
|
package/src/processEventQueue.js
CHANGED
|
@@ -20,7 +20,7 @@ const processEventQueue = async (context, eventType, eventSubType, startTime = n
|
|
|
20
20
|
try {
|
|
21
21
|
let eventTypeInstance;
|
|
22
22
|
const eventConfig = config.getEventConfig(eventType, eventSubType);
|
|
23
|
-
const [err, EventTypeClass] = resilientRequire(eventConfig);
|
|
23
|
+
const [err, EventTypeClass] = await resilientRequire(eventConfig);
|
|
24
24
|
if (!eventConfig || err || !(typeof EventTypeClass.constructor === "function")) {
|
|
25
25
|
cds.log(COMPONENT_NAME).error("No Implementation found in the provided configuration file.", {
|
|
26
26
|
eventType,
|
|
@@ -331,12 +331,24 @@ const _processEvent = async (eventTypeInstance, processContext, key, queueEntrie
|
|
|
331
331
|
}
|
|
332
332
|
};
|
|
333
333
|
|
|
334
|
-
const resilientRequire = (eventConfig) => {
|
|
334
|
+
const resilientRequire = async (eventConfig) => {
|
|
335
335
|
try {
|
|
336
336
|
const path = eventConfig?.impl;
|
|
337
337
|
const internal = eventConfig?.internalEvent;
|
|
338
|
-
const
|
|
339
|
-
|
|
338
|
+
const filePath = pathLib.join(internal ? __dirname : process.cwd(), path);
|
|
339
|
+
const fileExtension = pathLib.extname(filePath);
|
|
340
|
+
switch (fileExtension) {
|
|
341
|
+
case ".js":
|
|
342
|
+
return [null, require(filePath)];
|
|
343
|
+
case ".mjs":
|
|
344
|
+
return [null, (await import(`file://${filePath}`)).default];
|
|
345
|
+
case "":
|
|
346
|
+
try {
|
|
347
|
+
return [null, require(filePath)];
|
|
348
|
+
} catch {
|
|
349
|
+
return [null, (await import(`file://${filePath}`)).default];
|
|
350
|
+
}
|
|
351
|
+
}
|
|
340
352
|
} catch (err) {
|
|
341
353
|
return [err, null];
|
|
342
354
|
}
|
package/src/runner/runner.js
CHANGED
|
@@ -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
|
};
|
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
|
|