@cap-js-community/event-queue 1.7.2 → 1.7.4

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
@@ -4,6 +4,7 @@ const cds = require("@sap/cds");
4
4
  const cdsPackage = require("@sap/cds/package.json");
5
5
 
6
6
  const eventQueue = require("./src");
7
+ const EventQueueError = require("./src/EventQueueError");
7
8
  const COMPONENT_NAME = "/eventQueue/plugin";
8
9
  const SERVE_COMMAND = "serve";
9
10
 
@@ -18,5 +19,10 @@ if ((doLegacyBuildDetection && isBuild) || (!doLegacyBuildDetection && !isServe)
18
19
  }
19
20
 
20
21
  if (Object.keys(cds.env.eventQueue ?? {}).length) {
21
- module.exports = eventQueue.initialize().catch((err) => cds.log(COMPONENT_NAME).error(err));
22
+ module.exports = eventQueue.initialize().catch((err) => {
23
+ if (EventQueueError.isRedisConnectionFailure(err) && eventQueue.config.crashOnRedisUnavailable) {
24
+ throw err;
25
+ }
26
+ cds.log(COMPONENT_NAME).error(err);
27
+ });
22
28
  }
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.2",
3
+ "version": "1.7.4",
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",
@@ -41,7 +41,7 @@ const ERROR_CODES_META = {
41
41
  message: "error during create client with redis-cache service",
42
42
  },
43
43
  [ERROR_CODES.REDIS_LOCAL_NO_RECONNECT]: {
44
- message: "disabled reconnect, because we are not running on cloud foundry",
44
+ message: "disabled reconnect, because not running on cloud foundry",
45
45
  },
46
46
  [ERROR_CODES.MISSING_TABLE_DEFINITION]: {
47
47
  message: "Could not find table in csn. Make sure the provided table name is correct and the table is known by CDS.",
@@ -135,7 +135,7 @@ class EventQueueError extends VError {
135
135
  return new EventQueueError(
136
136
  {
137
137
  name: ERROR_CODES.REDIS_CREATE_CLIENT,
138
- cause: err,
138
+ ...(err && { cause: err }),
139
139
  },
140
140
  message
141
141
  );
@@ -325,6 +325,10 @@ class EventQueueError extends VError {
325
325
  message
326
326
  );
327
327
  }
328
+
329
+ static isRedisConnectionFailure(err) {
330
+ return err instanceof VError && err.name === ERROR_CODES.REDIS_CREATE_CLIENT;
331
+ }
328
332
  }
329
333
 
330
334
  module.exports = EventQueueError;
package/src/config.js CHANGED
@@ -77,6 +77,8 @@ class Config {
77
77
  #unsubscribedTenants = {};
78
78
  #cronTimezone;
79
79
  #publishEventBlockList;
80
+ #crashOnRedisUnavailable;
81
+ #tenantIdFilterCb;
80
82
  static #instance;
81
83
  constructor() {
82
84
  this.#logger = cds.log(COMPONENT_NAME);
@@ -510,6 +512,22 @@ class Config {
510
512
  this.#publishEventBlockList = value;
511
513
  }
512
514
 
515
+ get crashOnRedisUnavailable() {
516
+ return this.#crashOnRedisUnavailable;
517
+ }
518
+
519
+ set crashOnRedisUnavailable(value) {
520
+ this.#crashOnRedisUnavailable = value;
521
+ }
522
+
523
+ get tenantIdFilterCb() {
524
+ return this.#tenantIdFilterCb;
525
+ }
526
+
527
+ set tenantIdFilterCb(value) {
528
+ this.#tenantIdFilterCb = value;
529
+ }
530
+
513
531
  set globalTxTimeout(value) {
514
532
  this.#globalTxTimeout = value;
515
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/index.d.ts CHANGED
@@ -182,6 +182,8 @@ declare class Config {
182
182
  hasEventAfterCommitFlag(type: string, subType: string): boolean;
183
183
  _checkRedisIsBound(): boolean;
184
184
  checkRedisEnabled(): boolean;
185
+ publishEventBlockList(): boolean;
186
+ crashOnRedisUnavailable(): boolean;
185
187
  attachConfigChangeHandler(): void;
186
188
  attachRedisUnsubscribeHandler(): void;
187
189
  executeUnsubscribeHandlers(tenantId: string): void;
package/src/initialize.js CHANGED
@@ -16,6 +16,7 @@ const eventQueueAsOutbox = require("./outbox/eventQueueAsOutbox");
16
16
  const { getAllTenantIds } = require("./shared/cdsHelper");
17
17
  const { EventProcessingStatus } = require("./constants");
18
18
  const distributedLock = require("./shared/distributedLock");
19
+ const EventQueueError = require("./EventQueueError");
19
20
 
20
21
  const readFileAsync = promisify(fs.readFile);
21
22
 
@@ -39,6 +40,8 @@ const CONFIG_VARS = [
39
40
  ["enableCAPTelemetry", false],
40
41
  ["cronTimezone", null],
41
42
  ["publishEventBlockList", true],
43
+ ["crashOnRedisUnavailable", false],
44
+ ["tenantIdFilterCb", null],
42
45
  ];
43
46
 
44
47
  /**
@@ -61,6 +64,8 @@ const CONFIG_VARS = [
61
64
  * @param {boolean} [options.enableCAPTelemetry=false] - Enable telemetry for CAP.
62
65
  * @param {string} [options.cronTimezone=null] - Default timezone for cron jobs.
63
66
  * @param {string} [options.publishEventBlockList=true] - If redis is available event blocklist is distributed to all application instances
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
64
69
  */
65
70
  const initialize = async (options = {}) => {
66
71
  if (config.initialized) {
@@ -89,6 +94,9 @@ const initialize = async (options = {}) => {
89
94
  });
90
95
  if (redisEnabled) {
91
96
  config.redisEnabled = await redis.connectionCheck(config.redisOptions);
97
+ if (!config.redisEnabled && config.crashOnRedisUnavailable) {
98
+ throw EventQueueError.redisConnectionFailure();
99
+ }
92
100
  }
93
101
  config.fileContent = await readConfigFromFile(config.configFilePath);
94
102
 
@@ -139,17 +147,21 @@ const registerEventProcessors = () => {
139
147
 
140
148
  const errorHandler = (err) => cds.log(COMPONENT).error("error during init runner", err);
141
149
 
142
- if (!config.isMultiTenancy) {
143
- runner.singleTenant().catch(errorHandler);
144
- return;
145
- }
146
-
147
150
  if (config.redisEnabled) {
148
151
  initEventQueueRedisSubscribe();
149
152
  config.attachConfigChangeHandler();
150
- runner.multiTenancyRedis().catch(errorHandler);
151
- } else {
153
+ if (config.isMultiTenancy) {
154
+ runner.multiTenancyRedis().catch(errorHandler);
155
+ } else {
156
+ runner.singleTenantRedis().catch(errorHandler);
157
+ }
158
+ return;
159
+ }
160
+
161
+ if (config.isMultiTenancy) {
152
162
  runner.multiTenancyDb().catch(errorHandler);
163
+ } else {
164
+ runner.singleTenantDb().catch(errorHandler);
153
165
  }
154
166
  };
155
167
 
@@ -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
  }
@@ -27,12 +27,14 @@ let OFFSET_FIRST_RUN = 10 * 1000;
27
27
  let tenantIdHash;
28
28
  let singleRunDone;
29
29
 
30
- const singleTenant = () => _scheduleFunction(_checkPeriodicEventsSingleTenantOneTime, _singleTenantDb);
30
+ const singleTenantDb = () => _scheduleFunction(_checkPeriodicEventsSingleTenantOneTime, _singleTenantDb);
31
31
 
32
32
  const multiTenancyDb = () => _scheduleFunction(async () => {}, _multiTenancyDb);
33
33
 
34
34
  const multiTenancyRedis = () => _scheduleFunction(async () => {}, _multiTenancyRedis);
35
35
 
36
+ const singleTenantRedis = () => _scheduleFunction(_checkPeriodicEventsSingleTenantOneTime, _singleTenantRedis);
37
+
36
38
  const _scheduleFunction = async (singleRunFn, periodicFn) => {
37
39
  const logger = cds.log(COMPONENT_NAME);
38
40
  const eventsForAutomaticRun = eventQueueConfig.allEvents;
@@ -132,7 +134,7 @@ const _executeEventsAllTenantsRedis = async (tenantIds) => {
132
134
  async () => {
133
135
  tx.context.user = new cds.User.Privileged({
134
136
  id: config.userId,
135
- authInfo: await common.getAuthInfo(tenantId),
137
+ authInfo: await common.getTokenInfo(tenantId),
136
138
  });
137
139
  const entries = await openEvents.getOpenQueueEntries(tx, false);
138
140
  logger.info("broadcasting events for run", {
@@ -166,10 +168,10 @@ const _executeEventsAllTenants = async (tenantIds, runId) => {
166
168
  let tenantContext;
167
169
  const events = await trace(
168
170
  { id, tenant: tenantId },
169
- "fetch-openEvents-and-authInfo",
171
+ "fetch-openEvents-and-tokenInfo",
170
172
  async () => {
171
173
  const user = await cds.tx({ tenant: tenantId }, async () => {
172
- 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) });
173
175
  });
174
176
  tenantContext = {
175
177
  tenant: tenantId,
@@ -227,7 +229,7 @@ const _executePeriodicEventsAllTenants = async (tenantIds) => {
227
229
  for (const tenantId of tenantIds) {
228
230
  try {
229
231
  const user = await cds.tx({ tenant: tenantId }, async () => {
230
- 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) });
231
233
  });
232
234
  const tenantContext = {
233
235
  tenant: tenantId,
@@ -258,7 +260,7 @@ const _singleTenantDb = async () => {
258
260
  const id = cds.utils.uuid();
259
261
  const events = await trace(
260
262
  { id },
261
- "fetch-openEvents-and-authInfo",
263
+ "fetch-openEvents-and-tokenInfo",
262
264
  async () => {
263
265
  return await cds.tx({}, async (tx) => {
264
266
  return await openEvents.getOpenQueueEntries(tx);
@@ -300,6 +302,54 @@ const _singleTenantDb = async () => {
300
302
  );
301
303
  };
302
304
 
305
+ const _singleTenantRedis = async () => {
306
+ const id = cds.utils.uuid();
307
+ const logger = cds.log(COMPONENT_NAME);
308
+ try {
309
+ // NOTE: do checks for open events on one app instance distribute from this instance to all others
310
+ const dummyContext = new cds.EventContext({});
311
+ const couldAcquireLock = await trace(
312
+ dummyContext,
313
+ "acquire-lock-master-runner",
314
+ async () => {
315
+ return await distributedLock.acquireLock(dummyContext, EVENT_QUEUE_RUN_REDIS_CHECK, {
316
+ expiryTime: eventQueueConfig.runInterval * 0.95,
317
+ tenantScoped: false,
318
+ });
319
+ },
320
+ { newRootSpan: true }
321
+ );
322
+ if (!couldAcquireLock) {
323
+ return;
324
+ }
325
+
326
+ await trace(
327
+ { id },
328
+ "get-openEvents-and-publish",
329
+ async () => {
330
+ return await cds.tx({}, async (tx) => {
331
+ const entries = await openEvents.getOpenQueueEntries(tx, false);
332
+ logger.info("broadcasting events for run", {
333
+ entries: entries.length,
334
+ });
335
+ if (!entries.length) {
336
+ return;
337
+ }
338
+ // Do not wait until this is finished - as broadcastEvent has a retry mechanism and can delay this loop
339
+ redisPub.broadcastEvent(null, entries).catch((err) => {
340
+ logger.error("broadcasting event failed", err, {
341
+ entries: entries.length,
342
+ });
343
+ });
344
+ });
345
+ },
346
+ { newRootSpan: true }
347
+ );
348
+ } catch (err) {
349
+ logger.info("executing event queue run for single tenant via redis", err);
350
+ }
351
+ };
352
+
303
353
  const _acquireRunId = async (context) => {
304
354
  let runId = randomUUID();
305
355
  const couldSetValue = await distributedLock.setValueWithExpire(context, EVENT_QUEUE_RUN_ID, runId, {
@@ -401,9 +451,33 @@ const _multiTenancyPeriodicEvents = async (tenantIds) => {
401
451
  }
402
452
  };
403
453
 
404
- const _checkPeriodicEventsSingleTenantOneTime = async () =>
405
- eventQueueConfig.updatePeriodicEvents &&
406
- cds.tx({}, async (tx) => await periodicEvents.checkAndInsertPeriodicEvents(tx.context));
454
+ const _checkPeriodicEventsSingleTenantOneTime = async () => {
455
+ const logger = cds.log(COMPONENT_NAME);
456
+ if (!eventQueueConfig.updatePeriodicEvents || !eventQueueConfig.periodicEvents.length) {
457
+ logger.info("updating of periodic events is disabled or no periodic events configured", {
458
+ updateEnabled: eventQueueConfig.updatePeriodicEvents,
459
+ events: eventQueueConfig.periodicEvents.length,
460
+ });
461
+ return;
462
+ }
463
+
464
+ const dummyContext = new cds.EventContext({});
465
+ return await trace(
466
+ dummyContext,
467
+ "update-periodic-events",
468
+ async () => {
469
+ const couldAcquireLock = await distributedLock.acquireLock(dummyContext, EVENT_QUEUE_UPDATE_PERIODIC_EVENTS, {
470
+ expiryTime: 60 * 1000,
471
+ tenantScoped: false,
472
+ });
473
+ if (!couldAcquireLock) {
474
+ return;
475
+ }
476
+ return await cds.tx({}, async (tx) => await periodicEvents.checkAndInsertPeriodicEvents(tx.context));
477
+ },
478
+ { newRootSpan: true }
479
+ );
480
+ };
407
481
 
408
482
  const _checkPeriodicEventsSingleTenant = async (context) => {
409
483
  const logger = cds.log(COMPONENT_NAME);
@@ -417,7 +491,7 @@ const _checkPeriodicEventsSingleTenant = async (context) => {
417
491
  try {
418
492
  logger.info("executing updating periodic events", {
419
493
  tenantId: context.tenant,
420
- subdomain: context.user?.authInfo?.getSubdomain(),
494
+ subdomain: context.user?.tokenInfo?.extAttributes?.zdn,
421
495
  });
422
496
  await periodicEvents.checkAndInsertPeriodicEvents(context);
423
497
  } catch (err) {
@@ -429,12 +503,14 @@ const _checkPeriodicEventsSingleTenant = async (context) => {
429
503
  };
430
504
 
431
505
  module.exports = {
432
- singleTenant,
506
+ singleTenantDb,
433
507
  multiTenancyDb,
434
508
  multiTenancyRedis,
509
+ singleTenantRedis,
435
510
  __: {
436
511
  _singleTenantDb,
437
512
  _multiTenancyRedis,
513
+ _singleTenantRedis,
438
514
  _multiTenancyDb,
439
515
  _calculateOffsetForFirstRun,
440
516
  _acquireRunId,
@@ -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,45 +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) => {
91
+ const getTokenInfo = async (tenantId) => {
92
+ if (!isTenantIdValidCb(TenantIdCheckTypes.getTokenInfo, tenantId)) {
93
+ return null;
94
+ }
95
+
90
96
  if (!cds.requires?.auth?.credentials) {
91
- return null; // no credentials not authInfo
97
+ return null; // no credentials not tokenInfo
92
98
  }
93
99
  if (!cds.requires?.auth.kind.match(/jwt|xsuaa/i)) {
94
100
  cds.log(COMPONENT_NAME).warn("Only 'jwt' or 'xsuaa' are supported as values for auth.kind.");
95
101
  return null;
96
102
  }
97
103
 
98
- getAuthInfo._authInfoCache = getAuthInfo._authInfoCache ?? {};
99
- const authInfoCache = getAuthInfo._authInfoCache;
104
+ getTokenInfo._tokenInfoCache = getTokenInfo._tokenInfoCache ?? {};
105
+ const tokenInfoCache = getTokenInfo._tokenInfoCache;
100
106
  // not existing or existing but expired
101
107
  if (
102
- !authInfoCache[tenantId] ||
103
- (authInfoCache[tenantId] && authInfoCache[tenantId].expireTs && Date.now() > authInfoCache[tenantId].expireTs)
108
+ !tokenInfoCache[tenantId] ||
109
+ (tokenInfoCache[tenantId] && tokenInfoCache[tenantId].expireTs && Date.now() > tokenInfoCache[tenantId].expireTs)
104
110
  ) {
105
- authInfoCache[tenantId] ??= {};
106
- authInfoCache[tenantId].value = _getNewAuthInfo(tenantId);
107
- 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;
108
123
  }
109
- return await authInfoCache[tenantId].value;
110
124
  };
111
125
 
112
126
  module.exports = {
@@ -115,8 +129,9 @@ module.exports = {
115
129
  isValidDate,
116
130
  processChunkedSync,
117
131
  hashStringTo32Bit,
118
- getAuthInfo,
132
+ getTokenInfo,
133
+ isTenantIdValidCb,
119
134
  __: {
120
- clearAuthInfoCache: () => (getAuthInfo._authInfoCache = {}),
135
+ clearTokenInfoCache: () => (getTokenInfo._tokenInfoCache = {}),
121
136
  },
122
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
  }
@@ -50,25 +50,27 @@ const _createClientBase = (redisOptions) => {
50
50
  }
51
51
  };
52
52
 
53
- const createClientAndConnect = async (options, errorHandlerCreateClient) => {
53
+ const createClientAndConnect = async (options, errorHandlerCreateClient, isConnectionCheck) => {
54
54
  try {
55
55
  const client = _createClientBase(options);
56
- await client.connect();
57
- client.on("error", (err) => {
58
- const dateNow = Date.now();
59
- if (dateNow - lastErrorLog > LOG_AFTER_SEC * 1000) {
60
- cds.log(COMPONENT_NAME).error("error from redis client for pub/sub failed", err);
61
- lastErrorLog = dateNow;
62
- }
63
- });
56
+ if (!isConnectionCheck) {
57
+ client.on("error", (err) => {
58
+ const dateNow = Date.now();
59
+ if (dateNow - lastErrorLog > LOG_AFTER_SEC * 1000) {
60
+ cds.log(COMPONENT_NAME).error("error from redis client for pub/sub failed", err);
61
+ lastErrorLog = dateNow;
62
+ }
63
+ });
64
64
 
65
- client.on("reconnecting", () => {
66
- const dateNow = Date.now();
67
- if (dateNow - lastErrorLog > LOG_AFTER_SEC * 1000) {
68
- cds.log(COMPONENT_NAME).info("redis client trying reconnect...");
69
- lastErrorLog = dateNow;
70
- }
71
- });
65
+ client.on("reconnecting", () => {
66
+ const dateNow = Date.now();
67
+ if (dateNow - lastErrorLog > LOG_AFTER_SEC * 1000) {
68
+ cds.log(COMPONENT_NAME).info("redis client trying reconnect...");
69
+ lastErrorLog = dateNow;
70
+ }
71
+ });
72
+ }
73
+ await client.connect();
72
74
  return client;
73
75
  } catch (err) {
74
76
  errorHandlerCreateClient(err);
@@ -119,7 +121,7 @@ const _resilientClientClose = async (client) => {
119
121
 
120
122
  const connectionCheck = async (options) => {
121
123
  return new Promise((resolve, reject) => {
122
- createClientAndConnect(options, reject)
124
+ createClientAndConnect(options, reject, true)
123
125
  .then((client) => {
124
126
  if (client) {
125
127
  _resilientClientClose(client);
@@ -131,7 +133,10 @@ const connectionCheck = async (options) => {
131
133
  .catch(reject);
132
134
  })
133
135
  .then(() => true)
134
- .catch(() => false);
136
+ .catch((err) => {
137
+ cds.log(COMPONENT_NAME).error("Redis connection check failed! Falling back to NO_REDIS mode", err);
138
+ return false;
139
+ });
135
140
  };
136
141
 
137
142
  module.exports = {