@cap-js-community/event-queue 1.4.5 → 1.4.7
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 +7 -7
- package/src/config.js +54 -2
- package/src/dbHandler.js +3 -3
- package/src/index.d.ts +72 -0
- package/src/initialize.js +18 -0
- package/src/processEventQueue.js +5 -0
- package/src/shared/eventScheduler.js +6 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cap-js-community/event-queue",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.7",
|
|
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",
|
|
@@ -44,16 +44,16 @@
|
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
46
|
"@sap/xssec": "^3.6.1",
|
|
47
|
-
"redis": "^4.6.
|
|
47
|
+
"redis": "^4.6.14",
|
|
48
48
|
"verror": "^1.10.1",
|
|
49
|
-
"yaml": "^2.4.
|
|
49
|
+
"yaml": "^2.4.2"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
|
-
"@cap-js/hana": "^0.
|
|
53
|
-
"@cap-js/sqlite": "^1.
|
|
54
|
-
"@sap/cds": "^7.
|
|
52
|
+
"@cap-js/hana": "^0.4.0",
|
|
53
|
+
"@cap-js/sqlite": "^1.7.1",
|
|
54
|
+
"@sap/cds": "^7.9.2",
|
|
55
55
|
"@sap/cds-dk": "^7.8.0",
|
|
56
|
-
"eslint": "^8.
|
|
56
|
+
"eslint": "^8.57.0",
|
|
57
57
|
"eslint-config-prettier": "^9.1.0",
|
|
58
58
|
"eslint-plugin-jest": "^27.9.0",
|
|
59
59
|
"eslint-plugin-node": "^11.1.0",
|
package/src/config.js
CHANGED
|
@@ -10,7 +10,8 @@ const { Priorities } = require("./constants");
|
|
|
10
10
|
const FOR_UPDATE_TIMEOUT = 10;
|
|
11
11
|
const GLOBAL_TX_TIMEOUT = 30 * 60 * 1000;
|
|
12
12
|
const REDIS_CONFIG_CHANNEL = "EVENT_QUEUE_CONFIG_CHANNEL";
|
|
13
|
-
const
|
|
13
|
+
const REDIS_OFFBOARD_TENANT_CHANNEL = "REDIS_OFFBOARD_TENANT_CHANNEL";
|
|
14
|
+
const REDIS_CONFIG_BLOCKLIST_CHANNEL = "EVENT_QUEUE_REDIS_CONFIG_BLOCKLIST_CHANNEL";
|
|
14
15
|
const COMPONENT_NAME = "/eventQueue/config";
|
|
15
16
|
const MIN_INTERVAL_SEC = 10;
|
|
16
17
|
const DEFAULT_LOAD = 1;
|
|
@@ -19,8 +20,8 @@ const SUFFIX_PERIODIC = "_PERIODIC";
|
|
|
19
20
|
const COMMAND_BLOCK = "EVENT_QUEUE_EVENT_BLOCK";
|
|
20
21
|
const COMMAND_UNBLOCK = "EVENT_QUEUE_EVENT_UNBLOCK";
|
|
21
22
|
const CAP_EVENT_TYPE = "CAP_OUTBOX";
|
|
22
|
-
|
|
23
23
|
const CAP_PARALLEL_DEFAULT = 5;
|
|
24
|
+
const DELETE_TENANT_BLOCK_AFTER_MS = 5 * 60 * 1000;
|
|
24
25
|
|
|
25
26
|
const BASE_PERIODIC_EVENTS = [
|
|
26
27
|
{
|
|
@@ -67,6 +68,8 @@ class Config {
|
|
|
67
68
|
#cleanupLocksAndEventsForDev;
|
|
68
69
|
#redisOptions;
|
|
69
70
|
#insertEventsBeforeCommit;
|
|
71
|
+
#unsubscribeHandlers = [];
|
|
72
|
+
#unsubscribedTenants = {};
|
|
70
73
|
static #instance;
|
|
71
74
|
constructor() {
|
|
72
75
|
this.#logger = cds.log(COMPONENT_NAME);
|
|
@@ -126,6 +129,51 @@ class Config {
|
|
|
126
129
|
});
|
|
127
130
|
}
|
|
128
131
|
|
|
132
|
+
attachRedisUnsubscribeHandler() {
|
|
133
|
+
this.#logger.info("attached redis handle for unsubscribe events");
|
|
134
|
+
redis.subscribeRedisChannel(this.#redisOptions, REDIS_OFFBOARD_TENANT_CHANNEL, (messageData) => {
|
|
135
|
+
try {
|
|
136
|
+
const { tenantId } = JSON.parse(messageData);
|
|
137
|
+
this.#logger.info("received unsubscribe broadcast event", { tenantId });
|
|
138
|
+
this.executeUnsubscribeHandlers(tenantId);
|
|
139
|
+
} catch (err) {
|
|
140
|
+
this.#logger.error("could not parse unsubscribe broadcast event", err, {
|
|
141
|
+
messageData,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
executeUnsubscribeHandlers(tenantId) {
|
|
148
|
+
this.#unsubscribedTenants[tenantId] = true;
|
|
149
|
+
setTimeout(() => delete this.#unsubscribedTenants[tenantId], DELETE_TENANT_BLOCK_AFTER_MS);
|
|
150
|
+
for (const unsubscribeHandler of this.#unsubscribeHandlers) {
|
|
151
|
+
try {
|
|
152
|
+
unsubscribeHandler(tenantId);
|
|
153
|
+
} catch (err) {
|
|
154
|
+
this.#logger.error("could executing unsubscribe handler", err, {
|
|
155
|
+
tenantId,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
handleUnsubscribe(tenantId) {
|
|
162
|
+
if (this.redisEnabled) {
|
|
163
|
+
redis
|
|
164
|
+
.publishMessage(this.#redisOptions, REDIS_OFFBOARD_TENANT_CHANNEL, JSON.stringify({ tenantId }))
|
|
165
|
+
.catch((error) => {
|
|
166
|
+
this.#logger.error(`publishing tenant unsubscribe failed. tenantId: ${tenantId}`, error);
|
|
167
|
+
});
|
|
168
|
+
} else {
|
|
169
|
+
this.executeUnsubscribeHandlers(tenantId);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
attachUnsubscribeHandler(cb) {
|
|
174
|
+
this.#unsubscribeHandlers.push(cb);
|
|
175
|
+
}
|
|
176
|
+
|
|
129
177
|
publishConfigChange(key, value) {
|
|
130
178
|
if (!this.redisEnabled) {
|
|
131
179
|
this.#logger.info("redis not connected, config change won't be published", { key, value });
|
|
@@ -320,6 +368,10 @@ class Config {
|
|
|
320
368
|
delete this.#eventMap[this.generateKey(type, subType)];
|
|
321
369
|
}
|
|
322
370
|
|
|
371
|
+
isTenantUnsubscribed(tenantId) {
|
|
372
|
+
return this.#unsubscribedTenants[tenantId];
|
|
373
|
+
}
|
|
374
|
+
|
|
323
375
|
get fileContent() {
|
|
324
376
|
return this.#config;
|
|
325
377
|
}
|
package/src/dbHandler.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const cds = require("@sap/cds");
|
|
4
4
|
|
|
5
|
-
const
|
|
5
|
+
const redisPub = require("./redis/redisPub");
|
|
6
6
|
const config = require("./config");
|
|
7
7
|
|
|
8
8
|
const COMPONENT_NAME = "/eventQueue/dbHandler";
|
|
@@ -25,7 +25,7 @@ const registerEventQueueDbHandler = (dbService) => {
|
|
|
25
25
|
req.tx._ = req.tx._ ?? {};
|
|
26
26
|
req.tx._.eventQueuePublishEvents = req.tx._.eventQueuePublishEvents ?? {};
|
|
27
27
|
const eventQueuePublishEvents = req.tx._.eventQueuePublishEvents;
|
|
28
|
-
const data = Array.isArray(req.
|
|
28
|
+
const data = Array.isArray(req.query.INSERT.entries) ? req.query.INSERT.entries : [req.query.INSERT.entries];
|
|
29
29
|
const eventCombinations = Object.keys(
|
|
30
30
|
data.reduce((result, event) => {
|
|
31
31
|
const key = [event.type, event.subType].join("##");
|
|
@@ -45,7 +45,7 @@ const registerEventQueueDbHandler = (dbService) => {
|
|
|
45
45
|
return { type, subType };
|
|
46
46
|
});
|
|
47
47
|
|
|
48
|
-
broadcastEvent(req.tenant, events).catch((err) => {
|
|
48
|
+
redisPub.broadcastEvent(req.tenant, events).catch((err) => {
|
|
49
49
|
cds.log(COMPONENT_NAME).error("db handler failure during broadcasting event", err, {
|
|
50
50
|
tenant: req.tenant,
|
|
51
51
|
events,
|
package/src/index.d.ts
CHANGED
|
@@ -136,3 +136,75 @@ export function processEventQueue(
|
|
|
136
136
|
eventSubType: string,
|
|
137
137
|
startTime: Date
|
|
138
138
|
): Promise<any>;
|
|
139
|
+
|
|
140
|
+
declare class Config {
|
|
141
|
+
constructor();
|
|
142
|
+
|
|
143
|
+
getEventConfig(type: string, subType: string): any;
|
|
144
|
+
isCapOutboxEvent(type: string): boolean;
|
|
145
|
+
hasEventAfterCommitFlag(type: string, subType: string): boolean;
|
|
146
|
+
_checkRedisIsBound(): boolean;
|
|
147
|
+
checkRedisEnabled(): boolean;
|
|
148
|
+
attachConfigChangeHandler(): void;
|
|
149
|
+
attachRedisUnsubscribeHandler(): void;
|
|
150
|
+
executeUnsubscribeHandlers(tenantId: string): void;
|
|
151
|
+
handleUnsubscribe(tenantId: string): void;
|
|
152
|
+
attachUnsubscribeHandler(cb: Function): void;
|
|
153
|
+
publishConfigChange(key: string, value: any): void;
|
|
154
|
+
blockEvent(type: string, subType: string, isPeriodic: boolean, tenant?: string): void;
|
|
155
|
+
clearPeriodicEventBlockList(): void;
|
|
156
|
+
unblockEvent(type: string, subType: string, isPeriodic: boolean, tenant?: string): void;
|
|
157
|
+
addCAPOutboxEvent(serviceName: string, config: any): void;
|
|
158
|
+
isEventBlocked(type: string, subType: string, isPeriodicEvent: boolean, tenant: string): boolean;
|
|
159
|
+
get isEventQueueActive(): boolean;
|
|
160
|
+
set isEventQueueActive(value: boolean);
|
|
161
|
+
set fileContent(config: any);
|
|
162
|
+
get fileContent(): any;
|
|
163
|
+
get events(): any[];
|
|
164
|
+
get periodicEvents(): any[];
|
|
165
|
+
isPeriodicEvent(type: string, subType: string): boolean;
|
|
166
|
+
get allEvents(): any[];
|
|
167
|
+
get forUpdateTimeout(): number;
|
|
168
|
+
get globalTxTimeout(): number;
|
|
169
|
+
set forUpdateTimeout(value: number);
|
|
170
|
+
set globalTxTimeout(value: number);
|
|
171
|
+
get runInterval(): number | null;
|
|
172
|
+
set runInterval(value: number);
|
|
173
|
+
get redisEnabled(): boolean | null;
|
|
174
|
+
set redisEnabled(value: boolean | null);
|
|
175
|
+
get initialized(): boolean;
|
|
176
|
+
set initialized(value: boolean);
|
|
177
|
+
get instanceLoadLimit(): number;
|
|
178
|
+
set instanceLoadLimit(value: number);
|
|
179
|
+
get isEventBlockedCb(): any;
|
|
180
|
+
set isEventBlockedCb(value: any);
|
|
181
|
+
get tableNameEventQueue(): string;
|
|
182
|
+
get tableNameEventLock(): string;
|
|
183
|
+
set configFilePath(value: string | null);
|
|
184
|
+
get configFilePath(): string | null;
|
|
185
|
+
set processEventsAfterPublish(value: any);
|
|
186
|
+
get processEventsAfterPublish(): any;
|
|
187
|
+
set skipCsnCheck(value: any);
|
|
188
|
+
get skipCsnCheck(): any;
|
|
189
|
+
set disableRedis(value: any);
|
|
190
|
+
get disableRedis(): any;
|
|
191
|
+
set updatePeriodicEvents(value: any);
|
|
192
|
+
get updatePeriodicEvents(): any;
|
|
193
|
+
set registerAsEventProcessor(value: any);
|
|
194
|
+
get registerAsEventProcessor(): any;
|
|
195
|
+
set thresholdLoggingEventProcessing(value: any);
|
|
196
|
+
get thresholdLoggingEventProcessing(): any;
|
|
197
|
+
set useAsCAPOutbox(value: any);
|
|
198
|
+
get useAsCAPOutbox(): any;
|
|
199
|
+
set userId(value: any);
|
|
200
|
+
get userId(): any;
|
|
201
|
+
set cleanupLocksAndEventsForDev(value: any);
|
|
202
|
+
get cleanupLocksAndEventsForDev(): any;
|
|
203
|
+
set redisOptions(value: any);
|
|
204
|
+
get redisOptions(): any;
|
|
205
|
+
set insertEventsBeforeCommit(value: any);
|
|
206
|
+
get insertEventsBeforeCommit(): any;
|
|
207
|
+
get isMultiTenancy(): boolean;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export const config: Config;
|
package/src/initialize.js
CHANGED
|
@@ -128,6 +128,24 @@ const readConfigFromFile = async (configFilepath) => {
|
|
|
128
128
|
};
|
|
129
129
|
|
|
130
130
|
const registerEventProcessors = () => {
|
|
131
|
+
cds.on("listening", () => {
|
|
132
|
+
cds.connect
|
|
133
|
+
.to("cds.xt.DeploymentService")
|
|
134
|
+
.then((ds) => {
|
|
135
|
+
cds.log(COMPONENT).info("event-queue unsubscribe handler registered", {
|
|
136
|
+
redisEnabled: config.redisEnabled,
|
|
137
|
+
});
|
|
138
|
+
ds.after("unsubscribe", async (_, req) => {
|
|
139
|
+
const { tenant } = req.data;
|
|
140
|
+
config.handleUnsubscribe(tenant);
|
|
141
|
+
});
|
|
142
|
+
})
|
|
143
|
+
.catch(
|
|
144
|
+
() => {} // ignore errors as the DeploymentService is most of the time only available in the mtx sidecar
|
|
145
|
+
);
|
|
146
|
+
});
|
|
147
|
+
config.redisEnabled && config.attachRedisUnsubscribeHandler();
|
|
148
|
+
|
|
131
149
|
if (!config.registerAsEventProcessor) {
|
|
132
150
|
return;
|
|
133
151
|
}
|
package/src/processEventQueue.js
CHANGED
|
@@ -268,10 +268,15 @@ const _checkEventIsBlocked = async (baseInstance) => {
|
|
|
268
268
|
);
|
|
269
269
|
}
|
|
270
270
|
|
|
271
|
+
if (!eventBlocked) {
|
|
272
|
+
eventBlocked = config.isTenantUnsubscribed(baseInstance.context.tenant);
|
|
273
|
+
}
|
|
274
|
+
|
|
271
275
|
if (eventBlocked) {
|
|
272
276
|
baseInstance.logger.info("skipping run because event is blocked by configuration", {
|
|
273
277
|
type: baseInstance.eventType,
|
|
274
278
|
subType: baseInstance.eventSubType,
|
|
279
|
+
tenantUnsubscribed: config.isTenantUnsubscribed(baseInstance.context.tenant),
|
|
275
280
|
});
|
|
276
281
|
}
|
|
277
282
|
return eventBlocked;
|
|
@@ -10,7 +10,10 @@ const COMPONENT_NAME = "/eventQueue/shared/eventScheduler";
|
|
|
10
10
|
let instance;
|
|
11
11
|
class EventScheduler {
|
|
12
12
|
#scheduledEvents = {};
|
|
13
|
-
|
|
13
|
+
#eventsByTenants = {};
|
|
14
|
+
constructor() {
|
|
15
|
+
config.attachUnsubscribeHandler(this.clearForTenant.bind(this));
|
|
16
|
+
}
|
|
14
17
|
|
|
15
18
|
scheduleEvent(tenantId, type, subType, startAfter) {
|
|
16
19
|
const { date, relative } = this.calculateOffset(type, subType, startAfter);
|
|
@@ -37,6 +40,8 @@ class EventScheduler {
|
|
|
37
40
|
}, relative).unref();
|
|
38
41
|
}
|
|
39
42
|
|
|
43
|
+
clearForTenant() {}
|
|
44
|
+
|
|
40
45
|
calculateOffset(type, subType, startAfter) {
|
|
41
46
|
const eventConfig = config.getEventConfig(type, subType);
|
|
42
47
|
const scheduleWithoutDelay = config.isPeriodicEvent(type, subType) && eventConfig.interval < 30 * 1000;
|