@cap-js-community/event-queue 1.7.2 → 1.7.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/cds-plugin.js +7 -1
- package/db/Event.cds +1 -0
- package/package.json +4 -4
- package/src/EventQueueError.js +6 -2
- package/src/config.js +18 -0
- package/src/constants.js +5 -0
- package/src/index.d.ts +2 -0
- package/src/initialize.js +19 -7
- package/src/outbox/EventQueueGenericOutboxHandler.js +1 -1
- package/src/redis/redisPub.js +1 -1
- package/src/redis/redisSub.js +3 -3
- package/src/runner/openEvents.js +19 -4
- package/src/runner/runner.js +87 -11
- package/src/shared/SetIntervalDriftSafe.js +0 -8
- package/src/shared/cdsHelper.js +3 -4
- package/src/shared/common.js +38 -23
- package/src/shared/distributedLock.js +5 -2
- package/src/shared/eventScheduler.js +1 -10
- package/src/shared/redis.js +23 -18
package/cds-plugin.js
CHANGED
|
@@ -4,6 +4,7 @@ const cds = require("@sap/cds");
|
|
|
4
4
|
const cdsPackage = require("@sap/cds/package.json");
|
|
5
5
|
|
|
6
6
|
const eventQueue = require("./src");
|
|
7
|
+
const EventQueueError = require("./src/EventQueueError");
|
|
7
8
|
const COMPONENT_NAME = "/eventQueue/plugin";
|
|
8
9
|
const SERVE_COMMAND = "serve";
|
|
9
10
|
|
|
@@ -18,5 +19,10 @@ if ((doLegacyBuildDetection && isBuild) || (!doLegacyBuildDetection && !isServe)
|
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
if (Object.keys(cds.env.eventQueue ?? {}).length) {
|
|
21
|
-
module.exports = eventQueue.initialize().catch((err) =>
|
|
22
|
+
module.exports = eventQueue.initialize().catch((err) => {
|
|
23
|
+
if (EventQueueError.isRedisConnectionFailure(err) && eventQueue.config.crashOnRedisUnavailable) {
|
|
24
|
+
throw err;
|
|
25
|
+
}
|
|
26
|
+
cds.log(COMPONENT_NAME).error(err);
|
|
27
|
+
});
|
|
22
28
|
}
|
package/db/Event.cds
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cap-js-community/event-queue",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.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",
|
|
@@ -47,13 +47,13 @@
|
|
|
47
47
|
"cron-parser": "^4.9.0",
|
|
48
48
|
"redis": "^4.7.0",
|
|
49
49
|
"verror": "^1.10.1",
|
|
50
|
-
"yaml": "^2.
|
|
50
|
+
"yaml": "^2.6.1"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
53
|
"@cap-js/hana": "^1.3.0",
|
|
54
54
|
"@cap-js/sqlite": "^1.7.3",
|
|
55
|
-
"@sap/cds": "^8.
|
|
56
|
-
"@sap/cds-dk": "^8.
|
|
55
|
+
"@sap/cds": "^8.4.2",
|
|
56
|
+
"@sap/cds-dk": "^8.4.2",
|
|
57
57
|
"eslint": "^8.57.0",
|
|
58
58
|
"eslint-config-prettier": "^9.1.0",
|
|
59
59
|
"eslint-plugin-jest": "^28.6.0",
|
package/src/EventQueueError.js
CHANGED
|
@@ -41,7 +41,7 @@ const ERROR_CODES_META = {
|
|
|
41
41
|
message: "error during create client with redis-cache service",
|
|
42
42
|
},
|
|
43
43
|
[ERROR_CODES.REDIS_LOCAL_NO_RECONNECT]: {
|
|
44
|
-
message: "disabled reconnect, because
|
|
44
|
+
message: "disabled reconnect, because not running on cloud foundry",
|
|
45
45
|
},
|
|
46
46
|
[ERROR_CODES.MISSING_TABLE_DEFINITION]: {
|
|
47
47
|
message: "Could not find table in csn. Make sure the provided table name is correct and the table is known by CDS.",
|
|
@@ -135,7 +135,7 @@ class EventQueueError extends VError {
|
|
|
135
135
|
return new EventQueueError(
|
|
136
136
|
{
|
|
137
137
|
name: ERROR_CODES.REDIS_CREATE_CLIENT,
|
|
138
|
-
cause: err,
|
|
138
|
+
...(err && { cause: err }),
|
|
139
139
|
},
|
|
140
140
|
message
|
|
141
141
|
);
|
|
@@ -325,6 +325,10 @@ class EventQueueError extends VError {
|
|
|
325
325
|
message
|
|
326
326
|
);
|
|
327
327
|
}
|
|
328
|
+
|
|
329
|
+
static isRedisConnectionFailure(err) {
|
|
330
|
+
return err instanceof VError && err.name === ERROR_CODES.REDIS_CREATE_CLIENT;
|
|
331
|
+
}
|
|
328
332
|
}
|
|
329
333
|
|
|
330
334
|
module.exports = EventQueueError;
|
package/src/config.js
CHANGED
|
@@ -77,6 +77,8 @@ class Config {
|
|
|
77
77
|
#unsubscribedTenants = {};
|
|
78
78
|
#cronTimezone;
|
|
79
79
|
#publishEventBlockList;
|
|
80
|
+
#crashOnRedisUnavailable;
|
|
81
|
+
#tenantIdFilterCb;
|
|
80
82
|
static #instance;
|
|
81
83
|
constructor() {
|
|
82
84
|
this.#logger = cds.log(COMPONENT_NAME);
|
|
@@ -510,6 +512,22 @@ class Config {
|
|
|
510
512
|
this.#publishEventBlockList = value;
|
|
511
513
|
}
|
|
512
514
|
|
|
515
|
+
get crashOnRedisUnavailable() {
|
|
516
|
+
return this.#crashOnRedisUnavailable;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
set crashOnRedisUnavailable(value) {
|
|
520
|
+
this.#crashOnRedisUnavailable = value;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
get tenantIdFilterCb() {
|
|
524
|
+
return this.#tenantIdFilterCb;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
set tenantIdFilterCb(value) {
|
|
528
|
+
this.#tenantIdFilterCb = value;
|
|
529
|
+
}
|
|
530
|
+
|
|
513
531
|
set globalTxTimeout(value) {
|
|
514
532
|
this.#globalTxTimeout = value;
|
|
515
533
|
}
|
package/src/constants.js
CHANGED
|
@@ -7,6 +7,7 @@ module.exports = {
|
|
|
7
7
|
Done: 2,
|
|
8
8
|
Error: 3,
|
|
9
9
|
Exceeded: 4,
|
|
10
|
+
Suspended: 5,
|
|
10
11
|
},
|
|
11
12
|
TransactionMode: {
|
|
12
13
|
isolated: "isolated",
|
|
@@ -19,4 +20,8 @@ module.exports = {
|
|
|
19
20
|
High: "high",
|
|
20
21
|
VeryHigh: "veryHigh",
|
|
21
22
|
},
|
|
23
|
+
TenantIdCheckTypes: {
|
|
24
|
+
getAllTenantIds: "getAllTenantIds",
|
|
25
|
+
getTokenInfo: "getTokenInfo",
|
|
26
|
+
},
|
|
22
27
|
};
|
package/src/index.d.ts
CHANGED
|
@@ -182,6 +182,8 @@ declare class Config {
|
|
|
182
182
|
hasEventAfterCommitFlag(type: string, subType: string): boolean;
|
|
183
183
|
_checkRedisIsBound(): boolean;
|
|
184
184
|
checkRedisEnabled(): boolean;
|
|
185
|
+
publishEventBlockList(): boolean;
|
|
186
|
+
crashOnRedisUnavailable(): boolean;
|
|
185
187
|
attachConfigChangeHandler(): void;
|
|
186
188
|
attachRedisUnsubscribeHandler(): void;
|
|
187
189
|
executeUnsubscribeHandlers(tenantId: string): void;
|
package/src/initialize.js
CHANGED
|
@@ -16,6 +16,7 @@ const eventQueueAsOutbox = require("./outbox/eventQueueAsOutbox");
|
|
|
16
16
|
const { getAllTenantIds } = require("./shared/cdsHelper");
|
|
17
17
|
const { EventProcessingStatus } = require("./constants");
|
|
18
18
|
const distributedLock = require("./shared/distributedLock");
|
|
19
|
+
const EventQueueError = require("./EventQueueError");
|
|
19
20
|
|
|
20
21
|
const readFileAsync = promisify(fs.readFile);
|
|
21
22
|
|
|
@@ -39,6 +40,8 @@ const CONFIG_VARS = [
|
|
|
39
40
|
["enableCAPTelemetry", false],
|
|
40
41
|
["cronTimezone", null],
|
|
41
42
|
["publishEventBlockList", true],
|
|
43
|
+
["crashOnRedisUnavailable", false],
|
|
44
|
+
["tenantIdFilterCb", null],
|
|
42
45
|
];
|
|
43
46
|
|
|
44
47
|
/**
|
|
@@ -61,6 +64,8 @@ const CONFIG_VARS = [
|
|
|
61
64
|
* @param {boolean} [options.enableCAPTelemetry=false] - Enable telemetry for CAP.
|
|
62
65
|
* @param {string} [options.cronTimezone=null] - Default timezone for cron jobs.
|
|
63
66
|
* @param {string} [options.publishEventBlockList=true] - If redis is available event blocklist is distributed to all application instances
|
|
67
|
+
* @param {string} [options.crashOnRedisUnavailable=true] - If enabled an error is thrown if the redis connection check is not successful
|
|
68
|
+
* @param {function} [options.tenantIdFilterCb=null] - Allows to set customer filter function to filter the tenants ids which should be processed in the event-queue
|
|
64
69
|
*/
|
|
65
70
|
const initialize = async (options = {}) => {
|
|
66
71
|
if (config.initialized) {
|
|
@@ -89,6 +94,9 @@ const initialize = async (options = {}) => {
|
|
|
89
94
|
});
|
|
90
95
|
if (redisEnabled) {
|
|
91
96
|
config.redisEnabled = await redis.connectionCheck(config.redisOptions);
|
|
97
|
+
if (!config.redisEnabled && config.crashOnRedisUnavailable) {
|
|
98
|
+
throw EventQueueError.redisConnectionFailure();
|
|
99
|
+
}
|
|
92
100
|
}
|
|
93
101
|
config.fileContent = await readConfigFromFile(config.configFilePath);
|
|
94
102
|
|
|
@@ -139,17 +147,21 @@ const registerEventProcessors = () => {
|
|
|
139
147
|
|
|
140
148
|
const errorHandler = (err) => cds.log(COMPONENT).error("error during init runner", err);
|
|
141
149
|
|
|
142
|
-
if (!config.isMultiTenancy) {
|
|
143
|
-
runner.singleTenant().catch(errorHandler);
|
|
144
|
-
return;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
150
|
if (config.redisEnabled) {
|
|
148
151
|
initEventQueueRedisSubscribe();
|
|
149
152
|
config.attachConfigChangeHandler();
|
|
150
|
-
|
|
151
|
-
|
|
153
|
+
if (config.isMultiTenancy) {
|
|
154
|
+
runner.multiTenancyRedis().catch(errorHandler);
|
|
155
|
+
} else {
|
|
156
|
+
runner.singleTenantRedis().catch(errorHandler);
|
|
157
|
+
}
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (config.isMultiTenancy) {
|
|
152
162
|
runner.multiTenancyDb().catch(errorHandler);
|
|
163
|
+
} else {
|
|
164
|
+
runner.singleTenantDb().catch(errorHandler);
|
|
153
165
|
}
|
|
154
166
|
};
|
|
155
167
|
|
|
@@ -27,7 +27,7 @@ class EventQueueGenericOutboxHandler extends EventQueueBaseClass {
|
|
|
27
27
|
delete msg.contextUser;
|
|
28
28
|
processContext.user = new cds.User.Privileged({
|
|
29
29
|
id: userId,
|
|
30
|
-
authInfo: await common.
|
|
30
|
+
authInfo: await common.getTokenInfo(processContext.tenant),
|
|
31
31
|
});
|
|
32
32
|
processContext._eventQueue = { processor: this, key, queueEntries, payload };
|
|
33
33
|
await cds.unboxed(service).tx(processContext)[invocationFn](msg);
|
package/src/redis/redisPub.js
CHANGED
|
@@ -109,7 +109,7 @@ const _processLocalWithoutRedis = async (tenantId, events) => {
|
|
|
109
109
|
let context = {};
|
|
110
110
|
if (tenantId) {
|
|
111
111
|
const user = await cds.tx({ tenant: tenantId }, async () => {
|
|
112
|
-
return new cds.User.Privileged({ id: config.userId,
|
|
112
|
+
return new cds.User.Privileged({ id: config.userId, tokenInfo: await common.getTokenInfo(tenantId) });
|
|
113
113
|
});
|
|
114
114
|
context = {
|
|
115
115
|
tenant: tenantId,
|
package/src/redis/redisSub.js
CHANGED
|
@@ -41,14 +41,14 @@ const _messageHandlerProcessEvents = async (messageData) => {
|
|
|
41
41
|
const service = await cds.connect.to(subType);
|
|
42
42
|
cds.outboxed(service);
|
|
43
43
|
} catch (err) {
|
|
44
|
-
logger.
|
|
44
|
+
logger.warn("could not connect to outboxed service", err, {
|
|
45
45
|
type,
|
|
46
46
|
subType,
|
|
47
47
|
});
|
|
48
48
|
return;
|
|
49
49
|
}
|
|
50
50
|
} else {
|
|
51
|
-
logger.
|
|
51
|
+
logger.warn("cannot find configuration for published event. Event won't be processed", {
|
|
52
52
|
type,
|
|
53
53
|
subType,
|
|
54
54
|
});
|
|
@@ -66,7 +66,7 @@ const _messageHandlerProcessEvents = async (messageData) => {
|
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
const user = await cds.tx({ tenant: tenantId }, async () => {
|
|
69
|
-
return new cds.User.Privileged({ id: config.userId,
|
|
69
|
+
return new cds.User.Privileged({ id: config.userId, tokenInfo: await common.getTokenInfo(tenantId) });
|
|
70
70
|
});
|
|
71
71
|
const tenantContext = {
|
|
72
72
|
tenant: tenantId,
|
package/src/runner/openEvents.js
CHANGED
|
@@ -5,6 +5,8 @@ const cds = require("@sap/cds");
|
|
|
5
5
|
const eventConfig = require("../config");
|
|
6
6
|
const { EventProcessingStatus } = require("../constants");
|
|
7
7
|
|
|
8
|
+
const MS_IN_DAYS = 24 * 60 * 60 * 1000;
|
|
9
|
+
|
|
8
10
|
const getOpenQueueEntries = async (tx, filterAppSpecificEvents = true) => {
|
|
9
11
|
const startTime = new Date();
|
|
10
12
|
const refDateStartAfter = new Date(startTime.getTime() + eventConfig.runInterval * 1.2);
|
|
@@ -21,7 +23,11 @@ const getOpenQueueEntries = async (tx, filterAppSpecificEvents = true) => {
|
|
|
21
23
|
EventProcessingStatus.InProgress,
|
|
22
24
|
"AND lastAttemptTimestamp <=",
|
|
23
25
|
new Date(startTime.getTime() - eventConfig.globalTxTimeout).toISOString(),
|
|
24
|
-
") )"
|
|
26
|
+
") ) AND (createdAt >=",
|
|
27
|
+
new Date(startTime.getTime() - 30 * MS_IN_DAYS).toISOString(),
|
|
28
|
+
" OR startAfter >=",
|
|
29
|
+
new Date(startTime.getTime() - 30 * MS_IN_DAYS).toISOString(),
|
|
30
|
+
")"
|
|
25
31
|
)
|
|
26
32
|
.columns("type", "subType")
|
|
27
33
|
.groupBy("type", "subType")
|
|
@@ -30,9 +36,13 @@ const getOpenQueueEntries = async (tx, filterAppSpecificEvents = true) => {
|
|
|
30
36
|
const result = [];
|
|
31
37
|
for (const { type, subType } of entries) {
|
|
32
38
|
if (eventConfig.isCapOutboxEvent(type)) {
|
|
33
|
-
|
|
39
|
+
cds.connect
|
|
34
40
|
.to(subType)
|
|
35
41
|
.then((service) => {
|
|
42
|
+
if (!filterAppSpecificEvents) {
|
|
43
|
+
return; // will be done in finally
|
|
44
|
+
}
|
|
45
|
+
|
|
36
46
|
if (!service) {
|
|
37
47
|
return;
|
|
38
48
|
}
|
|
@@ -45,7 +55,12 @@ const getOpenQueueEntries = async (tx, filterAppSpecificEvents = true) => {
|
|
|
45
55
|
result.push({ type, subType });
|
|
46
56
|
}
|
|
47
57
|
})
|
|
48
|
-
.catch(() => {})
|
|
58
|
+
.catch(() => {})
|
|
59
|
+
.finally(() => {
|
|
60
|
+
if (!filterAppSpecificEvents) {
|
|
61
|
+
result.push({ type, subType });
|
|
62
|
+
}
|
|
63
|
+
});
|
|
49
64
|
} else {
|
|
50
65
|
if (filterAppSpecificEvents) {
|
|
51
66
|
if (
|
|
@@ -55,7 +70,7 @@ const getOpenQueueEntries = async (tx, filterAppSpecificEvents = true) => {
|
|
|
55
70
|
result.push({ type, subType });
|
|
56
71
|
}
|
|
57
72
|
} else {
|
|
58
|
-
|
|
73
|
+
result.push({ type, subType });
|
|
59
74
|
}
|
|
60
75
|
}
|
|
61
76
|
}
|
package/src/runner/runner.js
CHANGED
|
@@ -27,12 +27,14 @@ let OFFSET_FIRST_RUN = 10 * 1000;
|
|
|
27
27
|
let tenantIdHash;
|
|
28
28
|
let singleRunDone;
|
|
29
29
|
|
|
30
|
-
const
|
|
30
|
+
const singleTenantDb = () => _scheduleFunction(_checkPeriodicEventsSingleTenantOneTime, _singleTenantDb);
|
|
31
31
|
|
|
32
32
|
const multiTenancyDb = () => _scheduleFunction(async () => {}, _multiTenancyDb);
|
|
33
33
|
|
|
34
34
|
const multiTenancyRedis = () => _scheduleFunction(async () => {}, _multiTenancyRedis);
|
|
35
35
|
|
|
36
|
+
const singleTenantRedis = () => _scheduleFunction(_checkPeriodicEventsSingleTenantOneTime, _singleTenantRedis);
|
|
37
|
+
|
|
36
38
|
const _scheduleFunction = async (singleRunFn, periodicFn) => {
|
|
37
39
|
const logger = cds.log(COMPONENT_NAME);
|
|
38
40
|
const eventsForAutomaticRun = eventQueueConfig.allEvents;
|
|
@@ -132,7 +134,7 @@ const _executeEventsAllTenantsRedis = async (tenantIds) => {
|
|
|
132
134
|
async () => {
|
|
133
135
|
tx.context.user = new cds.User.Privileged({
|
|
134
136
|
id: config.userId,
|
|
135
|
-
authInfo: await common.
|
|
137
|
+
authInfo: await common.getTokenInfo(tenantId),
|
|
136
138
|
});
|
|
137
139
|
const entries = await openEvents.getOpenQueueEntries(tx, false);
|
|
138
140
|
logger.info("broadcasting events for run", {
|
|
@@ -166,10 +168,10 @@ const _executeEventsAllTenants = async (tenantIds, runId) => {
|
|
|
166
168
|
let tenantContext;
|
|
167
169
|
const events = await trace(
|
|
168
170
|
{ id, tenant: tenantId },
|
|
169
|
-
"fetch-openEvents-and-
|
|
171
|
+
"fetch-openEvents-and-tokenInfo",
|
|
170
172
|
async () => {
|
|
171
173
|
const user = await cds.tx({ tenant: tenantId }, async () => {
|
|
172
|
-
return new cds.User.Privileged({ id: config.userId,
|
|
174
|
+
return new cds.User.Privileged({ id: config.userId, tokenInfo: await common.getTokenInfo(tenantId) });
|
|
173
175
|
});
|
|
174
176
|
tenantContext = {
|
|
175
177
|
tenant: tenantId,
|
|
@@ -227,7 +229,7 @@ const _executePeriodicEventsAllTenants = async (tenantIds) => {
|
|
|
227
229
|
for (const tenantId of tenantIds) {
|
|
228
230
|
try {
|
|
229
231
|
const user = await cds.tx({ tenant: tenantId }, async () => {
|
|
230
|
-
return new cds.User.Privileged({ id: config.userId,
|
|
232
|
+
return new cds.User.Privileged({ id: config.userId, tokenInfo: await common.getTokenInfo(tenantId) });
|
|
231
233
|
});
|
|
232
234
|
const tenantContext = {
|
|
233
235
|
tenant: tenantId,
|
|
@@ -258,7 +260,7 @@ const _singleTenantDb = async () => {
|
|
|
258
260
|
const id = cds.utils.uuid();
|
|
259
261
|
const events = await trace(
|
|
260
262
|
{ id },
|
|
261
|
-
"fetch-openEvents-and-
|
|
263
|
+
"fetch-openEvents-and-tokenInfo",
|
|
262
264
|
async () => {
|
|
263
265
|
return await cds.tx({}, async (tx) => {
|
|
264
266
|
return await openEvents.getOpenQueueEntries(tx);
|
|
@@ -300,6 +302,54 @@ const _singleTenantDb = async () => {
|
|
|
300
302
|
);
|
|
301
303
|
};
|
|
302
304
|
|
|
305
|
+
const _singleTenantRedis = async () => {
|
|
306
|
+
const id = cds.utils.uuid();
|
|
307
|
+
const logger = cds.log(COMPONENT_NAME);
|
|
308
|
+
try {
|
|
309
|
+
// NOTE: do checks for open events on one app instance distribute from this instance to all others
|
|
310
|
+
const dummyContext = new cds.EventContext({});
|
|
311
|
+
const couldAcquireLock = await trace(
|
|
312
|
+
dummyContext,
|
|
313
|
+
"acquire-lock-master-runner",
|
|
314
|
+
async () => {
|
|
315
|
+
return await distributedLock.acquireLock(dummyContext, EVENT_QUEUE_RUN_REDIS_CHECK, {
|
|
316
|
+
expiryTime: eventQueueConfig.runInterval * 0.95,
|
|
317
|
+
tenantScoped: false,
|
|
318
|
+
});
|
|
319
|
+
},
|
|
320
|
+
{ newRootSpan: true }
|
|
321
|
+
);
|
|
322
|
+
if (!couldAcquireLock) {
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
await trace(
|
|
327
|
+
{ id },
|
|
328
|
+
"get-openEvents-and-publish",
|
|
329
|
+
async () => {
|
|
330
|
+
return await cds.tx({}, async (tx) => {
|
|
331
|
+
const entries = await openEvents.getOpenQueueEntries(tx, false);
|
|
332
|
+
logger.info("broadcasting events for run", {
|
|
333
|
+
entries: entries.length,
|
|
334
|
+
});
|
|
335
|
+
if (!entries.length) {
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
// Do not wait until this is finished - as broadcastEvent has a retry mechanism and can delay this loop
|
|
339
|
+
redisPub.broadcastEvent(null, entries).catch((err) => {
|
|
340
|
+
logger.error("broadcasting event failed", err, {
|
|
341
|
+
entries: entries.length,
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
});
|
|
345
|
+
},
|
|
346
|
+
{ newRootSpan: true }
|
|
347
|
+
);
|
|
348
|
+
} catch (err) {
|
|
349
|
+
logger.info("executing event queue run for single tenant via redis", err);
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
|
|
303
353
|
const _acquireRunId = async (context) => {
|
|
304
354
|
let runId = randomUUID();
|
|
305
355
|
const couldSetValue = await distributedLock.setValueWithExpire(context, EVENT_QUEUE_RUN_ID, runId, {
|
|
@@ -401,9 +451,33 @@ const _multiTenancyPeriodicEvents = async (tenantIds) => {
|
|
|
401
451
|
}
|
|
402
452
|
};
|
|
403
453
|
|
|
404
|
-
const _checkPeriodicEventsSingleTenantOneTime = async () =>
|
|
405
|
-
|
|
406
|
-
|
|
454
|
+
const _checkPeriodicEventsSingleTenantOneTime = async () => {
|
|
455
|
+
const logger = cds.log(COMPONENT_NAME);
|
|
456
|
+
if (!eventQueueConfig.updatePeriodicEvents || !eventQueueConfig.periodicEvents.length) {
|
|
457
|
+
logger.info("updating of periodic events is disabled or no periodic events configured", {
|
|
458
|
+
updateEnabled: eventQueueConfig.updatePeriodicEvents,
|
|
459
|
+
events: eventQueueConfig.periodicEvents.length,
|
|
460
|
+
});
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const dummyContext = new cds.EventContext({});
|
|
465
|
+
return await trace(
|
|
466
|
+
dummyContext,
|
|
467
|
+
"update-periodic-events",
|
|
468
|
+
async () => {
|
|
469
|
+
const couldAcquireLock = await distributedLock.acquireLock(dummyContext, EVENT_QUEUE_UPDATE_PERIODIC_EVENTS, {
|
|
470
|
+
expiryTime: 60 * 1000,
|
|
471
|
+
tenantScoped: false,
|
|
472
|
+
});
|
|
473
|
+
if (!couldAcquireLock) {
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
return await cds.tx({}, async (tx) => await periodicEvents.checkAndInsertPeriodicEvents(tx.context));
|
|
477
|
+
},
|
|
478
|
+
{ newRootSpan: true }
|
|
479
|
+
);
|
|
480
|
+
};
|
|
407
481
|
|
|
408
482
|
const _checkPeriodicEventsSingleTenant = async (context) => {
|
|
409
483
|
const logger = cds.log(COMPONENT_NAME);
|
|
@@ -417,7 +491,7 @@ const _checkPeriodicEventsSingleTenant = async (context) => {
|
|
|
417
491
|
try {
|
|
418
492
|
logger.info("executing updating periodic events", {
|
|
419
493
|
tenantId: context.tenant,
|
|
420
|
-
subdomain: context.user?.
|
|
494
|
+
subdomain: context.user?.tokenInfo?.extAttributes?.zdn,
|
|
421
495
|
});
|
|
422
496
|
await periodicEvents.checkAndInsertPeriodicEvents(context);
|
|
423
497
|
} catch (err) {
|
|
@@ -429,12 +503,14 @@ const _checkPeriodicEventsSingleTenant = async (context) => {
|
|
|
429
503
|
};
|
|
430
504
|
|
|
431
505
|
module.exports = {
|
|
432
|
-
|
|
506
|
+
singleTenantDb,
|
|
433
507
|
multiTenancyDb,
|
|
434
508
|
multiTenancyRedis,
|
|
509
|
+
singleTenantRedis,
|
|
435
510
|
__: {
|
|
436
511
|
_singleTenantDb,
|
|
437
512
|
_multiTenancyRedis,
|
|
513
|
+
_singleTenantRedis,
|
|
438
514
|
_multiTenancyDb,
|
|
439
515
|
_calculateOffsetForFirstRun,
|
|
440
516
|
_acquireRunId,
|
|
@@ -2,8 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
const COMPONENT = "eventQueue/SetIntervalDriftSafe";
|
|
4
4
|
|
|
5
|
-
const ALLOWED_SHIFT_IN_PROCENT = 0.1;
|
|
6
|
-
|
|
7
5
|
class SetIntervalDriftSafe {
|
|
8
6
|
#adjustedInterval;
|
|
9
7
|
#interval;
|
|
@@ -21,12 +19,6 @@ class SetIntervalDriftSafe {
|
|
|
21
19
|
const now = Date.now();
|
|
22
20
|
if (this.#expectedCycleTime === 0) {
|
|
23
21
|
this.#expectedCycleTime = now + this.#interval;
|
|
24
|
-
} else if (
|
|
25
|
-
Math.abs(now + this.#interval - this.#nextTickScheduledFor - this.#interval) >
|
|
26
|
-
this.#interval * ALLOWED_SHIFT_IN_PROCENT
|
|
27
|
-
) {
|
|
28
|
-
this.#logger.log("overlapping ticks, skipping this run");
|
|
29
|
-
return;
|
|
30
22
|
} else {
|
|
31
23
|
this.#adjustedInterval = this.#interval - (now - this.#expectedCycleTime);
|
|
32
24
|
this.#expectedCycleTime += this.#interval;
|
package/src/shared/cdsHelper.js
CHANGED
|
@@ -5,6 +5,7 @@ const cds = require("@sap/cds");
|
|
|
5
5
|
|
|
6
6
|
const config = require("../config");
|
|
7
7
|
const common = require("./common");
|
|
8
|
+
const { TenantIdCheckTypes } = require("../constants");
|
|
8
9
|
|
|
9
10
|
const VERROR_CLUSTER_NAME = "ExecuteInNewTransactionError";
|
|
10
11
|
const COMPONENT_NAME = "/eventQueue/cdsHelper";
|
|
@@ -23,7 +24,7 @@ async function executeInNewTransaction(context = {}, transactionTag, fn, args, {
|
|
|
23
24
|
const parameters = Array.isArray(args) ? args : [args];
|
|
24
25
|
const logger = cds.log(COMPONENT_NAME);
|
|
25
26
|
try {
|
|
26
|
-
const user = new cds.User.Privileged({ id: config.userId,
|
|
27
|
+
const user = new cds.User.Privileged({ id: config.userId, tokenInfo: await common.getTokenInfo(context.tenant) });
|
|
27
28
|
if (cds.db.kind === "hana") {
|
|
28
29
|
await cds.tx(
|
|
29
30
|
{
|
|
@@ -116,11 +117,9 @@ const getAllTenantIds = async () => {
|
|
|
116
117
|
const response = await ssp.get("/tenant");
|
|
117
118
|
return response
|
|
118
119
|
.map((tenant) => tenant.subscribedTenantId ?? tenant.tenant)
|
|
119
|
-
.filter((tenantId) =>
|
|
120
|
+
.filter((tenantId) => common.isTenantIdValidCb(TenantIdCheckTypes.getAllTenantIds, tenantId));
|
|
120
121
|
};
|
|
121
122
|
|
|
122
|
-
const isFakeTenant = (tenantId) => /00000000-0000-4000-8000-\d{12}/.test(tenantId);
|
|
123
|
-
|
|
124
123
|
module.exports = {
|
|
125
124
|
executeInNewTransaction,
|
|
126
125
|
TriggerRollback,
|
package/src/shared/common.js
CHANGED
|
@@ -4,6 +4,8 @@ const crypto = require("crypto");
|
|
|
4
4
|
|
|
5
5
|
const cds = require("@sap/cds");
|
|
6
6
|
const xssec = require("@sap/xssec");
|
|
7
|
+
const config = require("../config");
|
|
8
|
+
const { TenantIdCheckTypes } = require("../constants");
|
|
7
9
|
|
|
8
10
|
const MARGIN_AUTH_INFO_EXPIRY = 60 * 1000;
|
|
9
11
|
const COMPONENT_NAME = "/eventQueue/common";
|
|
@@ -68,45 +70,57 @@ const processChunkedSync = (inputs, chunkSize, chunkHandler) => {
|
|
|
68
70
|
|
|
69
71
|
const hashStringTo32Bit = (value) => crypto.createHash("sha256").update(String(value)).digest("base64").slice(0, 32);
|
|
70
72
|
|
|
71
|
-
const
|
|
72
|
-
const
|
|
73
|
-
|
|
73
|
+
const _getNewTokenInfo = async (tenantId) => {
|
|
74
|
+
const tokenInfoCache = getTokenInfo._tokenInfoCache;
|
|
75
|
+
tokenInfoCache[tenantId] = tokenInfoCache[tenantId] ?? {};
|
|
74
76
|
try {
|
|
75
|
-
if (!
|
|
76
|
-
|
|
77
|
+
if (!_getNewTokenInfo._xsuaaService) {
|
|
78
|
+
_getNewTokenInfo._xsuaaService = new xssec.XsuaaService(cds.requires.auth.credentials);
|
|
77
79
|
}
|
|
78
|
-
const authService =
|
|
80
|
+
const authService = _getNewTokenInfo._xsuaaService;
|
|
79
81
|
const token = await authService.fetchClientCredentialsToken({ zid: tenantId });
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
return
|
|
82
|
+
const tokenInfo = new xssec.XsuaaToken(token.access_token);
|
|
83
|
+
tokenInfoCache[tenantId].expireTs = tokenInfo.getExpirationDate().getTime() - MARGIN_AUTH_INFO_EXPIRY;
|
|
84
|
+
return tokenInfo;
|
|
83
85
|
} catch (err) {
|
|
84
|
-
|
|
85
|
-
cds.log(COMPONENT_NAME).warn("failed to request
|
|
86
|
+
tokenInfoCache[tenantId] = null;
|
|
87
|
+
cds.log(COMPONENT_NAME).warn("failed to request tokenInfo", err);
|
|
86
88
|
}
|
|
87
89
|
};
|
|
88
90
|
|
|
89
|
-
const
|
|
91
|
+
const getTokenInfo = async (tenantId) => {
|
|
92
|
+
if (!isTenantIdValidCb(TenantIdCheckTypes.getTokenInfo, tenantId)) {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
|
|
90
96
|
if (!cds.requires?.auth?.credentials) {
|
|
91
|
-
return null; // no credentials not
|
|
97
|
+
return null; // no credentials not tokenInfo
|
|
92
98
|
}
|
|
93
99
|
if (!cds.requires?.auth.kind.match(/jwt|xsuaa/i)) {
|
|
94
100
|
cds.log(COMPONENT_NAME).warn("Only 'jwt' or 'xsuaa' are supported as values for auth.kind.");
|
|
95
101
|
return null;
|
|
96
102
|
}
|
|
97
103
|
|
|
98
|
-
|
|
99
|
-
const
|
|
104
|
+
getTokenInfo._tokenInfoCache = getTokenInfo._tokenInfoCache ?? {};
|
|
105
|
+
const tokenInfoCache = getTokenInfo._tokenInfoCache;
|
|
100
106
|
// not existing or existing but expired
|
|
101
107
|
if (
|
|
102
|
-
!
|
|
103
|
-
(
|
|
108
|
+
!tokenInfoCache[tenantId] ||
|
|
109
|
+
(tokenInfoCache[tenantId] && tokenInfoCache[tenantId].expireTs && Date.now() > tokenInfoCache[tenantId].expireTs)
|
|
104
110
|
) {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
111
|
+
tokenInfoCache[tenantId] ??= {};
|
|
112
|
+
tokenInfoCache[tenantId].value = _getNewTokenInfo(tenantId);
|
|
113
|
+
tokenInfoCache[tenantId].expireTs = null;
|
|
114
|
+
}
|
|
115
|
+
return await tokenInfoCache[tenantId].value;
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const isTenantIdValidCb = (checkType, tenantId) => {
|
|
119
|
+
if (config.tenantIdFilterCb) {
|
|
120
|
+
return config.tenantIdFilterCb(checkType, tenantId);
|
|
121
|
+
} else {
|
|
122
|
+
return true;
|
|
108
123
|
}
|
|
109
|
-
return await authInfoCache[tenantId].value;
|
|
110
124
|
};
|
|
111
125
|
|
|
112
126
|
module.exports = {
|
|
@@ -115,8 +129,9 @@ module.exports = {
|
|
|
115
129
|
isValidDate,
|
|
116
130
|
processChunkedSync,
|
|
117
131
|
hashStringTo32Bit,
|
|
118
|
-
|
|
132
|
+
getTokenInfo,
|
|
133
|
+
isTenantIdValidCb,
|
|
119
134
|
__: {
|
|
120
|
-
|
|
135
|
+
clearTokenInfoCache: () => (getTokenInfo._tokenInfoCache = {}),
|
|
121
136
|
},
|
|
122
137
|
};
|
|
@@ -60,7 +60,7 @@ const checkLockExistsAndReturnValue = async (context, key, { tenantScoped = true
|
|
|
60
60
|
const _acquireLockRedis = async (context, fullKey, expiryTime, { value = "true", overrideValue = false } = {}) => {
|
|
61
61
|
const client = await redis.createMainClientAndConnect(config.redisOptions);
|
|
62
62
|
const result = await client.set(fullKey, value, {
|
|
63
|
-
PX: expiryTime,
|
|
63
|
+
PX: Math.round(expiryTime),
|
|
64
64
|
...(overrideValue ? null : { NX: true }),
|
|
65
65
|
});
|
|
66
66
|
const isOk = result === REDIS_COMMAND_OK;
|
|
@@ -117,7 +117,10 @@ const _acquireLockDB = async (context, fullKey, expiryTime, { value = "true", ov
|
|
|
117
117
|
.where("code =", fullKey)
|
|
118
118
|
);
|
|
119
119
|
}
|
|
120
|
-
if (
|
|
120
|
+
if (
|
|
121
|
+
overrideValue ||
|
|
122
|
+
(currentEntry && new Date(currentEntry.createdAt).getTime() + Math.round(expiryTime) <= Date.now())
|
|
123
|
+
) {
|
|
121
124
|
await tx.run(
|
|
122
125
|
UPDATE.entity(config.tableNameEventLock)
|
|
123
126
|
.set({
|
|
@@ -51,19 +51,10 @@ class EventScheduler {
|
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
calculateOffset(type, subType, startAfter) {
|
|
54
|
-
const
|
|
55
|
-
const scheduleWithoutDelay = config.isPeriodicEvent(type, subType) && eventConfig.interval < 30 * 1000;
|
|
56
|
-
const date = scheduleWithoutDelay ? startAfter : this.calculateFutureTime(startAfter, 10);
|
|
57
|
-
|
|
54
|
+
const date = startAfter;
|
|
58
55
|
return { date, relative: date.getTime() - Date.now() };
|
|
59
56
|
}
|
|
60
57
|
|
|
61
|
-
calculateFutureTime(date, seoncds) {
|
|
62
|
-
const startAfterSeconds = date.getSeconds();
|
|
63
|
-
const secondsUntil = seoncds - (startAfterSeconds % seoncds);
|
|
64
|
-
return new Date(date.getTime() + secondsUntil * 1000);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
58
|
clearScheduledEvents() {
|
|
68
59
|
this.#scheduledEvents = {};
|
|
69
60
|
}
|
package/src/shared/redis.js
CHANGED
|
@@ -50,25 +50,27 @@ const _createClientBase = (redisOptions) => {
|
|
|
50
50
|
}
|
|
51
51
|
};
|
|
52
52
|
|
|
53
|
-
const createClientAndConnect = async (options, errorHandlerCreateClient) => {
|
|
53
|
+
const createClientAndConnect = async (options, errorHandlerCreateClient, isConnectionCheck) => {
|
|
54
54
|
try {
|
|
55
55
|
const client = _createClientBase(options);
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
56
|
+
if (!isConnectionCheck) {
|
|
57
|
+
client.on("error", (err) => {
|
|
58
|
+
const dateNow = Date.now();
|
|
59
|
+
if (dateNow - lastErrorLog > LOG_AFTER_SEC * 1000) {
|
|
60
|
+
cds.log(COMPONENT_NAME).error("error from redis client for pub/sub failed", err);
|
|
61
|
+
lastErrorLog = dateNow;
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
64
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
65
|
+
client.on("reconnecting", () => {
|
|
66
|
+
const dateNow = Date.now();
|
|
67
|
+
if (dateNow - lastErrorLog > LOG_AFTER_SEC * 1000) {
|
|
68
|
+
cds.log(COMPONENT_NAME).info("redis client trying reconnect...");
|
|
69
|
+
lastErrorLog = dateNow;
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
await client.connect();
|
|
72
74
|
return client;
|
|
73
75
|
} catch (err) {
|
|
74
76
|
errorHandlerCreateClient(err);
|
|
@@ -119,7 +121,7 @@ const _resilientClientClose = async (client) => {
|
|
|
119
121
|
|
|
120
122
|
const connectionCheck = async (options) => {
|
|
121
123
|
return new Promise((resolve, reject) => {
|
|
122
|
-
createClientAndConnect(options, reject)
|
|
124
|
+
createClientAndConnect(options, reject, true)
|
|
123
125
|
.then((client) => {
|
|
124
126
|
if (client) {
|
|
125
127
|
_resilientClientClose(client);
|
|
@@ -131,7 +133,10 @@ const connectionCheck = async (options) => {
|
|
|
131
133
|
.catch(reject);
|
|
132
134
|
})
|
|
133
135
|
.then(() => true)
|
|
134
|
-
.catch(() =>
|
|
136
|
+
.catch((err) => {
|
|
137
|
+
cds.log(COMPONENT_NAME).error("Redis connection check failed! Falling back to NO_REDIS mode", err);
|
|
138
|
+
return false;
|
|
139
|
+
});
|
|
135
140
|
};
|
|
136
141
|
|
|
137
142
|
module.exports = {
|