@cap-js-community/event-queue 1.2.1 → 1.2.3
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 +2 -2
- package/src/EventQueueError.js +15 -0
- package/src/EventQueueProcessorBase.js +51 -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.3",
|
|
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": [
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"node": ">=18"
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
|
-
"redis": "4.6.
|
|
45
|
+
"redis": "4.6.13",
|
|
46
46
|
"verror": "1.10.1",
|
|
47
47
|
"yaml": "2.3.4"
|
|
48
48
|
},
|
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;
|
|
@@ -21,6 +21,8 @@ const SELECT_LIMIT_EVENTS_PER_TICK = 100;
|
|
|
21
21
|
const TRIES_FOR_EXCEEDED_EVENTS = 3;
|
|
22
22
|
const EVENT_START_AFTER_HEADROOM = 3 * 1000;
|
|
23
23
|
|
|
24
|
+
let serviceBindingCache = {};
|
|
25
|
+
|
|
24
26
|
class EventQueueProcessorBase {
|
|
25
27
|
#eventsWithExceededTries = [];
|
|
26
28
|
#exceededTriesExceeded = [];
|
|
@@ -532,6 +534,7 @@ class EventQueueProcessorBase {
|
|
|
532
534
|
let result = [];
|
|
533
535
|
const refDateStartAfter = new Date(Date.now() + this.#config.runInterval * 1.2);
|
|
534
536
|
await executeInNewTransaction(this.__baseContext, "eventQueue-getQueueEntriesAndSetToInProgress", async (tx) => {
|
|
537
|
+
await this.checkTxConsistency(tx);
|
|
535
538
|
const entries = await tx.run(
|
|
536
539
|
SELECT.from(this.#config.tableNameEventQueue)
|
|
537
540
|
.forUpdate({ wait: this.#config.forUpdateTimeout })
|
|
@@ -658,6 +661,54 @@ class EventQueueProcessorBase {
|
|
|
658
661
|
return result;
|
|
659
662
|
}
|
|
660
663
|
|
|
664
|
+
async checkTxConsistency(tx) {
|
|
665
|
+
if (!this.#config.enableTxConsistencyCheck) {
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
const errorHandler = (err) =>
|
|
670
|
+
this.logger.error("tx consistency check failed!", err, {
|
|
671
|
+
type: this.eventType,
|
|
672
|
+
subType: this.eventSubType,
|
|
673
|
+
txTenant: tx.context.tenant,
|
|
674
|
+
globalCdsTenant: cds.context.tenant,
|
|
675
|
+
});
|
|
676
|
+
let txSchema, serviceManagerSchema;
|
|
677
|
+
try {
|
|
678
|
+
const schemaPromise = tx.run("SELECT CURRENT_SCHEMA FROM DUMMY");
|
|
679
|
+
const [schema, serviceManagerBindings] = await Promise.allSettled([schemaPromise, this.#getServiceBindings()]);
|
|
680
|
+
if (schema.reason) {
|
|
681
|
+
errorHandler(schema.reason);
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
if (serviceManagerBindings.reason) {
|
|
685
|
+
errorHandler(serviceManagerBindings.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
|
+
|
|
702
|
+
async #getServiceBindings() {
|
|
703
|
+
if (serviceBindingCache && serviceBindingCache.exipreTs >= Date.now()) {
|
|
704
|
+
return serviceBindingCache.value;
|
|
705
|
+
}
|
|
706
|
+
const mtxServiceManager = require("@sap/cds-mtxs/srv/plugins/hana/srv-mgr");
|
|
707
|
+
serviceBindingCache.value = await mtxServiceManager.getAll();
|
|
708
|
+
serviceBindingCache.exipreTs = Date.now() + 10 * 60 * 1000;
|
|
709
|
+
return serviceBindingCache.value;
|
|
710
|
+
}
|
|
711
|
+
|
|
661
712
|
async #selectLastSuccessfulPeriodicTimestamp() {
|
|
662
713
|
const entry = await SELECT.one
|
|
663
714
|
.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
|