@cap-js-community/event-queue 2.1.0 → 2.1.1
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/package.json +15 -15
- package/src/EventQueueError.js +15 -0
- package/src/EventQueueProcessorBase.js +5 -4
- package/src/config.js +14 -4
- package/src/initialize.js +2 -0
- package/src/shared/SetIntervalDriftSafe.js +0 -6
- package/src/shared/common.js +4 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cap-js-community/event-queue",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.1",
|
|
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",
|
|
@@ -41,30 +41,30 @@
|
|
|
41
41
|
"upgrade-lock": "npx shx rm -rf package-lock.json node_modules && npm i --package-lock"
|
|
42
42
|
},
|
|
43
43
|
"engines": {
|
|
44
|
-
"node": ">=
|
|
44
|
+
"node": "^20.19.0 || ^22.13.0 || >=24"
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
47
|
"@cap-js-community/common": "^0.4.0",
|
|
48
48
|
"@sap/xssec": "^4.13.0",
|
|
49
49
|
"cron-parser": "^5.5.0",
|
|
50
50
|
"verror": "^1.10.1",
|
|
51
|
-
"yaml": "^2.8.
|
|
51
|
+
"yaml": "^2.8.4"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
|
-
"@
|
|
55
|
-
"@cap-js/
|
|
56
|
-
"@cap-js/
|
|
57
|
-
"@cap-js/
|
|
58
|
-
"@
|
|
54
|
+
"@cap-js/cds-test": "^1.0.1",
|
|
55
|
+
"@cap-js/db-service": "^2.11.0",
|
|
56
|
+
"@cap-js/hana": "^2.8.0",
|
|
57
|
+
"@cap-js/sqlite": "^2.4.0",
|
|
58
|
+
"@eslint/js": "^10.0.1",
|
|
59
59
|
"@opentelemetry/api": "^1.9.1",
|
|
60
|
-
"@sap/cds": "^9.
|
|
61
|
-
"@sap/cds-dk": "^9.
|
|
62
|
-
"eslint": "^
|
|
60
|
+
"@sap/cds": "^9.9.1",
|
|
61
|
+
"@sap/cds-dk": "^9.9.0",
|
|
62
|
+
"eslint": "^10.3.0",
|
|
63
63
|
"eslint-config-prettier": "^10.1.8",
|
|
64
|
-
"eslint-plugin-jest": "^29.
|
|
65
|
-
"
|
|
66
|
-
"jest": "^
|
|
67
|
-
"prettier": "^
|
|
64
|
+
"eslint-plugin-jest": "^29.15.2",
|
|
65
|
+
"globals": "^17.6.0",
|
|
66
|
+
"jest": "^30.3.0",
|
|
67
|
+
"prettier": "^3.8.3"
|
|
68
68
|
},
|
|
69
69
|
"homepage": "https://cap-js-community.github.io/event-queue/",
|
|
70
70
|
"repository": {
|
package/src/EventQueueError.js
CHANGED
|
@@ -28,6 +28,7 @@ const ERROR_CODES = {
|
|
|
28
28
|
APP_INSTANCES_FORMAT: "APP_INSTANCES_FORMAT",
|
|
29
29
|
MULTI_INSTANCE_PROCESSING_NOT_ALLOWED: "MULTI_INSTANCE_PROCESSING_NOT_ALLOWED",
|
|
30
30
|
INVALID_CLUSTER_HANDLER_RESULT: "INVALID_CLUSTER_HANDLER_RESULT",
|
|
31
|
+
INVALID_AUTH_CACHE_EXPIRY_REDUCTION_MAX_PERCENT: "INVALID_AUTH_CACHE_EXPIRY_REDUCTION_MAX_PERCENT",
|
|
31
32
|
};
|
|
32
33
|
|
|
33
34
|
const ERROR_CODES_META = {
|
|
@@ -107,6 +108,9 @@ const ERROR_CODES_META = {
|
|
|
107
108
|
[ERROR_CODES.MULTI_INSTANCE_PROCESSING_NOT_ALLOWED]: {
|
|
108
109
|
message: "The config multiInstanceProcessing is currently only allowed for ad-hoc events and single-tenant-apps.",
|
|
109
110
|
},
|
|
111
|
+
[ERROR_CODES.INVALID_AUTH_CACHE_EXPIRY_REDUCTION_MAX_PERCENT]: {
|
|
112
|
+
message: "authCacheExpiryReductionMaxPercent must be a number between 0 and 80.",
|
|
113
|
+
},
|
|
110
114
|
};
|
|
111
115
|
|
|
112
116
|
class EventQueueError extends VError {
|
|
@@ -375,6 +379,17 @@ class EventQueueError extends VError {
|
|
|
375
379
|
);
|
|
376
380
|
}
|
|
377
381
|
|
|
382
|
+
static invalidAuthCacheExpiryReductionMaxPercent(value) {
|
|
383
|
+
const { message } = ERROR_CODES_META[ERROR_CODES.INVALID_AUTH_CACHE_EXPIRY_REDUCTION_MAX_PERCENT];
|
|
384
|
+
return new EventQueueError(
|
|
385
|
+
{
|
|
386
|
+
name: ERROR_CODES.INVALID_AUTH_CACHE_EXPIRY_REDUCTION_MAX_PERCENT,
|
|
387
|
+
info: { value },
|
|
388
|
+
},
|
|
389
|
+
message
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
|
|
378
393
|
static invalidClusterHandlerResult(clusterKey, propertyName) {
|
|
379
394
|
const { message } = ERROR_CODES_META[ERROR_CODES.INVALID_CLUSTER_HANDLER_RESULT];
|
|
380
395
|
return new EventQueueError(
|
|
@@ -69,7 +69,7 @@ class EventQueueProcessorBase {
|
|
|
69
69
|
this.__parallelEventProcessing = LIMIT_PARALLEL_EVENT_PROCESSING;
|
|
70
70
|
}
|
|
71
71
|
this.__concurrentEventProcessing = this.#eventConfig.multiInstanceProcessing;
|
|
72
|
-
this.__retryAttempts = this.#isPeriodic ? 1 : this.#eventConfig.retryAttempts ?? DEFAULT_RETRY_ATTEMPTS;
|
|
72
|
+
this.__retryAttempts = this.#isPeriodic ? 1 : (this.#eventConfig.retryAttempts ?? DEFAULT_RETRY_ATTEMPTS);
|
|
73
73
|
this.__selectMaxChunkSize = this.#eventConfig.selectMaxChunkSize ?? SELECT_LIMIT_EVENTS_PER_TICK;
|
|
74
74
|
this.__selectNextChunk = !!this.#eventConfig.checkForNextChunk;
|
|
75
75
|
this.__transactionMode = this.#eventConfig.transactionMode ?? TransactionMode.isolated;
|
|
@@ -457,7 +457,8 @@ class EventQueueProcessorBase {
|
|
|
457
457
|
eventSubType: this.#eventSubType,
|
|
458
458
|
statusMap,
|
|
459
459
|
});
|
|
460
|
-
const
|
|
460
|
+
const tsMs = Date.now();
|
|
461
|
+
const ts = new Date(tsMs).toISOString();
|
|
461
462
|
const updateData = Object.entries(statusMap).reduce((result, [id, data]) => {
|
|
462
463
|
const key = this.allowedFieldsEventHandler
|
|
463
464
|
.map((name) => [name, data[name]])
|
|
@@ -503,7 +504,7 @@ class EventQueueProcessorBase {
|
|
|
503
504
|
|
|
504
505
|
if (!data.startAfter && [EventProcessingStatus.Error, EventProcessingStatus.Open].includes(data.status)) {
|
|
505
506
|
data.startAfter = new Date(
|
|
506
|
-
|
|
507
|
+
tsMs +
|
|
507
508
|
(data.status === EventProcessingStatus.Error
|
|
508
509
|
? this.#eventConfig.retryFailedAfter
|
|
509
510
|
: this.#eventConfig.retryOpenAfter)
|
|
@@ -954,7 +955,7 @@ class EventQueueProcessorBase {
|
|
|
954
955
|
* the function 'addEventWithPayloadForProcessing'. This function is called after the clustering and before the
|
|
955
956
|
* process-events-steps. The event data is available with this.eventProcessingMap.
|
|
956
957
|
*/
|
|
957
|
-
|
|
958
|
+
|
|
958
959
|
async beforeProcessingEvents() {}
|
|
959
960
|
|
|
960
961
|
async isOutdatedAndKeepAlive() {
|
package/src/config.js
CHANGED
|
@@ -90,8 +90,6 @@ class Config {
|
|
|
90
90
|
#redisEnabled;
|
|
91
91
|
#initialized;
|
|
92
92
|
#instanceLoadLimit;
|
|
93
|
-
#tableNameEventQueue;
|
|
94
|
-
#tableNameEventLock;
|
|
95
93
|
#isEventQueueActive;
|
|
96
94
|
#configFilePath;
|
|
97
95
|
#processEventsAfterPublish;
|
|
@@ -122,6 +120,7 @@ class Config {
|
|
|
122
120
|
#disableProcessingOfSuspendedTenants;
|
|
123
121
|
#namespace;
|
|
124
122
|
#processingNamespaces;
|
|
123
|
+
#authCacheExpiryReductionMaxPercent;
|
|
125
124
|
static #instance;
|
|
126
125
|
constructor() {
|
|
127
126
|
this.#logger = cds.log(COMPONENT_NAME);
|
|
@@ -132,12 +131,11 @@ class Config {
|
|
|
132
131
|
this.#redisEnabled = null;
|
|
133
132
|
this.#initialized = false;
|
|
134
133
|
this.#instanceLoadLimit = 100;
|
|
135
|
-
this.#tableNameEventQueue = null;
|
|
136
|
-
this.#tableNameEventLock = null;
|
|
137
134
|
this.#isEventQueueActive = true;
|
|
138
135
|
this.#configFilePath = null;
|
|
139
136
|
this.#processEventsAfterPublish = null;
|
|
140
137
|
this.#disableRedis = null;
|
|
138
|
+
this.#authCacheExpiryReductionMaxPercent = 10;
|
|
141
139
|
this.#env = getEnvInstance();
|
|
142
140
|
}
|
|
143
141
|
|
|
@@ -337,6 +335,7 @@ class Config {
|
|
|
337
335
|
(typeof cds.requires.outbox === "object" && cds.requires.outbox) || {},
|
|
338
336
|
(typeof cds.requires.queue === "object" && cds.requires.queue) || {},
|
|
339
337
|
(typeof cds.env.requires[serviceName]?.outbox === "object" && cds.env.requires[serviceName].outbox) || {},
|
|
338
|
+
(typeof cds.env.requires[serviceName]?.outboxed === "object" && cds.env.requires[serviceName].outboxed) || {},
|
|
340
339
|
(typeof cds.env.requires[serviceName]?.queued === "object" && cds.env.requires[serviceName].queued) || {}
|
|
341
340
|
);
|
|
342
341
|
}
|
|
@@ -799,6 +798,17 @@ class Config {
|
|
|
799
798
|
return this.#configFilePath;
|
|
800
799
|
}
|
|
801
800
|
|
|
801
|
+
set authCacheExpiryReductionMaxPercent(value) {
|
|
802
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0 || value > 80) {
|
|
803
|
+
throw EventQueueError.invalidAuthCacheExpiryReductionMaxPercent(value);
|
|
804
|
+
}
|
|
805
|
+
this.#authCacheExpiryReductionMaxPercent = value;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
get authCacheExpiryReductionMaxPercent() {
|
|
809
|
+
return this.#authCacheExpiryReductionMaxPercent;
|
|
810
|
+
}
|
|
811
|
+
|
|
802
812
|
set processEventsAfterPublish(value) {
|
|
803
813
|
this.#processEventsAfterPublish = value;
|
|
804
814
|
}
|
package/src/initialize.js
CHANGED
|
@@ -51,6 +51,7 @@ const CONFIG_VARS = [
|
|
|
51
51
|
["namespace", "default"],
|
|
52
52
|
["processingNamespaces", ["default"]],
|
|
53
53
|
["collectEventQueueMetrics", false],
|
|
54
|
+
["authCacheExpiryReductionMaxPercent", 10],
|
|
54
55
|
];
|
|
55
56
|
|
|
56
57
|
/**
|
|
@@ -81,6 +82,7 @@ const CONFIG_VARS = [
|
|
|
81
82
|
* @param {string} [options.namespace=default] - Default namespace in which events are published
|
|
82
83
|
* @param {string} [options.processingNamespaces=[default]] - Namespaces which the application processes
|
|
83
84
|
* @param {boolean} [options.collectEventQueueMetrics=false] - Enable collection of event queue metrics (pending/inProgress counters) stored in Redis and exposed via OpenTelemetry gauges.
|
|
85
|
+
* @param {number} [options.authCacheExpiryReductionMaxPercent=10] - Maximum percentage (allowed range 0-80) by which the XSUAA token TTL is shortened when stored in the authContext cache. The actual reduction is randomized per token fetch in `[0, value]` so multi-tenant deployments do not all expire their cached tokens at the same time and stampede the XSUAA token endpoint. Values outside the allowed range throw on initialization.
|
|
84
86
|
*/
|
|
85
87
|
const initialize = async (options = {}) => {
|
|
86
88
|
if (config.initialized) {
|
|
@@ -1,20 +1,15 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
-
const COMPONENT = "eventQueue/SetIntervalDriftSafe";
|
|
4
|
-
|
|
5
3
|
class SetIntervalDriftSafe {
|
|
6
4
|
#adjustedInterval;
|
|
7
5
|
#interval;
|
|
8
6
|
#expectedCycleTime = 0;
|
|
9
|
-
#nextTickScheduledFor;
|
|
10
|
-
#logger;
|
|
11
7
|
#shouldRun = true;
|
|
12
8
|
#lastTimeoutId;
|
|
13
9
|
|
|
14
10
|
constructor(interval) {
|
|
15
11
|
this.#interval = interval;
|
|
16
12
|
this.#adjustedInterval = interval;
|
|
17
|
-
this.#logger = cds.log(COMPONENT);
|
|
18
13
|
}
|
|
19
14
|
|
|
20
15
|
start(fn) {
|
|
@@ -34,7 +29,6 @@ class SetIntervalDriftSafe {
|
|
|
34
29
|
this.#adjustedInterval = this.#interval - (now - this.#expectedCycleTime);
|
|
35
30
|
this.#expectedCycleTime += this.#interval;
|
|
36
31
|
}
|
|
37
|
-
this.#nextTickScheduledFor = now + this.#adjustedInterval;
|
|
38
32
|
const timeoutId = setTimeout(() => {
|
|
39
33
|
if (!this.#shouldRun) {
|
|
40
34
|
return;
|
package/src/shared/common.js
CHANGED
|
@@ -99,7 +99,10 @@ const _getNewAuthContext = async (tenantId) => {
|
|
|
99
99
|
const token = await authService.fetchClientCredentialsToken({ zid: tenantId });
|
|
100
100
|
const tokenInfo = new xssec.XsuaaToken(token.access_token);
|
|
101
101
|
const authInfo = new xssec.XsuaaSecurityContext(authService, tokenInfo);
|
|
102
|
-
|
|
102
|
+
const ttl = tokenInfo.getExpirationDate().getTime() - Date.now();
|
|
103
|
+
// Randomized to avoid synchronized cache expiry across tenants stampeding XSUAA on token refresh.
|
|
104
|
+
const reductionPercent = Math.random() * config.authCacheExpiryReductionMaxPercent;
|
|
105
|
+
return [ttl * (1 - reductionPercent / 100), [null, authInfo]];
|
|
103
106
|
} catch (err) {
|
|
104
107
|
cds.log(COMPONENT_NAME).warn("failed to request authContext", {
|
|
105
108
|
err: err.message,
|