@cap-js-community/event-queue 1.10.0-beta.1 → 1.10.0-beta.2
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 +1 -1
- package/src/config.js +25 -12
- package/src/initialize.js +1 -0
- package/src/outbox/EventQueueGenericOutboxHandler.js +55 -26
- package/src/outbox/eventQueueAsOutbox.js +2 -1
- package/src/runner/runner.js +4 -4
- package/src/shared/distributedLock.js +2 -3
- package/src/shared/redis.js +5 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cap-js-community/event-queue",
|
|
3
|
-
"version": "1.10.0-beta.
|
|
3
|
+
"version": "1.10.0-beta.2",
|
|
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",
|
package/src/config.js
CHANGED
|
@@ -10,9 +10,12 @@ const { Priorities } = require("./constants");
|
|
|
10
10
|
|
|
11
11
|
const FOR_UPDATE_TIMEOUT = 10;
|
|
12
12
|
const GLOBAL_TX_TIMEOUT = 30 * 60 * 1000;
|
|
13
|
-
const
|
|
13
|
+
const REDIS_PREFIX = "EVENT_QUEUE";
|
|
14
|
+
const REDIS_CONFIG_CHANNEL = "CONFIG_CHANNEL";
|
|
14
15
|
const REDIS_OFFBOARD_TENANT_CHANNEL = "REDIS_OFFBOARD_TENANT_CHANNEL";
|
|
15
|
-
const REDIS_CONFIG_BLOCKLIST_CHANNEL = "
|
|
16
|
+
const REDIS_CONFIG_BLOCKLIST_CHANNEL = "REDIS_CONFIG_BLOCKLIST_CHANNEL";
|
|
17
|
+
const COMMAND_BLOCK = "EVENT_BLOCK";
|
|
18
|
+
const COMMAND_UNBLOCK = "EVENT_UNBLOCK";
|
|
16
19
|
const COMPONENT_NAME = "/eventQueue/config";
|
|
17
20
|
const MIN_INTERVAL_SEC = 10;
|
|
18
21
|
const DEFAULT_LOAD = 1;
|
|
@@ -22,8 +25,6 @@ const DEFAULT_KEEP_ALIVE_INTERVAL = 60;
|
|
|
22
25
|
const DEFAULT_MAX_FACTOR_STUCK_2_KEEP_ALIVE_INTERVAL = 3.5;
|
|
23
26
|
const DEFAULT_INHERIT_TRACE_CONTEXT = true;
|
|
24
27
|
const SUFFIX_PERIODIC = "_PERIODIC";
|
|
25
|
-
const COMMAND_BLOCK = "EVENT_QUEUE_EVENT_BLOCK";
|
|
26
|
-
const COMMAND_UNBLOCK = "EVENT_QUEUE_EVENT_UNBLOCK";
|
|
27
28
|
const CAP_EVENT_TYPE = "CAP_OUTBOX";
|
|
28
29
|
const CAP_PARALLEL_DEFAULT = 5;
|
|
29
30
|
const DELETE_TENANT_BLOCK_AFTER_MS = 5 * 60 * 1000;
|
|
@@ -106,6 +107,7 @@ class Config {
|
|
|
106
107
|
#unsubscribeHandlers = [];
|
|
107
108
|
#unsubscribedTenants = {};
|
|
108
109
|
#cronTimezone;
|
|
110
|
+
#redisNamespace;
|
|
109
111
|
#publishEventBlockList;
|
|
110
112
|
#crashOnRedisUnavailable;
|
|
111
113
|
#tenantIdFilterTokenInfoCb;
|
|
@@ -182,7 +184,7 @@ class Config {
|
|
|
182
184
|
|
|
183
185
|
attachConfigChangeHandler() {
|
|
184
186
|
this.#attachBlockListChangeHandler();
|
|
185
|
-
redis.subscribeRedisChannel(this
|
|
187
|
+
redis.subscribeRedisChannel(this.redisOptions, REDIS_CONFIG_CHANNEL, (messageData) => {
|
|
186
188
|
try {
|
|
187
189
|
const { key, value } = JSON.parse(messageData);
|
|
188
190
|
if (this[key] !== value) {
|
|
@@ -199,7 +201,7 @@ class Config {
|
|
|
199
201
|
|
|
200
202
|
attachRedisUnsubscribeHandler() {
|
|
201
203
|
this.#logger.info("attached redis handle for unsubscribe events");
|
|
202
|
-
redis.subscribeRedisChannel(this
|
|
204
|
+
redis.subscribeRedisChannel(this.redisOptions, REDIS_OFFBOARD_TENANT_CHANNEL, (messageData) => {
|
|
203
205
|
try {
|
|
204
206
|
const { tenantId } = JSON.parse(messageData);
|
|
205
207
|
this.#logger.info("received unsubscribe broadcast event", { tenantId });
|
|
@@ -229,7 +231,7 @@ class Config {
|
|
|
229
231
|
handleUnsubscribe(tenantId) {
|
|
230
232
|
if (this.redisEnabled) {
|
|
231
233
|
redis
|
|
232
|
-
.publishMessage(this
|
|
234
|
+
.publishMessage(this.redisOptions, REDIS_OFFBOARD_TENANT_CHANNEL, JSON.stringify({ tenantId }))
|
|
233
235
|
.catch((error) => {
|
|
234
236
|
this.#logger.error(`publishing tenant unsubscribe failed. tenantId: ${tenantId}`, error);
|
|
235
237
|
});
|
|
@@ -247,13 +249,13 @@ class Config {
|
|
|
247
249
|
this.#logger.info("redis not connected, config change won't be published", { key, value });
|
|
248
250
|
return;
|
|
249
251
|
}
|
|
250
|
-
redis.publishMessage(this
|
|
252
|
+
redis.publishMessage(this.redisOptions, REDIS_CONFIG_CHANNEL, JSON.stringify({ key, value })).catch((error) => {
|
|
251
253
|
this.#logger.error(`publishing config change failed key: ${key}, value: ${value}`, error);
|
|
252
254
|
});
|
|
253
255
|
}
|
|
254
256
|
|
|
255
257
|
#attachBlockListChangeHandler() {
|
|
256
|
-
redis.subscribeRedisChannel(this
|
|
258
|
+
redis.subscribeRedisChannel(this.redisOptions, REDIS_CONFIG_BLOCKLIST_CHANNEL, (messageData) => {
|
|
257
259
|
try {
|
|
258
260
|
const { command, key, tenant } = JSON.parse(messageData);
|
|
259
261
|
if (command === COMMAND_BLOCK) {
|
|
@@ -283,7 +285,7 @@ class Config {
|
|
|
283
285
|
|
|
284
286
|
redis
|
|
285
287
|
.publishMessage(
|
|
286
|
-
this
|
|
288
|
+
this.redisOptions,
|
|
287
289
|
REDIS_CONFIG_BLOCKLIST_CHANNEL,
|
|
288
290
|
JSON.stringify({ command: COMMAND_BLOCK, key, tenant })
|
|
289
291
|
)
|
|
@@ -316,7 +318,7 @@ class Config {
|
|
|
316
318
|
|
|
317
319
|
redis
|
|
318
320
|
.publishMessage(
|
|
319
|
-
this
|
|
321
|
+
this.redisOptions,
|
|
320
322
|
REDIS_CONFIG_BLOCKLIST_CHANNEL,
|
|
321
323
|
JSON.stringify({ command: COMMAND_UNBLOCK, key, tenant })
|
|
322
324
|
)
|
|
@@ -874,7 +876,18 @@ class Config {
|
|
|
874
876
|
}
|
|
875
877
|
|
|
876
878
|
get redisOptions() {
|
|
877
|
-
return
|
|
879
|
+
return {
|
|
880
|
+
...this.#redisOptions,
|
|
881
|
+
redisNamespace: `${[REDIS_PREFIX, this.redisNamespace].filter((a) => a).join("_")}`,
|
|
882
|
+
};
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
set redisNamespace(value) {
|
|
886
|
+
this.#redisNamespace = value;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
get redisNamespace() {
|
|
890
|
+
return this.#redisNamespace;
|
|
878
891
|
}
|
|
879
892
|
|
|
880
893
|
set insertEventsBeforeCommit(value) {
|
package/src/initialize.js
CHANGED
|
@@ -63,13 +63,11 @@ class EventQueueGenericOutboxHandler extends EventQueueBaseClass {
|
|
|
63
63
|
} else {
|
|
64
64
|
const msg = new cds.Request({
|
|
65
65
|
event: "clusterQueueEntries",
|
|
66
|
-
data: { queueEntriesWithPayloadMap: genericClusterEvents },
|
|
67
66
|
eventQueue: {
|
|
68
67
|
processor: this,
|
|
69
|
-
clusterByPayloadProperty: (propertyName) =>
|
|
70
|
-
|
|
71
|
-
clusterByEventProperty: (propertyName) =>
|
|
72
|
-
EventQueueGenericOutboxHandler.clusterByEventProperty(genericClusterEvents, propertyName),
|
|
68
|
+
clusterByPayloadProperty: (propertyName, cb) =>
|
|
69
|
+
this.clusterByPayloadProperty(genericClusterEvents, propertyName, cb),
|
|
70
|
+
clusterByEventProperty: (propertyName) => this.clusterByEventProperty(genericClusterEvents, propertyName),
|
|
73
71
|
},
|
|
74
72
|
});
|
|
75
73
|
const handlerCluster = await this.__srvUnboxed.tx(this.context).send(msg);
|
|
@@ -80,13 +78,12 @@ class EventQueueGenericOutboxHandler extends EventQueueBaseClass {
|
|
|
80
78
|
for (const actionName in specificClusterEvents) {
|
|
81
79
|
const msg = new cds.Request({
|
|
82
80
|
event: `clusterQueueEntries.${actionName}`,
|
|
83
|
-
data: { queueEntriesWithPayloadMap: specificClusterEvents[actionName] },
|
|
84
81
|
eventQueue: {
|
|
85
82
|
processor: this,
|
|
86
|
-
clusterByPayloadProperty: (propertyName) =>
|
|
87
|
-
|
|
88
|
-
clusterByEventProperty: (propertyName) =>
|
|
89
|
-
|
|
83
|
+
clusterByPayloadProperty: (propertyName, cb) =>
|
|
84
|
+
this.clusterByPayloadProperty(specificClusterEvents[actionName], propertyName, cb),
|
|
85
|
+
clusterByEventProperty: (propertyName, cb) =>
|
|
86
|
+
this.clusterByEventProperty(specificClusterEvents[actionName], propertyName, cb),
|
|
90
87
|
},
|
|
91
88
|
});
|
|
92
89
|
const handlerCluster = await this.__srvUnboxed.tx(this.context).send(msg);
|
|
@@ -94,26 +91,58 @@ class EventQueueGenericOutboxHandler extends EventQueueBaseClass {
|
|
|
94
91
|
}
|
|
95
92
|
}
|
|
96
93
|
|
|
97
|
-
|
|
98
|
-
return Object.entries(queueEntriesWithPayloadMap).reduce((result, [, { queueEntry, payload }]) => {
|
|
99
|
-
result
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
94
|
+
clusterBase(queueEntriesWithPayloadMap, propertyName, refCb, cb) {
|
|
95
|
+
return Object.entries(queueEntriesWithPayloadMap).reduce((result, [, { queueEntry, payload }], index) => {
|
|
96
|
+
const ref = refCb(result, payload, queueEntry);
|
|
97
|
+
ref.queueEntries.push(queueEntry);
|
|
98
|
+
if (cb) {
|
|
99
|
+
const clusterResult = cb(ref.payload.data, queueEntry.payload.data, index);
|
|
100
|
+
ref.payload.data ??= clusterResult;
|
|
101
|
+
}
|
|
104
102
|
return result;
|
|
105
103
|
}, {});
|
|
106
104
|
}
|
|
107
105
|
|
|
108
|
-
|
|
109
|
-
return
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
106
|
+
clusterByPayloadProperty(queueEntriesWithPayloadMap, propertyName, cb) {
|
|
107
|
+
return this.clusterBase(
|
|
108
|
+
queueEntriesWithPayloadMap,
|
|
109
|
+
propertyName,
|
|
110
|
+
(result, payload) => {
|
|
111
|
+
const parts = propertyName.split(".");
|
|
112
|
+
const data = JSON.parse(JSON.stringify(payload.data));
|
|
113
|
+
let ref = payload;
|
|
114
|
+
for (const part of parts) {
|
|
115
|
+
ref = ref[part];
|
|
116
|
+
}
|
|
117
|
+
result[ref[propertyName]] ??= {
|
|
118
|
+
queueEntries: [],
|
|
119
|
+
payload: { ...payload, data },
|
|
120
|
+
};
|
|
121
|
+
return result[ref[propertyName]];
|
|
122
|
+
},
|
|
123
|
+
cb
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
clusterByEventProperty(queueEntriesWithPayloadMap, propertyName, cb) {
|
|
128
|
+
return this.clusterBase(
|
|
129
|
+
queueEntriesWithPayloadMap,
|
|
130
|
+
propertyName,
|
|
131
|
+
(result, payload, queueEntry) => {
|
|
132
|
+
const parts = propertyName.split(".");
|
|
133
|
+
const payloadCopy = JSON.parse(JSON.stringify(payload));
|
|
134
|
+
let ref = queueEntry;
|
|
135
|
+
for (const part of parts) {
|
|
136
|
+
ref = ref[part];
|
|
137
|
+
}
|
|
138
|
+
result[queueEntry[propertyName]] ??= {
|
|
139
|
+
queueEntries: [],
|
|
140
|
+
payload: payloadCopy,
|
|
141
|
+
};
|
|
142
|
+
return result[queueEntry[propertyName]];
|
|
143
|
+
},
|
|
144
|
+
cb
|
|
145
|
+
);
|
|
117
146
|
}
|
|
118
147
|
|
|
119
148
|
#clusterByAction(queueEntriesWithPayloadMap) {
|
|
@@ -47,7 +47,8 @@ function outboxed(srv, customOpts) {
|
|
|
47
47
|
customOpts || {}
|
|
48
48
|
);
|
|
49
49
|
config.addCAPOutboxEventBase(srv.name, outboxOpts);
|
|
50
|
-
|
|
50
|
+
// TODO: check req.event ?? req.method
|
|
51
|
+
const specificSettings = config.getCdsOutboxEventSpecificConfig(srv.name, req.event);
|
|
51
52
|
if (specificSettings) {
|
|
52
53
|
outboxOpts = config.addCAPOutboxEventSpecificAction(srv.name, req.event);
|
|
53
54
|
}
|
package/src/runner/runner.js
CHANGED
|
@@ -18,10 +18,10 @@ const { runEventCombinationForTenant } = require("./runnerHelper");
|
|
|
18
18
|
const { trace } = require("../shared/openTelemetry");
|
|
19
19
|
|
|
20
20
|
const COMPONENT_NAME = "/eventQueue/runner";
|
|
21
|
-
const EVENT_QUEUE_RUN_ID = "
|
|
22
|
-
const EVENT_QUEUE_RUN_TS = "
|
|
23
|
-
const EVENT_QUEUE_RUN_REDIS_CHECK = "
|
|
24
|
-
const EVENT_QUEUE_UPDATE_PERIODIC_EVENTS = "
|
|
21
|
+
const EVENT_QUEUE_RUN_ID = "RUN_ID";
|
|
22
|
+
const EVENT_QUEUE_RUN_TS = "RUN_TS";
|
|
23
|
+
const EVENT_QUEUE_RUN_REDIS_CHECK = "RUN_REDIS_CHECK";
|
|
24
|
+
const EVENT_QUEUE_UPDATE_PERIODIC_EVENTS = "UPDATE_PERIODIC_EVENTS";
|
|
25
25
|
let OFFSET_FIRST_RUN = 10 * 1000;
|
|
26
26
|
|
|
27
27
|
let tenantIdHash;
|
|
@@ -4,7 +4,6 @@ const redis = require("./redis");
|
|
|
4
4
|
const config = require("../config");
|
|
5
5
|
const cdsHelper = require("./cdsHelper");
|
|
6
6
|
|
|
7
|
-
const KEY_PREFIX = "EVENT_QUEUE";
|
|
8
7
|
const existingLocks = {};
|
|
9
8
|
const REDIS_COMMAND_OK = "OK";
|
|
10
9
|
const COMPONENT_NAME = "/eventQueue/distributedLock";
|
|
@@ -176,10 +175,10 @@ const _acquireLockDB = async (context, fullKey, expiryTime, { value = "true", ov
|
|
|
176
175
|
};
|
|
177
176
|
|
|
178
177
|
const _generateKey = (context, tenantScoped, key) => {
|
|
179
|
-
const keyParts = [];
|
|
178
|
+
const keyParts = [config.redisOptions.redisNamespace];
|
|
180
179
|
tenantScoped && keyParts.push(context.tenant);
|
|
181
180
|
keyParts.push(key);
|
|
182
|
-
return `${
|
|
181
|
+
return `${keyParts.join("##")}`;
|
|
183
182
|
};
|
|
184
183
|
|
|
185
184
|
const shutdownHandler = async () => {
|
package/src/shared/redis.js
CHANGED
|
@@ -46,6 +46,7 @@ const _createClientBase = (redisOptions = {}) => {
|
|
|
46
46
|
password: redisOptions.password ?? options.password ?? credentials.password,
|
|
47
47
|
socket,
|
|
48
48
|
});
|
|
49
|
+
delete socketOptions.redisNamespace;
|
|
49
50
|
if (credentials.cluster_mode) {
|
|
50
51
|
return redis.createCluster({
|
|
51
52
|
rootNodes: [socketOptions],
|
|
@@ -106,9 +107,10 @@ const _subscribeChannels = (options, subscribedChannels, errorHandlerCreateClien
|
|
|
106
107
|
if (client._subscribedChannels[channel]) {
|
|
107
108
|
continue;
|
|
108
109
|
}
|
|
109
|
-
|
|
110
|
+
const prefixedChannelName = [options.redisNamespace, channel].join("_");
|
|
111
|
+
cds.log(COMPONENT_NAME).info("subscribe redis client connected channel", { channel: prefixedChannelName });
|
|
110
112
|
client
|
|
111
|
-
.subscribe(
|
|
113
|
+
.subscribe(prefixedChannelName, fn)
|
|
112
114
|
.then(() => {
|
|
113
115
|
client._subscribedChannels ??= {};
|
|
114
116
|
client._subscribedChannels[channel] = 1;
|
|
@@ -133,7 +135,7 @@ const _subscribeChannels = (options, subscribedChannels, errorHandlerCreateClien
|
|
|
133
135
|
|
|
134
136
|
const publishMessage = async (options, channel, message) => {
|
|
135
137
|
const client = await createMainClientAndConnect(options);
|
|
136
|
-
return await client.publish(channel, message);
|
|
138
|
+
return await client.publish([options.redisNamespace, channel].join("_"), message);
|
|
137
139
|
};
|
|
138
140
|
|
|
139
141
|
const closeMainClient = async () => {
|