@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cap-js-community/event-queue",
3
- "version": "1.2.1",
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": [
@@ -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);
@@ -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();
@@ -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 () => {
@@ -70,17 +70,16 @@ const createClientAndConnect = async (errorHandlerCreateClient) => {
70
70
  return client;
71
71
  };
72
72
 
73
- const subscribeRedisChannel = (channel, subscribeCb) => {
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, subscribeCb), 5 * 1000).unref();
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, subscribeCb).catch(errorHandlerCreateClient);
82
+ client.subscribe(channel, subscribeHandler).catch(errorHandlerCreateClient);
84
83
  })
85
84
  .catch((err) => {
86
85
  cds