@cap-js-community/event-queue 1.11.0-beta.3 → 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 +18 -10
- package/src/constants.js +1 -1
- package/src/index.d.ts +3 -3
- package/src/initialize.js +1 -0
- package/src/outbox/EventQueueGenericOutboxHandler.js +3 -1
- 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 +3 -1
- package/src/shared/lazyCache.js +148 -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
|
@@ -114,11 +114,12 @@ class Config {
|
|
|
114
114
|
#redisNamespace;
|
|
115
115
|
#publishEventBlockList;
|
|
116
116
|
#crashOnRedisUnavailable;
|
|
117
|
-
#
|
|
117
|
+
#tenantIdFilterAuthContextCb;
|
|
118
118
|
#tenantIdFilterEventProcessingCb;
|
|
119
119
|
#configEvents;
|
|
120
120
|
#configPeriodicEvents;
|
|
121
121
|
#enableAdminService;
|
|
122
|
+
#disableProcessingOfSuspendedTenants;
|
|
122
123
|
static #instance;
|
|
123
124
|
constructor() {
|
|
124
125
|
this.#logger = cds.log(COMPONENT_NAME);
|
|
@@ -196,9 +197,7 @@ class Config {
|
|
|
196
197
|
result = config.value.test(this.#env.applicationName);
|
|
197
198
|
} else {
|
|
198
199
|
const shouldBeProcessedBasedOnAppName = appNameConfig[this.#env.applicationName];
|
|
199
|
-
|
|
200
|
-
result = config.value === this.#env.applicationName;
|
|
201
|
-
}
|
|
200
|
+
result = !!shouldBeProcessedBasedOnAppName;
|
|
202
201
|
}
|
|
203
202
|
if (result) {
|
|
204
203
|
break;
|
|
@@ -484,10 +483,11 @@ class Config {
|
|
|
484
483
|
delete base.interval;
|
|
485
484
|
}
|
|
486
485
|
|
|
487
|
-
|
|
486
|
+
const subType = `${name}.${fnName}`;
|
|
487
|
+
result[subType] = Object.assign(
|
|
488
488
|
{
|
|
489
489
|
type: CAP_EVENT_TYPE,
|
|
490
|
-
subType
|
|
490
|
+
subType,
|
|
491
491
|
impl: "./outbox/EventQueueGenericOutboxHandler",
|
|
492
492
|
internalEvent: true,
|
|
493
493
|
},
|
|
@@ -771,12 +771,12 @@ class Config {
|
|
|
771
771
|
this.#crashOnRedisUnavailable = value;
|
|
772
772
|
}
|
|
773
773
|
|
|
774
|
-
get
|
|
775
|
-
return this.#
|
|
774
|
+
get tenantIdFilterAuthContext() {
|
|
775
|
+
return this.#tenantIdFilterAuthContextCb;
|
|
776
776
|
}
|
|
777
777
|
|
|
778
|
-
set
|
|
779
|
-
this.#
|
|
778
|
+
set tenantIdFilterAuthContext(value) {
|
|
779
|
+
this.#tenantIdFilterAuthContextCb = value;
|
|
780
780
|
}
|
|
781
781
|
|
|
782
782
|
get tenantIdFilterEventProcessing() {
|
|
@@ -985,6 +985,14 @@ class Config {
|
|
|
985
985
|
this.#enableAdminService = value;
|
|
986
986
|
}
|
|
987
987
|
|
|
988
|
+
get disableProcessingOfSuspendedTenants() {
|
|
989
|
+
return this.#disableProcessingOfSuspendedTenants;
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
set disableProcessingOfSuspendedTenants(value) {
|
|
993
|
+
this.#disableProcessingOfSuspendedTenants = value;
|
|
994
|
+
}
|
|
995
|
+
|
|
988
996
|
/**
|
|
989
997
|
@return { Config }
|
|
990
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
|
@@ -327,9 +327,11 @@ class EventQueueGenericOutboxHandler extends EventQueueBaseClass {
|
|
|
327
327
|
}
|
|
328
328
|
|
|
329
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
336
|
if (reg) {
|
|
335
337
|
reg.user = context.user;
|
|
@@ -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
|
};
|
|
@@ -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 (
|
|
@@ -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
|
+
};
|
|
@@ -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
|
|