@cap-js-community/event-queue 1.4.1 → 1.4.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 +3 -3
- package/src/EventQueueProcessorBase.js +21 -10
- package/src/config.js +9 -0
- package/src/dbHandler.js +24 -0
- package/src/initialize.js +7 -1
- package/src/outbox/EventQueueGenericOutboxHandler.js +1 -0
- package/src/publishEvent.js +10 -4
- package/src/runner/openEvents.js +1 -0
- package/src/shared/distributedLock.js +15 -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.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
|
"files": [
|
|
@@ -50,8 +50,8 @@
|
|
|
50
50
|
"devDependencies": {
|
|
51
51
|
"@cap-js/hana": "^0.1.0",
|
|
52
52
|
"@cap-js/sqlite": "^1.5.0",
|
|
53
|
-
"@sap/cds": "^7.
|
|
54
|
-
"@sap/cds-dk": "^7.
|
|
53
|
+
"@sap/cds": "^7.8.0",
|
|
54
|
+
"@sap/cds-dk": "^7.8.0",
|
|
55
55
|
"eslint": "^8.56.0",
|
|
56
56
|
"eslint-config-prettier": "^9.1.0",
|
|
57
57
|
"eslint-plugin-jest": "^27.9.0",
|
|
@@ -279,22 +279,33 @@ class EventQueueProcessorBase {
|
|
|
279
279
|
eventSubType: this.#eventSubType,
|
|
280
280
|
});
|
|
281
281
|
const statusMap = this.commitOnEventLevel || returnMap ? {} : this.__statusMap;
|
|
282
|
-
|
|
283
|
-
queueEntryProcessingStatusTuple.forEach(([id, processingStatus]) =>
|
|
284
|
-
this.#determineAndAddEventStatusToMap(id, processingStatus, statusMap)
|
|
285
|
-
);
|
|
286
|
-
} catch (error) {
|
|
282
|
+
const errorHandler = (error) => {
|
|
287
283
|
queueEntries.forEach((queueEntry) =>
|
|
288
284
|
this.#determineAndAddEventStatusToMap(queueEntry.ID, EventProcessingStatus.Error, statusMap)
|
|
289
285
|
);
|
|
290
286
|
this.logger.error(
|
|
291
287
|
"The supplied status tuple doesn't have the required structure. Setting all entries to error.",
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
288
|
+
...[
|
|
289
|
+
error,
|
|
290
|
+
{
|
|
291
|
+
eventType: this.#eventType,
|
|
292
|
+
eventSubType: this.#eventSubType,
|
|
293
|
+
},
|
|
294
|
+
].filter((a) => a)
|
|
295
|
+
);
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
if (!queueEntryProcessingStatusTuple) {
|
|
299
|
+
errorHandler();
|
|
300
|
+
return statusMap;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
try {
|
|
304
|
+
queueEntryProcessingStatusTuple.forEach(([id, processingStatus]) =>
|
|
305
|
+
this.#determineAndAddEventStatusToMap(id, processingStatus, statusMap)
|
|
297
306
|
);
|
|
307
|
+
} catch (error) {
|
|
308
|
+
errorHandler(error);
|
|
298
309
|
}
|
|
299
310
|
return statusMap;
|
|
300
311
|
}
|
package/src/config.js
CHANGED
|
@@ -66,6 +66,7 @@ class Config {
|
|
|
66
66
|
#userId;
|
|
67
67
|
#cleanupLocksAndEventsForDev;
|
|
68
68
|
#redisOptions;
|
|
69
|
+
#insertEventsBeforeCommit;
|
|
69
70
|
static #instance;
|
|
70
71
|
constructor() {
|
|
71
72
|
this.#logger = cds.log(COMPONENT_NAME);
|
|
@@ -494,6 +495,14 @@ class Config {
|
|
|
494
495
|
return this.#redisOptions;
|
|
495
496
|
}
|
|
496
497
|
|
|
498
|
+
set insertEventsBeforeCommit(value) {
|
|
499
|
+
this.#insertEventsBeforeCommit = value;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
get insertEventsBeforeCommit() {
|
|
503
|
+
return this.#insertEventsBeforeCommit;
|
|
504
|
+
}
|
|
505
|
+
|
|
497
506
|
get isMultiTenancy() {
|
|
498
507
|
return !!cds.requires.multitenancy;
|
|
499
508
|
}
|
package/src/dbHandler.js
CHANGED
|
@@ -6,8 +6,17 @@ const { broadcastEvent } = require("./redis/redisPub");
|
|
|
6
6
|
const config = require("./config");
|
|
7
7
|
|
|
8
8
|
const COMPONENT_NAME = "/eventQueue/dbHandler";
|
|
9
|
+
const registeredHandlers = {
|
|
10
|
+
eventQueueDbHandler: false,
|
|
11
|
+
beforeDbHandler: false,
|
|
12
|
+
};
|
|
9
13
|
|
|
10
14
|
const registerEventQueueDbHandler = (dbService) => {
|
|
15
|
+
if (registeredHandlers.eventQueueDbHandler) {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
registeredHandlers.eventQueueDbHandler = true;
|
|
11
20
|
const def = dbService.model.definitions[config.tableNameEventQueue];
|
|
12
21
|
dbService.after("CREATE", def, (_, req) => {
|
|
13
22
|
if (req.tx._skipEventQueueBroadcase) {
|
|
@@ -46,6 +55,21 @@ const registerEventQueueDbHandler = (dbService) => {
|
|
|
46
55
|
});
|
|
47
56
|
};
|
|
48
57
|
|
|
58
|
+
const registerBeforeDbHandler = (dbService) => {
|
|
59
|
+
if (!config.insertEventsBeforeCommit || registeredHandlers.beforeDbHandler) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
registeredHandlers.beforeDbHandler = true;
|
|
64
|
+
dbService.before("COMMIT", async (req) => {
|
|
65
|
+
if (req.context._eventQueueEvents?.length) {
|
|
66
|
+
await cds.tx(req).run(INSERT.into(config.tableNameEventQueue).entries(req.context._eventQueueEvents));
|
|
67
|
+
req.context._eventQueueEvents = null;
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
};
|
|
71
|
+
|
|
49
72
|
module.exports = {
|
|
50
73
|
registerEventQueueDbHandler,
|
|
74
|
+
registerBeforeDbHandler,
|
|
51
75
|
};
|
package/src/initialize.js
CHANGED
|
@@ -15,6 +15,7 @@ const redis = require("./shared/redis");
|
|
|
15
15
|
const eventQueueAsOutbox = require("./outbox/eventQueueAsOutbox");
|
|
16
16
|
const { getAllTenantIds } = require("./shared/cdsHelper");
|
|
17
17
|
const { EventProcessingStatus } = require("./constants");
|
|
18
|
+
const distributedLock = require("./shared/distributedLock");
|
|
18
19
|
|
|
19
20
|
const readFileAsync = promisify(fs.readFile);
|
|
20
21
|
|
|
@@ -34,6 +35,7 @@ const CONFIG_VARS = [
|
|
|
34
35
|
["userId", null],
|
|
35
36
|
["cleanupLocksAndEventsForDev", false],
|
|
36
37
|
["redisOptions", {}],
|
|
38
|
+
["insertEventsBeforeCommit", false],
|
|
37
39
|
];
|
|
38
40
|
|
|
39
41
|
const initialize = async ({
|
|
@@ -49,6 +51,7 @@ const initialize = async ({
|
|
|
49
51
|
userId,
|
|
50
52
|
cleanupLocksAndEventsForDev,
|
|
51
53
|
redisOptions,
|
|
54
|
+
insertEventsBeforeCommit,
|
|
52
55
|
} = {}) => {
|
|
53
56
|
if (config.initialized) {
|
|
54
57
|
return;
|
|
@@ -67,7 +70,8 @@ const initialize = async ({
|
|
|
67
70
|
useAsCAPOutbox,
|
|
68
71
|
userId,
|
|
69
72
|
cleanupLocksAndEventsForDev,
|
|
70
|
-
redisOptions
|
|
73
|
+
redisOptions,
|
|
74
|
+
insertEventsBeforeCommit
|
|
71
75
|
);
|
|
72
76
|
|
|
73
77
|
const logger = cds.log(COMPONENT);
|
|
@@ -77,6 +81,7 @@ const initialize = async ({
|
|
|
77
81
|
cds.on("connect", (service) => {
|
|
78
82
|
if (service.name === "db") {
|
|
79
83
|
config.processEventsAfterPublish && dbHandler.registerEventQueueDbHandler(service);
|
|
84
|
+
config.insertEventsBeforeCommit && dbHandler.registerBeforeDbHandler(service);
|
|
80
85
|
config.cleanupLocksAndEventsForDev && registerCleanupForDevDb().catch(() => {});
|
|
81
86
|
initFinished.then(registerEventProcessors);
|
|
82
87
|
}
|
|
@@ -166,6 +171,7 @@ const mixConfigVarsWithEnv = (...args) => {
|
|
|
166
171
|
|
|
167
172
|
const registerCdsShutdown = () => {
|
|
168
173
|
cds.on("shutdown", async () => {
|
|
174
|
+
await distributedLock.shutdownHandler();
|
|
169
175
|
await Promise.allSettled([redis.closeMainClient(), closeSubscribeClient()]);
|
|
170
176
|
});
|
|
171
177
|
};
|
|
@@ -23,6 +23,7 @@ class EventQueueGenericOutboxHandler extends EventQueueBaseClass {
|
|
|
23
23
|
delete msg._fromSend;
|
|
24
24
|
delete msg.contextUser;
|
|
25
25
|
processContext.user = new cds.User.Privileged(userId);
|
|
26
|
+
processContext._eventQueue = { processor: this, key, queueEntries, payload };
|
|
26
27
|
await cds.unboxed(service).tx(processContext)[invocationFn](msg);
|
|
27
28
|
} catch (err) {
|
|
28
29
|
status = EventProcessingStatus.Error;
|
package/src/publishEvent.js
CHANGED
|
@@ -45,10 +45,16 @@ const publishEvent = async (tx, events, skipBroadcast = false) => {
|
|
|
45
45
|
throw EventQueueError.manuelPeriodicEventInsert(type, subType);
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
48
|
+
|
|
49
|
+
if (config.insertEventsBeforeCommit) {
|
|
50
|
+
tx.context._eventQueueEvents ??= [];
|
|
51
|
+
tx.context._eventQueueEvents = tx.context._eventQueueEvents.concat(events);
|
|
52
|
+
} else {
|
|
53
|
+
tx._skipEventQueueBroadcase = skipBroadcast;
|
|
54
|
+
const result = await tx.run(INSERT.into(config.tableNameEventQueue).entries(eventsForProcessing));
|
|
55
|
+
tx._skipEventQueueBroadcase = false;
|
|
56
|
+
return result;
|
|
57
|
+
}
|
|
52
58
|
};
|
|
53
59
|
|
|
54
60
|
module.exports = {
|
package/src/runner/openEvents.js
CHANGED
|
@@ -31,6 +31,7 @@ const getOpenQueueEntries = async (tx) => {
|
|
|
31
31
|
for (const { type, subType } of entries) {
|
|
32
32
|
if (type.startsWith("CAP_OUTBOX")) {
|
|
33
33
|
if (cds.requires[subType]) {
|
|
34
|
+
await cds.connect.to(subType).catch(() => {});
|
|
34
35
|
result.push({ type, subType });
|
|
35
36
|
} else {
|
|
36
37
|
const service = await cds.connect.to(subType).catch(() => {});
|
|
@@ -6,6 +6,10 @@ const cdsHelper = require("./cdsHelper");
|
|
|
6
6
|
|
|
7
7
|
const KEY_PREFIX = "EVENT_QUEUE";
|
|
8
8
|
|
|
9
|
+
const existingLocks = {};
|
|
10
|
+
|
|
11
|
+
const REDIS_COMMAND_OK = "OK";
|
|
12
|
+
|
|
9
13
|
const acquireLock = async (context, key, { tenantScoped = true, expiryTime = config.globalTxTimeout } = {}) => {
|
|
10
14
|
const fullKey = _generateKey(context, tenantScoped, key);
|
|
11
15
|
if (config.redisEnabled) {
|
|
@@ -59,7 +63,11 @@ const _acquireLockRedis = async (context, fullKey, expiryTime, { value = "true",
|
|
|
59
63
|
PX: expiryTime,
|
|
60
64
|
...(overrideValue ? null : { NX: true }),
|
|
61
65
|
});
|
|
62
|
-
|
|
66
|
+
const isOk = result === REDIS_COMMAND_OK;
|
|
67
|
+
if (isOk) {
|
|
68
|
+
existingLocks[fullKey] = 1;
|
|
69
|
+
}
|
|
70
|
+
return isOk;
|
|
63
71
|
};
|
|
64
72
|
|
|
65
73
|
const _checkLockExistsRedis = async (context, fullKey) => {
|
|
@@ -78,6 +86,7 @@ const _checkLockExistsDb = async (context, fullKey) => {
|
|
|
78
86
|
const _releaseLockRedis = async (context, fullKey) => {
|
|
79
87
|
const client = await redis.createMainClientAndConnect(config.redisOptions);
|
|
80
88
|
await client.del(fullKey);
|
|
89
|
+
delete existingLocks[fullKey];
|
|
81
90
|
};
|
|
82
91
|
|
|
83
92
|
const _releaseLockDb = async (context, fullKey) => {
|
|
@@ -133,9 +142,14 @@ const _generateKey = (context, tenantScoped, key) => {
|
|
|
133
142
|
return `${KEY_PREFIX}_${keyParts.join("##")}`;
|
|
134
143
|
};
|
|
135
144
|
|
|
145
|
+
const shutdownHandler = async () => {
|
|
146
|
+
await Promise.allSettled(Object.keys(existingLocks).map((key) => _releaseLockRedis(null, key)));
|
|
147
|
+
};
|
|
148
|
+
|
|
136
149
|
module.exports = {
|
|
137
150
|
acquireLock,
|
|
138
151
|
releaseLock,
|
|
139
152
|
checkLockExistsAndReturnValue,
|
|
140
153
|
setValueWithExpire,
|
|
154
|
+
shutdownHandler,
|
|
141
155
|
};
|