@cap-js-community/event-queue 1.2.1 → 1.2.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/EventQueueError.js +15 -0
- package/src/EventQueueProcessorBase.js +41 -0
- package/src/config.js +9 -0
- package/src/index.js +0 -6
- package/src/initialize.js +4 -1
- package/src/processEventQueue.js +10 -5
- package/src/shared/cdsHelper.js +15 -9
- package/src/shared/redis.js +4 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cap-js-community/event-queue",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.2",
|
|
4
4
|
"description": "An event queue that enables secure transactional processing of asynchronous 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": [
|
package/src/EventQueueError.js
CHANGED
|
@@ -17,6 +17,7 @@ const ERROR_CODES = {
|
|
|
17
17
|
DUPLICATE_EVENT_REGISTRATION: "DUPLICATE_EVENT_REGISTRATION",
|
|
18
18
|
NO_MANUEL_INSERT_OF_PERIODIC: "NO_MANUEL_INSERT_OF_PERIODIC",
|
|
19
19
|
LOAD_HIGHER_THAN_LIMIT: "LOAD_HIGHER_THAN_LIMIT",
|
|
20
|
+
SCHEMA_TENANT_MISMATCH: "SCHEMA_TENANT_MISMATCH",
|
|
20
21
|
};
|
|
21
22
|
|
|
22
23
|
const ERROR_CODES_META = {
|
|
@@ -63,6 +64,9 @@ const ERROR_CODES_META = {
|
|
|
63
64
|
[ERROR_CODES.LOAD_HIGHER_THAN_LIMIT]: {
|
|
64
65
|
message: "The defined load of an event is higher than the maximum defined limit. Check your configuration!",
|
|
65
66
|
},
|
|
67
|
+
[ERROR_CODES.SCHEMA_TENANT_MISMATCH]: {
|
|
68
|
+
message: "The db client associated to the tenant context does not match! Processing will be skipped.",
|
|
69
|
+
},
|
|
66
70
|
};
|
|
67
71
|
|
|
68
72
|
class EventQueueError extends VError {
|
|
@@ -220,6 +224,17 @@ class EventQueueError extends VError {
|
|
|
220
224
|
message
|
|
221
225
|
);
|
|
222
226
|
}
|
|
227
|
+
|
|
228
|
+
static dbClientSchemaMismatch(tenantId, dbClientSchema, serviceManagerSchema) {
|
|
229
|
+
const { message } = ERROR_CODES_META[ERROR_CODES.SCHEMA_TENANT_MISMATCH];
|
|
230
|
+
return new EventQueueError(
|
|
231
|
+
{
|
|
232
|
+
name: ERROR_CODES.SCHEMA_TENANT_MISMATCH,
|
|
233
|
+
info: { tenantId, dbClientSchema, serviceManagerSchema },
|
|
234
|
+
},
|
|
235
|
+
message
|
|
236
|
+
);
|
|
237
|
+
}
|
|
223
238
|
}
|
|
224
239
|
|
|
225
240
|
module.exports = EventQueueError;
|
|
@@ -532,6 +532,7 @@ class EventQueueProcessorBase {
|
|
|
532
532
|
let result = [];
|
|
533
533
|
const refDateStartAfter = new Date(Date.now() + this.#config.runInterval * 1.2);
|
|
534
534
|
await executeInNewTransaction(this.__baseContext, "eventQueue-getQueueEntriesAndSetToInProgress", async (tx) => {
|
|
535
|
+
await this.checkTxConsistency(tx);
|
|
535
536
|
const entries = await tx.run(
|
|
536
537
|
SELECT.from(this.#config.tableNameEventQueue)
|
|
537
538
|
.forUpdate({ wait: this.#config.forUpdateTimeout })
|
|
@@ -658,6 +659,46 @@ class EventQueueProcessorBase {
|
|
|
658
659
|
return result;
|
|
659
660
|
}
|
|
660
661
|
|
|
662
|
+
async checkTxConsistency(tx) {
|
|
663
|
+
if (!this.#config.enableTxConsistencyCheck) {
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
const errorHandler = (err) =>
|
|
668
|
+
this.logger.error("tx consistency check failed!", err, {
|
|
669
|
+
type: this.eventType,
|
|
670
|
+
subType: this.eventSubType,
|
|
671
|
+
txTenant: tx.context.tenant,
|
|
672
|
+
globalCdsTenant: cds.context.tenant,
|
|
673
|
+
});
|
|
674
|
+
let txSchema, serviceManagerSchema;
|
|
675
|
+
try {
|
|
676
|
+
const mtxServiceManager = require("@sap/cds-mtxs/srv/plugins/hana/srv-mgr");
|
|
677
|
+
const schemaPromise = tx.run("SELECT CURRENT_SCHEMA FROM DUMMY");
|
|
678
|
+
const serviceManagerBindingsPromise = mtxServiceManager.getAll();
|
|
679
|
+
const [schema, serviceManagerBindings] = await Promise.allSettled([schemaPromise, serviceManagerBindingsPromise]);
|
|
680
|
+
if (schema.reason) {
|
|
681
|
+
errorHandler(schema.reason);
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
if (serviceManagerBindings.reason) {
|
|
685
|
+
errorHandler(schema.reason);
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
txSchema = schema.value[0].CURRENT_SCHEMA;
|
|
690
|
+
serviceManagerSchema = serviceManagerBindings.value.find((t) => t.labels.tenant_id[0] === tx.context.tenant)
|
|
691
|
+
.credentials.schema;
|
|
692
|
+
} catch (err) {
|
|
693
|
+
errorHandler(err);
|
|
694
|
+
}
|
|
695
|
+
if (txSchema !== serviceManagerSchema) {
|
|
696
|
+
const err = EventQueueError.dbClientSchemaMismatch(tx.context.tenant, txSchema, serviceManagerSchema);
|
|
697
|
+
errorHandler(err);
|
|
698
|
+
throw err;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
|
|
661
702
|
async #selectLastSuccessfulPeriodicTimestamp() {
|
|
662
703
|
const entry = await SELECT.one
|
|
663
704
|
.from(this.#config.tableNameEventQueue)
|
package/src/config.js
CHANGED
|
@@ -56,6 +56,7 @@ class Config {
|
|
|
56
56
|
#thresholdLoggingEventProcessing;
|
|
57
57
|
#useAsCAPOutbox;
|
|
58
58
|
#userId;
|
|
59
|
+
#enableTxConsistencyCheck;
|
|
59
60
|
static #instance;
|
|
60
61
|
constructor() {
|
|
61
62
|
this.#logger = cds.log(COMPONENT_NAME);
|
|
@@ -462,6 +463,14 @@ class Config {
|
|
|
462
463
|
return this.#userId;
|
|
463
464
|
}
|
|
464
465
|
|
|
466
|
+
set enableTxConsistencyCheck(value) {
|
|
467
|
+
this.#enableTxConsistencyCheck = value;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
get enableTxConsistencyCheck() {
|
|
471
|
+
return this.#enableTxConsistencyCheck;
|
|
472
|
+
}
|
|
473
|
+
|
|
465
474
|
get isMultiTenancy() {
|
|
466
475
|
return !!cds.requires.multitenancy;
|
|
467
476
|
}
|
package/src/index.js
CHANGED
|
@@ -1,13 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
// TODO: how to deal with fatal logs
|
|
4
|
-
// TODO: think about situations where isInitialized need to be checked - publishEvent access config which is not initialized
|
|
5
4
|
// TODO: add tests for config --> similar to csn check
|
|
6
|
-
// TODO: redis client check reconnect strategy
|
|
7
|
-
|
|
8
|
-
// TODO: for test
|
|
9
|
-
// --> deeper look into the functions e.g. getQueueEntriesAndSetToInProgress
|
|
10
|
-
// TODO: add test for commit on event level and stuff like that
|
|
11
5
|
|
|
12
6
|
module.exports = {
|
|
13
7
|
...require("./initialize"),
|
package/src/initialize.js
CHANGED
|
@@ -37,6 +37,7 @@ const CONFIG_VARS = [
|
|
|
37
37
|
["thresholdLoggingEventProcessing", 50],
|
|
38
38
|
["useAsCAPOutbox", false],
|
|
39
39
|
["userId", null],
|
|
40
|
+
["enableTxConsistencyCheck", false],
|
|
40
41
|
];
|
|
41
42
|
|
|
42
43
|
const initialize = async ({
|
|
@@ -53,6 +54,7 @@ const initialize = async ({
|
|
|
53
54
|
thresholdLoggingEventProcessing,
|
|
54
55
|
useAsCAPOutbox,
|
|
55
56
|
userId,
|
|
57
|
+
enableTxConsistencyCheck,
|
|
56
58
|
} = {}) => {
|
|
57
59
|
// TODO: initialize check:
|
|
58
60
|
// - content of yaml check
|
|
@@ -76,7 +78,8 @@ const initialize = async ({
|
|
|
76
78
|
updatePeriodicEvents,
|
|
77
79
|
thresholdLoggingEventProcessing,
|
|
78
80
|
useAsCAPOutbox,
|
|
79
|
-
userId
|
|
81
|
+
userId,
|
|
82
|
+
enableTxConsistencyCheck
|
|
80
83
|
);
|
|
81
84
|
|
|
82
85
|
const logger = cds.log(COMPONENT);
|
package/src/processEventQueue.js
CHANGED
|
@@ -34,7 +34,7 @@ const processEventQueue = async (context, eventType, eventSubType, startTime = n
|
|
|
34
34
|
return;
|
|
35
35
|
}
|
|
36
36
|
if (baseInstance.isPeriodicEvent) {
|
|
37
|
-
return await processPeriodicEvent(baseInstance);
|
|
37
|
+
return await processPeriodicEvent(context, baseInstance);
|
|
38
38
|
}
|
|
39
39
|
eventConfig.startTime = startTime;
|
|
40
40
|
while (shouldContinue) {
|
|
@@ -93,6 +93,9 @@ const processEventQueue = async (context, eventType, eventSubType, startTime = n
|
|
|
93
93
|
}
|
|
94
94
|
} catch (err) {
|
|
95
95
|
cds.log(COMPONENT_NAME).error("Processing event queue failed with unexpected error.", err, {
|
|
96
|
+
tenantId: context?.tenant,
|
|
97
|
+
tenantIdBase: baseInstance?.context?.tenant,
|
|
98
|
+
globalTenantId: cds.context?.tenant,
|
|
96
99
|
eventType,
|
|
97
100
|
eventSubType,
|
|
98
101
|
});
|
|
@@ -115,10 +118,7 @@ const reevaluateShouldContinue = (eventTypeInstance, iterationCounter, startTime
|
|
|
115
118
|
return false;
|
|
116
119
|
};
|
|
117
120
|
|
|
118
|
-
const processPeriodicEvent = async (eventTypeInstance) => {
|
|
119
|
-
let queueEntry;
|
|
120
|
-
let processNext = true;
|
|
121
|
-
|
|
121
|
+
const processPeriodicEvent = async (context, eventTypeInstance) => {
|
|
122
122
|
const isPeriodicEventBlockedCb = config.isPeriodicEventBlockedCb;
|
|
123
123
|
const params = [eventTypeInstance.eventType, eventTypeInstance.eventSubType, eventTypeInstance.context.tenant];
|
|
124
124
|
let eventBlocked = false;
|
|
@@ -145,6 +145,8 @@ const processPeriodicEvent = async (eventTypeInstance) => {
|
|
|
145
145
|
}
|
|
146
146
|
|
|
147
147
|
try {
|
|
148
|
+
let queueEntry;
|
|
149
|
+
let processNext = true;
|
|
148
150
|
while (processNext) {
|
|
149
151
|
await executeInNewTransaction(
|
|
150
152
|
eventTypeInstance.context,
|
|
@@ -207,6 +209,9 @@ const processPeriodicEvent = async (eventTypeInstance) => {
|
|
|
207
209
|
cds.log(COMPONENT_NAME).error("Processing periodic events failed with unexpected error.", err, {
|
|
208
210
|
eventType: eventTypeInstance?.eventType,
|
|
209
211
|
eventSubType: eventTypeInstance?.eventSubType,
|
|
212
|
+
tenantId: context?.tenant,
|
|
213
|
+
tenantIdBase: eventTypeInstance?.context?.tenant,
|
|
214
|
+
globalTenantId: cds.context?.tenant,
|
|
210
215
|
});
|
|
211
216
|
} finally {
|
|
212
217
|
await eventTypeInstance?.handleReleaseLock();
|
package/src/shared/cdsHelper.js
CHANGED
|
@@ -100,16 +100,22 @@ const getSubdomainForTenantId = async (tenantId) => {
|
|
|
100
100
|
return null;
|
|
101
101
|
}
|
|
102
102
|
if (subdomainCache[tenantId]) {
|
|
103
|
-
return subdomainCache[tenantId];
|
|
104
|
-
}
|
|
105
|
-
try {
|
|
106
|
-
const ssp = await cds.connect.to("cds.xt.SaasProvisioningService");
|
|
107
|
-
const response = await ssp.get("/tenant", { subscribedTenantId: tenantId });
|
|
108
|
-
subdomainCache[tenantId] = response.subscribedSubdomain;
|
|
109
|
-
return response.subscribedSubdomain;
|
|
110
|
-
} catch (err) {
|
|
111
|
-
return null;
|
|
103
|
+
return await subdomainCache[tenantId];
|
|
112
104
|
}
|
|
105
|
+
subdomainCache[tenantId] = new Promise((resolve) => {
|
|
106
|
+
cds.connect
|
|
107
|
+
.to("cds.xt.SaasProvisioningService")
|
|
108
|
+
.then((ssp) => {
|
|
109
|
+
ssp
|
|
110
|
+
.get("/tenant", { subscribedTenantId: tenantId })
|
|
111
|
+
.then((response) => {
|
|
112
|
+
resolve(response.subscribedSubdomain);
|
|
113
|
+
})
|
|
114
|
+
.catch(() => resolve(null));
|
|
115
|
+
})
|
|
116
|
+
.catch(() => resolve(null));
|
|
117
|
+
});
|
|
118
|
+
return await subdomainCache[tenantId];
|
|
113
119
|
};
|
|
114
120
|
|
|
115
121
|
const getAllTenantIds = async () => {
|
package/src/shared/redis.js
CHANGED
|
@@ -70,17 +70,16 @@ const createClientAndConnect = async (errorHandlerCreateClient) => {
|
|
|
70
70
|
return client;
|
|
71
71
|
};
|
|
72
72
|
|
|
73
|
-
const subscribeRedisChannel = (channel,
|
|
73
|
+
const subscribeRedisChannel = (channel, subscribeHandler) => {
|
|
74
74
|
const errorHandlerCreateClient = (err) => {
|
|
75
75
|
cds.log(COMPONENT_NAME).error(`error from redis client for pub/sub failed for channel ${channel}`, err);
|
|
76
76
|
subscriberChannelClientPromise[channel] = null;
|
|
77
|
-
setTimeout(() => subscribeRedisChannel(channel,
|
|
77
|
+
setTimeout(() => subscribeRedisChannel(channel, subscribeHandler), 5 * 1000).unref();
|
|
78
78
|
};
|
|
79
|
-
subscriberChannelClientPromise[channel] = createClientAndConnect(errorHandlerCreateClient)
|
|
80
|
-
subscriberChannelClientPromise[channel]
|
|
79
|
+
subscriberChannelClientPromise[channel] = createClientAndConnect(errorHandlerCreateClient)
|
|
81
80
|
.then((client) => {
|
|
82
81
|
cds.log(COMPONENT_NAME).info("subscribe redis client connected channel", { channel });
|
|
83
|
-
client.subscribe(channel,
|
|
82
|
+
client.subscribe(channel, subscribeHandler).catch(errorHandlerCreateClient);
|
|
84
83
|
})
|
|
85
84
|
.catch((err) => {
|
|
86
85
|
cds
|