@cap-js-community/event-queue 1.7.3 → 1.8.0

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/db/Event.cds CHANGED
@@ -9,6 +9,7 @@ type Status: Integer enum {
9
9
  Done = 2;
10
10
  Error = 3;
11
11
  Exceeded = 4;
12
+ Suspended = 5;
12
13
  }
13
14
 
14
15
  entity Event: cuid {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cap-js-community/event-queue",
3
- "version": "1.7.3",
3
+ "version": "1.8.0",
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
  "types": "src/index.d.ts",
@@ -47,13 +47,13 @@
47
47
  "cron-parser": "^4.9.0",
48
48
  "redis": "^4.7.0",
49
49
  "verror": "^1.10.1",
50
- "yaml": "^2.5.1"
50
+ "yaml": "^2.6.1"
51
51
  },
52
52
  "devDependencies": {
53
53
  "@cap-js/hana": "^1.3.0",
54
54
  "@cap-js/sqlite": "^1.7.3",
55
- "@sap/cds": "^8.3.0",
56
- "@sap/cds-dk": "^8.3.0",
55
+ "@sap/cds": "^8.4.2",
56
+ "@sap/cds-dk": "^8.4.2",
57
57
  "eslint": "^8.57.0",
58
58
  "eslint-config-prettier": "^9.1.0",
59
59
  "eslint-plugin-jest": "^28.6.0",
package/src/config.js CHANGED
@@ -78,6 +78,7 @@ class Config {
78
78
  #cronTimezone;
79
79
  #publishEventBlockList;
80
80
  #crashOnRedisUnavailable;
81
+ #tenantIdFilterCb;
81
82
  static #instance;
82
83
  constructor() {
83
84
  this.#logger = cds.log(COMPONENT_NAME);
@@ -519,6 +520,14 @@ class Config {
519
520
  this.#crashOnRedisUnavailable = value;
520
521
  }
521
522
 
523
+ get tenantIdFilterCb() {
524
+ return this.#tenantIdFilterCb;
525
+ }
526
+
527
+ set tenantIdFilterCb(value) {
528
+ this.#tenantIdFilterCb = value;
529
+ }
530
+
522
531
  set globalTxTimeout(value) {
523
532
  this.#globalTxTimeout = value;
524
533
  }
package/src/constants.js CHANGED
@@ -7,6 +7,7 @@ module.exports = {
7
7
  Done: 2,
8
8
  Error: 3,
9
9
  Exceeded: 4,
10
+ Suspended: 5,
10
11
  },
11
12
  TransactionMode: {
12
13
  isolated: "isolated",
@@ -19,4 +20,8 @@ module.exports = {
19
20
  High: "high",
20
21
  VeryHigh: "veryHigh",
21
22
  },
23
+ TenantIdCheckTypes: {
24
+ getAllTenantIds: "getAllTenantIds",
25
+ getTokenInfo: "getTokenInfo",
26
+ },
22
27
  };
package/src/initialize.js CHANGED
@@ -41,6 +41,7 @@ const CONFIG_VARS = [
41
41
  ["cronTimezone", null],
42
42
  ["publishEventBlockList", true],
43
43
  ["crashOnRedisUnavailable", false],
44
+ ["tenantIdFilterCb", null],
44
45
  ];
45
46
 
46
47
  /**
@@ -64,6 +65,7 @@ const CONFIG_VARS = [
64
65
  * @param {string} [options.cronTimezone=null] - Default timezone for cron jobs.
65
66
  * @param {string} [options.publishEventBlockList=true] - If redis is available event blocklist is distributed to all application instances
66
67
  * @param {string} [options.crashOnRedisUnavailable=true] - If enabled an error is thrown if the redis connection check is not successful
68
+ * @param {function} [options.tenantIdFilterCb=null] - Allows to set customer filter function to filter the tenants ids which should be processed in the event-queue
67
69
  */
68
70
  const initialize = async (options = {}) => {
69
71
  if (config.initialized) {
@@ -27,7 +27,7 @@ class EventQueueGenericOutboxHandler extends EventQueueBaseClass {
27
27
  delete msg.contextUser;
28
28
  processContext.user = new cds.User.Privileged({
29
29
  id: userId,
30
- authInfo: await common.getAuthInfo(processContext.tenant),
30
+ authInfo: await common.getTokenInfo(processContext.tenant),
31
31
  });
32
32
  processContext._eventQueue = { processor: this, key, queueEntries, payload };
33
33
  await cds.unboxed(service).tx(processContext)[invocationFn](msg);
@@ -109,7 +109,7 @@ const _processLocalWithoutRedis = async (tenantId, events) => {
109
109
  let context = {};
110
110
  if (tenantId) {
111
111
  const user = await cds.tx({ tenant: tenantId }, async () => {
112
- return new cds.User.Privileged({ id: config.userId, authInfo: await common.getAuthInfo(tenantId) });
112
+ return new cds.User.Privileged({ id: config.userId, tokenInfo: await common.getTokenInfo(tenantId) });
113
113
  });
114
114
  context = {
115
115
  tenant: tenantId,
@@ -41,14 +41,14 @@ const _messageHandlerProcessEvents = async (messageData) => {
41
41
  const service = await cds.connect.to(subType);
42
42
  cds.outboxed(service);
43
43
  } catch (err) {
44
- logger.error("could not connect to outboxed service", err, {
44
+ logger.warn("could not connect to outboxed service", err, {
45
45
  type,
46
46
  subType,
47
47
  });
48
48
  return;
49
49
  }
50
50
  } else {
51
- logger.error("cannot find configuration for published event. Event won't be processed", {
51
+ logger.warn("cannot find configuration for published event. Event won't be processed", {
52
52
  type,
53
53
  subType,
54
54
  });
@@ -66,7 +66,7 @@ const _messageHandlerProcessEvents = async (messageData) => {
66
66
  }
67
67
 
68
68
  const user = await cds.tx({ tenant: tenantId }, async () => {
69
- return new cds.User.Privileged({ id: config.userId, authInfo: await common.getAuthInfo(tenantId) });
69
+ return new cds.User.Privileged({ id: config.userId, tokenInfo: await common.getTokenInfo(tenantId) });
70
70
  });
71
71
  const tenantContext = {
72
72
  tenant: tenantId,
@@ -5,6 +5,8 @@ const cds = require("@sap/cds");
5
5
  const eventConfig = require("../config");
6
6
  const { EventProcessingStatus } = require("../constants");
7
7
 
8
+ const MS_IN_DAYS = 24 * 60 * 60 * 1000;
9
+
8
10
  const getOpenQueueEntries = async (tx, filterAppSpecificEvents = true) => {
9
11
  const startTime = new Date();
10
12
  const refDateStartAfter = new Date(startTime.getTime() + eventConfig.runInterval * 1.2);
@@ -21,7 +23,11 @@ const getOpenQueueEntries = async (tx, filterAppSpecificEvents = true) => {
21
23
  EventProcessingStatus.InProgress,
22
24
  "AND lastAttemptTimestamp <=",
23
25
  new Date(startTime.getTime() - eventConfig.globalTxTimeout).toISOString(),
24
- ") )"
26
+ ") ) AND (createdAt >=",
27
+ new Date(startTime.getTime() - 30 * MS_IN_DAYS).toISOString(),
28
+ " OR startAfter >=",
29
+ new Date(startTime.getTime() - 30 * MS_IN_DAYS).toISOString(),
30
+ ")"
25
31
  )
26
32
  .columns("type", "subType")
27
33
  .groupBy("type", "subType")
@@ -30,9 +36,13 @@ const getOpenQueueEntries = async (tx, filterAppSpecificEvents = true) => {
30
36
  const result = [];
31
37
  for (const { type, subType } of entries) {
32
38
  if (eventConfig.isCapOutboxEvent(type)) {
33
- await cds.connect
39
+ cds.connect
34
40
  .to(subType)
35
41
  .then((service) => {
42
+ if (!filterAppSpecificEvents) {
43
+ return; // will be done in finally
44
+ }
45
+
36
46
  if (!service) {
37
47
  return;
38
48
  }
@@ -45,7 +55,12 @@ const getOpenQueueEntries = async (tx, filterAppSpecificEvents = true) => {
45
55
  result.push({ type, subType });
46
56
  }
47
57
  })
48
- .catch(() => {});
58
+ .catch(() => {})
59
+ .finally(() => {
60
+ if (!filterAppSpecificEvents) {
61
+ result.push({ type, subType });
62
+ }
63
+ });
49
64
  } else {
50
65
  if (filterAppSpecificEvents) {
51
66
  if (
@@ -55,7 +70,7 @@ const getOpenQueueEntries = async (tx, filterAppSpecificEvents = true) => {
55
70
  result.push({ type, subType });
56
71
  }
57
72
  } else {
58
- eventConfig.getEventConfig(type, subType) && result.push({ type, subType });
73
+ result.push({ type, subType });
59
74
  }
60
75
  }
61
76
  }
@@ -134,7 +134,7 @@ const _executeEventsAllTenantsRedis = async (tenantIds) => {
134
134
  async () => {
135
135
  tx.context.user = new cds.User.Privileged({
136
136
  id: config.userId,
137
- authInfo: await common.getAuthInfo(tenantId),
137
+ authInfo: await common.getTokenInfo(tenantId),
138
138
  });
139
139
  const entries = await openEvents.getOpenQueueEntries(tx, false);
140
140
  logger.info("broadcasting events for run", {
@@ -168,10 +168,10 @@ const _executeEventsAllTenants = async (tenantIds, runId) => {
168
168
  let tenantContext;
169
169
  const events = await trace(
170
170
  { id, tenant: tenantId },
171
- "fetch-openEvents-and-authInfo",
171
+ "fetch-openEvents-and-tokenInfo",
172
172
  async () => {
173
173
  const user = await cds.tx({ tenant: tenantId }, async () => {
174
- return new cds.User.Privileged({ id: config.userId, authInfo: await common.getAuthInfo(tenantId) });
174
+ return new cds.User.Privileged({ id: config.userId, tokenInfo: await common.getTokenInfo(tenantId) });
175
175
  });
176
176
  tenantContext = {
177
177
  tenant: tenantId,
@@ -229,7 +229,7 @@ const _executePeriodicEventsAllTenants = async (tenantIds) => {
229
229
  for (const tenantId of tenantIds) {
230
230
  try {
231
231
  const user = await cds.tx({ tenant: tenantId }, async () => {
232
- return new cds.User.Privileged({ id: config.userId, authInfo: await common.getAuthInfo(tenantId) });
232
+ return new cds.User.Privileged({ id: config.userId, tokenInfo: await common.getTokenInfo(tenantId) });
233
233
  });
234
234
  const tenantContext = {
235
235
  tenant: tenantId,
@@ -260,7 +260,7 @@ const _singleTenantDb = async () => {
260
260
  const id = cds.utils.uuid();
261
261
  const events = await trace(
262
262
  { id },
263
- "fetch-openEvents-and-authInfo",
263
+ "fetch-openEvents-and-tokenInfo",
264
264
  async () => {
265
265
  return await cds.tx({}, async (tx) => {
266
266
  return await openEvents.getOpenQueueEntries(tx);
@@ -328,7 +328,7 @@ const _singleTenantRedis = async () => {
328
328
  "get-openEvents-and-publish",
329
329
  async () => {
330
330
  return await cds.tx({}, async (tx) => {
331
- const entries = await openEvents.getOpenQueueEntries(tx);
331
+ const entries = await openEvents.getOpenQueueEntries(tx, false);
332
332
  logger.info("broadcasting events for run", {
333
333
  entries: entries.length,
334
334
  });
@@ -491,7 +491,7 @@ const _checkPeriodicEventsSingleTenant = async (context) => {
491
491
  try {
492
492
  logger.info("executing updating periodic events", {
493
493
  tenantId: context.tenant,
494
- subdomain: context.user?.authInfo?.getSubdomain(),
494
+ subdomain: context.user?.tokenInfo?.extAttributes?.zdn,
495
495
  });
496
496
  await periodicEvents.checkAndInsertPeriodicEvents(context);
497
497
  } catch (err) {
@@ -2,8 +2,6 @@
2
2
 
3
3
  const COMPONENT = "eventQueue/SetIntervalDriftSafe";
4
4
 
5
- const ALLOWED_SHIFT_IN_PROCENT = 0.1;
6
-
7
5
  class SetIntervalDriftSafe {
8
6
  #adjustedInterval;
9
7
  #interval;
@@ -21,12 +19,6 @@ class SetIntervalDriftSafe {
21
19
  const now = Date.now();
22
20
  if (this.#expectedCycleTime === 0) {
23
21
  this.#expectedCycleTime = now + this.#interval;
24
- } else if (
25
- Math.abs(now + this.#interval - this.#nextTickScheduledFor - this.#interval) >
26
- this.#interval * ALLOWED_SHIFT_IN_PROCENT
27
- ) {
28
- this.#logger.log("overlapping ticks, skipping this run");
29
- return;
30
22
  } else {
31
23
  this.#adjustedInterval = this.#interval - (now - this.#expectedCycleTime);
32
24
  this.#expectedCycleTime += this.#interval;
@@ -5,6 +5,7 @@ const cds = require("@sap/cds");
5
5
 
6
6
  const config = require("../config");
7
7
  const common = require("./common");
8
+ const { TenantIdCheckTypes } = require("../constants");
8
9
 
9
10
  const VERROR_CLUSTER_NAME = "ExecuteInNewTransactionError";
10
11
  const COMPONENT_NAME = "/eventQueue/cdsHelper";
@@ -23,7 +24,7 @@ async function executeInNewTransaction(context = {}, transactionTag, fn, args, {
23
24
  const parameters = Array.isArray(args) ? args : [args];
24
25
  const logger = cds.log(COMPONENT_NAME);
25
26
  try {
26
- const user = new cds.User.Privileged({ id: config.userId, authInfo: await common.getAuthInfo(context.tenant) });
27
+ const user = new cds.User.Privileged({ id: config.userId, tokenInfo: await common.getTokenInfo(context.tenant) });
27
28
  if (cds.db.kind === "hana") {
28
29
  await cds.tx(
29
30
  {
@@ -116,11 +117,9 @@ const getAllTenantIds = async () => {
116
117
  const response = await ssp.get("/tenant");
117
118
  return response
118
119
  .map((tenant) => tenant.subscribedTenantId ?? tenant.tenant)
119
- .filter((tenantId) => !isFakeTenant(tenantId));
120
+ .filter((tenantId) => common.isTenantIdValidCb(TenantIdCheckTypes.getAllTenantIds, tenantId));
120
121
  };
121
122
 
122
- const isFakeTenant = (tenantId) => /00000000-0000-4000-8000-\d{12}/.test(tenantId);
123
-
124
123
  module.exports = {
125
124
  executeInNewTransaction,
126
125
  TriggerRollback,
@@ -4,6 +4,8 @@ const crypto = require("crypto");
4
4
 
5
5
  const cds = require("@sap/cds");
6
6
  const xssec = require("@sap/xssec");
7
+ const config = require("../config");
8
+ const { TenantIdCheckTypes } = require("../constants");
7
9
 
8
10
  const MARGIN_AUTH_INFO_EXPIRY = 60 * 1000;
9
11
  const COMPONENT_NAME = "/eventQueue/common";
@@ -68,49 +70,57 @@ const processChunkedSync = (inputs, chunkSize, chunkHandler) => {
68
70
 
69
71
  const hashStringTo32Bit = (value) => crypto.createHash("sha256").update(String(value)).digest("base64").slice(0, 32);
70
72
 
71
- const _getNewAuthInfo = async (tenantId) => {
72
- const authInfoCache = getAuthInfo._authInfoCache;
73
- authInfoCache[tenantId] = authInfoCache[tenantId] ?? {};
73
+ const _getNewTokenInfo = async (tenantId) => {
74
+ const tokenInfoCache = getTokenInfo._tokenInfoCache;
75
+ tokenInfoCache[tenantId] = tokenInfoCache[tenantId] ?? {};
74
76
  try {
75
- if (!_getNewAuthInfo._xsuaaService) {
76
- _getNewAuthInfo._xsuaaService = new xssec.XsuaaService(cds.requires.auth.credentials);
77
+ if (!_getNewTokenInfo._xsuaaService) {
78
+ _getNewTokenInfo._xsuaaService = new xssec.XsuaaService(cds.requires.auth.credentials);
77
79
  }
78
- const authService = _getNewAuthInfo._xsuaaService;
80
+ const authService = _getNewTokenInfo._xsuaaService;
79
81
  const token = await authService.fetchClientCredentialsToken({ zid: tenantId });
80
- const authInfo = await authService.createSecurityContext(token.access_token);
81
- authInfoCache[tenantId].expireTs = authInfo.getExpirationDate().getTime() - MARGIN_AUTH_INFO_EXPIRY;
82
- return authInfo;
82
+ const tokenInfo = new xssec.XsuaaToken(token.access_token);
83
+ tokenInfoCache[tenantId].expireTs = tokenInfo.getExpirationDate().getTime() - MARGIN_AUTH_INFO_EXPIRY;
84
+ return tokenInfo;
83
85
  } catch (err) {
84
- authInfoCache[tenantId] = null;
85
- cds.log(COMPONENT_NAME).warn("failed to request authInfo", err);
86
+ tokenInfoCache[tenantId] = null;
87
+ cds.log(COMPONENT_NAME).warn("failed to request tokenInfo", err);
86
88
  }
87
89
  };
88
90
 
89
- const getAuthInfo = async (tenantId) => {
90
- if (!tenantId) {
91
+ const getTokenInfo = async (tenantId) => {
92
+ if (!isTenantIdValidCb(TenantIdCheckTypes.getTokenInfo, tenantId)) {
91
93
  return null;
92
94
  }
93
95
 
94
96
  if (!cds.requires?.auth?.credentials) {
95
- return null; // no credentials not authInfo
97
+ return null; // no credentials not tokenInfo
96
98
  }
97
99
  if (!cds.requires?.auth.kind.match(/jwt|xsuaa/i)) {
98
100
  cds.log(COMPONENT_NAME).warn("Only 'jwt' or 'xsuaa' are supported as values for auth.kind.");
99
101
  return null;
100
102
  }
101
103
 
102
- getAuthInfo._authInfoCache = getAuthInfo._authInfoCache ?? {};
103
- const authInfoCache = getAuthInfo._authInfoCache;
104
+ getTokenInfo._tokenInfoCache = getTokenInfo._tokenInfoCache ?? {};
105
+ const tokenInfoCache = getTokenInfo._tokenInfoCache;
104
106
  // not existing or existing but expired
105
107
  if (
106
- !authInfoCache[tenantId] ||
107
- (authInfoCache[tenantId] && authInfoCache[tenantId].expireTs && Date.now() > authInfoCache[tenantId].expireTs)
108
+ !tokenInfoCache[tenantId] ||
109
+ (tokenInfoCache[tenantId] && tokenInfoCache[tenantId].expireTs && Date.now() > tokenInfoCache[tenantId].expireTs)
108
110
  ) {
109
- authInfoCache[tenantId] ??= {};
110
- authInfoCache[tenantId].value = _getNewAuthInfo(tenantId);
111
- authInfoCache[tenantId].expireTs = null;
111
+ tokenInfoCache[tenantId] ??= {};
112
+ tokenInfoCache[tenantId].value = _getNewTokenInfo(tenantId);
113
+ tokenInfoCache[tenantId].expireTs = null;
114
+ }
115
+ return await tokenInfoCache[tenantId].value;
116
+ };
117
+
118
+ const isTenantIdValidCb = (checkType, tenantId) => {
119
+ if (config.tenantIdFilterCb) {
120
+ return config.tenantIdFilterCb(checkType, tenantId);
121
+ } else {
122
+ return true;
112
123
  }
113
- return await authInfoCache[tenantId].value;
114
124
  };
115
125
 
116
126
  module.exports = {
@@ -119,8 +129,9 @@ module.exports = {
119
129
  isValidDate,
120
130
  processChunkedSync,
121
131
  hashStringTo32Bit,
122
- getAuthInfo,
132
+ getTokenInfo,
133
+ isTenantIdValidCb,
123
134
  __: {
124
- clearAuthInfoCache: () => (getAuthInfo._authInfoCache = {}),
135
+ clearTokenInfoCache: () => (getTokenInfo._tokenInfoCache = {}),
125
136
  },
126
137
  };
@@ -60,7 +60,7 @@ const checkLockExistsAndReturnValue = async (context, key, { tenantScoped = true
60
60
  const _acquireLockRedis = async (context, fullKey, expiryTime, { value = "true", overrideValue = false } = {}) => {
61
61
  const client = await redis.createMainClientAndConnect(config.redisOptions);
62
62
  const result = await client.set(fullKey, value, {
63
- PX: expiryTime,
63
+ PX: Math.round(expiryTime),
64
64
  ...(overrideValue ? null : { NX: true }),
65
65
  });
66
66
  const isOk = result === REDIS_COMMAND_OK;
@@ -117,7 +117,10 @@ const _acquireLockDB = async (context, fullKey, expiryTime, { value = "true", ov
117
117
  .where("code =", fullKey)
118
118
  );
119
119
  }
120
- if (overrideValue || (currentEntry && new Date(currentEntry.createdAt).getTime() + expiryTime <= Date.now())) {
120
+ if (
121
+ overrideValue ||
122
+ (currentEntry && new Date(currentEntry.createdAt).getTime() + Math.round(expiryTime) <= Date.now())
123
+ ) {
121
124
  await tx.run(
122
125
  UPDATE.entity(config.tableNameEventLock)
123
126
  .set({
@@ -51,19 +51,10 @@ class EventScheduler {
51
51
  }
52
52
 
53
53
  calculateOffset(type, subType, startAfter) {
54
- const eventConfig = config.getEventConfig(type, subType);
55
- const scheduleWithoutDelay = config.isPeriodicEvent(type, subType) && eventConfig.interval < 30 * 1000;
56
- const date = scheduleWithoutDelay ? startAfter : this.calculateFutureTime(startAfter, 10);
57
-
54
+ const date = startAfter;
58
55
  return { date, relative: date.getTime() - Date.now() };
59
56
  }
60
57
 
61
- calculateFutureTime(date, seoncds) {
62
- const startAfterSeconds = date.getSeconds();
63
- const secondsUntil = seoncds - (startAfterSeconds % seoncds);
64
- return new Date(date.getTime() + secondsUntil * 1000);
65
- }
66
-
67
58
  clearScheduledEvents() {
68
59
  this.#scheduledEvents = {};
69
60
  }