@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cap-js-community/event-queue",
3
- "version": "1.2.1",
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.12",
45
+ "redis": "4.6.13",
46
46
  "verror": "1.10.1",
47
47
  "yaml": "2.3.4"
48
48
  },
@@ -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);
@@ -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