@cap-js-community/event-queue 1.8.2 → 1.8.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cap-js-community/event-queue",
3
- "version": "1.8.2",
3
+ "version": "1.8.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",
@@ -546,6 +546,16 @@ class EventQueueProcessorBase {
546
546
  });
547
547
  }
548
548
 
549
+ handleErrorTx(error) {
550
+ this.logger.error("Error in commit|rollback transaction, check handlers and constraints!", error, {
551
+ eventType: this.#eventType,
552
+ eventSubType: this.#eventSubType,
553
+ });
554
+ this.__queueEntries.forEach((queueEntry) => {
555
+ this.#determineAndAddEventStatusToMap(queueEntry.ID, EventProcessingStatus.Error);
556
+ });
557
+ }
558
+
549
559
  handleInvalidPayloadReturned(queueEntry) {
550
560
  this.logger.error(
551
561
  "Undefined payload is not allowed. If status should be done, nulls needs to be returned" +
package/src/config.js CHANGED
@@ -58,7 +58,6 @@ class Config {
58
58
  #isEventQueueActive;
59
59
  #configFilePath;
60
60
  #processEventsAfterPublish;
61
- #skipCsnCheck;
62
61
  #registerAsEventProcessor;
63
62
  #disableRedis;
64
63
  #env;
@@ -78,7 +77,8 @@ class Config {
78
77
  #cronTimezone;
79
78
  #publishEventBlockList;
80
79
  #crashOnRedisUnavailable;
81
- #tenantIdFilterCb;
80
+ #tenantIdFilterTokenInfoCb;
81
+ #tenantIdFilterEventProcessingCb;
82
82
  static #instance;
83
83
  constructor() {
84
84
  this.#logger = cds.log(COMPONENT_NAME);
@@ -94,7 +94,6 @@ class Config {
94
94
  this.#isEventQueueActive = true;
95
95
  this.#configFilePath = null;
96
96
  this.#processEventsAfterPublish = null;
97
- this.#skipCsnCheck = null;
98
97
  this.#disableRedis = null;
99
98
  this.#env = getEnvInstance();
100
99
  this.#blockedEvents = {};
@@ -293,7 +292,10 @@ class Config {
293
292
 
294
293
  addCAPOutboxEvent(serviceName, config) {
295
294
  if (this.#eventMap[this.generateKey(CAP_EVENT_TYPE, serviceName)]) {
296
- return;
295
+ const index = this.#config.events.findIndex(
296
+ (event) => event.type === CAP_EVENT_TYPE && event.subType === serviceName
297
+ );
298
+ this.#config.events.splice(index, 1);
297
299
  }
298
300
 
299
301
  const eventConfig = {
@@ -530,12 +532,20 @@ class Config {
530
532
  this.#crashOnRedisUnavailable = value;
531
533
  }
532
534
 
533
- get tenantIdFilterCb() {
534
- return this.#tenantIdFilterCb;
535
+ get tenantIdFilterTokenInfo() {
536
+ return this.#tenantIdFilterTokenInfoCb;
537
+ }
538
+
539
+ set tenantIdFilterTokenInfo(value) {
540
+ this.#tenantIdFilterTokenInfoCb = value;
541
+ }
542
+
543
+ get tenantIdFilterEventProcessing() {
544
+ return this.#tenantIdFilterEventProcessingCb;
535
545
  }
536
546
 
537
- set tenantIdFilterCb(value) {
538
- this.#tenantIdFilterCb = value;
547
+ set tenantIdFilterEventProcessing(value) {
548
+ this.#tenantIdFilterEventProcessingCb = value;
539
549
  }
540
550
 
541
551
  set globalTxTimeout(value) {
@@ -617,14 +627,6 @@ class Config {
617
627
  return this.#processEventsAfterPublish;
618
628
  }
619
629
 
620
- set skipCsnCheck(value) {
621
- this.#skipCsnCheck = value;
622
- }
623
-
624
- get skipCsnCheck() {
625
- return this.#skipCsnCheck;
626
- }
627
-
628
630
  set disableRedis(value) {
629
631
  this.#disableRedis = value;
630
632
  }
package/src/constants.js CHANGED
@@ -21,7 +21,7 @@ module.exports = {
21
21
  VeryHigh: "veryHigh",
22
22
  },
23
23
  TenantIdCheckTypes: {
24
- getAllTenantIds: "getAllTenantIds",
24
+ eventProcessing: "eventProcessing",
25
25
  getTokenInfo: "getTokenInfo",
26
26
  },
27
27
  };
package/src/index.d.ts CHANGED
@@ -12,7 +12,7 @@ declare type EventProcessingStatusKeysType = keyof typeof EventProcessingStatus;
12
12
  export declare type EventProcessingStatusType = (typeof EventProcessingStatus)[EventProcessingStatusKeysType];
13
13
 
14
14
  export declare const TenantIdCheckTypes: {
15
- getAllTenantIds: "getAllTenantIds";
15
+ eventProcessing: "eventProcessing";
16
16
  getTokenInfo: "getTokenInfo";
17
17
  };
18
18
 
@@ -182,54 +182,63 @@ export function triggerEventProcessingRedis(
182
182
  declare class Config {
183
183
  constructor();
184
184
 
185
- getEventConfig(type: string, subType: string): any;
186
- isCapOutboxEvent(type: string): boolean;
187
- hasEventAfterCommitFlag(type: string, subType: string): boolean;
188
- _checkRedisIsBound(): boolean;
189
- checkRedisEnabled(): boolean;
190
- publishEventBlockList(): boolean;
191
- crashOnRedisUnavailable(): boolean;
185
+ getEventConfig(type: any, subType: any): any;
186
+ isCapOutboxEvent(type: any): boolean;
187
+ hasEventAfterCommitFlag(type: any, subType: any): any;
188
+ shouldBeProcessedInThisApplication(type: any, subType: any): boolean;
189
+ checkRedisEnabled(): any;
192
190
  attachConfigChangeHandler(): void;
193
191
  attachRedisUnsubscribeHandler(): void;
194
- executeUnsubscribeHandlers(tenantId: string): void;
195
- handleUnsubscribe(tenantId: string): void;
196
- attachUnsubscribeHandler(cb: Function): void;
197
- publishConfigChange(key: string, value: any): void;
198
- blockEvent(type: string, subType: string, isPeriodic: boolean, tenant?: string): void;
192
+ executeUnsubscribeHandlers(tenantId: any): void;
193
+ handleUnsubscribe(tenantId: any): void;
194
+ attachUnsubscribeHandler(cb: any): void;
195
+ publishConfigChange(key: any, value: any): void;
196
+ blockEvent(type: any, subType: any, isPeriodic: any, tenant?: string): void;
199
197
  clearPeriodicEventBlockList(): void;
200
- unblockEvent(type: string, subType: string, isPeriodic: boolean, tenant?: string): void;
201
- addCAPOutboxEvent(serviceName: string, config: any): void;
202
- isEventBlocked(type: string, subType: string, isPeriodicEvent: boolean, tenant: string): boolean;
203
- get isEventQueueActive(): boolean;
198
+ unblockEvent(type: any, subType: any, isPeriodic: any, tenant?: string): void;
199
+ addCAPOutboxEvent(serviceName: any, config: any): void;
200
+ isEventBlocked(type: any, subType: any, isPeriodicEvent: any, tenant: any): any;
204
201
  set isEventQueueActive(value: boolean);
202
+ get isEventQueueActive(): boolean;
205
203
  set fileContent(config: any);
206
204
  get fileContent(): any;
207
- get events(): any[];
208
- get periodicEvents(): any[];
209
- isPeriodicEvent(type: string, subType: string): boolean;
210
- get allEvents(): any[];
211
- get forUpdateTimeout(): number;
212
- get globalTxTimeout(): number;
205
+ generateKey(type: any, subType: any): string;
206
+ removeEvent(type: any, subType: any): void;
207
+ isTenantUnsubscribed(tenantId: any): any;
208
+ get events(): any;
209
+ get periodicEvents(): any;
210
+ isPeriodicEvent(type: any, subType: any): any;
211
+ get allEvents(): any;
213
212
  set forUpdateTimeout(value: number);
213
+ get forUpdateTimeout(): number;
214
214
  set globalTxTimeout(value: number);
215
- get runInterval(): number | null;
216
- set runInterval(value: number);
217
- get redisEnabled(): boolean | null;
218
- set redisEnabled(value: boolean | null);
219
- get initialized(): boolean;
215
+ get globalTxTimeout(): number;
216
+ set publishEventBlockList(value: any);
217
+ get publishEventBlockList(): any;
218
+ set crashOnRedisUnavailable(value: any);
219
+ get crashOnRedisUnavailable(): any;
220
+ set tenantIdFilterTokenInfo(value: any);
221
+ get tenantIdFilterTokenInfo(): any;
222
+ set tenantIdFilterEventProcessing(value: any);
223
+ get tenantIdFilterEventProcessing(): any;
224
+ set runInterval(value: any);
225
+ get runInterval(): any;
226
+ set redisEnabled(value: any);
227
+ get redisEnabled(): any;
220
228
  set initialized(value: boolean);
221
- get instanceLoadLimit(): number;
229
+ get initialized(): boolean;
230
+ set cronTimezone(value: any);
231
+ get cronTimezone(): any;
222
232
  set instanceLoadLimit(value: number);
223
- get isEventBlockedCb(): any;
233
+ get instanceLoadLimit(): number;
224
234
  set isEventBlockedCb(value: any);
235
+ get isEventBlockedCb(): any;
225
236
  get tableNameEventQueue(): string;
226
237
  get tableNameEventLock(): string;
227
- set configFilePath(value: string | null);
228
- get configFilePath(): string | null;
238
+ set configFilePath(value: any);
239
+ get configFilePath(): any;
229
240
  set processEventsAfterPublish(value: any);
230
241
  get processEventsAfterPublish(): any;
231
- set skipCsnCheck(value: any);
232
- get skipCsnCheck(): any;
233
242
  set disableRedis(value: any);
234
243
  get disableRedis(): any;
235
244
  set updatePeriodicEvents(value: any);
@@ -248,6 +257,8 @@ declare class Config {
248
257
  get redisOptions(): any;
249
258
  set insertEventsBeforeCommit(value: any);
250
259
  get insertEventsBeforeCommit(): any;
260
+ set enableCAPTelemetry(value: any);
261
+ get enableCAPTelemetry(): any;
251
262
  get isMultiTenancy(): boolean;
252
263
  }
253
264
 
package/src/initialize.js CHANGED
@@ -10,7 +10,7 @@ const VError = require("verror");
10
10
  const runner = require("./runner/runner");
11
11
  const dbHandler = require("./dbHandler");
12
12
  const config = require("./config");
13
- const { initEventQueueRedisSubscribe, closeSubscribeClient } = require("./redis/redisSub");
13
+ const redisSub = require("./redis/redisSub");
14
14
  const redis = require("./shared/redis");
15
15
  const eventQueueAsOutbox = require("./outbox/eventQueueAsOutbox");
16
16
  const { getAllTenantIds } = require("./shared/cdsHelper");
@@ -22,6 +22,7 @@ const readFileAsync = promisify(fs.readFile);
22
22
 
23
23
  const VERROR_CLUSTER_NAME = "EventQueueInitialization";
24
24
  const COMPONENT = "eventQueue/initialize";
25
+ const TIMEOUT_SHUTDOWN = 2500;
25
26
 
26
27
  const CONFIG_VARS = [
27
28
  ["configFilePath", null],
@@ -41,7 +42,6 @@ const CONFIG_VARS = [
41
42
  ["cronTimezone", null],
42
43
  ["publishEventBlockList", true],
43
44
  ["crashOnRedisUnavailable", false],
44
- ["tenantIdFilterCb", null],
45
45
  ];
46
46
 
47
47
  /**
@@ -65,7 +65,6 @@ const CONFIG_VARS = [
65
65
  * @param {string} [options.cronTimezone=null] - Default timezone for cron jobs.
66
66
  * @param {string} [options.publishEventBlockList=true] - If redis is available event blocklist is distributed to all application instances
67
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
69
68
  */
70
69
  const initialize = async (options = {}) => {
71
70
  if (config.initialized) {
@@ -148,7 +147,7 @@ const registerEventProcessors = () => {
148
147
  const errorHandler = (err) => cds.log(COMPONENT).error("error during init runner", err);
149
148
 
150
149
  if (config.redisEnabled) {
151
- initEventQueueRedisSubscribe();
150
+ redisSub.initEventQueueRedisSubscribe();
152
151
  config.attachConfigChangeHandler();
153
152
  if (config.isMultiTenancy) {
154
153
  runner.multiTenancyRedis().catch(errorHandler);
@@ -186,9 +185,24 @@ const mixConfigVarsWithEnv = (options) => {
186
185
  };
187
186
 
188
187
  const registerCdsShutdown = () => {
188
+ const isTestProfile = cds.env.profiles.find((profile) => profile.includes("test"));
189
+ if (isTestProfile || !config.redisEnabled) {
190
+ return;
191
+ }
189
192
  cds.on("shutdown", async () => {
190
- await distributedLock.shutdownHandler();
191
- await Promise.allSettled([redis.closeMainClient(), closeSubscribeClient()]);
193
+ return await new Promise((resolve) => {
194
+ const timeoutRef = setTimeout(() => {
195
+ clearTimeout(timeoutRef);
196
+ cds.log(COMPONENT).info("shutdown timeout reached - some locks might not have been released!");
197
+ resolve();
198
+ }, TIMEOUT_SHUTDOWN);
199
+ distributedLock.shutdownHandler().then(() =>
200
+ Promise.allSettled([redis.closeMainClient(), redis.closeSubscribeClient()]).then((result) => {
201
+ clearTimeout(timeoutRef);
202
+ resolve(result);
203
+ })
204
+ );
205
+ });
192
206
  });
193
207
  };
194
208
 
@@ -12,7 +12,13 @@ const COMPONENT_NAME = "/eventQueue/eventQueueAsOutbox";
12
12
  const EVENT_QUEUE_SPECIFIC_FIELDS = ["startAfter", "referenceEntity", "referenceEntityKey"];
13
13
 
14
14
  function outboxed(srv, customOpts) {
15
- // outbox max. once
15
+ if (!(new.target || customOpts)) {
16
+ const former = srv[OUTBOXED];
17
+ if (former) {
18
+ return former;
19
+ }
20
+ }
21
+
16
22
  const logger = cds.log(COMPONENT_NAME);
17
23
  const outboxOpts = Object.assign(
18
24
  {},
@@ -21,14 +27,8 @@ function outboxed(srv, customOpts) {
21
27
  customOpts || {}
22
28
  );
23
29
 
24
- if (!new.target) {
25
- const former = srv[OUTBOXED];
26
- if (former) {
27
- if (outboxOpts.kind === "persistent-outbox") {
28
- config.addCAPOutboxEvent(srv.name, outboxOpts);
29
- }
30
- return former;
31
- }
30
+ if (outboxOpts.kind === "persistent-outbox") {
31
+ config.addCAPOutboxEvent(srv.name, outboxOpts);
32
32
  }
33
33
 
34
34
  const originalSrv = srv[UNBOXED] || srv;
@@ -36,11 +36,9 @@ function outboxed(srv, customOpts) {
36
36
  outboxedSrv[UNBOXED] = originalSrv;
37
37
 
38
38
  if (!new.target) {
39
- Object.defineProperty(srv, OUTBOXED, { value: outboxedSrv });
40
- }
41
-
42
- if (outboxOpts.kind === "persistent-outbox") {
43
- config.addCAPOutboxEvent(srv.name, outboxOpts);
39
+ if (!srv[OUTBOXED]) {
40
+ Object.defineProperty(srv, OUTBOXED, { value: outboxedSrv });
41
+ }
44
42
  }
45
43
  outboxedSrv.handle = async function (req) {
46
44
  const context = req.context || cds.context;
@@ -201,56 +201,67 @@ const processPeriodicEvent = async (context, eventTypeInstance) => {
201
201
  }
202
202
  };
203
203
 
204
- const processEventMap = async (eventTypeInstance) => {
205
- eventTypeInstance.startPerformanceTracerEvents();
206
- await eventTypeInstance.beforeProcessingEvents();
207
- eventTypeInstance.logStartMessage();
208
- if (eventTypeInstance.commitOnEventLevel) {
209
- eventTypeInstance.txUsageAllowed = false;
204
+ const processEventMap = async (instance) => {
205
+ instance.startPerformanceTracerEvents();
206
+ await instance.beforeProcessingEvents();
207
+ instance.logStartMessage();
208
+ if (instance.commitOnEventLevel) {
209
+ instance.txUsageAllowed = false;
210
210
  }
211
211
  await limiter(
212
- eventTypeInstance.parallelEventProcessing,
213
- Object.entries(eventTypeInstance.eventProcessingMap),
212
+ instance.parallelEventProcessing,
213
+ Object.entries(instance.eventProcessingMap),
214
214
  async ([key, { queueEntries, payload }]) => {
215
- if (eventTypeInstance.commitOnEventLevel) {
215
+ if (instance.commitOnEventLevel) {
216
216
  let statusMap;
217
217
  await executeInNewTransaction(
218
- eventTypeInstance.baseContext,
219
- `eventQueue-processEvent-${eventTypeInstance.eventType}##${eventTypeInstance.eventSubType}`,
218
+ instance.baseContext,
219
+ `eventQueue-processEvent-${instance.eventType}##${instance.eventSubType}`,
220
220
  async (tx) => {
221
- statusMap = await _processEvent(eventTypeInstance, tx.context, key, queueEntries, payload);
222
- if (
223
- eventTypeInstance.statusMapContainsError(statusMap) ||
224
- eventTypeInstance.shouldRollbackTransaction(key)
225
- ) {
221
+ statusMap = await _processEvent(instance, tx.context, key, queueEntries, payload);
222
+ const shouldRollback =
223
+ instance.statusMapContainsError(statusMap) || instance.shouldRollbackTransaction(key);
224
+ if (shouldRollback) {
226
225
  await tx.rollback();
226
+ await _commitStatusInNewTx(instance, statusMap);
227
+ } else {
228
+ await instance.persistEventStatus(tx, {
229
+ skipChecks: true,
230
+ statusMap,
231
+ });
227
232
  }
228
233
  }
229
234
  );
230
- await executeInNewTransaction(
231
- eventTypeInstance.baseContext,
232
- `eventQueue-persistStatus-${eventTypeInstance.eventType}##${eventTypeInstance.eventSubType}`,
233
- async (tx) => {
234
- eventTypeInstance.processEventContext = tx.context;
235
- await eventTypeInstance.persistEventStatus(tx, {
236
- skipChecks: true,
237
- statusMap,
238
- });
239
- }
240
- );
241
235
  } else {
242
- await _processEvent(eventTypeInstance, eventTypeInstance.context, key, queueEntries, payload);
236
+ await _processEvent(instance, instance.context, key, queueEntries, payload);
243
237
  }
244
238
  }
245
- ).finally(() => {
246
- eventTypeInstance.clearEventProcessingContext();
247
- if (eventTypeInstance.commitOnEventLevel) {
248
- eventTypeInstance.txUsageAllowed = true;
249
- }
250
- });
251
- eventTypeInstance.endPerformanceTracerEvents();
239
+ )
240
+ .catch((err) => {
241
+ instance.handleErrorTx(err);
242
+ })
243
+ .finally(() => {
244
+ instance.clearEventProcessingContext();
245
+ if (instance.commitOnEventLevel) {
246
+ instance.txUsageAllowed = true;
247
+ }
248
+ });
249
+ instance.endPerformanceTracerEvents();
252
250
  };
253
251
 
252
+ const _commitStatusInNewTx = async (eventTypeInstance, statusMap) =>
253
+ await executeInNewTransaction(
254
+ eventTypeInstance.baseContext,
255
+ `eventQueue-persistStatus-${eventTypeInstance.eventType}##${eventTypeInstance.eventSubType}`,
256
+ async (tx) => {
257
+ eventTypeInstance.processEventContext = tx.context;
258
+ await eventTypeInstance.persistEventStatus(tx, {
259
+ skipChecks: true,
260
+ statusMap,
261
+ });
262
+ }
263
+ );
264
+
254
265
  const _checkEventIsBlocked = async (baseInstance) => {
255
266
  const isEventBlockedCb = config.isEventBlockedCb;
256
267
  let eventBlocked;
@@ -10,6 +10,7 @@ const config = require("../config");
10
10
  const common = require("../shared/common");
11
11
  const { runEventCombinationForTenant } = require("../runner/runnerHelper");
12
12
  const trace = require("../shared/openTelemetry");
13
+ const { TenantIdCheckTypes } = require("../constants");
13
14
 
14
15
  const EVENT_MESSAGE_CHANNEL = "EVENT_QUEUE_MESSAGE_CHANNEL";
15
16
  const COMPONENT_NAME = "/eventQueue/redisPub";
@@ -59,6 +60,10 @@ const broadcastEvent = async (tenantId, events, forceBroadcast = false) => {
59
60
  events = Array.isArray(events) ? events : [events];
60
61
  try {
61
62
  if (!config.redisEnabled) {
63
+ const tenantShouldBeProcessed = await common.isTenantIdValidCb(TenantIdCheckTypes.eventProcessing, tenantId);
64
+ if (!tenantShouldBeProcessed) {
65
+ return;
66
+ }
62
67
  await _processLocalWithoutRedis(tenantId, events);
63
68
  return;
64
69
  }
@@ -66,6 +71,9 @@ const broadcastEvent = async (tenantId, events, forceBroadcast = false) => {
66
71
  await trace(context, "broadcast-inserted-events", async () => {
67
72
  for (const { type, subType } of events) {
68
73
  const eventConfig = config.getEventConfig(type, subType);
74
+ if (!eventConfig) {
75
+ continue;
76
+ }
69
77
  for (let i = 0; i < TRIES_FOR_PUBLISH_PERIODIC_EVENT; i++) {
70
78
  const result = eventConfig.multiInstanceProcessing
71
79
  ? false
@@ -6,15 +6,16 @@ const redis = require("../shared/redis");
6
6
  const config = require("../config");
7
7
  const runnerHelper = require("../runner/runnerHelper");
8
8
  const common = require("../shared/common");
9
+ const { TenantIdCheckTypes } = require("../constants");
9
10
 
10
11
  const EVENT_MESSAGE_CHANNEL = "EVENT_QUEUE_MESSAGE_CHANNEL";
11
12
  const COMPONENT_NAME = "/eventQueue/redisSub";
12
- let subscriberClientPromise;
13
13
 
14
14
  const initEventQueueRedisSubscribe = () => {
15
- if (subscriberClientPromise || !config.redisEnabled) {
15
+ if (initEventQueueRedisSubscribe._initDone || !config.redisEnabled) {
16
16
  return;
17
17
  }
18
+ initEventQueueRedisSubscribe._initDone = true;
18
19
  redis.subscribeRedisChannel(config.redisOptions, EVENT_MESSAGE_CHANNEL, _messageHandlerProcessEvents);
19
20
  };
20
21
 
@@ -22,6 +23,10 @@ const _messageHandlerProcessEvents = async (messageData) => {
22
23
  const logger = cds.log(COMPONENT_NAME);
23
24
  try {
24
25
  const { lockId, tenantId, type, subType } = JSON.parse(messageData);
26
+ const tenantShouldBeProcessed = await common.isTenantIdValidCb(TenantIdCheckTypes.eventProcessing, tenantId);
27
+ if (!tenantShouldBeProcessed) {
28
+ return;
29
+ }
25
30
  logger.debug("received redis event", {
26
31
  tenantId,
27
32
  type,
@@ -83,20 +88,8 @@ const _messageHandlerProcessEvents = async (messageData) => {
83
88
  }
84
89
  };
85
90
 
86
- const closeSubscribeClient = async () => {
87
- try {
88
- const client = await subscriberClientPromise;
89
- if (client?.quit) {
90
- await client.quit();
91
- }
92
- } catch (err) {
93
- // ignore errors during shutdown
94
- }
95
- };
96
-
97
91
  module.exports = {
98
92
  initEventQueueRedisSubscribe,
99
- closeSubscribeClient,
100
93
  __: {
101
94
  _messageHandlerProcessEvents,
102
95
  },
@@ -111,17 +111,19 @@ const _executeEventsAllTenantsRedis = async (tenantIds) => {
111
111
  // NOTE: do checks for all tenants on the same app instance --> acquire lock tenant independent
112
112
  // distribute from this instance to all others
113
113
  const dummyContext = new cds.EventContext({});
114
- const couldAcquireLock = await trace(
115
- dummyContext,
116
- "acquire-lock-master-runner",
117
- async () => {
118
- return await distributedLock.acquireLock(dummyContext, EVENT_QUEUE_RUN_REDIS_CHECK, {
119
- expiryTime: eventQueueConfig.runInterval * 0.95,
120
- tenantScoped: false,
121
- });
122
- },
123
- { newRootSpan: true }
124
- );
114
+ const couldAcquireLock = config.tenantIdFilterEventProcessing
115
+ ? true
116
+ : await trace(
117
+ dummyContext,
118
+ "acquire-lock-master-runner",
119
+ async () => {
120
+ return await distributedLock.acquireLock(dummyContext, EVENT_QUEUE_RUN_REDIS_CHECK, {
121
+ expiryTime: eventQueueConfig.runInterval * 0.95,
122
+ tenantScoped: false,
123
+ });
124
+ },
125
+ { newRootSpan: true }
126
+ );
125
127
  if (!couldAcquireLock) {
126
128
  return;
127
129
  }
@@ -237,7 +239,7 @@ const _executePeriodicEventsAllTenants = async (tenantIds) => {
237
239
  };
238
240
  await cds.tx(tenantContext, async ({ context }) => {
239
241
  await trace(tenantContext, "update-periodic-events-for-tenant", async () => {
240
- if (!config.redisEnabled) {
242
+ if (!config.redisEnabled && !config.tenantIdFilterEventProcessing) {
241
243
  const couldAcquireLock = await distributedLock.acquireLock(context, EVENT_QUEUE_UPDATE_PERIODIC_EVENTS, {
242
244
  expiryTime: eventQueueConfig.runInterval * 0.95,
243
245
  });
@@ -433,7 +435,7 @@ const _multiTenancyPeriodicEvents = async (tenantIds) => {
433
435
  dummyContext,
434
436
  "update-periodic-events",
435
437
  async () => {
436
- if (config.redisEnabled) {
438
+ if (config.redisEnabled && !config.tenantIdFilterEventProcessing) {
437
439
  const couldAcquireLock = await distributedLock.acquireLock(dummyContext, EVENT_QUEUE_UPDATE_PERIODIC_EVENTS, {
438
440
  expiryTime: 60 * 1000, // short living lock --> assume we do not have 2 onboards within 1 minute
439
441
  tenantScoped: false,
@@ -23,6 +23,7 @@ const COMPONENT_NAME = "/eventQueue/cdsHelper";
23
23
  async function executeInNewTransaction(context = {}, transactionTag, fn, args, { info = {} } = {}) {
24
24
  const parameters = Array.isArray(args) ? args : [args];
25
25
  const logger = cds.log(COMPONENT_NAME);
26
+ let transactionRollbackPromise = Promise.resolve(false);
26
27
  try {
27
28
  const user = new cds.User.Privileged({ id: config.userId, tokenInfo: await common.getTokenInfo(context.tenant) });
28
29
  if (cds.db.kind === "hana") {
@@ -36,7 +37,15 @@ async function executeInNewTransaction(context = {}, transactionTag, fn, args, {
36
37
  },
37
38
  async (tx) => {
38
39
  tx.context._ = context._ ?? {};
39
- return await fn(tx, ...parameters);
40
+ return new Promise((outerResolve, outerReject) => {
41
+ transactionRollbackPromise = new Promise((resolve) => {
42
+ tx.context.on("succeeded", () => resolve(false));
43
+ tx.context.on("failed", () => resolve(true));
44
+ fn(tx, ...parameters)
45
+ .then(outerResolve)
46
+ .catch(outerReject);
47
+ });
48
+ });
40
49
  }
41
50
  );
42
51
  } else {
@@ -51,7 +60,10 @@ async function executeInNewTransaction(context = {}, transactionTag, fn, args, {
51
60
  user,
52
61
  headers: context.headers,
53
62
  },
54
- async (tx) => fn(tx, ...parameters)
63
+ async (tx) => {
64
+ await fn(tx, ...parameters);
65
+ transactionRollbackPromise = false;
66
+ }
55
67
  );
56
68
  } else {
57
69
  contextTx.context.user = user;
@@ -68,17 +80,24 @@ async function executeInNewTransaction(context = {}, transactionTag, fn, args, {
68
80
  // change rollback to no opt - closing tx would cause follow-up usage to fail.
69
81
  // the process that opened the tx needs to manage it
70
82
  };
71
- await fn(contextTx, ...parameters).finally(() => (contextTx.rollback = txRollback));
83
+ await fn(contextTx, ...parameters)
84
+ .then(() => (transactionRollbackPromise = false))
85
+ .finally(() => (contextTx.rollback = txRollback));
72
86
  }
73
87
  }
74
88
  } catch (err) {
89
+ const transactionRollback = await transactionRollbackPromise;
75
90
  if (err instanceof VError) {
76
91
  Object.assign(err.jse_info, {
77
92
  newTx: info,
78
93
  });
79
- throw err;
94
+ if (transactionRollback) {
95
+ throw err;
96
+ } else {
97
+ logger.error("business transaction commited but succeeded|done|failed threw a error!", err);
98
+ }
80
99
  } else {
81
- throw new VError(
100
+ const nestedError = new VError(
82
101
  {
83
102
  name: VERROR_CLUSTER_NAME,
84
103
  cause: err,
@@ -86,6 +105,11 @@ async function executeInNewTransaction(context = {}, transactionTag, fn, args, {
86
105
  },
87
106
  "Execution in new transaction failed"
88
107
  );
108
+ if (transactionRollback) {
109
+ throw err;
110
+ } else {
111
+ logger.error("business transaction commited but succeeded|done|failed threw a error!", nestedError);
112
+ }
89
113
  }
90
114
  } finally {
91
115
  logger.debug("Execution in new transaction finished", info);
@@ -109,7 +133,13 @@ const getAllTenantIds = async () => {
109
133
  const response = await ssp.get("/tenant");
110
134
  return response
111
135
  .map((tenant) => tenant.subscribedTenantId ?? tenant.tenant)
112
- .filter((tenantId) => common.isTenantIdValidCb(TenantIdCheckTypes.getAllTenantIds, tenantId));
136
+ .reduce(async (result, tenantId) => {
137
+ result = await result;
138
+ if (await common.isTenantIdValidCb(TenantIdCheckTypes.eventProcessing, tenantId)) {
139
+ result.push(tenantId);
140
+ }
141
+ return result;
142
+ }, []);
113
143
  };
114
144
 
115
145
  module.exports = {
@@ -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 VError = require("verror");
8
+
7
9
  const config = require("../config");
8
10
  const { TenantIdCheckTypes } = require("../constants");
9
11
 
@@ -44,7 +46,22 @@ const limiter = async (limit, payloads, iterator) => {
44
46
  }
45
47
  }
46
48
  }
47
- return Promise.allSettled(returnPromises);
49
+ return promiseAllDone(returnPromises);
50
+ };
51
+
52
+ const promiseAllDone = async (iterable) => {
53
+ const results = await Promise.allSettled(iterable);
54
+ const rejects = results.filter((entry) => {
55
+ return entry.status === "rejected";
56
+ });
57
+ if (rejects.length === 1) {
58
+ return Promise.reject(rejects[0].reason);
59
+ } else if (rejects.length > 1) {
60
+ return Promise.reject(new VError.MultiError(rejects.map((reject) => reject.reason)));
61
+ }
62
+ return results.map((entry) => {
63
+ return entry.value;
64
+ });
48
65
  };
49
66
 
50
67
  const isValidDate = (value) => {
@@ -115,10 +132,23 @@ const getTokenInfo = async (tenantId) => {
115
132
  return await tokenInfoCache[tenantId].value;
116
133
  };
117
134
 
118
- const isTenantIdValidCb = (checkType, tenantId) => {
119
- if (config.tenantIdFilterCb) {
120
- return config.tenantIdFilterCb(checkType, tenantId);
121
- } else {
135
+ const isTenantIdValidCb = async (checkType, tenantId) => {
136
+ let cb;
137
+ switch (checkType) {
138
+ case TenantIdCheckTypes.getTokenInfo:
139
+ cb = config.tenantIdFilterTokenInfo;
140
+ break;
141
+ case TenantIdCheckTypes.eventProcessing:
142
+ cb = config.tenantIdFilterEventProcessing;
143
+ break;
144
+ default:
145
+ cb = async () => true;
146
+ }
147
+
148
+ try {
149
+ return cb ? await cb(tenantId) : true;
150
+ } catch (err) {
151
+ cds.log(COMPONENT_NAME).error("failed in custom tenant id filter callback. Returning true.", err);
122
152
  return true;
123
153
  }
124
154
  };
@@ -131,6 +161,7 @@ module.exports = {
131
161
  hashStringTo32Bit,
132
162
  getTokenInfo,
133
163
  isTenantIdValidCb,
164
+ promiseAllDone,
134
165
  __: {
135
166
  clearTokenInfoCache: () => (getTokenInfo._tokenInfoCache = {}),
136
167
  },
package/src/shared/env.js CHANGED
@@ -18,7 +18,7 @@ class Env {
18
18
  }
19
19
 
20
20
  get redisRequires() {
21
- return cds.requires["redis-eventQueue"] ?? cds.requires["redis"];
21
+ return cds.requires["redis-eventQueue"] || cds.requires["redis"];
22
22
  }
23
23
 
24
24
  get applicationName() {
@@ -9,7 +9,8 @@ const COMPONENT_NAME = "/eventQueue/shared/redis";
9
9
  const LOG_AFTER_SEC = 5;
10
10
 
11
11
  let mainClientPromise;
12
- const subscriberChannelClientPromise = {};
12
+ let subscriberClientPromise;
13
+ const subscribedChannels = {};
13
14
  let lastErrorLog = Date.now();
14
15
 
15
16
  const createMainClientAndConnect = (options) => {
@@ -18,7 +19,8 @@ const createMainClientAndConnect = (options) => {
18
19
  }
19
20
 
20
21
  const errorHandlerCreateClient = (err) => {
21
- cds.log(COMPONENT_NAME).error("error from redis client for pub/sub failed", err);
22
+ mainClientPromise?.then?.(_resilientClientClose);
23
+ cds.log(COMPONENT_NAME).error("error from redis main client:", err);
22
24
  mainClientPromise = null;
23
25
  setTimeout(() => createMainClientAndConnect(options), LOG_AFTER_SEC * 1000).unref();
24
26
  };
@@ -63,7 +65,7 @@ const createClientAndConnect = async (options, errorHandlerCreateClient, isConne
63
65
  client.on("error", (err) => {
64
66
  const dateNow = Date.now();
65
67
  if (dateNow - lastErrorLog > LOG_AFTER_SEC * 1000) {
66
- cds.log(COMPONENT_NAME).error("error from redis client for pub/sub failed", err);
68
+ cds.log(COMPONENT_NAME).error("error redis client:", err);
67
69
  lastErrorLog = dateNow;
68
70
  }
69
71
  });
@@ -84,21 +86,48 @@ const createClientAndConnect = async (options, errorHandlerCreateClient, isConne
84
86
  };
85
87
 
86
88
  const subscribeRedisChannel = (options, channel, subscribeHandler) => {
89
+ subscribedChannels[channel] = subscribeHandler;
87
90
  const errorHandlerCreateClient = (err) => {
88
91
  cds.log(COMPONENT_NAME).error(`error from redis client for pub/sub failed for channel ${channel}`, err);
89
- subscriberChannelClientPromise[channel] = null;
90
- setTimeout(() => subscribeRedisChannel(options, channel, subscribeHandler), LOG_AFTER_SEC * 1000).unref();
92
+ subscriberClientPromise?.then?.(_resilientClientClose);
93
+ subscriberClientPromise = null;
94
+ setTimeout(() => _subscribeChannels(options, subscribedChannels, subscribeHandler), LOG_AFTER_SEC * 1000).unref();
91
95
  };
92
96
 
93
- subscriberChannelClientPromise[channel] = createClientAndConnect(options, errorHandlerCreateClient)
97
+ _subscribeChannels(options, { [channel]: subscribeHandler }, errorHandlerCreateClient);
98
+ };
99
+
100
+ const _subscribeChannels = (options, subscribedChannels, errorHandlerCreateClient) => {
101
+ subscriberClientPromise = createClientAndConnect(options, errorHandlerCreateClient)
94
102
  .then((client) => {
95
- cds.log(COMPONENT_NAME).info("subscribe redis client connected channel", { channel });
96
- client.subscribe(channel, subscribeHandler).catch(errorHandlerCreateClient);
103
+ for (const channel in subscribedChannels) {
104
+ const fn = subscribedChannels[channel];
105
+ client._subscribedChannels ??= {};
106
+ if (client._subscribedChannels[channel]) {
107
+ continue;
108
+ }
109
+ cds.log(COMPONENT_NAME).info("subscribe redis client connected channel", { channel });
110
+ client
111
+ .subscribe(channel, fn)
112
+ .then(() => {
113
+ client._subscribedChannels ??= {};
114
+ client._subscribedChannels[channel] = 1;
115
+ })
116
+ .catch(() => {
117
+ cds.log(COMPONENT_NAME).error("error subscribe to channel - retrying...");
118
+ setTimeout(() => _subscribeChannels(options, [channel], fn), LOG_AFTER_SEC * 1000).unref();
119
+ });
120
+ }
97
121
  })
98
122
  .catch((err) => {
99
123
  cds
100
124
  .log(COMPONENT_NAME)
101
- .error(`error from redis client for pub/sub failed during startup - trying to reconnect - ${channel}`, err);
125
+ .error(
126
+ `error from redis client for pub/sub failed during startup - trying to reconnect - ${Object.keys(
127
+ subscribedChannels
128
+ ).join(", ")}`,
129
+ err
130
+ );
102
131
  });
103
132
  };
104
133
 
@@ -108,11 +137,13 @@ const publishMessage = async (options, channel, message) => {
108
137
  };
109
138
 
110
139
  const closeMainClient = async () => {
111
- try {
112
- await _resilientClientClose(await mainClientPromise);
113
- } catch (err) {
114
- // ignore errors during shutdown
115
- }
140
+ await _resilientClientClose(await mainClientPromise);
141
+ cds.log(COMPONENT_NAME).info("main redis client closed!");
142
+ };
143
+
144
+ const closeSubscribeClient = async () => {
145
+ await _resilientClientClose(await subscriberClientPromise);
146
+ cds.log(COMPONENT_NAME).info("subscribe redis client closed!");
116
147
  };
117
148
 
118
149
  const _resilientClientClose = async (client) => {
@@ -121,7 +152,7 @@ const _resilientClientClose = async (client) => {
121
152
  await client.quit();
122
153
  }
123
154
  } catch (err) {
124
- // ignore errors during shutdown
155
+ cds.log(COMPONENT_NAME).info("error during redis close - continuing...", err);
125
156
  }
126
157
  };
127
158
 
@@ -151,5 +182,6 @@ module.exports = {
151
182
  subscribeRedisChannel,
152
183
  publishMessage,
153
184
  closeMainClient,
185
+ closeSubscribeClient,
154
186
  connectionCheck,
155
187
  };