@cap-js-community/event-queue 2.0.4 → 2.0.5
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 +1 -1
- package/package.json +9 -14
- package/src/EventQueueProcessorBase.js +25 -5
- package/src/config.js +34 -3
- package/src/initialize.js +1 -0
- package/src/outbox/EventQueueGenericOutboxHandler.js +116 -41
- package/src/outbox/eventQueueAsOutbox.js +30 -7
- package/src/publishEvent.js +8 -5
- package/src/redis/redisPub.js +5 -3
- package/src/shared/cdsHelper.js +1 -1
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": "2.0.
|
|
3
|
+
"version": "2.0.5",
|
|
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",
|
|
@@ -22,8 +22,6 @@
|
|
|
22
22
|
"multi-tenancy"
|
|
23
23
|
],
|
|
24
24
|
"scripts": {
|
|
25
|
-
"start": "PORT=4005 cds-serve",
|
|
26
|
-
"watch": "PORT=4005 cds watch",
|
|
27
25
|
"test:unit": "jest --selectProjects unit",
|
|
28
26
|
"test:integration": "jest --selectProjects integration --runInBand",
|
|
29
27
|
"voter:test:integration": "jest --selectProjects integration",
|
|
@@ -38,16 +36,15 @@
|
|
|
38
36
|
"eslint:ci": "eslint .",
|
|
39
37
|
"prettier": "prettier --write --loglevel error .",
|
|
40
38
|
"prettier:ci": "prettier --check .",
|
|
41
|
-
"prepareRelease": "npm prune --production",
|
|
42
39
|
"docs": "cd docs && bundle exec jekyll serve",
|
|
43
40
|
"docs:install": "cd docs && npx shx rm -rf vendor Gemfile.lock && bundle install",
|
|
44
41
|
"upgrade-lock": "npx shx rm -rf package-lock.json node_modules && npm i --package-lock"
|
|
45
42
|
},
|
|
46
43
|
"engines": {
|
|
47
|
-
"node": ">=
|
|
44
|
+
"node": ">=20"
|
|
48
45
|
},
|
|
49
46
|
"dependencies": {
|
|
50
|
-
"@cap-js-community/common": "^0.
|
|
47
|
+
"@cap-js-community/common": "^0.4.0",
|
|
51
48
|
"@sap/xssec": "^4.12.2",
|
|
52
49
|
"cron-parser": "^5.5.0",
|
|
53
50
|
"verror": "^1.10.1",
|
|
@@ -56,17 +53,16 @@
|
|
|
56
53
|
"devDependencies": {
|
|
57
54
|
"@actions/core": "^2.0.2",
|
|
58
55
|
"@cap-js/cds-test": "^0.4.1",
|
|
59
|
-
"@cap-js/db-service": "^2.8.
|
|
60
|
-
"@cap-js/hana": "^2.
|
|
61
|
-
"@cap-js/sqlite": "^2.1.
|
|
56
|
+
"@cap-js/db-service": "^2.8.2",
|
|
57
|
+
"@cap-js/hana": "^2.6.0",
|
|
58
|
+
"@cap-js/sqlite": "^2.1.3",
|
|
62
59
|
"@opentelemetry/api": "^1.9.0",
|
|
63
|
-
"@sap/cds": "^9.
|
|
64
|
-
"@sap/cds-dk": "^9.
|
|
60
|
+
"@sap/cds": "^9.7.0",
|
|
61
|
+
"@sap/cds-dk": "^9.7.0",
|
|
65
62
|
"eslint": "^8.57.1",
|
|
66
63
|
"eslint-config-prettier": "^10.1.8",
|
|
67
64
|
"eslint-plugin-jest": "^29.12.1",
|
|
68
65
|
"eslint-plugin-node": "^11.1.0",
|
|
69
|
-
"express": "^4.22.1",
|
|
70
66
|
"jest": "^29.7.0",
|
|
71
67
|
"prettier": "^2.8.8"
|
|
72
68
|
},
|
|
@@ -104,8 +100,7 @@
|
|
|
104
100
|
"requires": {
|
|
105
101
|
"xsuaa-eventQueue": {
|
|
106
102
|
"vcap": {
|
|
107
|
-
"label": "xsuaa"
|
|
108
|
-
"plan": "application"
|
|
103
|
+
"label": "xsuaa"
|
|
109
104
|
}
|
|
110
105
|
},
|
|
111
106
|
"redis-eventQueue": {
|
|
@@ -25,7 +25,7 @@ const TRIES_FOR_EXCEEDED_EVENTS = 3;
|
|
|
25
25
|
const EVENT_START_AFTER_HEADROOM = 3 * 1000;
|
|
26
26
|
const SUFFIX_PERIODIC = "_PERIODIC";
|
|
27
27
|
|
|
28
|
-
const ALLOWED_FIELDS_FOR_UPDATE = ["status", "startAfter", "error"];
|
|
28
|
+
const ALLOWED_FIELDS_FOR_UPDATE = ["status", "startAfter", "error", "nextData"];
|
|
29
29
|
|
|
30
30
|
class EventQueueProcessorBase {
|
|
31
31
|
#eventsWithExceededTries = [];
|
|
@@ -43,6 +43,7 @@ class EventQueueProcessorBase {
|
|
|
43
43
|
#currentKeepAlivePromise = Promise.resolve();
|
|
44
44
|
#etagMap;
|
|
45
45
|
#namespace;
|
|
46
|
+
#nextSagaEvents;
|
|
46
47
|
|
|
47
48
|
constructor(context, eventType, eventSubType, config) {
|
|
48
49
|
this.__context = context;
|
|
@@ -407,7 +408,7 @@ class EventQueueProcessorBase {
|
|
|
407
408
|
UPDATE.entity(this.#config.tableNameEventQueue)
|
|
408
409
|
.set({
|
|
409
410
|
status: status,
|
|
410
|
-
...(error && { error: this
|
|
411
|
+
...(error && { error: this._error2String(error) }),
|
|
411
412
|
})
|
|
412
413
|
.where({
|
|
413
414
|
ID: queueEntryIds,
|
|
@@ -468,6 +469,10 @@ class EventQueueProcessorBase {
|
|
|
468
469
|
return result;
|
|
469
470
|
}, {});
|
|
470
471
|
|
|
472
|
+
if (!Object.values(updateData).length) {
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
|
|
471
476
|
for (const { ids, data } of Object.values(updateData)) {
|
|
472
477
|
if (!("status" in data)) {
|
|
473
478
|
// TODO: Can this still happen?
|
|
@@ -493,16 +498,22 @@ class EventQueueProcessorBase {
|
|
|
493
498
|
}
|
|
494
499
|
|
|
495
500
|
if (data.error) {
|
|
496
|
-
data.error = this
|
|
501
|
+
data.error = this._error2String(data.error);
|
|
497
502
|
}
|
|
498
503
|
|
|
499
504
|
if (!data.startAfter && [EventProcessingStatus.Error, EventProcessingStatus.Open].includes(data.status)) {
|
|
500
505
|
data.startAfter = new Date(
|
|
501
506
|
Date.now() +
|
|
502
|
-
(
|
|
507
|
+
(data.status === EventProcessingStatus.Error
|
|
508
|
+
? this.#eventConfig.retryFailedAfter
|
|
509
|
+
: this.#eventConfig.retryOpenAfter)
|
|
503
510
|
);
|
|
504
511
|
}
|
|
505
512
|
|
|
513
|
+
if (data.status === EventProcessingStatus.Open && !("attempts" in data)) {
|
|
514
|
+
data.attempts = { "-=": 1 };
|
|
515
|
+
}
|
|
516
|
+
|
|
506
517
|
if (data.startAfter) {
|
|
507
518
|
this.#eventSchedulerInstance.scheduleEvent(
|
|
508
519
|
this.__context.tenant,
|
|
@@ -522,10 +533,15 @@ class EventQueueProcessorBase {
|
|
|
522
533
|
.where("ID IN", ids)
|
|
523
534
|
);
|
|
524
535
|
}
|
|
536
|
+
|
|
537
|
+
if (this.#nextSagaEvents?.length) {
|
|
538
|
+
await tx.run(INSERT.into(this.#config.tableNameEventQueue).entries(this.#nextSagaEvents));
|
|
539
|
+
this.#nextSagaEvents = [];
|
|
540
|
+
}
|
|
525
541
|
});
|
|
526
542
|
}
|
|
527
543
|
|
|
528
|
-
|
|
544
|
+
_error2String(error) {
|
|
529
545
|
return JSON.stringify(error, (_, value) => this.#errorReplacer(value));
|
|
530
546
|
}
|
|
531
547
|
|
|
@@ -1390,6 +1406,10 @@ class EventQueueProcessorBase {
|
|
|
1390
1406
|
get allowedFieldsEventHandler() {
|
|
1391
1407
|
return ALLOWED_FIELDS_FOR_UPDATE;
|
|
1392
1408
|
}
|
|
1409
|
+
|
|
1410
|
+
set nextSagaEvents(value) {
|
|
1411
|
+
this.#nextSagaEvents = value;
|
|
1412
|
+
}
|
|
1393
1413
|
}
|
|
1394
1414
|
|
|
1395
1415
|
module.exports = EventQueueProcessorBase;
|
package/src/config.js
CHANGED
|
@@ -28,6 +28,8 @@ const DEFAULT_RETRY_AFTER = 5 * 60 * 1000;
|
|
|
28
28
|
const PRIORITIES = Object.values(Priorities);
|
|
29
29
|
const UTC_DEFAULT = false;
|
|
30
30
|
const USE_CRON_TZ_DEFAULT = true;
|
|
31
|
+
const SAGA_SUCCESS = "#succeeded";
|
|
32
|
+
const SAGA_FAILED = "#failed";
|
|
31
33
|
|
|
32
34
|
const BASE_TABLES = {
|
|
33
35
|
EVENT: "sap.eventqueue.Event",
|
|
@@ -62,6 +64,7 @@ const ALLOWED_EVENT_OPTIONS_AD_HOC = [
|
|
|
62
64
|
"checkForNextChunk",
|
|
63
65
|
"retryFailedAfter",
|
|
64
66
|
"propagateHeaders",
|
|
67
|
+
"propagateContextProperties",
|
|
65
68
|
"retryOpenAfter",
|
|
66
69
|
"multiInstanceProcessing",
|
|
67
70
|
"kind",
|
|
@@ -236,7 +239,7 @@ class Config {
|
|
|
236
239
|
|
|
237
240
|
executeUnsubscribeHandlers(tenantId) {
|
|
238
241
|
this.#unsubscribedTenants[tenantId] = true;
|
|
239
|
-
setTimeout(() => delete this.#unsubscribedTenants[tenantId], DELETE_TENANT_BLOCK_AFTER_MS);
|
|
242
|
+
setTimeout(() => delete this.#unsubscribedTenants[tenantId], DELETE_TENANT_BLOCK_AFTER_MS).unref();
|
|
240
243
|
for (const unsubscribeHandler of this.#unsubscribeHandlers) {
|
|
241
244
|
try {
|
|
242
245
|
unsubscribeHandler(tenantId);
|
|
@@ -383,6 +386,20 @@ class Config {
|
|
|
383
386
|
result.adHoc
|
|
384
387
|
);
|
|
385
388
|
result.adHoc[key] = specificEventConfig;
|
|
389
|
+
const sagaSuccessKey = [fnName, SAGA_SUCCESS].join("/");
|
|
390
|
+
if (config.events[sagaSuccessKey]) {
|
|
391
|
+
const [sagaKey, sagaSpecificEventConfig] = this.addCAPOutboxEventSpecificAction(
|
|
392
|
+
srvConfig,
|
|
393
|
+
name,
|
|
394
|
+
fnName,
|
|
395
|
+
result.adHoc
|
|
396
|
+
);
|
|
397
|
+
result.adHoc[sagaKey] = sagaSpecificEventConfig;
|
|
398
|
+
} else {
|
|
399
|
+
const sagaConfig = { ...specificEventConfig };
|
|
400
|
+
sagaConfig.subType = [sagaConfig.subType, SAGA_SUCCESS].join("/");
|
|
401
|
+
result.adHoc[[key, SAGA_SUCCESS].join("/")] = sagaConfig;
|
|
402
|
+
}
|
|
386
403
|
}
|
|
387
404
|
}
|
|
388
405
|
return result;
|
|
@@ -405,11 +422,24 @@ class Config {
|
|
|
405
422
|
getCdsOutboxEventSpecificConfig(serviceName, action) {
|
|
406
423
|
const srv = cds.env.requires[serviceName];
|
|
407
424
|
const config = srv?.outbox ?? srv?.queued;
|
|
408
|
-
|
|
425
|
+
const specificConfig = config?.events?.[action];
|
|
426
|
+
|
|
427
|
+
if (specificConfig) {
|
|
409
428
|
return this.#mixCAPPropertyNamesWithEventQueueNames(config.events[action]);
|
|
410
|
-
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
if (!action) {
|
|
411
432
|
return null;
|
|
412
433
|
}
|
|
434
|
+
|
|
435
|
+
const [withoutSaga, sagaSuffix] = action.split("/");
|
|
436
|
+
if ([SAGA_FAILED, SAGA_SUCCESS].includes(sagaSuffix)) {
|
|
437
|
+
if (config?.events?.[withoutSaga]) {
|
|
438
|
+
return this.#mixCAPPropertyNamesWithEventQueueNames(config.events[withoutSaga]);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
return null;
|
|
413
443
|
}
|
|
414
444
|
|
|
415
445
|
#mapEnvEvents(events) {
|
|
@@ -488,6 +518,7 @@ class Config {
|
|
|
488
518
|
? Object.fromEntries(new Map(event.appInstances.map((a) => [a, true])))
|
|
489
519
|
: null;
|
|
490
520
|
event.propagateHeaders = event.propagateHeaders ?? [];
|
|
521
|
+
event.propagateContextProperties = event.propagateContextProperties ?? [];
|
|
491
522
|
event.retryFailedAfter = event.retryFailedAfter ?? DEFAULT_RETRY_AFTER;
|
|
492
523
|
event.retryOpenAfter = event.retryOpenAfter ?? DEFAULT_RETRY_AFTER;
|
|
493
524
|
}
|
package/src/initialize.js
CHANGED
|
@@ -14,6 +14,8 @@ const EVENT_QUEUE_ACTIONS = {
|
|
|
14
14
|
EXCEEDED: "eventQueueRetriesExceeded",
|
|
15
15
|
CLUSTER: "eventQueueCluster",
|
|
16
16
|
CHECK_AND_ADJUST: "eventQueueCheckAndAdjustPayload",
|
|
17
|
+
SAGA_SUCCESS: "#succeeded",
|
|
18
|
+
SAGA_FAILED: "#failed",
|
|
17
19
|
};
|
|
18
20
|
|
|
19
21
|
class EventQueueGenericOutboxHandler extends EventQueueBaseClass {
|
|
@@ -25,7 +27,7 @@ class EventQueueGenericOutboxHandler extends EventQueueBaseClass {
|
|
|
25
27
|
async getQueueEntriesAndSetToInProgress() {
|
|
26
28
|
const { srvName } = config.normalizeSubType(this.eventType, this.eventSubType);
|
|
27
29
|
this.__srv = await cds.connect.to(srvName);
|
|
28
|
-
this.__srvUnboxed = cds.
|
|
30
|
+
this.__srvUnboxed = cds.unqueued(this.__srv);
|
|
29
31
|
const { handlers, clusterRelevant, specificClusterRelevant } = this.__srvUnboxed.handlers.on.reduce(
|
|
30
32
|
(result, handler) => {
|
|
31
33
|
if (handler.on.startsWith(EVENT_QUEUE_ACTIONS.CLUSTER)) {
|
|
@@ -62,7 +64,7 @@ class EventQueueGenericOutboxHandler extends EventQueueBaseClass {
|
|
|
62
64
|
}
|
|
63
65
|
} else {
|
|
64
66
|
for (const actionName in genericClusterEvents) {
|
|
65
|
-
const
|
|
67
|
+
const req = new cds.Request({
|
|
66
68
|
event: EVENT_QUEUE_ACTIONS.CLUSTER,
|
|
67
69
|
user: this.context.user,
|
|
68
70
|
eventQueue: {
|
|
@@ -75,14 +77,14 @@ class EventQueueGenericOutboxHandler extends EventQueueBaseClass {
|
|
|
75
77
|
this.#clusterByDataProperty(actionName, genericClusterEvents[actionName], propertyName, cb),
|
|
76
78
|
},
|
|
77
79
|
});
|
|
78
|
-
const clusterResult = await this.__srvUnboxed.tx(this.context).send(
|
|
80
|
+
const clusterResult = await this.__srvUnboxed.tx(this.context).send(req);
|
|
79
81
|
if (this.#validateCluster(clusterResult)) {
|
|
80
82
|
Object.assign(clusterMap, clusterResult);
|
|
81
83
|
} else {
|
|
82
84
|
this.logger.error(
|
|
83
85
|
"cluster result of handler is not valid. Check the documentation for the expected structure. Continuing without clustering!",
|
|
84
86
|
{
|
|
85
|
-
handler:
|
|
87
|
+
handler: req.event,
|
|
86
88
|
clusterResult: JSON.stringify(clusterResult),
|
|
87
89
|
}
|
|
88
90
|
);
|
|
@@ -93,7 +95,7 @@ class EventQueueGenericOutboxHandler extends EventQueueBaseClass {
|
|
|
93
95
|
}
|
|
94
96
|
|
|
95
97
|
for (const actionName in specificClusterEvents) {
|
|
96
|
-
const
|
|
98
|
+
const req = new cds.Request({
|
|
97
99
|
event: `${EVENT_QUEUE_ACTIONS.CLUSTER}.${actionName}`,
|
|
98
100
|
user: this.context.user,
|
|
99
101
|
eventQueue: {
|
|
@@ -106,14 +108,14 @@ class EventQueueGenericOutboxHandler extends EventQueueBaseClass {
|
|
|
106
108
|
this.#clusterByDataProperty(actionName, specificClusterEvents[actionName], propertyName, cb),
|
|
107
109
|
},
|
|
108
110
|
});
|
|
109
|
-
const clusterResult = await this.__srvUnboxed.tx(this.context).send(
|
|
111
|
+
const clusterResult = await this.__srvUnboxed.tx(this.context).send(req);
|
|
110
112
|
if (this.#validateCluster(clusterResult)) {
|
|
111
113
|
Object.assign(clusterMap, clusterResult);
|
|
112
114
|
} else {
|
|
113
115
|
this.logger.error(
|
|
114
116
|
"cluster result of handler is not valid. Check the documentation for the expected structure. Continuing without clustering!",
|
|
115
117
|
{
|
|
116
|
-
handler:
|
|
118
|
+
handler: req.event,
|
|
117
119
|
clusterResult: JSON.stringify(clusterResult),
|
|
118
120
|
}
|
|
119
121
|
);
|
|
@@ -260,17 +262,17 @@ class EventQueueGenericOutboxHandler extends EventQueueBaseClass {
|
|
|
260
262
|
async checkEventAndGeneratePayload(queueEntry) {
|
|
261
263
|
const payload = await super.checkEventAndGeneratePayload(queueEntry);
|
|
262
264
|
const { event } = payload;
|
|
263
|
-
const handlerName = this.#checkHandlerExists(EVENT_QUEUE_ACTIONS.CHECK_AND_ADJUST, event);
|
|
265
|
+
const handlerName = this.#checkHandlerExists({ eventQueueFn: EVENT_QUEUE_ACTIONS.CHECK_AND_ADJUST, event });
|
|
264
266
|
if (!handlerName) {
|
|
265
267
|
return payload;
|
|
266
268
|
}
|
|
267
269
|
|
|
268
|
-
const {
|
|
270
|
+
const { req, userId } = this.#buildDispatchData(payload, {
|
|
269
271
|
queueEntries: [queueEntry],
|
|
270
272
|
});
|
|
271
|
-
|
|
272
|
-
await this.#setContextUser(this.context, userId,
|
|
273
|
-
const data = await this.__srvUnboxed.tx(this.context).send(
|
|
273
|
+
req.event = handlerName;
|
|
274
|
+
await this.#setContextUser(this.context, userId, req);
|
|
275
|
+
const data = await this.__srvUnboxed.tx(this.context).send(req);
|
|
274
276
|
if (data) {
|
|
275
277
|
payload.data = data;
|
|
276
278
|
return payload;
|
|
@@ -281,17 +283,17 @@ class EventQueueGenericOutboxHandler extends EventQueueBaseClass {
|
|
|
281
283
|
|
|
282
284
|
async hookForExceededEvents(exceededEvent) {
|
|
283
285
|
const { event } = exceededEvent.payload;
|
|
284
|
-
const handlerName = this.#checkHandlerExists(EVENT_QUEUE_ACTIONS.EXCEEDED, event);
|
|
286
|
+
const handlerName = this.#checkHandlerExists({ eventQueueFn: EVENT_QUEUE_ACTIONS.EXCEEDED, event });
|
|
285
287
|
if (!handlerName) {
|
|
286
288
|
return await super.hookForExceededEvents(exceededEvent);
|
|
287
289
|
}
|
|
288
290
|
|
|
289
|
-
const {
|
|
291
|
+
const { req, userId } = this.#buildDispatchData(exceededEvent.payload, {
|
|
290
292
|
queueEntries: [exceededEvent],
|
|
291
293
|
});
|
|
292
|
-
await this.#setContextUser(this.context, userId,
|
|
293
|
-
|
|
294
|
-
await this.__srvUnboxed.tx(this.context).send(
|
|
294
|
+
await this.#setContextUser(this.context, userId, req);
|
|
295
|
+
req.event = handlerName;
|
|
296
|
+
await this.__srvUnboxed.tx(this.context).send(req);
|
|
295
297
|
}
|
|
296
298
|
|
|
297
299
|
// NOTE: Currently not exposed to CAP service; we wait for a valid use case
|
|
@@ -299,57 +301,78 @@ class EventQueueGenericOutboxHandler extends EventQueueBaseClass {
|
|
|
299
301
|
return await super.beforeProcessingEvents();
|
|
300
302
|
}
|
|
301
303
|
|
|
302
|
-
#checkHandlerExists(eventQueueFn, event) {
|
|
303
|
-
|
|
304
|
+
#checkHandlerExists({ eventQueueFn, event, saga } = {}) {
|
|
305
|
+
if (eventQueueFn) {
|
|
306
|
+
const specificHandler = this.__onHandlers[[eventQueueFn, event].join(".")];
|
|
307
|
+
if (specificHandler) {
|
|
308
|
+
return specificHandler;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const genericHandler = this.__onHandlers[eventQueueFn];
|
|
312
|
+
return genericHandler ?? null;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (event.endsWith(EVENT_QUEUE_ACTIONS.SAGA_SUCCESS)) {
|
|
316
|
+
[event] = event.split("/");
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const specificHandler = this.__onHandlers[[event, saga].join("/")];
|
|
304
320
|
if (specificHandler) {
|
|
305
321
|
return specificHandler;
|
|
306
322
|
}
|
|
307
323
|
|
|
308
|
-
|
|
309
|
-
return genericHandler ?? null;
|
|
324
|
+
return this.__onHandlers[saga];
|
|
310
325
|
}
|
|
311
326
|
|
|
312
327
|
async processPeriodicEvent(processContext, key, queueEntry) {
|
|
313
328
|
const { actionName } = config.normalizeSubType(this.eventType, this.eventSubType);
|
|
314
|
-
const
|
|
315
|
-
await this.#setContextUser(processContext, config.userId,
|
|
316
|
-
await this.__srvUnboxed.tx(processContext).emit(
|
|
329
|
+
const req = new cds.Event({ event: actionName, eventQueue: { processor: this, key, queueEntries: [queueEntry] } });
|
|
330
|
+
await this.#setContextUser(processContext, config.userId, req);
|
|
331
|
+
await this.__srvUnboxed.tx(processContext).emit(req);
|
|
317
332
|
}
|
|
318
333
|
|
|
319
|
-
#buildDispatchData(
|
|
334
|
+
#buildDispatchData(payload, { key, queueEntries } = {}) {
|
|
320
335
|
const { useEventQueueUser } = this.eventConfig;
|
|
321
336
|
const userId = useEventQueueUser ? config.userId : payload.contextUser;
|
|
322
|
-
const
|
|
337
|
+
const req = payload._fromSend ? new cds.Request(payload) : new cds.Event(payload);
|
|
323
338
|
const invocationFn = payload._fromSend ? "send" : "emit";
|
|
324
|
-
delete
|
|
325
|
-
delete
|
|
326
|
-
|
|
327
|
-
|
|
339
|
+
delete req._fromSend;
|
|
340
|
+
delete req.contextUser;
|
|
341
|
+
req.eventQueue = { processor: this, key, queueEntries, payload };
|
|
342
|
+
|
|
343
|
+
if (this.eventConfig.propagateContextProperties?.length && this.transactionMode === "isolated" && cds.context) {
|
|
344
|
+
for (const prop of this.eventConfig.propagateContextProperties) {
|
|
345
|
+
req[prop] && (cds.context[prop] = req[prop]);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
return { req, userId, invocationFn };
|
|
328
350
|
}
|
|
329
351
|
|
|
330
|
-
async #setContextUser(context, userId,
|
|
352
|
+
async #setContextUser(context, userId, req) {
|
|
331
353
|
const authInfo = await common.getAuthContext(context.tenant);
|
|
332
354
|
context.user = new cds.User.Privileged({
|
|
333
355
|
id: userId,
|
|
334
356
|
authInfo,
|
|
335
357
|
tokenInfo: authInfo?.token,
|
|
336
358
|
});
|
|
337
|
-
if (
|
|
338
|
-
|
|
359
|
+
if (req) {
|
|
360
|
+
req.user = context.user;
|
|
339
361
|
}
|
|
340
362
|
}
|
|
341
363
|
|
|
342
364
|
async processEvent(processContext, key, queueEntries, payload) {
|
|
365
|
+
let statusTuple;
|
|
366
|
+
const { userId, invocationFn, req } = this.#buildDispatchData(payload, { key, queueEntries });
|
|
343
367
|
try {
|
|
344
|
-
|
|
345
|
-
await this
|
|
346
|
-
|
|
347
|
-
return this.#determineResultStatus(result, queueEntries);
|
|
368
|
+
await this.#setContextUser(processContext, userId, req);
|
|
369
|
+
const result = await this.__srvUnboxed.tx(processContext)[invocationFn](req);
|
|
370
|
+
statusTuple = this.#determineResultStatus(result, queueEntries);
|
|
348
371
|
} catch (err) {
|
|
349
372
|
this.logger.error("error processing outboxed service call", err, {
|
|
350
373
|
serviceName: this.eventSubType,
|
|
351
374
|
});
|
|
352
|
-
|
|
375
|
+
statusTuple = queueEntries.map((queueEntry) => [
|
|
353
376
|
queueEntry.ID,
|
|
354
377
|
{
|
|
355
378
|
status: EventProcessingStatus.Error,
|
|
@@ -357,13 +380,65 @@ class EventQueueGenericOutboxHandler extends EventQueueBaseClass {
|
|
|
357
380
|
},
|
|
358
381
|
]);
|
|
359
382
|
}
|
|
383
|
+
|
|
384
|
+
await this.#publishFollowupEvents(processContext, req, statusTuple);
|
|
385
|
+
return statusTuple;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
async #publishFollowupEvents(processContext, req, statusTuple) {
|
|
389
|
+
const succeeded = this.#checkHandlerExists({ event: req.event, saga: EVENT_QUEUE_ACTIONS.SAGA_SUCCESS });
|
|
390
|
+
const failed = this.#checkHandlerExists({ event: req.event, saga: EVENT_QUEUE_ACTIONS.SAGA_FAILED });
|
|
391
|
+
|
|
392
|
+
if (!succeeded && !failed) {
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (req.event.endsWith(EVENT_QUEUE_ACTIONS.SAGA_FAILED)) {
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// NOTE: required for #failed because tx is rolledback and new events would not be commmited!
|
|
401
|
+
const tx = cds.tx(processContext);
|
|
402
|
+
const nextEvents = tx._eventQueue?.events;
|
|
403
|
+
|
|
404
|
+
if (nextEvents?.length) {
|
|
405
|
+
tx._eventQueue.events = [];
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
for (const [, result] of statusTuple) {
|
|
409
|
+
const data = result.nextData ?? req.data;
|
|
410
|
+
if (
|
|
411
|
+
succeeded &&
|
|
412
|
+
result.status === EventProcessingStatus.Done &&
|
|
413
|
+
!req.event.endsWith(EVENT_QUEUE_ACTIONS.SAGA_SUCCESS)
|
|
414
|
+
) {
|
|
415
|
+
await this.__srv.tx(processContext).send(succeeded, data);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
if (failed && result.status === EventProcessingStatus.Error) {
|
|
419
|
+
result.error && (data.error = this._error2String(result.error));
|
|
420
|
+
await this.__srv.tx(processContext).send(failed, data);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
delete result.nextData;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
if (config.insertEventsBeforeCommit) {
|
|
427
|
+
this.nextSagaEvents = tx._eventQueue?.events;
|
|
428
|
+
} else {
|
|
429
|
+
this.nextSagaEvents = tx._eventQueue?.events.filter((event) => JSON.parse(event.payload).event === failed);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
if (tx._eventQueue) {
|
|
433
|
+
tx._eventQueue.events = nextEvents ?? [];
|
|
434
|
+
}
|
|
360
435
|
}
|
|
361
436
|
|
|
362
437
|
#determineResultStatus(result, queueEntries) {
|
|
363
438
|
const validStatusValues = Object.values(EventProcessingStatus);
|
|
364
439
|
const validStatus = validStatusValues.includes(result);
|
|
365
440
|
if (validStatus) {
|
|
366
|
-
return queueEntries.map((queueEntry) => [queueEntry.ID, result]);
|
|
441
|
+
return queueEntries.map((queueEntry) => [queueEntry.ID, { status: result }]);
|
|
367
442
|
}
|
|
368
443
|
|
|
369
444
|
if (result instanceof Object && !Array.isArray(result)) {
|
|
@@ -375,7 +450,7 @@ class EventQueueGenericOutboxHandler extends EventQueueBaseClass {
|
|
|
375
450
|
}
|
|
376
451
|
|
|
377
452
|
if (!Array.isArray(result)) {
|
|
378
|
-
return queueEntries.map((queueEntry) => [queueEntry.ID, EventProcessingStatus.Done]);
|
|
453
|
+
return queueEntries.map((queueEntry) => [queueEntry.ID, { status: EventProcessingStatus.Done }]);
|
|
379
454
|
}
|
|
380
455
|
|
|
381
456
|
const [firstEntry] = result;
|
|
@@ -430,7 +505,7 @@ class EventQueueGenericOutboxHandler extends EventQueueBaseClass {
|
|
|
430
505
|
if (valid) {
|
|
431
506
|
return result;
|
|
432
507
|
} else {
|
|
433
|
-
return queueEntries.map((queueEntry) => [queueEntry.ID, EventProcessingStatus.Done]);
|
|
508
|
+
return queueEntries.map((queueEntry) => [queueEntry.ID, { status: EventProcessingStatus.Done }]);
|
|
434
509
|
}
|
|
435
510
|
}
|
|
436
511
|
}
|
|
@@ -11,6 +11,8 @@ const CDS_EVENT_TYPE = "CAP_OUTBOX";
|
|
|
11
11
|
const COMPONENT_NAME = "/eventQueue/eventQueueAsOutbox";
|
|
12
12
|
const EVENT_QUEUE_SPECIFIC_FIELDS = ["startAfter", "referenceEntity", "referenceEntityKey", "namespace"];
|
|
13
13
|
|
|
14
|
+
const TO_COPY = ["inbound", "event", "data", "queue", "results", "method", "path", "params", "entity", "service"];
|
|
15
|
+
|
|
14
16
|
function outboxed(srv, customOpts) {
|
|
15
17
|
if (!(new.target || customOpts)) {
|
|
16
18
|
const former = srv[OUTBOXED];
|
|
@@ -43,8 +45,9 @@ function outboxed(srv, customOpts) {
|
|
|
43
45
|
|
|
44
46
|
const outboxOpts = config.getEventConfig(CDS_EVENT_TYPE, subType, srvConfig.namespace);
|
|
45
47
|
const eventHeaders = getPropagatedHeaders(outboxOpts, req);
|
|
48
|
+
const contextProperties = getPropagateContextProperties(outboxOpts, req);
|
|
46
49
|
if (["persistent-outbox", "persistent-queue"].includes(outboxOpts.kind)) {
|
|
47
|
-
await _mapToEventAndPublish(
|
|
50
|
+
await _mapToEventAndPublish(req, srvConfig.namespace, subType, eventHeaders, contextProperties);
|
|
48
51
|
return;
|
|
49
52
|
}
|
|
50
53
|
context.on("succeeded", async () => {
|
|
@@ -80,7 +83,17 @@ const getPropagatedHeaders = (config, req) => {
|
|
|
80
83
|
return Object.assign(propagateHeaders, req.headers);
|
|
81
84
|
};
|
|
82
85
|
|
|
83
|
-
const
|
|
86
|
+
const getPropagateContextProperties = (config, req) => {
|
|
87
|
+
return config.propagateContextProperties.reduce((properties, name) => {
|
|
88
|
+
if (name in req.tx.context) {
|
|
89
|
+
properties[name] = req.tx.context[name];
|
|
90
|
+
}
|
|
91
|
+
return properties;
|
|
92
|
+
}, {});
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const _mapToEventAndPublish = async (req, namespace, subType, eventHeaders, contextProperties) => {
|
|
96
|
+
const context = req.context || cds.context;
|
|
84
97
|
const eventQueueSpecificValues = {};
|
|
85
98
|
for (const header in req.headers ?? {}) {
|
|
86
99
|
for (const field of EVENT_QUEUE_SPECIFIC_FIELDS) {
|
|
@@ -91,17 +104,27 @@ const _mapToEventAndPublish = async (context, subType, req, eventHeaders, namesp
|
|
|
91
104
|
}
|
|
92
105
|
}
|
|
93
106
|
}
|
|
94
|
-
|
|
95
107
|
const event = {
|
|
96
108
|
contextUser: context.user.id,
|
|
97
109
|
...(req._fromSend || (req.reply && { _fromSend: true })), // send or emit
|
|
98
|
-
...(req.inbound && { inbound: req.inbound }),
|
|
99
|
-
...(req.event && { event: req.event }),
|
|
100
|
-
...(req.data && { data: req.data }),
|
|
101
110
|
...(eventHeaders && { headers: eventHeaders }),
|
|
102
|
-
...(
|
|
111
|
+
...(Object.keys(contextProperties).length && { ...contextProperties }),
|
|
103
112
|
};
|
|
104
113
|
|
|
114
|
+
for (const prop of TO_COPY) {
|
|
115
|
+
if (req[prop]) {
|
|
116
|
+
event[prop] = req[prop];
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (req.query) {
|
|
121
|
+
event.query = typeof req.query.flat === "function" ? req.query.flat() : req.query;
|
|
122
|
+
delete event.query._target;
|
|
123
|
+
delete event.query.__target;
|
|
124
|
+
delete event.query.target;
|
|
125
|
+
delete event.data; // `req.data` should be a getter to whatever is in `req.query`
|
|
126
|
+
}
|
|
127
|
+
|
|
105
128
|
await publishEvent(
|
|
106
129
|
cds.tx(context),
|
|
107
130
|
{
|
package/src/publishEvent.js
CHANGED
|
@@ -76,8 +76,9 @@ const publishEvent = async (
|
|
|
76
76
|
event.namespace = config.namespace;
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
|
+
_addEventsToContext(tx, events);
|
|
79
80
|
if (config.insertEventsBeforeCommit && !skipInsertEventsBeforeCommit) {
|
|
80
|
-
|
|
81
|
+
_registerHandler(tx, skipBroadcast);
|
|
81
82
|
} else {
|
|
82
83
|
let result;
|
|
83
84
|
tx._skipEventQueueBroadcast = skipBroadcast;
|
|
@@ -87,10 +88,7 @@ const publishEvent = async (
|
|
|
87
88
|
}
|
|
88
89
|
};
|
|
89
90
|
|
|
90
|
-
const
|
|
91
|
-
tx._eventQueue ??= { events: [], handlerRegistered: false };
|
|
92
|
-
tx._eventQueue.events = tx._eventQueue.events.concat(events);
|
|
93
|
-
|
|
91
|
+
const _registerHandler = (tx, skipBroadcast) => {
|
|
94
92
|
if (tx._eventQueue.handlerRegistered) {
|
|
95
93
|
return;
|
|
96
94
|
}
|
|
@@ -106,6 +104,11 @@ const _registerHandlerAndAddEvents = (tx, events, skipBroadcast) => {
|
|
|
106
104
|
});
|
|
107
105
|
};
|
|
108
106
|
|
|
107
|
+
const _addEventsToContext = (tx, events) => {
|
|
108
|
+
tx._eventQueue ??= { events: [], handlerRegistered: false };
|
|
109
|
+
tx._eventQueue.events = tx._eventQueue.events.concat(events);
|
|
110
|
+
};
|
|
111
|
+
|
|
109
112
|
module.exports = {
|
|
110
113
|
publishEvent,
|
|
111
114
|
};
|
package/src/redis/redisPub.js
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
-
const { promisify } = require("util");
|
|
4
|
-
|
|
5
3
|
const cds = require("@sap/cds");
|
|
6
4
|
|
|
7
5
|
const redis = require("../shared/redis");
|
|
@@ -17,7 +15,11 @@ const COMPONENT_NAME = "/eventQueue/redisPub";
|
|
|
17
15
|
const TRIES_FOR_PUBLISH_PERIODIC_EVENT = 10;
|
|
18
16
|
const SLEEP_TIME_FOR_PUBLISH_PERIODIC_EVENT = 30 * 1000;
|
|
19
17
|
|
|
20
|
-
const wait =
|
|
18
|
+
const wait = async (ms) => {
|
|
19
|
+
return new Promise((resolve) => {
|
|
20
|
+
setTimeout(resolve, ms).unref();
|
|
21
|
+
});
|
|
22
|
+
};
|
|
21
23
|
|
|
22
24
|
/**
|
|
23
25
|
* Broadcasts events to the event queue, either locally or through Redis.
|
package/src/shared/cdsHelper.js
CHANGED
|
@@ -130,7 +130,7 @@ const _getAllTenantBase = async () => {
|
|
|
130
130
|
if (cds.services["cds.xt.SaasProvisioningService"] || cds.services["saas-registry"]) {
|
|
131
131
|
break;
|
|
132
132
|
}
|
|
133
|
-
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
133
|
+
await new Promise((resolve) => setTimeout(resolve, 1000).unref());
|
|
134
134
|
}
|
|
135
135
|
|
|
136
136
|
const ssp = await cds.connect.to("cds.xt.SaasProvisioningService");
|