@cap-js-community/event-queue 1.11.0-beta.2 → 1.11.0-beta.4
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/Lock.cds +0 -1
- package/package.json +8 -8
- package/src/EventQueueProcessorBase.js +1 -1
- package/src/config.js +20 -11
- package/src/constants.js +1 -1
- package/src/index.d.ts +3 -3
- package/src/initialize.js +1 -0
- package/src/outbox/EventQueueGenericOutboxHandler.js +31 -29
- package/src/outbox/eventQueueAsOutbox.js +1 -1
- package/src/periodicEvents.js +13 -2
- package/src/processEventQueue.js +5 -2
- package/src/redis/redisPub.js +2 -1
- package/src/redis/redisSub.js +2 -1
- package/src/runner/openEvents.js +13 -20
- package/src/runner/runner.js +39 -24
- package/src/shared/cdsHelper.js +41 -15
- package/src/shared/common.js +29 -30
- package/src/shared/distributedLock.js +26 -27
- package/src/shared/lazyCache.js +148 -0
- package/src/shared/redis.js +10 -0
- package/srv/service/admin-service.cds +19 -9
- package/srv/service/admin-service.js +16 -2
package/db/Lock.cds
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cap-js-community/event-queue",
|
|
3
|
-
"version": "1.11.0-beta.
|
|
3
|
+
"version": "1.11.0-beta.4",
|
|
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",
|
|
@@ -48,23 +48,23 @@
|
|
|
48
48
|
},
|
|
49
49
|
"dependencies": {
|
|
50
50
|
"@sap/xssec": "^4.6.0",
|
|
51
|
-
"cron-parser": "^5.
|
|
51
|
+
"cron-parser": "^5.3.1",
|
|
52
52
|
"redis": "^4.7.0",
|
|
53
53
|
"verror": "^1.10.1",
|
|
54
54
|
"yaml": "^2.7.1"
|
|
55
55
|
},
|
|
56
56
|
"devDependencies": {
|
|
57
|
-
"@cap-js/cds-test": "^0.
|
|
58
|
-
"@cap-js/hana": "^
|
|
59
|
-
"@cap-js/sqlite": "^
|
|
60
|
-
"@sap/cds": "^
|
|
61
|
-
"@sap/cds-dk": "^
|
|
57
|
+
"@cap-js/cds-test": "^0.4.0",
|
|
58
|
+
"@cap-js/hana": "^2.2.0",
|
|
59
|
+
"@cap-js/sqlite": "^2.0.1",
|
|
60
|
+
"@sap/cds": "^9.3.1",
|
|
61
|
+
"@sap/cds-dk": "^9.3.1",
|
|
62
62
|
"eslint": "^8.57.0",
|
|
63
63
|
"eslint-config-prettier": "^9.1.0",
|
|
64
64
|
"eslint-plugin-jest": "^28.6.0",
|
|
65
65
|
"eslint-plugin-node": "^11.1.0",
|
|
66
66
|
"express": "^4.21.2",
|
|
67
|
-
"hdb": "^
|
|
67
|
+
"hdb": "^2.25.1",
|
|
68
68
|
"jest": "^29.7.0",
|
|
69
69
|
"prettier": "^2.8.8",
|
|
70
70
|
"sqlite3": "^5.1.7",
|
|
@@ -1016,7 +1016,7 @@ class EventQueueProcessorBase {
|
|
|
1016
1016
|
|
|
1017
1017
|
// NOTE: do not pass current date as we always want to calc. a future date
|
|
1018
1018
|
const cronExpression = CronExpressionParser.parse(this.#eventConfig.cron, {
|
|
1019
|
-
tz: eventConfig.tz,
|
|
1019
|
+
tz: this.#eventConfig.tz,
|
|
1020
1020
|
});
|
|
1021
1021
|
return cronExpression.next();
|
|
1022
1022
|
}
|
package/src/config.js
CHANGED
|
@@ -28,6 +28,7 @@ const DEFAULT_CHECK_FOR_NEXT_CHUNK = true;
|
|
|
28
28
|
const SUFFIX_PERIODIC = "_PERIODIC";
|
|
29
29
|
const CAP_EVENT_TYPE = "CAP_OUTBOX";
|
|
30
30
|
const CAP_PARALLEL_DEFAULT = 5;
|
|
31
|
+
const CAP_MAX_ATTEMPTS_DEFAULT = 5;
|
|
31
32
|
const DELETE_TENANT_BLOCK_AFTER_MS = 5 * 60 * 1000;
|
|
32
33
|
const PRIORITIES = Object.values(Priorities);
|
|
33
34
|
const UTC_DEFAULT = false;
|
|
@@ -113,11 +114,12 @@ class Config {
|
|
|
113
114
|
#redisNamespace;
|
|
114
115
|
#publishEventBlockList;
|
|
115
116
|
#crashOnRedisUnavailable;
|
|
116
|
-
#
|
|
117
|
+
#tenantIdFilterAuthContextCb;
|
|
117
118
|
#tenantIdFilterEventProcessingCb;
|
|
118
119
|
#configEvents;
|
|
119
120
|
#configPeriodicEvents;
|
|
120
121
|
#enableAdminService;
|
|
122
|
+
#disableProcessingOfSuspendedTenants;
|
|
121
123
|
static #instance;
|
|
122
124
|
constructor() {
|
|
123
125
|
this.#logger = cds.log(COMPONENT_NAME);
|
|
@@ -195,9 +197,7 @@ class Config {
|
|
|
195
197
|
result = config.value.test(this.#env.applicationName);
|
|
196
198
|
} else {
|
|
197
199
|
const shouldBeProcessedBasedOnAppName = appNameConfig[this.#env.applicationName];
|
|
198
|
-
|
|
199
|
-
result = config.value === this.#env.applicationName;
|
|
200
|
-
}
|
|
200
|
+
result = !!shouldBeProcessedBasedOnAppName;
|
|
201
201
|
}
|
|
202
202
|
if (result) {
|
|
203
203
|
break;
|
|
@@ -387,7 +387,7 @@ class Config {
|
|
|
387
387
|
kind: config.kind ?? "persistent-outbox",
|
|
388
388
|
selectMaxChunkSize: config.selectMaxChunkSize ?? config.chunkSize,
|
|
389
389
|
parallelEventProcessing: config.parallelEventProcessing ?? (config.parallel && CAP_PARALLEL_DEFAULT),
|
|
390
|
-
retryAttempts: config.retryAttempts ?? config.maxAttempts,
|
|
390
|
+
retryAttempts: config.retryAttempts ?? config.maxAttempts ?? CAP_MAX_ATTEMPTS_DEFAULT,
|
|
391
391
|
...config,
|
|
392
392
|
});
|
|
393
393
|
eventConfig.internalEvent = true;
|
|
@@ -483,10 +483,11 @@ class Config {
|
|
|
483
483
|
delete base.interval;
|
|
484
484
|
}
|
|
485
485
|
|
|
486
|
-
|
|
486
|
+
const subType = `${name}.${fnName}`;
|
|
487
|
+
result[subType] = Object.assign(
|
|
487
488
|
{
|
|
488
489
|
type: CAP_EVENT_TYPE,
|
|
489
|
-
subType
|
|
490
|
+
subType,
|
|
490
491
|
impl: "./outbox/EventQueueGenericOutboxHandler",
|
|
491
492
|
internalEvent: true,
|
|
492
493
|
},
|
|
@@ -770,12 +771,12 @@ class Config {
|
|
|
770
771
|
this.#crashOnRedisUnavailable = value;
|
|
771
772
|
}
|
|
772
773
|
|
|
773
|
-
get
|
|
774
|
-
return this.#
|
|
774
|
+
get tenantIdFilterAuthContext() {
|
|
775
|
+
return this.#tenantIdFilterAuthContextCb;
|
|
775
776
|
}
|
|
776
777
|
|
|
777
|
-
set
|
|
778
|
-
this.#
|
|
778
|
+
set tenantIdFilterAuthContext(value) {
|
|
779
|
+
this.#tenantIdFilterAuthContextCb = value;
|
|
779
780
|
}
|
|
780
781
|
|
|
781
782
|
get tenantIdFilterEventProcessing() {
|
|
@@ -984,6 +985,14 @@ class Config {
|
|
|
984
985
|
this.#enableAdminService = value;
|
|
985
986
|
}
|
|
986
987
|
|
|
988
|
+
get disableProcessingOfSuspendedTenants() {
|
|
989
|
+
return this.#disableProcessingOfSuspendedTenants;
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
set disableProcessingOfSuspendedTenants(value) {
|
|
993
|
+
this.#disableProcessingOfSuspendedTenants = value;
|
|
994
|
+
}
|
|
995
|
+
|
|
987
996
|
/**
|
|
988
997
|
@return { Config }
|
|
989
998
|
**/
|
package/src/constants.js
CHANGED
package/src/index.d.ts
CHANGED
|
@@ -14,7 +14,7 @@ export declare type EventProcessingStatusType = (typeof EventProcessingStatus)[E
|
|
|
14
14
|
|
|
15
15
|
export declare const TenantIdCheckTypes: {
|
|
16
16
|
eventProcessing: "eventProcessing";
|
|
17
|
-
|
|
17
|
+
getAuthContext: "getAuthContext";
|
|
18
18
|
};
|
|
19
19
|
|
|
20
20
|
export declare const TransactionMode: {
|
|
@@ -215,8 +215,8 @@ declare class Config {
|
|
|
215
215
|
get publishEventBlockList(): any;
|
|
216
216
|
set crashOnRedisUnavailable(value: any);
|
|
217
217
|
get crashOnRedisUnavailable(): any;
|
|
218
|
-
set
|
|
219
|
-
get
|
|
218
|
+
set tenantIdFilterAuthContext(value: any);
|
|
219
|
+
get tenantIdFilterAuthContext(): any;
|
|
220
220
|
set tenantIdFilterEventProcessing(value: any);
|
|
221
221
|
get tenantIdFilterEventProcessing(): any;
|
|
222
222
|
set runInterval(value: any);
|
package/src/initialize.js
CHANGED
|
@@ -61,7 +61,7 @@ class EventQueueGenericOutboxHandler extends EventQueueBaseClass {
|
|
|
61
61
|
}
|
|
62
62
|
} else {
|
|
63
63
|
for (const actionName in genericClusterEvents) {
|
|
64
|
-
const
|
|
64
|
+
const reg = new cds.Request({
|
|
65
65
|
event: EVENT_QUEUE_ACTIONS.CLUSTER,
|
|
66
66
|
user: this.context.user,
|
|
67
67
|
eventQueue: {
|
|
@@ -74,14 +74,14 @@ class EventQueueGenericOutboxHandler extends EventQueueBaseClass {
|
|
|
74
74
|
this.#clusterByDataProperty(actionName, genericClusterEvents[actionName], propertyName, cb),
|
|
75
75
|
},
|
|
76
76
|
});
|
|
77
|
-
const clusterResult = await this.__srvUnboxed.tx(this.context).send(
|
|
77
|
+
const clusterResult = await this.__srvUnboxed.tx(this.context).send(reg);
|
|
78
78
|
if (this.#validateCluster(clusterResult)) {
|
|
79
79
|
Object.assign(clusterMap, clusterResult);
|
|
80
80
|
} else {
|
|
81
81
|
this.logger.error(
|
|
82
82
|
"cluster result of handler is not valid. Check the documentation for the expected structure. Continuing without clustering!",
|
|
83
83
|
{
|
|
84
|
-
handler:
|
|
84
|
+
handler: reg.event,
|
|
85
85
|
clusterResult: JSON.stringify(clusterResult),
|
|
86
86
|
}
|
|
87
87
|
);
|
|
@@ -92,7 +92,7 @@ class EventQueueGenericOutboxHandler extends EventQueueBaseClass {
|
|
|
92
92
|
}
|
|
93
93
|
|
|
94
94
|
for (const actionName in specificClusterEvents) {
|
|
95
|
-
const
|
|
95
|
+
const reg = new cds.Request({
|
|
96
96
|
event: `${EVENT_QUEUE_ACTIONS.CLUSTER}.${actionName}`,
|
|
97
97
|
user: this.context.user,
|
|
98
98
|
eventQueue: {
|
|
@@ -105,14 +105,14 @@ class EventQueueGenericOutboxHandler extends EventQueueBaseClass {
|
|
|
105
105
|
this.#clusterByDataProperty(actionName, specificClusterEvents[actionName], propertyName, cb),
|
|
106
106
|
},
|
|
107
107
|
});
|
|
108
|
-
const clusterResult = await this.__srvUnboxed.tx(this.context).send(
|
|
108
|
+
const clusterResult = await this.__srvUnboxed.tx(this.context).send(reg);
|
|
109
109
|
if (this.#validateCluster(clusterResult)) {
|
|
110
110
|
Object.assign(clusterMap, clusterResult);
|
|
111
111
|
} else {
|
|
112
112
|
this.logger.error(
|
|
113
113
|
"cluster result of handler is not valid. Check the documentation for the expected structure. Continuing without clustering!",
|
|
114
114
|
{
|
|
115
|
-
handler:
|
|
115
|
+
handler: reg.event,
|
|
116
116
|
clusterResult: JSON.stringify(clusterResult),
|
|
117
117
|
}
|
|
118
118
|
);
|
|
@@ -264,12 +264,12 @@ class EventQueueGenericOutboxHandler extends EventQueueBaseClass {
|
|
|
264
264
|
return payload;
|
|
265
265
|
}
|
|
266
266
|
|
|
267
|
-
const {
|
|
267
|
+
const { reg, userId } = this.#buildDispatchData(this.context, payload, {
|
|
268
268
|
queueEntries: [queueEntry],
|
|
269
269
|
});
|
|
270
|
-
|
|
271
|
-
await this.#setContextUser(this.context, userId,
|
|
272
|
-
const data = await this.__srvUnboxed.tx(this.context).send(
|
|
270
|
+
reg.event = handlerName;
|
|
271
|
+
await this.#setContextUser(this.context, userId, reg);
|
|
272
|
+
const data = await this.__srvUnboxed.tx(this.context).send(reg);
|
|
273
273
|
if (data) {
|
|
274
274
|
payload.data = data;
|
|
275
275
|
return payload;
|
|
@@ -285,12 +285,12 @@ class EventQueueGenericOutboxHandler extends EventQueueBaseClass {
|
|
|
285
285
|
return await super.hookForExceededEvents(exceededEvent);
|
|
286
286
|
}
|
|
287
287
|
|
|
288
|
-
const {
|
|
288
|
+
const { reg, userId } = this.#buildDispatchData(this.context, exceededEvent.payload, {
|
|
289
289
|
queueEntries: [exceededEvent],
|
|
290
290
|
});
|
|
291
|
-
await this.#setContextUser(this.context, userId,
|
|
292
|
-
|
|
293
|
-
await this.__srvUnboxed.tx(this.context).send(
|
|
291
|
+
await this.#setContextUser(this.context, userId, reg);
|
|
292
|
+
reg.event = handlerName;
|
|
293
|
+
await this.__srvUnboxed.tx(this.context).send(reg);
|
|
294
294
|
}
|
|
295
295
|
|
|
296
296
|
// NOTE: Currently not exposed to CAP service; we wait for a valid use case
|
|
@@ -310,37 +310,39 @@ class EventQueueGenericOutboxHandler extends EventQueueBaseClass {
|
|
|
310
310
|
|
|
311
311
|
async processPeriodicEvent(processContext, key, queueEntry) {
|
|
312
312
|
const [, action] = this.eventSubType.split(".");
|
|
313
|
-
const
|
|
314
|
-
await this.#setContextUser(processContext, config.userId,
|
|
315
|
-
await this.__srvUnboxed.tx(processContext).emit(
|
|
313
|
+
const reg = new cds.Event({ event: action, eventQueue: { processor: this, key, queueEntries: [queueEntry] } });
|
|
314
|
+
await this.#setContextUser(processContext, config.userId, reg);
|
|
315
|
+
await this.__srvUnboxed.tx(processContext).emit(reg);
|
|
316
316
|
}
|
|
317
317
|
|
|
318
318
|
#buildDispatchData(context, payload, { key, queueEntries } = {}) {
|
|
319
319
|
const { useEventQueueUser } = this.eventConfig;
|
|
320
320
|
const userId = useEventQueueUser ? config.userId : payload.contextUser;
|
|
321
|
-
const
|
|
321
|
+
const reg = payload._fromSend ? new cds.Request(payload) : new cds.Event(payload);
|
|
322
322
|
const invocationFn = payload._fromSend ? "send" : "emit";
|
|
323
|
-
delete
|
|
324
|
-
delete
|
|
325
|
-
|
|
326
|
-
return {
|
|
323
|
+
delete reg._fromSend;
|
|
324
|
+
delete reg.contextUser;
|
|
325
|
+
reg.eventQueue = { processor: this, key, queueEntries, payload };
|
|
326
|
+
return { reg, userId, invocationFn };
|
|
327
327
|
}
|
|
328
328
|
|
|
329
|
-
async #setContextUser(context, userId,
|
|
329
|
+
async #setContextUser(context, userId, reg) {
|
|
330
|
+
const authInfo = await common.getAuthContext(context.tenant);
|
|
330
331
|
context.user = new cds.User.Privileged({
|
|
331
332
|
id: userId,
|
|
332
|
-
|
|
333
|
+
authInfo,
|
|
334
|
+
tokenInfo: authInfo?.token,
|
|
333
335
|
});
|
|
334
|
-
if (
|
|
335
|
-
|
|
336
|
+
if (reg) {
|
|
337
|
+
reg.user = context.user;
|
|
336
338
|
}
|
|
337
339
|
}
|
|
338
340
|
|
|
339
341
|
async processEvent(processContext, key, queueEntries, payload) {
|
|
340
342
|
try {
|
|
341
|
-
const { userId, invocationFn,
|
|
342
|
-
await this.#setContextUser(processContext, userId,
|
|
343
|
-
const result = await this.__srvUnboxed.tx(processContext)[invocationFn](
|
|
343
|
+
const { userId, invocationFn, reg } = this.#buildDispatchData(processContext, payload, { key, queueEntries });
|
|
344
|
+
await this.#setContextUser(processContext, userId, reg);
|
|
345
|
+
const result = await this.__srvUnboxed.tx(processContext)[invocationFn](reg);
|
|
344
346
|
return this.#determineResultStatus(result, queueEntries);
|
|
345
347
|
} catch (err) {
|
|
346
348
|
this.logger.error("error processing outboxed service call", err, {
|
|
@@ -52,7 +52,7 @@ function outboxed(srv, customOpts) {
|
|
|
52
52
|
outboxOpts = config.addCAPOutboxEventSpecificAction(srv.name, req.event);
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
if (
|
|
55
|
+
if (["persistent-outbox", "persistent-queue"].includes(outboxOpts.kind)) {
|
|
56
56
|
await _mapToEventAndPublish(context, srv.name, req, !!specificSettings);
|
|
57
57
|
return;
|
|
58
58
|
}
|
package/src/periodicEvents.js
CHANGED
|
@@ -7,11 +7,17 @@ const { EventProcessingStatus } = require("./constants");
|
|
|
7
7
|
const { processChunkedSync } = require("./shared/common");
|
|
8
8
|
const eventConfig = require("./config");
|
|
9
9
|
|
|
10
|
-
const COMPONENT_NAME = "/eventQueue/periodicEvents";
|
|
11
10
|
const CHUNK_SIZE_INSERT_PERIODIC_EVENTS = 4;
|
|
12
11
|
|
|
12
|
+
const ALLOWED_PERIODIC_SEC_DIFF = 30;
|
|
13
|
+
|
|
14
|
+
const COMPONENT_NAME = "/eventQueue/periodicEvents";
|
|
15
|
+
|
|
13
16
|
const checkAndInsertPeriodicEvents = async (context) => {
|
|
14
17
|
const now = new Date();
|
|
18
|
+
cds.log(COMPONENT_NAME).info("updating periodic events", {
|
|
19
|
+
tenant: context.tenant,
|
|
20
|
+
});
|
|
15
21
|
const tx = cds.tx(context);
|
|
16
22
|
const baseCqn = SELECT.from(eventConfig.tableNameEventQueue)
|
|
17
23
|
.where([
|
|
@@ -117,11 +123,16 @@ const _determineChangedCron = (existingEventsCron) => {
|
|
|
117
123
|
const config = eventConfig.getEventConfig(event.type, event.subType);
|
|
118
124
|
const eventStartAfter = new Date(event.startAfter);
|
|
119
125
|
const eventCreatedAt = new Date(event.createdAt);
|
|
126
|
+
const randomOffset = config.randomOffset ?? eventConfig.randomOffsetPeriodicEvents ?? 0;
|
|
120
127
|
const cronExpression = CronExpressionParser.parse(config.cron, {
|
|
121
128
|
currentDate: eventCreatedAt,
|
|
122
129
|
tz: config.tz,
|
|
123
130
|
});
|
|
124
|
-
|
|
131
|
+
// report as changed if diff created than ALLOWED_PERIODIC_SEC_DIFF + the random event offset seconds
|
|
132
|
+
return (
|
|
133
|
+
Math.abs(cronExpression.next().getTime() - eventStartAfter.getTime()) >
|
|
134
|
+
(ALLOWED_PERIODIC_SEC_DIFF + randomOffset) * 1000
|
|
135
|
+
);
|
|
125
136
|
});
|
|
126
137
|
};
|
|
127
138
|
|
package/src/processEventQueue.js
CHANGED
|
@@ -318,8 +318,11 @@ const _checkEventIsBlocked = async (baseInstance) => {
|
|
|
318
318
|
|
|
319
319
|
const _processEvent = async (eventTypeInstance, processContext, key, queueEntries, payload) => {
|
|
320
320
|
let traceContext;
|
|
321
|
-
if (
|
|
322
|
-
|
|
321
|
+
if (eventTypeInstance.inheritTraceContext) {
|
|
322
|
+
const uniqueTraceContext = [...new Set(queueEntries.map((entry) => entry.context?.traceContext).filter((a) => a))];
|
|
323
|
+
if (uniqueTraceContext.length === 1) {
|
|
324
|
+
traceContext = uniqueTraceContext[0];
|
|
325
|
+
}
|
|
323
326
|
}
|
|
324
327
|
|
|
325
328
|
return await trace(
|
package/src/redis/redisPub.js
CHANGED
|
@@ -119,7 +119,8 @@ const _processLocalWithoutRedis = async (tenantId, events) => {
|
|
|
119
119
|
let context = {};
|
|
120
120
|
if (tenantId) {
|
|
121
121
|
const user = await cds.tx({ tenant: tenantId }, async () => {
|
|
122
|
-
|
|
122
|
+
const authInfo = await common.getAuthContext(tenantId);
|
|
123
|
+
return new cds.User.Privileged({ id: config.userId, authInfo, tokenInfo: authInfo.token });
|
|
123
124
|
});
|
|
124
125
|
context = {
|
|
125
126
|
tenant: tenantId,
|
package/src/redis/redisSub.js
CHANGED
|
@@ -78,7 +78,8 @@ const _messageHandlerProcessEvents = async (messageData) => {
|
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
const user = await cds.tx({ tenant: tenantId }, async () => {
|
|
81
|
-
|
|
81
|
+
const authInfo = await common.getAuthContext(tenantId);
|
|
82
|
+
return new cds.User.Privileged({ id: config.userId, authInfo, tokenInfo: authInfo?.token });
|
|
82
83
|
});
|
|
83
84
|
const tenantContext = {
|
|
84
85
|
tenant: tenantId,
|
package/src/runner/openEvents.js
CHANGED
|
@@ -38,34 +38,27 @@ const getOpenQueueEntries = async (tx, filterAppSpecificEvents = true) => {
|
|
|
38
38
|
for (const { type, subType } of entries) {
|
|
39
39
|
if (eventConfig.isCapOutboxEvent(type)) {
|
|
40
40
|
const [srvName, actionName] = subType.split(".");
|
|
41
|
-
|
|
42
|
-
.to(srvName)
|
|
43
|
-
|
|
44
|
-
if (!filterAppSpecificEvents) {
|
|
45
|
-
return; // will be done in finally
|
|
46
|
-
}
|
|
47
|
-
|
|
41
|
+
try {
|
|
42
|
+
const service = await cds.connect.to(srvName);
|
|
43
|
+
if (filterAppSpecificEvents) {
|
|
48
44
|
if (!service) {
|
|
49
|
-
|
|
45
|
+
continue;
|
|
50
46
|
}
|
|
51
47
|
cds.outboxed(service);
|
|
52
48
|
if (actionName) {
|
|
53
49
|
config.addCAPOutboxEventSpecificAction(srvName, actionName);
|
|
54
50
|
}
|
|
55
|
-
if (
|
|
56
|
-
if (eventConfig.shouldBeProcessedInThisApplication(type, subType)) {
|
|
57
|
-
result.push({ type, subType });
|
|
58
|
-
}
|
|
59
|
-
} else {
|
|
60
|
-
result.push({ type, subType });
|
|
61
|
-
}
|
|
62
|
-
})
|
|
63
|
-
.catch(() => {})
|
|
64
|
-
.finally(() => {
|
|
65
|
-
if (!filterAppSpecificEvents) {
|
|
51
|
+
if (eventConfig.shouldBeProcessedInThisApplication(type, subType)) {
|
|
66
52
|
result.push({ type, subType });
|
|
67
53
|
}
|
|
68
|
-
}
|
|
54
|
+
}
|
|
55
|
+
} catch {
|
|
56
|
+
/* ignore catch */
|
|
57
|
+
} finally {
|
|
58
|
+
if (!filterAppSpecificEvents) {
|
|
59
|
+
result.push({ type, subType });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
69
62
|
} else {
|
|
70
63
|
if (filterAppSpecificEvents) {
|
|
71
64
|
if (
|
package/src/runner/runner.js
CHANGED
|
@@ -138,16 +138,22 @@ const _executeEventsAllTenantsRedis = async (tenantIds) => {
|
|
|
138
138
|
if (!couldAcquireLock) {
|
|
139
139
|
return;
|
|
140
140
|
}
|
|
141
|
+
} catch (err) {
|
|
142
|
+
logger.error("executing event queue run for multi instance and tenant failed", err);
|
|
143
|
+
}
|
|
141
144
|
|
|
142
|
-
|
|
145
|
+
for (const tenantId of tenantIds) {
|
|
146
|
+
try {
|
|
143
147
|
await cds.tx({ tenant: tenantId }, async (tx) => {
|
|
144
148
|
await trace(
|
|
145
149
|
tx.context,
|
|
146
150
|
"get-openEvents-and-publish",
|
|
147
151
|
async () => {
|
|
152
|
+
const authInfo = await common.getAuthContext(tenantId);
|
|
148
153
|
tx.context.user = new cds.User.Privileged({
|
|
149
154
|
id: config.userId,
|
|
150
|
-
|
|
155
|
+
authInfo,
|
|
156
|
+
tokenInfo: authInfo?.token,
|
|
151
157
|
});
|
|
152
158
|
const entries = await openEvents.getOpenQueueEntries(tx, false);
|
|
153
159
|
logger.info("broadcasting events for run", {
|
|
@@ -168,9 +174,9 @@ const _executeEventsAllTenantsRedis = async (tenantIds) => {
|
|
|
168
174
|
{ newRootSpan: true }
|
|
169
175
|
);
|
|
170
176
|
});
|
|
177
|
+
} catch (err) {
|
|
178
|
+
logger.error("broadcasting events for tenant failed", { tenantId }, err);
|
|
171
179
|
}
|
|
172
|
-
} catch (err) {
|
|
173
|
-
logger.info("executing event queue run for multi instance and tenant failed", err);
|
|
174
180
|
}
|
|
175
181
|
};
|
|
176
182
|
|
|
@@ -179,23 +185,30 @@ const _executeEventsAllTenants = async (tenantIds, runId) => {
|
|
|
179
185
|
for (const tenantId of tenantIds) {
|
|
180
186
|
const id = cds.utils.uuid();
|
|
181
187
|
let tenantContext;
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
188
|
+
let events;
|
|
189
|
+
try {
|
|
190
|
+
events = await trace(
|
|
191
|
+
{ id, tenant: tenantId },
|
|
192
|
+
"fetch-openEvents-and-authInfo",
|
|
193
|
+
async () => {
|
|
194
|
+
const user = await cds.tx({ tenant: tenantId }, async () => {
|
|
195
|
+
const authInfo = await common.getAuthContext(tenantId);
|
|
196
|
+
return new cds.User.Privileged({ id: config.userId, authInfo, tokenInfo: authInfo?.token });
|
|
197
|
+
});
|
|
198
|
+
tenantContext = {
|
|
199
|
+
tenant: tenantId,
|
|
200
|
+
user,
|
|
201
|
+
};
|
|
202
|
+
return await cds.tx(tenantContext, async (tx) => {
|
|
203
|
+
return await openEvents.getOpenQueueEntries(tx);
|
|
204
|
+
});
|
|
205
|
+
},
|
|
206
|
+
{ newRootSpan: true }
|
|
207
|
+
);
|
|
208
|
+
} catch (err) {
|
|
209
|
+
cds.log(COMPONENT_NAME).error("fetching open events for tenant failed", { tenantId }, err);
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
199
212
|
|
|
200
213
|
if (!events.length) {
|
|
201
214
|
continue;
|
|
@@ -248,7 +261,8 @@ const _executePeriodicEventsAllTenants = async (tenantIds) => {
|
|
|
248
261
|
for (const tenantId of tenantIds) {
|
|
249
262
|
try {
|
|
250
263
|
const user = await cds.tx({ tenant: tenantId }, async () => {
|
|
251
|
-
|
|
264
|
+
const authInfo = await common.getAuthContext(tenantId);
|
|
265
|
+
return new cds.User.Privileged({ id: config.userId, authInfo, tokenInfo: authInfo?.token });
|
|
252
266
|
});
|
|
253
267
|
const tenantContext = {
|
|
254
268
|
tenant: tenantId,
|
|
@@ -279,7 +293,7 @@ const _singleTenantDb = async () => {
|
|
|
279
293
|
const id = cds.utils.uuid();
|
|
280
294
|
const events = await trace(
|
|
281
295
|
{ id },
|
|
282
|
-
"fetch-openEvents-and-
|
|
296
|
+
"fetch-openEvents-and-authInfo",
|
|
283
297
|
async () => {
|
|
284
298
|
return await cds.tx({}, async (tx) => {
|
|
285
299
|
return await openEvents.getOpenQueueEntries(tx);
|
|
@@ -499,6 +513,7 @@ const _checkPeriodicEventsSingleTenantOneTime = async () => {
|
|
|
499
513
|
tenantScoped: false,
|
|
500
514
|
});
|
|
501
515
|
if (!couldAcquireLock) {
|
|
516
|
+
logger.info("skipping updating periodic events - lock not acquired");
|
|
502
517
|
return;
|
|
503
518
|
}
|
|
504
519
|
return await cds.tx({}, async (tx) => await periodicEvents.checkAndInsertPeriodicEvents(tx.context));
|
|
@@ -519,7 +534,7 @@ const _checkPeriodicEventsSingleTenant = async (context) => {
|
|
|
519
534
|
try {
|
|
520
535
|
logger.info("executing updating periodic events", {
|
|
521
536
|
tenantId: context.tenant,
|
|
522
|
-
subdomain: context.user?.
|
|
537
|
+
subdomain: context.user?.authInfo?.getSubdomain?.(),
|
|
523
538
|
});
|
|
524
539
|
await periodicEvents.checkAndInsertPeriodicEvents(context);
|
|
525
540
|
} catch (err) {
|
package/src/shared/cdsHelper.js
CHANGED
|
@@ -6,10 +6,13 @@ const cds = require("@sap/cds");
|
|
|
6
6
|
const config = require("../config");
|
|
7
7
|
const common = require("./common");
|
|
8
8
|
const { TenantIdCheckTypes } = require("../constants");
|
|
9
|
+
const { limiter } = require("./common");
|
|
9
10
|
|
|
10
11
|
const VERROR_CLUSTER_NAME = "ExecuteInNewTransactionError";
|
|
11
12
|
const COMPONENT_NAME = "/eventQueue/cdsHelper";
|
|
12
13
|
|
|
14
|
+
const CONCURRENCY_AUTH_INFO = 3;
|
|
15
|
+
|
|
13
16
|
/**
|
|
14
17
|
* Execute logic in a new managed CDS transaction context, auto-handling commit, rollback and error/exception situations.
|
|
15
18
|
* Includes logging of start, end and error situation with additional info object and unique transaction id (txId)
|
|
@@ -25,7 +28,8 @@ async function executeInNewTransaction(context = {}, transactionTag, fn, args, {
|
|
|
25
28
|
const logger = cds.log(COMPONENT_NAME);
|
|
26
29
|
let transactionRollbackPromise = Promise.resolve(false);
|
|
27
30
|
try {
|
|
28
|
-
const
|
|
31
|
+
const authInfo = await common.getAuthContext(context.tenant);
|
|
32
|
+
const user = new cds.User.Privileged({ id: config.userId, authInfo, tokenInfo: authInfo?.token });
|
|
29
33
|
if (cds.db.kind === "hana") {
|
|
30
34
|
await cds.tx(
|
|
31
35
|
{
|
|
@@ -139,18 +143,31 @@ const getAllTenantIds = async () => {
|
|
|
139
143
|
return null;
|
|
140
144
|
}
|
|
141
145
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
146
|
+
const tenantIds = response.map((tenant) => tenant.subscribedTenantId ?? tenant.tenant);
|
|
147
|
+
const suspendedTenants = {};
|
|
148
|
+
if (config.disableProcessingOfSuspendedTenants) {
|
|
149
|
+
await limiter(CONCURRENCY_AUTH_INFO, tenantIds, async (tenantId) => {
|
|
150
|
+
const result = await common.getAuthContext(tenantId, { returnError: true });
|
|
151
|
+
// NOTE: only 404 errors are propagated all others are ignored
|
|
152
|
+
if (result?.[0]) {
|
|
153
|
+
suspendedTenants[tenantId] = true;
|
|
154
|
+
cds.log(COMPONENT_NAME).info("skip event-queue processing, tenant suspended", { tenantId });
|
|
148
155
|
}
|
|
149
|
-
|
|
150
|
-
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return tenantIds.reduce(async (result, tenantId) => {
|
|
160
|
+
result = await result;
|
|
161
|
+
if (!suspendedTenants[tenantId] && (await common.isTenantIdValidCb(TenantIdCheckTypes.eventProcessing, tenantId))) {
|
|
162
|
+
result.push(tenantId);
|
|
163
|
+
}
|
|
164
|
+
return result;
|
|
165
|
+
}, []);
|
|
151
166
|
};
|
|
152
167
|
|
|
153
|
-
const
|
|
168
|
+
const TENANT_COLUMNS = ["subscribedSubdomain", "createdAt", "modifiedAt"];
|
|
169
|
+
|
|
170
|
+
const getAllTenantWithMetadata = async () => {
|
|
154
171
|
const response = await _getAllTenantBase();
|
|
155
172
|
if (!response) {
|
|
156
173
|
return null;
|
|
@@ -160,10 +177,19 @@ const getAllTenantWithSubdomain = async () => {
|
|
|
160
177
|
const tenantId = row.subscribedTenantId ?? row.tenant;
|
|
161
178
|
result = await result;
|
|
162
179
|
if (await common.isTenantIdValidCb(TenantIdCheckTypes.eventProcessing, tenantId)) {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
180
|
+
const data = Object.entries(row).reduce(
|
|
181
|
+
(result, [key, value]) => {
|
|
182
|
+
if (TENANT_COLUMNS.includes(key)) {
|
|
183
|
+
result[key] = value;
|
|
184
|
+
} else {
|
|
185
|
+
result.metadata[key] = value;
|
|
186
|
+
}
|
|
187
|
+
return result;
|
|
188
|
+
},
|
|
189
|
+
{ metadata: {} }
|
|
190
|
+
);
|
|
191
|
+
data.metadata = JSON.stringify(data.metadata);
|
|
192
|
+
result.push(data);
|
|
167
193
|
}
|
|
168
194
|
return result;
|
|
169
195
|
}, []);
|
|
@@ -172,5 +198,5 @@ const getAllTenantWithSubdomain = async () => {
|
|
|
172
198
|
module.exports = {
|
|
173
199
|
executeInNewTransaction,
|
|
174
200
|
getAllTenantIds,
|
|
175
|
-
|
|
201
|
+
getAllTenantWithMetadata,
|
|
176
202
|
};
|
package/src/shared/common.js
CHANGED
|
@@ -7,9 +7,11 @@ const xssec = require("@sap/xssec");
|
|
|
7
7
|
const VError = require("verror");
|
|
8
8
|
|
|
9
9
|
const config = require("../config");
|
|
10
|
+
const { ExpiringLazyCache } = require("./lazyCache");
|
|
10
11
|
const { TenantIdCheckTypes } = require("../constants");
|
|
11
12
|
|
|
12
|
-
const
|
|
13
|
+
const EXPIRE_TIME_TENANT_404 = 10 * 60 * 1000; // 10 minutes
|
|
14
|
+
|
|
13
15
|
const COMPONENT_NAME = "/eventQueue/common";
|
|
14
16
|
|
|
15
17
|
const arrayToFlatMap = (array, key = "ID") => {
|
|
@@ -87,64 +89,61 @@ const processChunkedSync = (inputs, chunkSize, chunkHandler) => {
|
|
|
87
89
|
|
|
88
90
|
const hashStringTo32Bit = (value) => crypto.createHash("sha256").update(String(value)).digest("base64").slice(0, 32);
|
|
89
91
|
|
|
90
|
-
const
|
|
91
|
-
const tokenInfoCache = getTokenInfo._tokenInfoCache;
|
|
92
|
-
tokenInfoCache[tenantId] = tokenInfoCache[tenantId] ?? {};
|
|
92
|
+
const _getNewAuthContext = async (tenantId) => {
|
|
93
93
|
try {
|
|
94
|
-
if (!
|
|
95
|
-
|
|
94
|
+
if (!_getNewAuthContext._xsuaaService) {
|
|
95
|
+
_getNewAuthContext._xsuaaService = new xssec.XsuaaService(cds.requires.auth.credentials);
|
|
96
96
|
}
|
|
97
|
-
const authService =
|
|
97
|
+
const authService = _getNewAuthContext._xsuaaService;
|
|
98
98
|
const token = await authService.fetchClientCredentialsToken({ zid: tenantId });
|
|
99
99
|
const tokenInfo = new xssec.XsuaaToken(token.access_token);
|
|
100
|
-
|
|
101
|
-
return tokenInfo;
|
|
100
|
+
const authInfo = new xssec.XsuaaSecurityContext(authService, tokenInfo);
|
|
101
|
+
return [tokenInfo.getExpirationDate().getTime() - Date.now(), [null, authInfo]];
|
|
102
102
|
} catch (err) {
|
|
103
|
-
|
|
104
|
-
cds.log(COMPONENT_NAME).warn("failed to request tokenInfo", {
|
|
103
|
+
cds.log(COMPONENT_NAME).warn("failed to request authContext", {
|
|
105
104
|
err: err.message,
|
|
106
105
|
responseCode: err.responseCode,
|
|
107
106
|
responseText: err.responseText,
|
|
108
107
|
});
|
|
108
|
+
|
|
109
|
+
if (err.responseCode === 404) {
|
|
110
|
+
return [EXPIRE_TIME_TENANT_404, [err, null]];
|
|
111
|
+
}
|
|
112
|
+
return [0, null];
|
|
109
113
|
}
|
|
110
114
|
};
|
|
111
115
|
|
|
112
|
-
const
|
|
113
|
-
if (!(await isTenantIdValidCb(TenantIdCheckTypes.
|
|
116
|
+
const getAuthContext = async (tenantId, { returnError = false } = {}) => {
|
|
117
|
+
if (!(await isTenantIdValidCb(TenantIdCheckTypes.getAuthContext, tenantId))) {
|
|
114
118
|
return null;
|
|
115
119
|
}
|
|
116
120
|
|
|
117
121
|
if (!cds.requires?.auth?.credentials) {
|
|
118
|
-
return null; // no credentials not
|
|
122
|
+
return null; // no credentials not authContext
|
|
119
123
|
}
|
|
120
124
|
|
|
121
125
|
if (!config.isMultiTenancy) {
|
|
122
126
|
return null; // does only make sense for multi tenancy
|
|
123
127
|
}
|
|
124
128
|
|
|
125
|
-
if (!cds.requires?.auth.kind.match(/jwt|xsuaa/i)) {
|
|
129
|
+
if (!cds.requires?.auth.kind.match(/jwt|xsuaa/i) && !cds.requires?.xsuaa) {
|
|
126
130
|
return null;
|
|
127
131
|
}
|
|
128
132
|
|
|
129
|
-
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
) {
|
|
136
|
-
tokenInfoCache[tenantId] ??= {};
|
|
137
|
-
tokenInfoCache[tenantId].value = _getNewTokenInfo(tenantId);
|
|
138
|
-
tokenInfoCache[tenantId].expireTs = null;
|
|
133
|
+
getAuthContext._cache = getAuthContext._cache ?? new ExpiringLazyCache();
|
|
134
|
+
const result = await getAuthContext._cache.getSetCb(tenantId, async () => _getNewAuthContext(tenantId));
|
|
135
|
+
if (returnError) {
|
|
136
|
+
return result;
|
|
137
|
+
} else {
|
|
138
|
+
return result?.[1];
|
|
139
139
|
}
|
|
140
|
-
return await tokenInfoCache[tenantId].value;
|
|
141
140
|
};
|
|
142
141
|
|
|
143
142
|
const isTenantIdValidCb = async (checkType, tenantId) => {
|
|
144
143
|
let cb;
|
|
145
144
|
switch (checkType) {
|
|
146
|
-
case TenantIdCheckTypes.
|
|
147
|
-
cb = config.
|
|
145
|
+
case TenantIdCheckTypes.getAuthContext:
|
|
146
|
+
cb = config.tenantIdFilterAuthContext;
|
|
148
147
|
break;
|
|
149
148
|
case TenantIdCheckTypes.eventProcessing:
|
|
150
149
|
cb = config.tenantIdFilterEventProcessing;
|
|
@@ -167,10 +166,10 @@ module.exports = {
|
|
|
167
166
|
isValidDate,
|
|
168
167
|
processChunkedSync,
|
|
169
168
|
hashStringTo32Bit,
|
|
170
|
-
|
|
169
|
+
getAuthContext,
|
|
171
170
|
isTenantIdValidCb,
|
|
172
171
|
promiseAllDone,
|
|
173
172
|
__: {
|
|
174
|
-
|
|
173
|
+
clearAuthContextCache: () => getAuthContext._cache?.clear(),
|
|
175
174
|
},
|
|
176
175
|
};
|
|
@@ -108,7 +108,7 @@ const _renewLockRedis = async (context, fullKey, expiryTime, { value = "true" }
|
|
|
108
108
|
|
|
109
109
|
const _checkLockExistsRedis = async (context, fullKey) => {
|
|
110
110
|
const client = await redis.createMainClientAndConnect(config.redisOptions);
|
|
111
|
-
return await client.
|
|
111
|
+
return await client.exists(fullKey);
|
|
112
112
|
};
|
|
113
113
|
|
|
114
114
|
const _checkLockExistsDb = async (context, fullKey) => {
|
|
@@ -121,8 +121,9 @@ const _checkLockExistsDb = async (context, fullKey) => {
|
|
|
121
121
|
|
|
122
122
|
const _releaseLockRedis = async (context, fullKey) => {
|
|
123
123
|
const client = await redis.createMainClientAndConnect(config.redisOptions);
|
|
124
|
-
await client.del(fullKey);
|
|
124
|
+
const result = await client.del(fullKey);
|
|
125
125
|
delete existingLocks[fullKey];
|
|
126
|
+
return result === 1;
|
|
126
127
|
};
|
|
127
128
|
|
|
128
129
|
const _releaseLockDb = async (context, fullKey) => {
|
|
@@ -130,6 +131,7 @@ const _releaseLockDb = async (context, fullKey) => {
|
|
|
130
131
|
await tx.run(DELETE.from(config.tableNameEventLock).where("code =", fullKey));
|
|
131
132
|
});
|
|
132
133
|
delete existingLocks[fullKey];
|
|
134
|
+
return true;
|
|
133
135
|
};
|
|
134
136
|
|
|
135
137
|
const _acquireLockDB = async (
|
|
@@ -191,39 +193,36 @@ const _generateKey = (context, tenantScoped, key) => {
|
|
|
191
193
|
};
|
|
192
194
|
|
|
193
195
|
const getAllLocksRedis = async () => {
|
|
194
|
-
const
|
|
195
|
-
const batchSize = 500;
|
|
196
|
-
const results = [];
|
|
197
|
-
let pipeline = client.multi();
|
|
196
|
+
const clientOrCluster = await redis.createMainClientAndConnect(config.redisOptions);
|
|
198
197
|
const output = [];
|
|
199
|
-
|
|
198
|
+
const results = [];
|
|
200
199
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
200
|
+
let clients;
|
|
201
|
+
if (redis.isClusterMode()) {
|
|
202
|
+
clients = clientOrCluster.masters.map((master) => master.client);
|
|
203
|
+
} else {
|
|
204
|
+
clients = [clientOrCluster];
|
|
205
|
+
}
|
|
207
206
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
subType
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
207
|
+
// NOTE: use SCAN because KEYS is not supported for cluster clients
|
|
208
|
+
for (const client of clients) {
|
|
209
|
+
for await (const key of client.scanIterator({ MATCH: "EVENT*", COUNT: 1000 })) {
|
|
210
|
+
const [, tenant, guidOrType, subType] = key.split("##");
|
|
211
|
+
if (!subType) {
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
215
214
|
|
|
216
|
-
|
|
215
|
+
const pipeline = client.multi();
|
|
216
|
+
output.push({
|
|
217
|
+
tenant: tenant,
|
|
218
|
+
type: guidOrType,
|
|
219
|
+
subType: subType,
|
|
220
|
+
});
|
|
221
|
+
pipeline.ttl(key).get(key);
|
|
217
222
|
const replies = await pipeline.exec();
|
|
218
223
|
results.push(...replies);
|
|
219
|
-
pipeline = client.multi();
|
|
220
|
-
count = 0;
|
|
221
224
|
}
|
|
222
225
|
}
|
|
223
|
-
if (count > 0) {
|
|
224
|
-
const replies = await pipeline.exec();
|
|
225
|
-
results.push(...replies);
|
|
226
|
-
}
|
|
227
226
|
|
|
228
227
|
let counter = 0;
|
|
229
228
|
for (const row of output) {
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const DEFAULT_SEPARATOR = "##";
|
|
4
|
+
const DEFAULT_EXPIRATION_GAP = 60 * 1000; // 60 seconds
|
|
5
|
+
|
|
6
|
+
class LazyCache {
|
|
7
|
+
constructor({ separator = DEFAULT_SEPARATOR } = {}) {
|
|
8
|
+
this.__data = Object.create(null);
|
|
9
|
+
this.__separator = separator;
|
|
10
|
+
}
|
|
11
|
+
_separator() {
|
|
12
|
+
return this.__separator;
|
|
13
|
+
}
|
|
14
|
+
_data() {
|
|
15
|
+
return this.__data;
|
|
16
|
+
}
|
|
17
|
+
async _dataSettled() {
|
|
18
|
+
return await Object.entries(this.__data).reduce(async (result, [key, value]) => {
|
|
19
|
+
(await result)[key] = await value;
|
|
20
|
+
return result;
|
|
21
|
+
}, Promise.resolve({}));
|
|
22
|
+
}
|
|
23
|
+
_key(keyOrKeys) {
|
|
24
|
+
return Array.isArray(keyOrKeys) ? keyOrKeys.join(this.__separator) : keyOrKeys;
|
|
25
|
+
}
|
|
26
|
+
has(keyOrKeys) {
|
|
27
|
+
return Object.prototype.hasOwnProperty.call(this.__data, this._key(keyOrKeys));
|
|
28
|
+
}
|
|
29
|
+
get(keyOrKeys) {
|
|
30
|
+
return this.__data[this._key(keyOrKeys)];
|
|
31
|
+
}
|
|
32
|
+
set(keyOrKeys, value) {
|
|
33
|
+
this.__data[this._key(keyOrKeys)] = value;
|
|
34
|
+
return this;
|
|
35
|
+
}
|
|
36
|
+
setCb(keyOrKeys, callback) {
|
|
37
|
+
const resultOrPromise = callback();
|
|
38
|
+
return this.set(
|
|
39
|
+
keyOrKeys,
|
|
40
|
+
resultOrPromise instanceof Promise
|
|
41
|
+
? resultOrPromise.catch((err) => {
|
|
42
|
+
this.delete(keyOrKeys);
|
|
43
|
+
return Promise.reject(err);
|
|
44
|
+
})
|
|
45
|
+
: resultOrPromise
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
getSetCb(keyOrKeys, callback) {
|
|
49
|
+
const key = this._key(keyOrKeys);
|
|
50
|
+
if (!this.has(key)) {
|
|
51
|
+
this.setCb(key, callback);
|
|
52
|
+
}
|
|
53
|
+
return this.get(key);
|
|
54
|
+
}
|
|
55
|
+
count() {
|
|
56
|
+
return Object.keys(this.__data).length;
|
|
57
|
+
}
|
|
58
|
+
delete(keyOrKeys) {
|
|
59
|
+
Reflect.deleteProperty(this.__data, this._key(keyOrKeys));
|
|
60
|
+
return this;
|
|
61
|
+
}
|
|
62
|
+
clear() {
|
|
63
|
+
this.__data = Object.create(null);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
class ExpiringLazyCache extends LazyCache {
|
|
68
|
+
constructor({ separator = DEFAULT_SEPARATOR, expirationGap = DEFAULT_EXPIRATION_GAP } = {}) {
|
|
69
|
+
super({ separator });
|
|
70
|
+
this.__expirationGap = expirationGap;
|
|
71
|
+
}
|
|
72
|
+
_expiringGap() {
|
|
73
|
+
return this.__expirationGap;
|
|
74
|
+
}
|
|
75
|
+
_isValid(expirationTime, currentTime = Date.now()) {
|
|
76
|
+
return expirationTime && currentTime <= expirationTime;
|
|
77
|
+
}
|
|
78
|
+
has(keyOrKeys, currentTime = Date.now()) {
|
|
79
|
+
if (!super.has(keyOrKeys)) {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
const [expirationTime] = super.get(keyOrKeys) ?? [];
|
|
83
|
+
return this._isValid(expirationTime, currentTime);
|
|
84
|
+
}
|
|
85
|
+
get(keyOrKeys, currentTime = Date.now()) {
|
|
86
|
+
const [expirationTime, value] = super.get(keyOrKeys) ?? [];
|
|
87
|
+
return this._isValid(expirationTime, currentTime) ? value : undefined;
|
|
88
|
+
}
|
|
89
|
+
// NOTE the expiration gap is substracted here, because we want to expire a
|
|
90
|
+
// little earlier than necessary.
|
|
91
|
+
// NOTE if the expiration is _less_ than the gap, the value is never valid,
|
|
92
|
+
// we still need to call set, because we want getSetCb to always return
|
|
93
|
+
// when the callback is used.
|
|
94
|
+
set(keyOrKeys, expiration, value, currentTime = Date.now()) {
|
|
95
|
+
return super.set(keyOrKeys, [currentTime + expiration - this.__expirationGap, value]);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
static _extract(result, extractor) {
|
|
99
|
+
if (!extractor) {
|
|
100
|
+
return result;
|
|
101
|
+
}
|
|
102
|
+
const { expiration, expiry, value, result: extractedResult } = extractor(result);
|
|
103
|
+
return [expiration ?? expiry, value ?? extractedResult];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// NOTE callback can either return a pair [expiration, value] or use an extractor to extract the right values from
|
|
107
|
+
// the callback's result
|
|
108
|
+
setCb(keyOrKeys, callback, { currentTime = Date.now(), extractor } = {}) {
|
|
109
|
+
const resultOrPromise = callback();
|
|
110
|
+
if (!(resultOrPromise instanceof Promise)) {
|
|
111
|
+
const [expiration, value] = ExpiringLazyCache._extract(resultOrPromise, extractor);
|
|
112
|
+
return this.set(keyOrKeys, expiration, value, currentTime);
|
|
113
|
+
}
|
|
114
|
+
return this.set(
|
|
115
|
+
keyOrKeys,
|
|
116
|
+
Infinity,
|
|
117
|
+
resultOrPromise
|
|
118
|
+
.catch((err) => {
|
|
119
|
+
this.delete(keyOrKeys);
|
|
120
|
+
return Promise.reject(err);
|
|
121
|
+
})
|
|
122
|
+
.then((result) => {
|
|
123
|
+
const [expiration, value] = ExpiringLazyCache._extract(result, extractor);
|
|
124
|
+
this.set(keyOrKeys, expiration, value, currentTime);
|
|
125
|
+
return value;
|
|
126
|
+
})
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// NOTE callback can either return a pair [expiration, value] or use an extractor to extract the right values from
|
|
131
|
+
// the callback's result
|
|
132
|
+
getSetCb(keyOrKeys, callback, { currentTime = Date.now(), extractor } = {}) {
|
|
133
|
+
const key = this._key(keyOrKeys);
|
|
134
|
+
if (!this.has(key, currentTime) || !super.has(key)) {
|
|
135
|
+
this.setCb(key, callback, { currentTime, extractor });
|
|
136
|
+
const [, value] = super.get(key);
|
|
137
|
+
return value;
|
|
138
|
+
}
|
|
139
|
+
return this.get(key, currentTime);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
module.exports = {
|
|
144
|
+
DEFAULT_EXPIRATION_GAP,
|
|
145
|
+
DEFAULT_SEPARATOR,
|
|
146
|
+
LazyCache,
|
|
147
|
+
ExpiringLazyCache,
|
|
148
|
+
};
|
package/src/shared/redis.js
CHANGED
|
@@ -178,6 +178,15 @@ const connectionCheck = async (options) => {
|
|
|
178
178
|
});
|
|
179
179
|
};
|
|
180
180
|
|
|
181
|
+
const isClusterMode = () => {
|
|
182
|
+
if (!("__clusterMode" in isClusterMode)) {
|
|
183
|
+
const env = getEnvInstance();
|
|
184
|
+
const { credentials } = env.redisRequires;
|
|
185
|
+
isClusterMode.__clusterMode = credentials.cluster_mode;
|
|
186
|
+
}
|
|
187
|
+
return isClusterMode.__clusterMode;
|
|
188
|
+
};
|
|
189
|
+
|
|
181
190
|
module.exports = {
|
|
182
191
|
createClientAndConnect,
|
|
183
192
|
createMainClientAndConnect,
|
|
@@ -186,4 +195,5 @@ module.exports = {
|
|
|
186
195
|
closeMainClient,
|
|
187
196
|
closeSubscribeClient,
|
|
188
197
|
connectionCheck,
|
|
198
|
+
isClusterMode,
|
|
189
199
|
};
|
|
@@ -13,14 +13,14 @@ service EventQueueAdminService {
|
|
|
13
13
|
null as space: String,
|
|
14
14
|
*
|
|
15
15
|
} actions {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
16
|
+
action setStatusAndAttempts(
|
|
17
|
+
// TODO: remove tenant as soon as CAP issue is fixed https://github.tools.sap/cap/issues/issues/18445
|
|
18
|
+
@mandatory
|
|
19
|
+
tenant: String,
|
|
20
|
+
status: db.Status,
|
|
21
|
+
@assert.range: [0,100]
|
|
22
|
+
attempts: Integer) returns Event;
|
|
23
|
+
}
|
|
24
24
|
|
|
25
25
|
@cds.persistence.skip
|
|
26
26
|
@readonly
|
|
@@ -32,12 +32,22 @@ service EventQueueAdminService {
|
|
|
32
32
|
space: String;
|
|
33
33
|
ttl: Integer;
|
|
34
34
|
createdAt: Integer;
|
|
35
|
-
}
|
|
35
|
+
} actions {
|
|
36
|
+
action releaseLock(
|
|
37
|
+
// TODO: remove tenant as soon as CAP issue is fixed https://github.tools.sap/cap/issues/issues/18445
|
|
38
|
+
@mandatory
|
|
39
|
+
tenant: String,
|
|
40
|
+
@mandatory
|
|
41
|
+
type: String,
|
|
42
|
+
@mandatory
|
|
43
|
+
subType: String) returns Boolean;
|
|
44
|
+
}
|
|
36
45
|
|
|
37
46
|
@readonly
|
|
38
47
|
@cds.persistence.skip
|
|
39
48
|
entity Tenant {
|
|
40
49
|
Key ID: String;
|
|
41
50
|
subdomain: String;
|
|
51
|
+
metadata: String;
|
|
42
52
|
}
|
|
43
53
|
}
|
|
@@ -5,6 +5,7 @@ const cdsHelper = require("../../src/shared/cdsHelper");
|
|
|
5
5
|
const { EventProcessingStatus } = require("../../src");
|
|
6
6
|
const config = require("../../src/config");
|
|
7
7
|
const distributedLock = require("../../src/shared/distributedLock");
|
|
8
|
+
const redisPub = require("../../src/redis/redisPub");
|
|
8
9
|
|
|
9
10
|
module.exports = class AdminService extends cds.ApplicationService {
|
|
10
11
|
async init() {
|
|
@@ -61,7 +62,7 @@ module.exports = class AdminService extends cds.ApplicationService {
|
|
|
61
62
|
});
|
|
62
63
|
|
|
63
64
|
this.on("READ", Tenant, async () => {
|
|
64
|
-
const tenants = await cdsHelper.
|
|
65
|
+
const tenants = await cdsHelper.getAllTenantWithMetadata();
|
|
65
66
|
return tenants ?? [];
|
|
66
67
|
});
|
|
67
68
|
|
|
@@ -82,14 +83,27 @@ module.exports = class AdminService extends cds.ApplicationService {
|
|
|
82
83
|
return req.reject(400, "No status or attempts provided");
|
|
83
84
|
}
|
|
84
85
|
|
|
85
|
-
await cds.tx({ tenant, headers: { "z-id": tenant } }, async () => {
|
|
86
|
+
const event = await cds.tx({ tenant, headers: { "z-id": tenant } }, async () => {
|
|
87
|
+
const event = await SELECT.one.from(EventDb).where({ ID: req.params[0].ID ?? req.params[0] });
|
|
86
88
|
await UPDATE.entity(EventDb)
|
|
87
89
|
.set(updateData)
|
|
88
90
|
.where({ ID: req.params[0].ID ?? req.params[0] });
|
|
91
|
+
return event;
|
|
92
|
+
});
|
|
93
|
+
redisPub.broadcastEvent(tenant, event).catch(() => {
|
|
94
|
+
/* ignore errors */
|
|
89
95
|
});
|
|
90
96
|
return await this.send(new cds.Request({ query: req.query, headers: req.headers }));
|
|
91
97
|
});
|
|
92
98
|
|
|
99
|
+
this.on("releaseLock", async (req) => {
|
|
100
|
+
cds.log("eventQueue").info("Releasing event-queue lock", req.data);
|
|
101
|
+
const { tenant, type, subType } = req.data;
|
|
102
|
+
return await cds.tx({ tenant }, async (tx) => {
|
|
103
|
+
return await distributedLock.releaseLock(tx.context, [type, subType].join("##"));
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
93
107
|
await super.init();
|
|
94
108
|
}
|
|
95
109
|
|