@cap-js-community/event-queue 1.10.4 → 1.10.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/index.cds +1 -0
- package/package.json +5 -2
- package/src/EventQueueProcessorBase.js +6 -6
- package/src/config.js +14 -1
- package/src/index.d.ts +2 -0
- package/src/initialize.js +24 -14
- package/src/processEventQueue.js +1 -1
- package/src/runner/runner.js +3 -2
- package/src/shared/cdsHelper.js +31 -3
- package/src/shared/distributedLock.js +60 -6
- package/src/shared/openTelemetry.js +11 -0
- package/srv/index.cds +1 -0
- package/srv/service/admin-service.cds +42 -0
- package/srv/service/admin-service.js +111 -0
- package/srv/service/index.cds +1 -0
package/index.cds
CHANGED
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cap-js-community/event-queue",
|
|
3
|
-
"version": "1.10.
|
|
3
|
+
"version": "1.10.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",
|
|
7
7
|
"files": [
|
|
8
8
|
"src",
|
|
9
|
+
"srv",
|
|
9
10
|
"db",
|
|
10
11
|
"cds-plugin.js",
|
|
11
12
|
"index.cds"
|
|
@@ -21,6 +22,8 @@
|
|
|
21
22
|
"multi-tenancy"
|
|
22
23
|
],
|
|
23
24
|
"scripts": {
|
|
25
|
+
"start": "PORT=4005 cds-serve",
|
|
26
|
+
"watch": "PORT=4005 cds watch",
|
|
24
27
|
"test:unit": "jest --testPathIgnorePatterns=\"/test-integration/\"",
|
|
25
28
|
"test:integration": "jest --testPathIgnorePatterns=\"/test/\" --runInBand --forceExit",
|
|
26
29
|
"voter:test:integration": "jest --testPathIgnorePatterns=\"/test/\" --forceExit",
|
|
@@ -45,7 +48,7 @@
|
|
|
45
48
|
},
|
|
46
49
|
"dependencies": {
|
|
47
50
|
"@sap/xssec": "^4.6.0",
|
|
48
|
-
"cron-parser": "^5.
|
|
51
|
+
"cron-parser": "^5.2.0",
|
|
49
52
|
"redis": "^4.7.0",
|
|
50
53
|
"verror": "^1.10.1",
|
|
51
54
|
"yaml": "^2.7.1"
|
|
@@ -76,7 +76,7 @@ class EventQueueProcessorBase {
|
|
|
76
76
|
this.__txMap = {};
|
|
77
77
|
this.__txRollback = {};
|
|
78
78
|
this.__queueEntries = [];
|
|
79
|
-
this.#keepAliveRunner = new SetIntervalDriftSafe(this.#eventConfig.keepAliveInterval);
|
|
79
|
+
this.#keepAliveRunner = new SetIntervalDriftSafe(this.#eventConfig.keepAliveInterval * 1000);
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
/**
|
|
@@ -617,7 +617,7 @@ class EventQueueProcessorBase {
|
|
|
617
617
|
"OR lastAttemptTimestamp IS NULL ) OR ( status =",
|
|
618
618
|
EventProcessingStatus.InProgress,
|
|
619
619
|
"AND lastAttemptTimestamp <=",
|
|
620
|
-
new Date(baseDate - this.#eventConfig.keepAliveMaxInProgressTime).toISOString(),
|
|
620
|
+
new Date(baseDate - this.#eventConfig.keepAliveMaxInProgressTime * 1000).toISOString(),
|
|
621
621
|
") )",
|
|
622
622
|
]
|
|
623
623
|
: [
|
|
@@ -628,7 +628,7 @@ class EventQueueProcessorBase {
|
|
|
628
628
|
") OR ( status =",
|
|
629
629
|
EventProcessingStatus.InProgress,
|
|
630
630
|
"AND lastAttemptTimestamp <=",
|
|
631
|
-
new Date(baseDate - this.#eventConfig.keepAliveMaxInProgressTime).toISOString(),
|
|
631
|
+
new Date(baseDate - this.#eventConfig.keepAliveMaxInProgressTime * 1000).toISOString(),
|
|
632
632
|
") )",
|
|
633
633
|
])
|
|
634
634
|
)
|
|
@@ -868,7 +868,7 @@ class EventQueueProcessorBase {
|
|
|
868
868
|
}
|
|
869
869
|
|
|
870
870
|
continuesKeepAlive() {
|
|
871
|
-
if (Date.now() - this.lockAcquiredTime.getTime() >= this.#eventConfig.keepAliveInterval) {
|
|
871
|
+
if (Date.now() - this.lockAcquiredTime.getTime() >= this.#eventConfig.keepAliveInterval * 1000) {
|
|
872
872
|
trace(this.baseContext, "keepAlive-between-iterations", async () => {
|
|
873
873
|
await this.#renewDistributedLock();
|
|
874
874
|
}).catch((err) => this.logger.error("renewing lock between intervals failed!", err));
|
|
@@ -961,7 +961,7 @@ class EventQueueProcessorBase {
|
|
|
961
961
|
const lockAcquired = await distributedLock.acquireLock(
|
|
962
962
|
this.__context,
|
|
963
963
|
[this.#eventType, this.#eventSubType].join("##"),
|
|
964
|
-
{ keepTrackOfLock: true, expiryTime: this.#eventConfig.keepAliveMaxInProgressTime }
|
|
964
|
+
{ keepTrackOfLock: true, expiryTime: this.#eventConfig.keepAliveMaxInProgressTime * 1000 }
|
|
965
965
|
);
|
|
966
966
|
if (!lockAcquired) {
|
|
967
967
|
this.logger.debug("no lock available, exit processing", {
|
|
@@ -983,7 +983,7 @@ class EventQueueProcessorBase {
|
|
|
983
983
|
const lockAcquired = await distributedLock.renewLock(
|
|
984
984
|
this.__context,
|
|
985
985
|
[this.#eventType, this.#eventSubType].join("##"),
|
|
986
|
-
{ expiryTime: this.#eventConfig.keepAliveMaxInProgressTime }
|
|
986
|
+
{ expiryTime: this.#eventConfig.keepAliveMaxInProgressTime * 1000 }
|
|
987
987
|
);
|
|
988
988
|
if (!lockAcquired) {
|
|
989
989
|
this.logger.error("renewing distributed lock failed!", {
|
package/src/config.js
CHANGED
|
@@ -117,6 +117,7 @@ class Config {
|
|
|
117
117
|
#tenantIdFilterEventProcessingCb;
|
|
118
118
|
#configEvents;
|
|
119
119
|
#configPeriodicEvents;
|
|
120
|
+
#enableAdminService;
|
|
120
121
|
static #instance;
|
|
121
122
|
constructor() {
|
|
122
123
|
this.#logger = cds.log(COMPONENT_NAME);
|
|
@@ -549,7 +550,7 @@ class Config {
|
|
|
549
550
|
event.load = event.load ?? DEFAULT_LOAD;
|
|
550
551
|
event.priority = event.priority ?? DEFAULT_PRIORITY;
|
|
551
552
|
event.increasePriorityOverTime = event.increasePriorityOverTime ?? DEFAULT_INCREASE_PRIORITY;
|
|
552
|
-
event.keepAliveInterval =
|
|
553
|
+
event.keepAliveInterval = event.keepAliveInterval ?? DEFAULT_KEEP_ALIVE_INTERVAL;
|
|
553
554
|
event.keepAliveMaxInProgressTime = event.keepAliveInterval * DEFAULT_MAX_FACTOR_STUCK_2_KEEP_ALIVE_INTERVAL;
|
|
554
555
|
event.checkForNextChunk = event.checkForNextChunk ?? DEFAULT_CHECK_FOR_NEXT_CHUNK;
|
|
555
556
|
}
|
|
@@ -971,6 +972,18 @@ class Config {
|
|
|
971
972
|
return this.#eventMap;
|
|
972
973
|
}
|
|
973
974
|
|
|
975
|
+
get developmentMode() {
|
|
976
|
+
return cds.env.profiles.find((profile) => profile === "development");
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
get enableAdminService() {
|
|
980
|
+
return this.#enableAdminService;
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
set enableAdminService(value) {
|
|
984
|
+
this.#enableAdminService = value;
|
|
985
|
+
}
|
|
986
|
+
|
|
974
987
|
/**
|
|
975
988
|
@return { Config }
|
|
976
989
|
**/
|
package/src/index.d.ts
CHANGED
|
@@ -257,6 +257,8 @@ declare class Config {
|
|
|
257
257
|
get insertEventsBeforeCommit(): any;
|
|
258
258
|
set enableTelemetry(value: any);
|
|
259
259
|
get enableTelemetry(): any;
|
|
260
|
+
set enableAdminService(value: any);
|
|
261
|
+
get enableAdminService(): any;
|
|
260
262
|
get isMultiTenancy(): boolean;
|
|
261
263
|
}
|
|
262
264
|
|
package/src/initialize.js
CHANGED
|
@@ -46,6 +46,7 @@ const CONFIG_VARS = [
|
|
|
46
46
|
["redisNamespace", null],
|
|
47
47
|
["publishEventBlockList", true],
|
|
48
48
|
["crashOnRedisUnavailable", false],
|
|
49
|
+
["enableAdminService", false],
|
|
49
50
|
];
|
|
50
51
|
|
|
51
52
|
/**
|
|
@@ -87,7 +88,7 @@ const initialize = async (options = {}) => {
|
|
|
87
88
|
"Event queue initialization skipped: no configFilePath provided, and event queue is not configured as a CAP outbox."
|
|
88
89
|
);
|
|
89
90
|
}
|
|
90
|
-
|
|
91
|
+
_disableAdminService();
|
|
91
92
|
const redisEnabled = config.checkRedisEnabled();
|
|
92
93
|
let resolveFn;
|
|
93
94
|
let initFinished = new Promise((resolve) => (resolveFn = resolve));
|
|
@@ -201,34 +202,31 @@ const mixConfigVarsWithEnv = (options) => {
|
|
|
201
202
|
};
|
|
202
203
|
|
|
203
204
|
const registerCdsShutdown = () => {
|
|
204
|
-
const isTestProfile = cds.env.profiles.find((profile) => profile.includes("test"));
|
|
205
|
-
if (isTestProfile || !config.redisEnabled) {
|
|
206
|
-
return;
|
|
207
|
-
}
|
|
208
205
|
cds.on("shutdown", async () => {
|
|
209
206
|
return await new Promise((resolve) => {
|
|
210
|
-
|
|
207
|
+
let timeoutRef;
|
|
208
|
+
timeoutRef = setTimeout(() => {
|
|
211
209
|
clearTimeout(timeoutRef);
|
|
212
210
|
cds.log(COMPONENT).info("shutdown timeout reached - some locks might not have been released!");
|
|
213
211
|
resolve();
|
|
214
212
|
}, TIMEOUT_SHUTDOWN);
|
|
215
|
-
distributedLock.shutdownHandler().then(() =>
|
|
216
|
-
Promise.allSettled(
|
|
213
|
+
distributedLock.shutdownHandler().then(() => {
|
|
214
|
+
Promise.allSettled(
|
|
215
|
+
config.redisEnabled ? [redis.closeMainClient(), redis.closeSubscribeClient()] : [Promise.resolve()]
|
|
216
|
+
).then((result) => {
|
|
217
217
|
clearTimeout(timeoutRef);
|
|
218
218
|
resolve(result);
|
|
219
|
-
})
|
|
220
|
-
);
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
221
|
});
|
|
222
222
|
});
|
|
223
223
|
};
|
|
224
224
|
|
|
225
225
|
const registerCleanupForDevDb = async () => {
|
|
226
|
-
|
|
227
|
-
if (!profile || process.env.NODE_ENV === "production") {
|
|
226
|
+
if (!config.developmentMode) {
|
|
228
227
|
return;
|
|
229
228
|
}
|
|
230
|
-
|
|
231
|
-
const tenantIds = await getAllTenantIds();
|
|
229
|
+
const tenantIds = config.isMultiTenancy ? await getAllTenantIds() : [null];
|
|
232
230
|
for (const tenantId of tenantIds) {
|
|
233
231
|
await cds.tx({ tenant: tenantId }, async (tx) => {
|
|
234
232
|
await tx.run(DELETE.from(config.tableNameEventLock));
|
|
@@ -260,6 +258,18 @@ const _registerUnsubscribe = () => {
|
|
|
260
258
|
});
|
|
261
259
|
};
|
|
262
260
|
|
|
261
|
+
const _disableAdminService = () => {
|
|
262
|
+
if (config.enableAdminService) {
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
cds.on("loaded", (model) => {
|
|
266
|
+
const srvDefinition = model.definitions["EventQueueAdminService"];
|
|
267
|
+
if (srvDefinition) {
|
|
268
|
+
srvDefinition["@protocol"] = "none";
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
};
|
|
272
|
+
|
|
263
273
|
module.exports = {
|
|
264
274
|
initialize,
|
|
265
275
|
};
|
package/src/processEventQueue.js
CHANGED
|
@@ -339,7 +339,7 @@ const _processEvent = async (eventTypeInstance, processContext, key, queueEntrie
|
|
|
339
339
|
return eventTypeInstance.handleErrorDuringProcessing(err, queueEntries);
|
|
340
340
|
}
|
|
341
341
|
},
|
|
342
|
-
{ traceContext }
|
|
342
|
+
{ traceContext, attributes: { eventIds: queueEntries.map(({ ID }) => ID) } }
|
|
343
343
|
);
|
|
344
344
|
};
|
|
345
345
|
|
package/src/runner/runner.js
CHANGED
|
@@ -22,7 +22,8 @@ const EVENT_QUEUE_RUN_ID = "RUN_ID";
|
|
|
22
22
|
const EVENT_QUEUE_RUN_TS = "RUN_TS";
|
|
23
23
|
const EVENT_QUEUE_RUN_REDIS_CHECK = "RUN_REDIS_CHECK";
|
|
24
24
|
const EVENT_QUEUE_UPDATE_PERIODIC_EVENTS = "UPDATE_PERIODIC_EVENTS";
|
|
25
|
-
|
|
25
|
+
|
|
26
|
+
let OFFSET_FIRST_RUN;
|
|
26
27
|
|
|
27
28
|
let tenantIdHash;
|
|
28
29
|
let singleRunDone;
|
|
@@ -399,7 +400,7 @@ const _acquireRunId = async (context) => {
|
|
|
399
400
|
};
|
|
400
401
|
|
|
401
402
|
const _calculateOffsetForFirstRun = async () => {
|
|
402
|
-
let offsetDependingOnLastRun = OFFSET_FIRST_RUN;
|
|
403
|
+
let offsetDependingOnLastRun = OFFSET_FIRST_RUN ?? (config.developmentMode ? 500 : 10 * 1000);
|
|
403
404
|
const now = Date.now();
|
|
404
405
|
// NOTE: this is only supported with Redis, because this is a tenant agnostic information
|
|
405
406
|
// currently there is no proper place to store this information beside t0 schema
|
package/src/shared/cdsHelper.js
CHANGED
|
@@ -116,21 +116,29 @@ async function executeInNewTransaction(context = {}, transactionTag, fn, args, {
|
|
|
116
116
|
}
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
-
const
|
|
119
|
+
const _getAllTenantBase = async () => {
|
|
120
120
|
if (!config.isMultiTenancy) {
|
|
121
121
|
return null;
|
|
122
122
|
}
|
|
123
123
|
|
|
124
124
|
// NOTE: tmp workaround until cds-mtxs fixes the connect.to service
|
|
125
125
|
for (let i = 0; i < 10; i++) {
|
|
126
|
-
if (cds.services["saas-registry"]) {
|
|
126
|
+
if (cds.services["cds.xt.SaasProvisioningService"] || cds.services["saas-registry"]) {
|
|
127
127
|
break;
|
|
128
128
|
}
|
|
129
129
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
130
130
|
}
|
|
131
131
|
|
|
132
132
|
const ssp = await cds.connect.to("cds.xt.SaasProvisioningService");
|
|
133
|
-
|
|
133
|
+
return await ssp.get("/tenant");
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const getAllTenantIds = async () => {
|
|
137
|
+
const response = await _getAllTenantBase();
|
|
138
|
+
if (!response) {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
|
|
134
142
|
return response
|
|
135
143
|
.map((tenant) => tenant.subscribedTenantId ?? tenant.tenant)
|
|
136
144
|
.reduce(async (result, tenantId) => {
|
|
@@ -142,7 +150,27 @@ const getAllTenantIds = async () => {
|
|
|
142
150
|
}, []);
|
|
143
151
|
};
|
|
144
152
|
|
|
153
|
+
const getAllTenantWithSubdomain = async () => {
|
|
154
|
+
const response = await _getAllTenantBase();
|
|
155
|
+
if (!response) {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return response.reduce(async (result, row) => {
|
|
160
|
+
const tenantId = row.subscribedTenantId ?? row.tenant;
|
|
161
|
+
result = await result;
|
|
162
|
+
if (await common.isTenantIdValidCb(TenantIdCheckTypes.eventProcessing, tenantId)) {
|
|
163
|
+
result.push({
|
|
164
|
+
ID: tenantId,
|
|
165
|
+
subdomain: row.subscribedSubdomain,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
return result;
|
|
169
|
+
}, []);
|
|
170
|
+
};
|
|
171
|
+
|
|
145
172
|
module.exports = {
|
|
146
173
|
executeInNewTransaction,
|
|
147
174
|
getAllTenantIds,
|
|
175
|
+
getAllTenantWithSubdomain,
|
|
148
176
|
};
|
|
@@ -17,7 +17,7 @@ const acquireLock = async (
|
|
|
17
17
|
if (config.redisEnabled) {
|
|
18
18
|
return await _acquireLockRedis(context, fullKey, expiryTime, { keepTrackOfLock });
|
|
19
19
|
} else {
|
|
20
|
-
return await _acquireLockDB(context, fullKey, expiryTime);
|
|
20
|
+
return await _acquireLockDB(context, fullKey, expiryTime, { keepTrackOfLock });
|
|
21
21
|
}
|
|
22
22
|
};
|
|
23
23
|
|
|
@@ -73,7 +73,7 @@ const _acquireLockRedis = async (
|
|
|
73
73
|
context,
|
|
74
74
|
fullKey,
|
|
75
75
|
expiryTime,
|
|
76
|
-
{ value =
|
|
76
|
+
{ value = Date.now(), overrideValue = false, keepTrackOfLock } = {}
|
|
77
77
|
) => {
|
|
78
78
|
const client = await redis.createMainClientAndConnect(config.redisOptions);
|
|
79
79
|
const result = await client.set(fullKey, value, {
|
|
@@ -82,7 +82,7 @@ const _acquireLockRedis = async (
|
|
|
82
82
|
});
|
|
83
83
|
const isOk = result === REDIS_COMMAND_OK;
|
|
84
84
|
if (isOk && keepTrackOfLock) {
|
|
85
|
-
existingLocks[fullKey] =
|
|
85
|
+
existingLocks[fullKey] = context.tenant;
|
|
86
86
|
}
|
|
87
87
|
return isOk;
|
|
88
88
|
};
|
|
@@ -129,9 +129,15 @@ const _releaseLockDb = async (context, fullKey) => {
|
|
|
129
129
|
await cdsHelper.executeInNewTransaction(context, "distributedLock-release", async (tx) => {
|
|
130
130
|
await tx.run(DELETE.from(config.tableNameEventLock).where("code =", fullKey));
|
|
131
131
|
});
|
|
132
|
+
delete existingLocks[fullKey];
|
|
132
133
|
};
|
|
133
134
|
|
|
134
|
-
const _acquireLockDB = async (
|
|
135
|
+
const _acquireLockDB = async (
|
|
136
|
+
context,
|
|
137
|
+
fullKey,
|
|
138
|
+
expiryTime,
|
|
139
|
+
{ value = "true", overrideValue = false, keepTrackOfLock } = {}
|
|
140
|
+
) => {
|
|
135
141
|
let result;
|
|
136
142
|
await cdsHelper.executeInNewTransaction(context, "distributedLock-acquire", async (tx) => {
|
|
137
143
|
try {
|
|
@@ -171,6 +177,9 @@ const _acquireLockDB = async (context, fullKey, expiryTime, { value = "true", ov
|
|
|
171
177
|
}
|
|
172
178
|
}
|
|
173
179
|
});
|
|
180
|
+
if (result && keepTrackOfLock) {
|
|
181
|
+
existingLocks[fullKey] = context.tenant;
|
|
182
|
+
}
|
|
174
183
|
return result;
|
|
175
184
|
};
|
|
176
185
|
|
|
@@ -181,14 +190,58 @@ const _generateKey = (context, tenantScoped, key) => {
|
|
|
181
190
|
return `${keyParts.join("##")}`;
|
|
182
191
|
};
|
|
183
192
|
|
|
193
|
+
const getAllLocksRedis = async () => {
|
|
194
|
+
const client = await redis.createMainClientAndConnect(config.redisOptions);
|
|
195
|
+
const keys = await client.keys(`${config.redisOptions.redisNamespace}*`);
|
|
196
|
+
const relevantKeys = {};
|
|
197
|
+
for (const key of keys) {
|
|
198
|
+
const [, tenant, guidOrType, subType] = key.split("##");
|
|
199
|
+
if (subType) {
|
|
200
|
+
relevantKeys[key] = { tenant, guidOrType, subType };
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (!Object.keys(relevantKeys)) {
|
|
205
|
+
return [];
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const pipeline = client.multi();
|
|
209
|
+
for (const key in relevantKeys) {
|
|
210
|
+
pipeline.ttl(key);
|
|
211
|
+
pipeline.get(key);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const replies = await pipeline.exec();
|
|
215
|
+
|
|
216
|
+
let counter = 0;
|
|
217
|
+
const result = [];
|
|
218
|
+
for (const value of Object.values(relevantKeys)) {
|
|
219
|
+
const ttl = replies[counter];
|
|
220
|
+
const createdAt = replies[counter + 1];
|
|
221
|
+
result.push({
|
|
222
|
+
tenant: value.tenant,
|
|
223
|
+
type: value.guidOrType,
|
|
224
|
+
subType: value.subType,
|
|
225
|
+
ttl,
|
|
226
|
+
createdAt,
|
|
227
|
+
});
|
|
228
|
+
counter = counter + 2;
|
|
229
|
+
}
|
|
230
|
+
return result;
|
|
231
|
+
};
|
|
232
|
+
|
|
184
233
|
const shutdownHandler = async () => {
|
|
185
234
|
const logger = cds.log(COMPONENT_NAME);
|
|
186
235
|
logger.info("received shutdown event, trying to release all locks", {
|
|
187
236
|
numberOfLocks: Object.keys(existingLocks).length,
|
|
188
237
|
});
|
|
189
238
|
const result = await Promise.allSettled(
|
|
190
|
-
Object.
|
|
191
|
-
|
|
239
|
+
Object.entries(existingLocks).map(async ([key, tenant]) => {
|
|
240
|
+
if (config.redisEnabled) {
|
|
241
|
+
await _releaseLockRedis({ tenant }, key);
|
|
242
|
+
} else {
|
|
243
|
+
await _releaseLockDb({ tenant }, key);
|
|
244
|
+
}
|
|
192
245
|
logger.info("lock released", { key });
|
|
193
246
|
})
|
|
194
247
|
);
|
|
@@ -206,4 +259,5 @@ module.exports = {
|
|
|
206
259
|
setValueWithExpire,
|
|
207
260
|
shutdownHandler,
|
|
208
261
|
renewLock,
|
|
262
|
+
getAllLocksRedis,
|
|
209
263
|
};
|
|
@@ -85,11 +85,22 @@ const _startOtelTrace = async (ctxWithSpan, traceContext, span, fn) => {
|
|
|
85
85
|
const _setAttributes = (context, span, attributes) => {
|
|
86
86
|
span.setAttribute("sap.tenancy.tenant_id", context.tenant);
|
|
87
87
|
span.setAttribute("sap.correlation_id", context.id);
|
|
88
|
+
_sanitizeAttributes(attributes);
|
|
88
89
|
for (const attributeKey in attributes) {
|
|
89
90
|
span.setAttribute(attributeKey, attributes[attributeKey]);
|
|
90
91
|
}
|
|
91
92
|
};
|
|
92
93
|
|
|
94
|
+
const _sanitizeAttributes = (attributes = {}) => {
|
|
95
|
+
for (const attributeKey in attributes) {
|
|
96
|
+
attributes[attributeKey] =
|
|
97
|
+
typeof attributes[attributeKey] !== "string"
|
|
98
|
+
? JSON.stringify(attributes[attributeKey])
|
|
99
|
+
: attributes[attributeKey];
|
|
100
|
+
}
|
|
101
|
+
return attributes;
|
|
102
|
+
};
|
|
103
|
+
|
|
93
104
|
const getCurrentTraceContext = () => {
|
|
94
105
|
if (!otel) {
|
|
95
106
|
return null;
|
package/srv/index.cds
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
using from './service';
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
using sap.eventqueue as db from '../../db';
|
|
2
|
+
|
|
3
|
+
@path: 'event-queue/admin'
|
|
4
|
+
@impl: './admin-service.js'
|
|
5
|
+
@requires: 'internal-user'
|
|
6
|
+
service EventQueueAdminService {
|
|
7
|
+
|
|
8
|
+
@readonly
|
|
9
|
+
@cds.persistence.skip
|
|
10
|
+
entity Event as projection on db.Event {
|
|
11
|
+
null as tenant: String,
|
|
12
|
+
null as landscape: String,
|
|
13
|
+
null as space: String,
|
|
14
|
+
*
|
|
15
|
+
} actions {
|
|
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
|
+
|
|
25
|
+
@cds.persistence.exists
|
|
26
|
+
entity Lock {
|
|
27
|
+
key tenant: String;
|
|
28
|
+
key type: String;
|
|
29
|
+
key subType: String;
|
|
30
|
+
landscape: String;
|
|
31
|
+
space: String;
|
|
32
|
+
ttl: Integer;
|
|
33
|
+
createdAt: Integer;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
@readonly
|
|
37
|
+
@cds.persistence.skip
|
|
38
|
+
entity Tenant {
|
|
39
|
+
Key ID: String;
|
|
40
|
+
subdomain: String;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const cds = require("@sap/cds");
|
|
4
|
+
const cdsHelper = require("../../src/shared/cdsHelper");
|
|
5
|
+
const { EventProcessingStatus } = require("../../src");
|
|
6
|
+
const config = require("../../src/config");
|
|
7
|
+
const distributedLock = require("../../src/shared/distributedLock");
|
|
8
|
+
|
|
9
|
+
module.exports = class AdminService extends cds.ApplicationService {
|
|
10
|
+
async init() {
|
|
11
|
+
const { Event: EventService, Tenant, Lock: LockService } = this.entities();
|
|
12
|
+
const { Event: EventDb } = cds.db.entities("sap.eventqueue");
|
|
13
|
+
const { landscape, space } = this.getLandscapeAndSpace();
|
|
14
|
+
|
|
15
|
+
this.before("*", (req) => {
|
|
16
|
+
if (!config.enableAdminService) {
|
|
17
|
+
req.reject(403, "Admin service is disabled by configuration");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (req.target.name === Tenant.name) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
const headers = Object.assign({}, req.headers, req.req?.headers);
|
|
24
|
+
const tenant = headers["z-id"] ?? req.data.tenant;
|
|
25
|
+
|
|
26
|
+
if (config.isMultiTenancy && tenant == null) {
|
|
27
|
+
req.reject(400, "Missing tenant ID in request header (z-id)");
|
|
28
|
+
}
|
|
29
|
+
req.headers ??= {};
|
|
30
|
+
req.headers["z-id"] = tenant;
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
this.on("READ", EventService, async (req) => {
|
|
34
|
+
const tenant = req.headers["z-id"];
|
|
35
|
+
return await cds.tx({ tenant: tenant }, async (tx) => {
|
|
36
|
+
if (req.query.SELECT.from.ref[0].id) {
|
|
37
|
+
req.query.SELECT.from.ref[0].id = EventDb.name;
|
|
38
|
+
} else {
|
|
39
|
+
req.query.SELECT.from.ref[0] = EventDb.name;
|
|
40
|
+
}
|
|
41
|
+
const events = await tx.run(req.query);
|
|
42
|
+
return events?.map((event) => {
|
|
43
|
+
event.landscape = landscape;
|
|
44
|
+
event.space = space;
|
|
45
|
+
event.tenant = tenant;
|
|
46
|
+
return event;
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
this.on("READ", LockService, async () => {
|
|
52
|
+
if (!config.redisEnabled) {
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
const locks = await distributedLock.getAllLocksRedis();
|
|
56
|
+
return locks.map((lock) => ({
|
|
57
|
+
...lock,
|
|
58
|
+
landscape: landscape,
|
|
59
|
+
space: space,
|
|
60
|
+
}));
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
this.on("READ", Tenant, async () => {
|
|
64
|
+
const tenants = await cdsHelper.getAllTenantWithSubdomain();
|
|
65
|
+
return tenants ?? [];
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
this.on("setStatusAndAttempts", async (req) => {
|
|
69
|
+
const tenant = req.headers["z-id"];
|
|
70
|
+
cds.log("eventQueue").info("Restarting processing for event queue");
|
|
71
|
+
const updateData = {};
|
|
72
|
+
|
|
73
|
+
if (Number.isInteger(req.data.attempts)) {
|
|
74
|
+
updateData.attempts = req.data.attempts;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (Object.values(EventProcessingStatus).includes(req.data.status)) {
|
|
78
|
+
updateData.status = req.data.status;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (!Object.keys(updateData).length) {
|
|
82
|
+
return req.reject(400, "No status or attempts provided");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
await cds.tx({ tenant, headers: { "z-id": tenant } }, async () => {
|
|
86
|
+
await UPDATE.entity(EventDb)
|
|
87
|
+
.set(updateData)
|
|
88
|
+
.where({ ID: req.params[0].ID ?? req.params[0] });
|
|
89
|
+
});
|
|
90
|
+
return await this.send(new cds.Request({ query: req.query, headers: req.headers }));
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
await super.init();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
getLandscapeAndSpace() {
|
|
97
|
+
const url = cds.requires.db.credentials.url;
|
|
98
|
+
if (!url) {
|
|
99
|
+
return { landscape: "eu10-canary", space: "local-dev" };
|
|
100
|
+
}
|
|
101
|
+
const match = url.match(/https?:\/\/[^.]+\.authentication\.([^.]+)\.hana\.ondemand\.com/);
|
|
102
|
+
const landscape = (match?.[1] ?? "sap") === "sap" ? "eu10-canary" : match?.[1];
|
|
103
|
+
let space = "local-dev";
|
|
104
|
+
try {
|
|
105
|
+
space = JSON.parse(process.env.VCAP_APPLICATION)?.space_name;
|
|
106
|
+
} catch {
|
|
107
|
+
/* empty */
|
|
108
|
+
}
|
|
109
|
+
return { landscape, space };
|
|
110
|
+
}
|
|
111
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
using from './admin-service';
|