@cap-js-community/event-queue 1.4.0 → 1.4.1

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/cds-plugin.js CHANGED
@@ -6,5 +6,5 @@ const eventQueue = require("./src");
6
6
  const COMPONENT_NAME = "/eventQueue/plugin";
7
7
 
8
8
  if (!cds.build.register && cds.env.eventQueue) {
9
- eventQueue.initialize().catch((err) => cds.log(COMPONENT_NAME).error(err));
9
+ module.exports = eventQueue.initialize().catch((err) => cds.log(COMPONENT_NAME).error(err));
10
10
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cap-js-community/event-queue",
3
- "version": "1.4.0",
3
+ "version": "1.4.1",
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": [
@@ -42,12 +42,13 @@
42
42
  "node": ">=18"
43
43
  },
44
44
  "dependencies": {
45
- "redis": "4.6.13",
46
- "verror": "1.10.1",
47
- "yaml": "2.4.1"
45
+ "@sap/xssec": "^3.6.1",
46
+ "redis": "^4.6.13",
47
+ "verror": "^1.10.1",
48
+ "yaml": "^2.4.1"
48
49
  },
49
50
  "devDependencies": {
50
- "@cap-js/hana": "^0.0.6",
51
+ "@cap-js/hana": "^0.1.0",
51
52
  "@cap-js/sqlite": "^1.5.0",
52
53
  "@sap/cds": "^7.7.0",
53
54
  "@sap/cds-dk": "^7.5.1",
@@ -23,8 +23,6 @@ const TRIES_FOR_EXCEEDED_EVENTS = 3;
23
23
  const EVENT_START_AFTER_HEADROOM = 3 * 1000;
24
24
  const ETAG_CHECK_AFTER_MIN = 10;
25
25
 
26
- let serviceBindingCache = null;
27
-
28
26
  class EventQueueProcessorBase {
29
27
  #eventsWithExceededTries = [];
30
28
  #exceededTriesExceeded = [];
@@ -72,8 +70,6 @@ class EventQueueProcessorBase {
72
70
  this.__txMap = {};
73
71
  this.__txRollback = {};
74
72
  this.__queueEntries = [];
75
-
76
- this.#checkGlobalContextToLocalContext();
77
73
  }
78
74
 
79
75
  /**
@@ -537,10 +533,7 @@ class EventQueueProcessorBase {
537
533
  async getQueueEntriesAndSetToInProgress() {
538
534
  let result = [];
539
535
  const refDateStartAfter = new Date(Date.now() + this.#config.runInterval * 1.2);
540
- this.#checkGlobalContextToLocalContext();
541
536
  await executeInNewTransaction(this.__baseContext, "eventQueue-getQueueEntriesAndSetToInProgress", async (tx) => {
542
- this.#checkGlobalContextToLocalContext();
543
- await this.checkTxConsistency(tx);
544
537
  const entries = await tx.run(
545
538
  SELECT.from(this.#config.tableNameEventQueue)
546
539
  .forUpdate({ wait: this.#config.forUpdateTimeout })
@@ -667,57 +660,6 @@ class EventQueueProcessorBase {
667
660
  return result;
668
661
  }
669
662
 
670
- async checkTxConsistency(tx) {
671
- if (!this.#config.enableTxConsistencyCheck) {
672
- return;
673
- }
674
-
675
- const errorHandler = (err) =>
676
- this.logger.error("tx consistency check failed!", err, {
677
- type: this.eventType,
678
- subType: this.eventSubType,
679
- txTenant: tx.context.tenant,
680
- globalCdsTenant: cds.context.tenant,
681
- });
682
- let txSchema, serviceManagerSchema;
683
- try {
684
- const schemaPromise = tx.run("SELECT CURRENT_SCHEMA FROM DUMMY");
685
- const [schema, serviceManagerBindings] = await Promise.allSettled([schemaPromise, this.#getServiceBindings()]);
686
- if (schema.reason) {
687
- errorHandler(schema.reason);
688
- return;
689
- }
690
- if (serviceManagerBindings.reason) {
691
- errorHandler(serviceManagerBindings.reason);
692
- return;
693
- }
694
-
695
- txSchema = schema.value[0].CURRENT_SCHEMA;
696
- serviceManagerSchema = serviceManagerBindings.value.find((t) => t.labels.tenant_id[0] === tx.context.tenant)
697
- .credentials.schema;
698
- } catch (err) {
699
- errorHandler(err);
700
- }
701
- if (serviceManagerSchema && txSchema !== serviceManagerSchema) {
702
- const err = EventQueueError.dbClientSchemaMismatch(tx.context.tenant, txSchema, serviceManagerSchema);
703
- errorHandler(err);
704
- throw err;
705
- }
706
- }
707
-
708
- async #getServiceBindings() {
709
- if (!(serviceBindingCache && serviceBindingCache.expireTs >= Date.now())) {
710
- const mtxServiceManager = require("@sap/cds-mtxs/srv/plugins/hana/srv-mgr");
711
- serviceBindingCache = {
712
- expireTs: Date.now() + 10 * 60 * 1000,
713
- value: mtxServiceManager.getAll().catch(() => {
714
- serviceBindingCache = null;
715
- }),
716
- };
717
- }
718
- return await serviceBindingCache.value;
719
- }
720
-
721
663
  async #selectLastSuccessfulPeriodicTimestamp() {
722
664
  const entry = await SELECT.one
723
665
  .from(this.#config.tableNameEventQueue)
@@ -730,48 +672,6 @@ class EventQueueProcessorBase {
730
672
  return entry.lastAttemptsTs;
731
673
  }
732
674
 
733
- #checkGlobalContextToLocalContext() {
734
- if (!this.#config.enableTxConsistencyCheck) {
735
- return;
736
- }
737
- if (this.__context.tenant !== cds.context.tenant) {
738
- throw EventQueueError.globalCdsContextNotMatchingLocal(
739
- JSON.stringify(
740
- {
741
- correlationId: cds.context.id,
742
- tenantId: cds.context.tenant,
743
- timestamp: cds.context.timestamp,
744
- base: JSON.stringify(
745
- {
746
- correlationId: cds.context.context?.id,
747
- tenantId: cds.context.context?.tenant,
748
- timestamp: cds.context.context?.timestamp,
749
- },
750
- null,
751
- 2
752
- ),
753
- },
754
- null,
755
- 2
756
- ),
757
- JSON.stringify(
758
- {
759
- correlationId: this.__context.id,
760
- tenantId: this.__context.tenant,
761
- timestamp: this.__context.timestamp,
762
- base: JSON.stringify({
763
- correlationId: this.__context.context?.id,
764
- tenantId: this.__context.context?.tenant,
765
- timestamp: this.__context.context?.timestamp,
766
- }),
767
- },
768
- null,
769
- 2
770
- )
771
- );
772
- }
773
- }
774
-
775
675
  #handleDelayedEvents(delayedEvents) {
776
676
  for (const delayedEvent of delayedEvents) {
777
677
  this.#eventSchedulerInstance.scheduleEvent(
package/src/config.js CHANGED
@@ -64,7 +64,6 @@ class Config {
64
64
  #thresholdLoggingEventProcessing;
65
65
  #useAsCAPOutbox;
66
66
  #userId;
67
- #enableTxConsistencyCheck;
68
67
  #cleanupLocksAndEventsForDev;
69
68
  #redisOptions;
70
69
  static #instance;
@@ -479,14 +478,6 @@ class Config {
479
478
  return this.#userId;
480
479
  }
481
480
 
482
- set enableTxConsistencyCheck(value) {
483
- this.#enableTxConsistencyCheck = value;
484
- }
485
-
486
- get enableTxConsistencyCheck() {
487
- return this.#enableTxConsistencyCheck;
488
- }
489
-
490
481
  set cleanupLocksAndEventsForDev(value) {
491
482
  this.#cleanupLocksAndEventsForDev = value;
492
483
  }
package/src/initialize.js CHANGED
@@ -32,7 +32,6 @@ const CONFIG_VARS = [
32
32
  ["thresholdLoggingEventProcessing", 50],
33
33
  ["useAsCAPOutbox", false],
34
34
  ["userId", null],
35
- ["enableTxConsistencyCheck", false],
36
35
  ["cleanupLocksAndEventsForDev", false],
37
36
  ["redisOptions", {}],
38
37
  ];
@@ -48,7 +47,6 @@ const initialize = async ({
48
47
  thresholdLoggingEventProcessing,
49
48
  useAsCAPOutbox,
50
49
  userId,
51
- enableTxConsistencyCheck,
52
50
  cleanupLocksAndEventsForDev,
53
51
  redisOptions,
54
52
  } = {}) => {
@@ -68,7 +66,6 @@ const initialize = async ({
68
66
  thresholdLoggingEventProcessing,
69
67
  useAsCAPOutbox,
70
68
  userId,
71
- enableTxConsistencyCheck,
72
69
  cleanupLocksAndEventsForDev,
73
70
  redisOptions
74
71
  );
@@ -96,9 +96,6 @@ const processEventQueue = async (context, eventType, eventSubType, startTime = n
96
96
  }
97
97
  } catch (err) {
98
98
  cds.log(COMPONENT_NAME).error("Processing event queue failed with unexpected error.", err, {
99
- tenantId: context?.tenant,
100
- tenantIdBase: baseInstance?.context?.tenant,
101
- globalTenantId: cds.context?.tenant,
102
99
  eventType,
103
100
  eventSubType,
104
101
  });
@@ -188,9 +185,6 @@ const processPeriodicEvent = async (context, eventTypeInstance) => {
188
185
  cds.log(COMPONENT_NAME).error("Processing periodic events failed with unexpected error.", err, {
189
186
  eventType: eventTypeInstance?.eventType,
190
187
  eventSubType: eventTypeInstance?.eventSubType,
191
- tenantId: context?.tenant,
192
- tenantIdBase: eventTypeInstance?.context?.tenant,
193
- globalTenantId: cds.context?.tenant,
194
188
  });
195
189
  } finally {
196
190
  await eventTypeInstance?.handleReleaseLock();
@@ -7,7 +7,7 @@ const cds = require("@sap/cds");
7
7
  const redis = require("../shared/redis");
8
8
  const { checkLockExistsAndReturnValue } = require("../shared/distributedLock");
9
9
  const config = require("../config");
10
- const { getSubdomainForTenantId } = require("../shared/cdsHelper");
10
+ const common = require("../shared/common");
11
11
  const { runEventCombinationForTenant } = require("../runner/runnerHelper");
12
12
 
13
13
  const EVENT_MESSAGE_CHANNEL = "EVENT_QUEUE_MESSAGE_CHANNEL";
@@ -29,13 +29,10 @@ const broadcastEvent = async (tenantId, events) => {
29
29
  if (config.registerAsEventProcessor) {
30
30
  let context = {};
31
31
  if (tenantId) {
32
- const subdomain = await getSubdomainForTenantId(tenantId);
33
- const user = new cds.User.Privileged(config.userId);
32
+ const user = new cds.User.Privileged({ id: config.userId, authInfo: await common.getAuthInfo(tenantId) });
34
33
  context = {
35
- // NOTE: we need this because of logging otherwise logs would not contain the subdomain
36
34
  tenant: tenantId,
37
35
  user,
38
- http: { req: { authInfo: { getSubdomain: () => subdomain } } },
39
36
  };
40
37
  }
41
38
 
@@ -4,8 +4,8 @@ const cds = require("@sap/cds");
4
4
 
5
5
  const redis = require("../shared/redis");
6
6
  const config = require("../config");
7
- const { getSubdomainForTenantId } = require("../shared/cdsHelper");
8
7
  const runnerHelper = require("../runner/runnerHelper");
8
+ const common = require("../shared/common");
9
9
 
10
10
  const EVENT_MESSAGE_CHANNEL = "EVENT_QUEUE_MESSAGE_CHANNEL";
11
11
  const COMPONENT_NAME = "/eventQueue/redisSub";
@@ -35,13 +35,10 @@ const _messageHandlerProcessEvents = async (messageData) => {
35
35
  return;
36
36
  }
37
37
 
38
- const subdomain = await getSubdomainForTenantId(tenantId);
39
- const user = new cds.User.Privileged(config.userId);
38
+ const user = new cds.User.Privileged({ id: config.userId, authInfo: await common.getAuthInfo(tenantId) });
40
39
  const tenantContext = {
41
40
  tenant: tenantId,
42
41
  user,
43
- // NOTE: we need this because of logging otherwise logs would not contain the subdomain
44
- http: { req: { authInfo: { getSubdomain: () => subdomain } } },
45
42
  };
46
43
 
47
44
  if (!config.getEventConfig(type, subType)) {
@@ -9,9 +9,8 @@ const WorkerQueue = require("../shared/WorkerQueue");
9
9
  const cdsHelper = require("../shared/cdsHelper");
10
10
  const distributedLock = require("../shared/distributedLock");
11
11
  const SetIntervalDriftSafe = require("../shared/SetIntervalDriftSafe");
12
- const { getSubdomainForTenantId } = require("../shared/cdsHelper");
13
12
  const periodicEvents = require("../periodicEvents");
14
- const { hashStringTo32Bit } = require("../shared/common");
13
+ const common = require("../shared/common");
15
14
  const config = require("../config");
16
15
  const redisPub = require("../redis/redisPub");
17
16
  const openEvents = require("./openEvents");
@@ -80,7 +79,7 @@ const _multiTenancyRedis = async () => {
80
79
  };
81
80
 
82
81
  const _checkPeriodicEventUpdate = async (tenantIds) => {
83
- const hash = hashStringTo32Bit(JSON.stringify(tenantIds));
82
+ const hash = common.hashStringTo32Bit(JSON.stringify(tenantIds));
84
83
  if (!tenantIdHash) {
85
84
  tenantIdHash = hash;
86
85
  return await _multiTenancyPeriodicEvents(tenantIds).catch((err) => {
@@ -137,13 +136,10 @@ const _executeEventsAllTenants = async (tenantIds, runId) => {
137
136
  const promises = [];
138
137
 
139
138
  for (const tenantId of tenantIds) {
140
- const subdomain = await getSubdomainForTenantId(tenantId);
141
- const user = new cds.User.Privileged(config.userId);
139
+ const user = new cds.User.Privileged({ id: config.userId, authInfo: await common.getAuthInfo(tenantId) });
142
140
  const tenantContext = {
143
141
  tenant: tenantId,
144
142
  user,
145
- // NOTE: we need this because of logging otherwise logs would not contain the subdomain
146
- http: { req: { authInfo: { getSubdomain: () => subdomain } } },
147
143
  };
148
144
  const events = await cds.tx(tenantContext, async (tx) => {
149
145
  return await openEvents.getOpenQueueEntries(tx);
@@ -184,13 +180,10 @@ const _executeEventsAllTenants = async (tenantIds, runId) => {
184
180
  const _executePeriodicEventsAllTenants = async (tenantIds) => {
185
181
  for (const tenantId of tenantIds) {
186
182
  try {
187
- const subdomain = await getSubdomainForTenantId(tenantId);
188
- const user = new cds.User.Privileged(config.userId);
183
+ const user = new cds.User.Privileged({ id: config.userId, authInfo: await common.getAuthInfo(tenantId) });
189
184
  const tenantContext = {
190
185
  tenant: tenantId,
191
186
  user,
192
- // NOTE: we need this because of logging otherwise logs would not contain the subdomain
193
- http: { req: { authInfo: { getSubdomain: () => subdomain } } },
194
187
  };
195
188
  await cds.tx(tenantContext, async ({ context }) => {
196
189
  if (!config.redisEnabled) {
@@ -350,7 +343,7 @@ const _checkPeriodicEventsSingleTenant = async (context) => {
350
343
  try {
351
344
  logger.info("executing updating periodic events", {
352
345
  tenantId: context.tenant,
353
- subdomain: context.http?.req.authInfo.getSubdomain(),
346
+ subdomain: context.user?.authInfo?.getSubdomain(),
354
347
  });
355
348
  await periodicEvents.checkAndInsertPeriodicEvents(context);
356
349
  } catch (err) {
@@ -4,8 +4,7 @@ const VError = require("verror");
4
4
  const cds = require("@sap/cds");
5
5
 
6
6
  const config = require("../config");
7
-
8
- const subdomainCache = {};
7
+ const common = require("./common");
9
8
 
10
9
  const VERROR_CLUSTER_NAME = "ExecuteInNewTransactionError";
11
10
  const COMPONENT_NAME = "/eventQueue/cdsHelper";
@@ -24,7 +23,7 @@ async function executeInNewTransaction(context = {}, transactionTag, fn, args, {
24
23
  const parameters = Array.isArray(args) ? args : [args];
25
24
  const logger = cds.log(COMPONENT_NAME);
26
25
  try {
27
- const user = new cds.User.Privileged(config.userId);
26
+ const user = new cds.User.Privileged({ id: config.userId, authInfo: await common.getAuthInfo(context.tenant) });
28
27
  if (cds.db.kind === "hana") {
29
28
  await cds.tx(
30
29
  {
@@ -33,7 +32,6 @@ async function executeInNewTransaction(context = {}, transactionTag, fn, args, {
33
32
  locale: context.locale,
34
33
  user,
35
34
  headers: context.headers,
36
- http: context.http,
37
35
  },
38
36
  async (tx) => {
39
37
  tx.context._ = context._ ?? {};
@@ -51,7 +49,6 @@ async function executeInNewTransaction(context = {}, transactionTag, fn, args, {
51
49
  locale: context.locale,
52
50
  user,
53
51
  headers: context.headers,
54
- http: context.http,
55
52
  },
56
53
  async (tx) => fn(tx, ...parameters)
57
54
  );
@@ -102,29 +99,6 @@ class TriggerRollback extends VError {
102
99
  }
103
100
  }
104
101
 
105
- const getSubdomainForTenantId = async (tenantId) => {
106
- if (!config.isMultiTenancy) {
107
- return null;
108
- }
109
- if (subdomainCache[tenantId]) {
110
- return await subdomainCache[tenantId];
111
- }
112
- subdomainCache[tenantId] = new Promise((resolve) => {
113
- cds.connect
114
- .to("cds.xt.SaasProvisioningService")
115
- .then((ssp) => {
116
- ssp
117
- .get("/tenant", { subscribedTenantId: tenantId })
118
- .then((response) => {
119
- resolve(response.subscribedSubdomain);
120
- })
121
- .catch(() => resolve(null));
122
- })
123
- .catch(() => resolve(null));
124
- });
125
- return await subdomainCache[tenantId];
126
- };
127
-
128
102
  const getAllTenantIds = async () => {
129
103
  if (!config.isMultiTenancy) {
130
104
  return null;
@@ -150,6 +124,5 @@ const isFakeTenant = (tenantId) => /00000000-0000-4000-8000-\d{12}/.test(tenantI
150
124
  module.exports = {
151
125
  executeInNewTransaction,
152
126
  TriggerRollback,
153
- getSubdomainForTenantId,
154
127
  getAllTenantIds,
155
128
  };
@@ -1,6 +1,18 @@
1
1
  "use strict";
2
2
 
3
3
  const crypto = require("crypto");
4
+ const { promisify } = require("util");
5
+
6
+ const cds = require("@sap/cds");
7
+ const xssec = require("@sap/xssec");
8
+
9
+ const getAuthTokenAsync = promisify(xssec.requests.requestClientCredentialsToken);
10
+ const getCreateSecurityContextAsync = promisify(xssec.createSecurityContext);
11
+
12
+ let authInfoCache = {};
13
+ const MARGIN_AUTH_INFO_EXPIRY = 60 * 1000;
14
+ const COMPONENT_NAME = "/eventQueue/common";
15
+
4
16
  const arrayToFlatMap = (array, key = "ID") => {
5
17
  return array.reduce((result, element) => {
6
18
  result[element[key]] = element;
@@ -61,4 +73,43 @@ const processChunkedSync = (inputs, chunkSize, chunkHandler) => {
61
73
 
62
74
  const hashStringTo32Bit = (value) => crypto.createHash("sha256").update(String(value)).digest("base64").slice(0, 32);
63
75
 
64
- module.exports = { arrayToFlatMap, limiter, isValidDate, processChunkedSync, hashStringTo32Bit };
76
+ const _getNewAuthInfo = async (tenantId) => {
77
+ try {
78
+ const token = await getAuthTokenAsync(null, cds.requires.auth.credentials, null, tenantId);
79
+ const authInfo = await getCreateSecurityContextAsync(token, cds.requires.auth.credentials);
80
+ authInfoCache[tenantId].expireTs = authInfo.getExpirationDate().getTime() - MARGIN_AUTH_INFO_EXPIRY;
81
+ return authInfo;
82
+ } catch (err) {
83
+ authInfoCache[tenantId] = null;
84
+ cds.log(COMPONENT_NAME).warn("failed to request authInfo", err);
85
+ }
86
+ };
87
+
88
+ const getAuthInfo = async (tenantId) => {
89
+ if (!cds.requires?.auth?.credentials) {
90
+ return null; // no credentials not authInfo
91
+ }
92
+
93
+ // not existing or existing but expired
94
+ if (
95
+ !authInfoCache[tenantId] ||
96
+ (authInfoCache[tenantId] && authInfoCache[tenantId].expireTs && Date.now() > authInfoCache[tenantId].expireTs)
97
+ ) {
98
+ authInfoCache[tenantId] ??= {};
99
+ authInfoCache[tenantId].value = _getNewAuthInfo(tenantId);
100
+ authInfoCache[tenantId].expireTs = null;
101
+ }
102
+ return await authInfoCache[tenantId].value;
103
+ };
104
+
105
+ module.exports = {
106
+ arrayToFlatMap,
107
+ limiter,
108
+ isValidDate,
109
+ processChunkedSync,
110
+ hashStringTo32Bit,
111
+ getAuthInfo,
112
+ __: {
113
+ clearAuthInfoCache: () => (authInfoCache = {}),
114
+ },
115
+ };