@cap-js-community/event-queue 1.3.6 → 1.4.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cap-js-community/event-queue",
3
- "version": "1.3.6",
3
+ "version": "1.4.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
  "files": [
@@ -10,7 +10,7 @@ const { arrayToFlatMap } = require("./shared/common");
10
10
  const eventScheduler = require("./shared/eventScheduler");
11
11
  const eventConfig = require("./config");
12
12
  const PerformanceTracer = require("./shared/PerformanceTracer");
13
- const { broadcastEvent } = require("./redisPubSub");
13
+ const { broadcastEvent } = require("./redis/redisPub");
14
14
 
15
15
  const IMPLEMENT_ERROR_MESSAGE = "needs to be reimplemented";
16
16
  const COMPONENT_NAME = "/eventQueue/EventQueueProcessorBase";
@@ -1113,7 +1113,7 @@ class EventQueueProcessorBase {
1113
1113
 
1114
1114
  broadCastEvent() {
1115
1115
  setTimeout(() => {
1116
- broadcastEvent(this.__baseContext.tenant, this.#eventType, this.#eventSubType).catch((err) => {
1116
+ broadcastEvent(this.__baseContext.tenant, { type: this.#eventType, subType: this.#eventSubType }).catch((err) => {
1117
1117
  this.logger.error("could not execute scheduled event", err, {
1118
1118
  tenantId: this.__baseContext.tenant,
1119
1119
  type: this.#eventType,
package/src/config.js CHANGED
@@ -34,6 +34,11 @@ const BASE_PERIODIC_EVENTS = [
34
34
  },
35
35
  ];
36
36
 
37
+ const BASE_TABLES = {
38
+ EVENT: "sap.eventqueue.Event",
39
+ LOCK: "sap.eventqueue.Lock",
40
+ };
41
+
37
42
  class Config {
38
43
  #logger;
39
44
  #config;
@@ -61,6 +66,7 @@ class Config {
61
66
  #userId;
62
67
  #enableTxConsistencyCheck;
63
68
  #cleanupLocksAndEventsForDev;
69
+ #redisOptions;
64
70
  static #instance;
65
71
  constructor() {
66
72
  this.#logger = cds.log(COMPONENT_NAME);
@@ -105,7 +111,7 @@ class Config {
105
111
 
106
112
  attachConfigChangeHandler() {
107
113
  this.#attachBlockListChangeHandler();
108
- redis.subscribeRedisChannel(REDIS_CONFIG_CHANNEL, (messageData) => {
114
+ redis.subscribeRedisChannel(this.#redisOptions, REDIS_CONFIG_CHANNEL, (messageData) => {
109
115
  try {
110
116
  const { key, value } = JSON.parse(messageData);
111
117
  if (this[key] !== value) {
@@ -125,13 +131,13 @@ class Config {
125
131
  this.#logger.info("redis not connected, config change won't be published", { key, value });
126
132
  return;
127
133
  }
128
- redis.publishMessage(REDIS_CONFIG_CHANNEL, JSON.stringify({ key, value })).catch((error) => {
134
+ redis.publishMessage(this.#redisOptions, REDIS_CONFIG_CHANNEL, JSON.stringify({ key, value })).catch((error) => {
129
135
  this.#logger.error(`publishing config change failed key: ${key}, value: ${value}`, error);
130
136
  });
131
137
  }
132
138
 
133
139
  #attachBlockListChangeHandler() {
134
- redis.subscribeRedisChannel(REDIS_CONFIG_BLOCKLIST_CHANNEL, (messageData) => {
140
+ redis.subscribeRedisChannel(this.#redisOptions, REDIS_CONFIG_BLOCKLIST_CHANNEL, (messageData) => {
135
141
  try {
136
142
  const { command, key, tenant } = JSON.parse(messageData);
137
143
  if (command === COMMAND_BLOCK) {
@@ -160,7 +166,11 @@ class Config {
160
166
  }
161
167
 
162
168
  redis
163
- .publishMessage(REDIS_CONFIG_BLOCKLIST_CHANNEL, JSON.stringify({ command: COMMAND_BLOCK, key, tenant }))
169
+ .publishMessage(
170
+ this.#redisOptions,
171
+ REDIS_CONFIG_BLOCKLIST_CHANNEL,
172
+ JSON.stringify({ command: COMMAND_BLOCK, key, tenant })
173
+ )
164
174
  .catch((error) => {
165
175
  this.#logger.error(`publishing config block failed key: ${key}`, error);
166
176
  });
@@ -189,7 +199,11 @@ class Config {
189
199
  }
190
200
 
191
201
  redis
192
- .publishMessage(REDIS_CONFIG_BLOCKLIST_CHANNEL, JSON.stringify({ command: COMMAND_UNBLOCK, key, tenant }))
202
+ .publishMessage(
203
+ this.#redisOptions,
204
+ REDIS_CONFIG_BLOCKLIST_CHANNEL,
205
+ JSON.stringify({ command: COMMAND_UNBLOCK, key, tenant })
206
+ )
193
207
  .catch((error) => {
194
208
  this.#logger.error(`publishing config block failed key: ${key}`, error);
195
209
  });
@@ -386,19 +400,11 @@ class Config {
386
400
  }
387
401
 
388
402
  get tableNameEventQueue() {
389
- return this.#tableNameEventQueue;
390
- }
391
-
392
- set tableNameEventQueue(value) {
393
- this.#tableNameEventQueue = value;
403
+ return BASE_TABLES.EVENT;
394
404
  }
395
405
 
396
406
  get tableNameEventLock() {
397
- return this.#tableNameEventLock;
398
- }
399
-
400
- set tableNameEventLock(value) {
401
- this.#tableNameEventLock = value;
407
+ return BASE_TABLES.LOCK;
402
408
  }
403
409
 
404
410
  set configFilePath(value) {
@@ -489,6 +495,14 @@ class Config {
489
495
  return this.#cleanupLocksAndEventsForDev;
490
496
  }
491
497
 
498
+ set redisOptions(value) {
499
+ this.#redisOptions = value;
500
+ }
501
+
502
+ get redisOptions() {
503
+ return this.#redisOptions;
504
+ }
505
+
492
506
  get isMultiTenancy() {
493
507
  return !!cds.requires.multitenancy;
494
508
  }
package/src/dbHandler.js CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  const cds = require("@sap/cds");
4
4
 
5
- const { broadcastEvent } = require("./redisPubSub");
5
+ const { broadcastEvent } = require("./redis/redisPub");
6
6
  const config = require("./config");
7
7
 
8
8
  const COMPONENT_NAME = "/eventQueue/dbHandler";
@@ -31,14 +31,17 @@ const registerEventQueueDbHandler = (dbService) => {
31
31
 
32
32
  eventCombinations.length &&
33
33
  req.on("succeeded", () => {
34
- for (const eventCombination of eventCombinations) {
35
- broadcastEvent(req.tenant, ...eventCombination.split("##")).catch((err) => {
36
- cds.log(COMPONENT_NAME).error("db handler failure during broadcasting event", err, {
37
- tenant: req.tenant,
38
- eventCombination,
39
- });
34
+ const events = eventCombinations.map((eventCombination) => {
35
+ const [type, subType] = eventCombination.split("##");
36
+ return { type, subType };
37
+ });
38
+
39
+ broadcastEvent(req.tenant, events).catch((err) => {
40
+ cds.log(COMPONENT_NAME).error("db handler failure during broadcasting event", err, {
41
+ tenant: req.tenant,
42
+ events,
40
43
  });
41
- }
44
+ });
42
45
  });
43
46
  });
44
47
  };
package/src/initialize.js CHANGED
@@ -7,11 +7,10 @@ const cds = require("@sap/cds");
7
7
  const yaml = require("yaml");
8
8
  const VError = require("verror");
9
9
 
10
- const EventQueueError = require("./EventQueueError");
11
- const runner = require("./runner");
10
+ const runner = require("./runner/runner");
12
11
  const dbHandler = require("./dbHandler");
13
12
  const config = require("./config");
14
- const { initEventQueueRedisSubscribe, closeSubscribeClient } = require("./redisPubSub");
13
+ const { initEventQueueRedisSubscribe, closeSubscribeClient } = require("./redis/redisSub");
15
14
  const redis = require("./shared/redis");
16
15
  const eventQueueAsOutbox = require("./outbox/eventQueueAsOutbox");
17
16
  const { getAllTenantIds } = require("./shared/cdsHelper");
@@ -21,26 +20,21 @@ const readFileAsync = promisify(fs.readFile);
21
20
 
22
21
  const VERROR_CLUSTER_NAME = "EventQueueInitialization";
23
22
  const COMPONENT = "eventQueue/initialize";
24
- const BASE_TABLES = {
25
- EVENT: "sap.eventqueue.Event",
26
- LOCK: "sap.eventqueue.Lock",
27
- };
23
+
28
24
  const CONFIG_VARS = [
29
25
  ["configFilePath", null],
30
26
  ["registerAsEventProcessor", true],
31
27
  ["processEventsAfterPublish", true],
32
28
  ["isEventQueueActive", true],
33
29
  ["runInterval", 25 * 60 * 1000],
34
- ["tableNameEventQueue", BASE_TABLES.EVENT],
35
- ["tableNameEventLock", BASE_TABLES.LOCK],
36
30
  ["disableRedis", true],
37
- ["skipCsnCheck", false],
38
31
  ["updatePeriodicEvents", true],
39
32
  ["thresholdLoggingEventProcessing", 50],
40
33
  ["useAsCAPOutbox", false],
41
34
  ["userId", null],
42
35
  ["enableTxConsistencyCheck", false],
43
36
  ["cleanupLocksAndEventsForDev", false],
37
+ ["redisOptions", {}],
44
38
  ];
45
39
 
46
40
  const initialize = async ({
@@ -49,16 +43,14 @@ const initialize = async ({
49
43
  processEventsAfterPublish,
50
44
  isEventQueueActive,
51
45
  runInterval,
52
- tableNameEventQueue,
53
- tableNameEventLock,
54
46
  disableRedis,
55
- skipCsnCheck,
56
47
  updatePeriodicEvents,
57
48
  thresholdLoggingEventProcessing,
58
49
  useAsCAPOutbox,
59
50
  userId,
60
51
  enableTxConsistencyCheck,
61
52
  cleanupLocksAndEventsForDev,
53
+ redisOptions,
62
54
  } = {}) => {
63
55
  if (config.initialized) {
64
56
  return;
@@ -71,16 +63,14 @@ const initialize = async ({
71
63
  processEventsAfterPublish,
72
64
  isEventQueueActive,
73
65
  runInterval,
74
- tableNameEventQueue,
75
- tableNameEventLock,
76
66
  disableRedis,
77
- skipCsnCheck,
78
67
  updatePeriodicEvents,
79
68
  thresholdLoggingEventProcessing,
80
69
  useAsCAPOutbox,
81
70
  userId,
82
71
  enableTxConsistencyCheck,
83
- cleanupLocksAndEventsForDev
72
+ cleanupLocksAndEventsForDev,
73
+ redisOptions
84
74
  );
85
75
 
86
76
  const logger = cds.log(COMPONENT);
@@ -95,12 +85,10 @@ const initialize = async ({
95
85
  }
96
86
  });
97
87
  if (redisEnabled) {
98
- config.redisEnabled = await redis.connectionCheck();
88
+ config.redisEnabled = await redis.connectionCheck(config.redisOptions);
99
89
  }
100
90
  config.fileContent = await readConfigFromFile(config.configFilePath);
101
91
 
102
- !config.skipCsnCheck && (await csnCheck());
103
-
104
92
  monkeyPatchCAPOutbox();
105
93
  registerCdsShutdown();
106
94
  logger.info("event queue initialized", {
@@ -172,50 +160,6 @@ const monkeyPatchCAPOutbox = () => {
172
160
  }
173
161
  };
174
162
 
175
- const csnCheck = async () => {
176
- cds.on("loaded", async (csn) => {
177
- if (csn.namespace === "cds.xt") {
178
- return;
179
- }
180
- const eventCsn = csn.definitions[config.tableNameEventQueue];
181
- if (!eventCsn) {
182
- throw EventQueueError.missingTableInCsn(config.tableNameEventQueue);
183
- }
184
-
185
- const lockCsn = csn.definitions[config.tableNameEventLock];
186
- if (!lockCsn) {
187
- throw EventQueueError.missingTableInCsn(config.tableNameEventLock);
188
- }
189
-
190
- if (config.tableNameEventQueue === BASE_TABLES.EVENT && config.tableNameEventLock === BASE_TABLES.LOCK) {
191
- return; // no need to check base tables
192
- }
193
-
194
- const baseEvent = csn.definitions["sap.eventqueue.Event"];
195
- const baseLock = csn.definitions["sap.eventqueue.Lock"];
196
-
197
- checkCustomTable(baseEvent, eventCsn);
198
- checkCustomTable(baseLock, lockCsn);
199
- });
200
- };
201
-
202
- const checkCustomTable = (baseCsn, customCsn) => {
203
- for (const columnName in baseCsn.elements) {
204
- if (!customCsn.elements[columnName]) {
205
- throw EventQueueError.missingElementInTable(customCsn.name, columnName);
206
- }
207
-
208
- if (
209
- customCsn.elements[columnName].type !== "cds.Association" &&
210
- customCsn.elements[columnName].type !== baseCsn.elements[columnName].type &&
211
- columnName === "status" &&
212
- customCsn.elements[columnName].type !== "cds.Integer"
213
- ) {
214
- throw EventQueueError.typeMismatchInTable(customCsn.name, columnName);
215
- }
216
- }
217
- };
218
-
219
163
  const mixConfigVarsWithEnv = (...args) => {
220
164
  CONFIG_VARS.forEach(([configName, defaultValue], index) => {
221
165
  const configValue = args[index];
@@ -0,0 +1,93 @@
1
+ "use strict";
2
+
3
+ const { promisify } = require("util");
4
+
5
+ const cds = require("@sap/cds");
6
+
7
+ const redis = require("../shared/redis");
8
+ const { checkLockExistsAndReturnValue } = require("../shared/distributedLock");
9
+ const config = require("../config");
10
+ const { getSubdomainForTenantId } = require("../shared/cdsHelper");
11
+ const { runEventCombinationForTenant } = require("../runner/runnerHelper");
12
+
13
+ const EVENT_MESSAGE_CHANNEL = "EVENT_QUEUE_MESSAGE_CHANNEL";
14
+ const COMPONENT_NAME = "/eventQueue/redisPub";
15
+ const TRIES_FOR_PUBLISH_PERIODIC_EVENT = 10;
16
+ const SLEEP_TIME_FOR_PUBLISH_PERIODIC_EVENT = 30 * 1000;
17
+
18
+ const wait = promisify(setTimeout);
19
+
20
+ const broadcastEvent = async (tenantId, events) => {
21
+ const logger = cds.log(COMPONENT_NAME);
22
+ events = Array.isArray(events) ? events : [events];
23
+ try {
24
+ if (!config.isEventQueueActive) {
25
+ cds.log(COMPONENT_NAME).info("Skipping processing because runner is deactivated!", {});
26
+ return;
27
+ }
28
+ if (!config.redisEnabled) {
29
+ if (config.registerAsEventProcessor) {
30
+ let context = {};
31
+ if (tenantId) {
32
+ const subdomain = await getSubdomainForTenantId(tenantId);
33
+ const user = new cds.User.Privileged(config.userId);
34
+ context = {
35
+ // NOTE: we need this because of logging otherwise logs would not contain the subdomain
36
+ tenant: tenantId,
37
+ user,
38
+ http: { req: { authInfo: { getSubdomain: () => subdomain } } },
39
+ };
40
+ }
41
+
42
+ return await cds.tx(context, async ({ context }) => {
43
+ for (const { type, subType } of events) {
44
+ await runEventCombinationForTenant(context, type, subType);
45
+ }
46
+ });
47
+ }
48
+ return;
49
+ }
50
+ for (const { type, subType } of events) {
51
+ const eventConfig = config.getEventConfig(type, subType);
52
+ for (let i = 0; i < TRIES_FOR_PUBLISH_PERIODIC_EVENT; i++) {
53
+ const result = await checkLockExistsAndReturnValue(
54
+ new cds.EventContext({ tenant: tenantId }),
55
+ [type, subType].join("##")
56
+ );
57
+ if (result) {
58
+ logger.debug("skip publish redis event as no lock is available", {
59
+ type,
60
+ subType,
61
+ index: i,
62
+ isPeriodic: eventConfig.isPeriodic,
63
+ waitInterval: SLEEP_TIME_FOR_PUBLISH_PERIODIC_EVENT,
64
+ });
65
+ if (!eventConfig.isPeriodic) {
66
+ break;
67
+ }
68
+ await wait(SLEEP_TIME_FOR_PUBLISH_PERIODIC_EVENT);
69
+ continue;
70
+ }
71
+ logger.debug("publishing redis event", {
72
+ tenantId,
73
+ type,
74
+ subType,
75
+ });
76
+ await redis.publishMessage(
77
+ config.redisOptions,
78
+ EVENT_MESSAGE_CHANNEL,
79
+ JSON.stringify({ tenantId, type, subType })
80
+ );
81
+ break;
82
+ }
83
+ }
84
+ } catch (err) {
85
+ logger.error("publish events failed!", err, {
86
+ tenantId,
87
+ });
88
+ }
89
+ };
90
+
91
+ module.exports = {
92
+ broadcastEvent,
93
+ };
@@ -0,0 +1,95 @@
1
+ "use strict";
2
+
3
+ const cds = require("@sap/cds");
4
+
5
+ const redis = require("../shared/redis");
6
+ const config = require("../config");
7
+ const { getSubdomainForTenantId } = require("../shared/cdsHelper");
8
+ const runnerHelper = require("../runner/runnerHelper");
9
+
10
+ const EVENT_MESSAGE_CHANNEL = "EVENT_QUEUE_MESSAGE_CHANNEL";
11
+ const COMPONENT_NAME = "/eventQueue/redisSub";
12
+ let subscriberClientPromise;
13
+
14
+ const initEventQueueRedisSubscribe = () => {
15
+ if (subscriberClientPromise || !config.redisEnabled) {
16
+ return;
17
+ }
18
+ redis.subscribeRedisChannel(config.redisOptions, EVENT_MESSAGE_CHANNEL, _messageHandlerProcessEvents);
19
+ };
20
+
21
+ const _messageHandlerProcessEvents = async (messageData) => {
22
+ const logger = cds.log(COMPONENT_NAME);
23
+ try {
24
+ const { tenantId, type, subType } = JSON.parse(messageData);
25
+ logger.debug("received redis event", {
26
+ tenantId,
27
+ type,
28
+ subType,
29
+ });
30
+ if (!config.isEventQueueActive) {
31
+ cds.log(COMPONENT_NAME).info("Skipping processing because runner is deactivated!", {
32
+ type,
33
+ subType,
34
+ });
35
+ return;
36
+ }
37
+
38
+ const subdomain = await getSubdomainForTenantId(tenantId);
39
+ const user = new cds.User.Privileged(config.userId);
40
+ const tenantContext = {
41
+ tenant: tenantId,
42
+ user,
43
+ // NOTE: we need this because of logging otherwise logs would not contain the subdomain
44
+ http: { req: { authInfo: { getSubdomain: () => subdomain } } },
45
+ };
46
+
47
+ if (!config.getEventConfig(type, subType)) {
48
+ if (config.isCapOutboxEvent(type)) {
49
+ try {
50
+ const service = await cds.connect.to(subType);
51
+ cds.outboxed(service);
52
+ } catch (err) {
53
+ logger.error("could not connect to outboxed service", err, {
54
+ type,
55
+ subType,
56
+ });
57
+ return;
58
+ }
59
+ } else {
60
+ logger.error("cannot find configuration for published event. Event won't be processed", {
61
+ type,
62
+ subType,
63
+ });
64
+ return;
65
+ }
66
+ }
67
+
68
+ return await cds.tx(tenantContext, async ({ context }) => {
69
+ return await runnerHelper.runEventCombinationForTenant(context, type, subType);
70
+ });
71
+ } catch (err) {
72
+ logger.error("could not parse event information", {
73
+ messageData,
74
+ });
75
+ }
76
+ };
77
+
78
+ const closeSubscribeClient = async () => {
79
+ try {
80
+ const client = await subscriberClientPromise;
81
+ if (client?.quit) {
82
+ await client.quit();
83
+ }
84
+ } catch (err) {
85
+ // ignore errors during shutdown
86
+ }
87
+ };
88
+
89
+ module.exports = {
90
+ initEventQueueRedisSubscribe,
91
+ closeSubscribeClient,
92
+ __: {
93
+ _messageHandlerProcessEvents,
94
+ },
95
+ };
@@ -0,0 +1,52 @@
1
+ "use strict";
2
+
3
+ const cds = require("@sap/cds");
4
+
5
+ const eventConfig = require("../config");
6
+ const { EventProcessingStatus } = require("../constants");
7
+
8
+ const getOpenQueueEntries = async (tx) => {
9
+ const startTime = new Date();
10
+ const refDateStartAfter = new Date(startTime.getTime() + eventConfig.runInterval * 1.2);
11
+ const entries = await tx.run(
12
+ SELECT.from(eventConfig.tableNameEventQueue)
13
+ .where(
14
+ "( startAfter IS NULL OR startAfter <=",
15
+ refDateStartAfter.toISOString(),
16
+ " ) AND ( status =",
17
+ EventProcessingStatus.Open,
18
+ "OR ( status =",
19
+ EventProcessingStatus.Error,
20
+ ") OR ( status =",
21
+ EventProcessingStatus.InProgress,
22
+ "AND lastAttemptTimestamp <=",
23
+ new Date(startTime.getTime() - eventConfig.globalTxTimeout).toISOString(),
24
+ ") )"
25
+ )
26
+ .columns("type", "subType")
27
+ .groupBy("type", "subType")
28
+ );
29
+
30
+ const result = [];
31
+ for (const { type, subType } of entries) {
32
+ if (type.startsWith("CAP_OUTBOX")) {
33
+ if (cds.requires[subType]) {
34
+ result.push({ type, subType });
35
+ } else {
36
+ const service = await cds.connect.to(subType).catch(() => {});
37
+ if (service) {
38
+ result.push({ type, subType });
39
+ }
40
+ }
41
+ } else {
42
+ if (eventConfig.getEventConfig(type, subType)) {
43
+ result.push({ type, subType });
44
+ }
45
+ }
46
+ }
47
+ return result;
48
+ };
49
+
50
+ module.exports = {
51
+ getOpenQueueEntries,
52
+ };
@@ -1,32 +1,33 @@
1
1
  "use strict";
2
2
 
3
3
  const { randomUUID } = require("crypto");
4
- const { AsyncResource } = require("async_hooks");
5
4
 
6
5
  const cds = require("@sap/cds");
7
6
 
8
- const eventQueueConfig = require("./config");
9
- const { processEventQueue } = require("./processEventQueue");
10
- const WorkerQueue = require("./shared/WorkerQueue");
11
- const cdsHelper = require("./shared/cdsHelper");
12
- const distributedLock = require("./shared/distributedLock");
13
- const SetIntervalDriftSafe = require("./shared/SetIntervalDriftSafe");
14
- const { getSubdomainForTenantId } = require("./shared/cdsHelper");
15
- const periodicEvents = require("./periodicEvents");
16
- const { hashStringTo32Bit } = require("./shared/common");
17
- const config = require("./config");
18
- const { Priorities } = require("./constants");
7
+ const eventQueueConfig = require("../config");
8
+ const WorkerQueue = require("../shared/WorkerQueue");
9
+ const cdsHelper = require("../shared/cdsHelper");
10
+ const distributedLock = require("../shared/distributedLock");
11
+ const SetIntervalDriftSafe = require("../shared/SetIntervalDriftSafe");
12
+ const { getSubdomainForTenantId } = require("../shared/cdsHelper");
13
+ const periodicEvents = require("../periodicEvents");
14
+ const { hashStringTo32Bit } = require("../shared/common");
15
+ const config = require("../config");
16
+ const redisPub = require("../redis/redisPub");
17
+ const openEvents = require("./openEvents");
18
+ const { runEventCombinationForTenant } = require("./runnerHelper");
19
19
 
20
20
  const COMPONENT_NAME = "/eventQueue/runner";
21
21
  const EVENT_QUEUE_RUN_ID = "EVENT_QUEUE_RUN_ID";
22
22
  const EVENT_QUEUE_RUN_TS = "EVENT_QUEUE_RUN_TS";
23
- const EVENT_QUEUE_RUN_PERIODIC_EVENT = "EVENT_QUEUE_RUN_PERIODIC_EVENT";
23
+ const EVENT_QUEUE_RUN_REDIS_CHECK = "EVENT_QUEUE_RUN_REDIS_CHECK";
24
+ const EVENT_QUEUE_UPDATE_PERIODIC_EVENTS = "EVENT_QUEUE_UPDATE_PERIODIC_EVENTS";
24
25
  const OFFSET_FIRST_RUN = 10 * 1000;
25
26
 
26
27
  let tenantIdHash;
27
28
  let singleRunDone;
28
29
 
29
- const singleTenant = () => _scheduleFunction(_checkPeriodicEventsSingleTenant, _singleTenantDb);
30
+ const singleTenant = () => _scheduleFunction(_checkPeriodicEventsSingleTenantOneTime, _singleTenantDb);
30
31
 
31
32
  const multiTenancyDb = () => _scheduleFunction(async () => {}, _multiTenancyDb);
32
33
 
@@ -69,19 +70,10 @@ const _scheduleFunction = async (singleRunFn, periodicFn) => {
69
70
  const _multiTenancyRedis = async () => {
70
71
  const logger = cds.log(COMPONENT_NAME);
71
72
  try {
72
- const emptyContext = new cds.EventContext({});
73
73
  logger.info("executing event queue run for multi instance and tenant");
74
74
  const tenantIds = await cdsHelper.getAllTenantIds();
75
75
  await _checkPeriodicEventUpdate(tenantIds);
76
-
77
- const runId = await _acquireRunId(emptyContext);
78
-
79
- if (!runId) {
80
- logger.error("could not acquire runId, skip processing events!");
81
- return;
82
- }
83
-
84
- return await _executeEventsAllTenants(tenantIds, runId);
76
+ return await _executeEventsAllTenantsRedis(tenantIds);
85
77
  } catch (err) {
86
78
  logger.info("executing event queue run for multi instance and tenant failed", err);
87
79
  }
@@ -104,80 +96,119 @@ const _checkPeriodicEventUpdate = async (tenantIds) => {
104
96
  }
105
97
  };
106
98
 
107
- const _executeEventsAllTenants = (tenantIds, runId) => {
108
- const events = eventQueueConfig.allEvents;
109
- const product = tenantIds.reduce((result, tenantId) => {
110
- events.forEach((event) => {
111
- result.push([tenantId, event]);
99
+ const _executeEventsAllTenantsRedis = async (tenantIds) => {
100
+ const logger = cds.log(COMPONENT_NAME);
101
+ try {
102
+ // NOTE: do checks for all tenants on the same app instance --> acquire lock tenant independent
103
+ // distribute from this instance to all others
104
+ const dummyContext = new cds.EventContext({});
105
+ const couldAcquireLock = await distributedLock.acquireLock(dummyContext, EVENT_QUEUE_RUN_REDIS_CHECK, {
106
+ expiryTime: eventQueueConfig.runInterval * 0.95,
107
+ tenantScoped: false,
112
108
  });
113
- return result;
114
- }, []);
109
+ if (!couldAcquireLock) {
110
+ return;
111
+ }
115
112
 
116
- return Promise.allSettled(
117
- product.map(async ([tenantId, eventConfig]) => {
118
- const subdomain = await getSubdomainForTenantId(tenantId);
119
- const user = new cds.User.Privileged(config.userId);
120
- const tenantContext = {
121
- tenant: tenantId,
122
- user,
123
- // NOTE: we need this because of logging otherwise logs would not contain the subdomain
124
- http: { req: { authInfo: { getSubdomain: () => subdomain } } },
125
- };
126
- const label = `${eventConfig.type}_${eventConfig.subType}`;
127
- return await WorkerQueue.instance.addToQueue(eventConfig.load, label, eventConfig.priority, async () => {
128
- return await cds.tx(tenantContext, async ({ context }) => {
129
- try {
130
- const lockId = `${runId}_${label}`;
131
- const couldAcquireLock = await distributedLock.acquireLock(context, lockId, {
132
- expiryTime: eventQueueConfig.runInterval * 0.95,
133
- });
134
- if (!couldAcquireLock) {
135
- return;
136
- }
137
- await runEventCombinationForTenant(context, eventConfig.type, eventConfig.subType, true);
138
- } catch (err) {
139
- cds.log(COMPONENT_NAME).error("executing event-queue run for tenant failed", {
140
- tenantId,
141
- });
142
- }
113
+ for (const tenantId of tenantIds) {
114
+ await cds.tx({ tenant: tenantId }, async (tx) => {
115
+ const entries = await openEvents.getOpenQueueEntries(tx);
116
+ logger.info("broadcasting events for run", {
117
+ tenantId,
118
+ entries: entries.length,
119
+ });
120
+ if (!entries.length) {
121
+ return;
122
+ }
123
+ await redisPub.broadcastEvent(tenantId, entries).catch((err) => {
124
+ logger.error("broadcasting event failed", err, {
125
+ tenantId,
126
+ entries: entries.length,
127
+ });
143
128
  });
144
129
  });
145
- })
146
- );
130
+ }
131
+ } catch (err) {
132
+ logger.info("executing event queue run for multi instance and tenant failed", err);
133
+ }
147
134
  };
148
135
 
149
- const _executePeriodicEventsAllTenants = async (tenantIds, runId) => {
150
- return await Promise.allSettled(
151
- tenantIds.map(async (tenantId) => {
152
- const label = `UPDATE_PERIODIC_EVENTS_${tenantId}`;
153
- return await WorkerQueue.instance.addToQueue(1, label, Priorities.Low, async () => {
154
- try {
155
- const subdomain = await getSubdomainForTenantId(tenantId);
156
- const user = new cds.User.Privileged(config.userId);
157
- const tenantContext = {
158
- tenant: tenantId,
159
- user,
160
- // NOTE: we need this because of logging otherwise logs would not contain the subdomain
161
- http: { req: { authInfo: { getSubdomain: () => subdomain } } },
162
- };
136
+ const _executeEventsAllTenants = async (tenantIds, runId) => {
137
+ const promises = [];
138
+
139
+ for (const tenantId of tenantIds) {
140
+ const subdomain = await getSubdomainForTenantId(tenantId);
141
+ const user = new cds.User.Privileged(config.userId);
142
+ const tenantContext = {
143
+ tenant: tenantId,
144
+ user,
145
+ // NOTE: we need this because of logging otherwise logs would not contain the subdomain
146
+ http: { req: { authInfo: { getSubdomain: () => subdomain } } },
147
+ };
148
+ const events = await cds.tx(tenantContext, async (tx) => {
149
+ return await openEvents.getOpenQueueEntries(tx);
150
+ });
151
+
152
+ if (!events.length) {
153
+ continue;
154
+ }
163
155
 
156
+ promises.concat(
157
+ events.map(async (openEvent) => {
158
+ const eventConfig = config.getEventConfig(openEvent.type, openEvent.subType);
159
+ const label = `${eventConfig.type}_${eventConfig.subType}`;
160
+ return await WorkerQueue.instance.addToQueue(eventConfig.load, label, eventConfig.priority, async () => {
164
161
  return await cds.tx(tenantContext, async ({ context }) => {
165
- const couldAcquireLock = await distributedLock.acquireLock(context, runId, {
166
- expiryTime: eventQueueConfig.runInterval * 0.95,
167
- });
168
- if (!couldAcquireLock) {
169
- return;
162
+ try {
163
+ const lockId = `${runId}_${label}`;
164
+ const couldAcquireLock = await distributedLock.acquireLock(context, lockId, {
165
+ expiryTime: eventQueueConfig.runInterval * 0.95,
166
+ });
167
+ if (!couldAcquireLock) {
168
+ return;
169
+ }
170
+ await runEventCombinationForTenant(context, eventConfig.type, eventConfig.subType, true);
171
+ } catch (err) {
172
+ cds.log(COMPONENT_NAME).error("executing event-queue run for tenant failed", {
173
+ tenantId,
174
+ });
170
175
  }
171
- await _checkPeriodicEventsSingleTenant(context);
172
176
  });
173
- } catch (err) {
174
- cds.log(COMPONENT_NAME).error("executing event-queue run for tenant failed", {
175
- tenantId,
177
+ });
178
+ })
179
+ );
180
+ }
181
+ return Promise.allSettled(promises);
182
+ };
183
+
184
+ const _executePeriodicEventsAllTenants = async (tenantIds) => {
185
+ for (const tenantId of tenantIds) {
186
+ try {
187
+ const subdomain = await getSubdomainForTenantId(tenantId);
188
+ const user = new cds.User.Privileged(config.userId);
189
+ const tenantContext = {
190
+ tenant: tenantId,
191
+ user,
192
+ // NOTE: we need this because of logging otherwise logs would not contain the subdomain
193
+ http: { req: { authInfo: { getSubdomain: () => subdomain } } },
194
+ };
195
+ await cds.tx(tenantContext, async ({ context }) => {
196
+ if (!config.redisEnabled) {
197
+ const couldAcquireLock = await distributedLock.acquireLock(context, EVENT_QUEUE_UPDATE_PERIODIC_EVENTS, {
198
+ expiryTime: eventQueueConfig.runInterval * 0.95,
176
199
  });
200
+ if (!couldAcquireLock) {
201
+ return;
202
+ }
177
203
  }
204
+ await _checkPeriodicEventsSingleTenant(context);
178
205
  });
179
- })
180
- );
206
+ } catch (err) {
207
+ cds.log(COMPONENT_NAME).error("executing event-queue run for tenant failed", {
208
+ tenantId,
209
+ });
210
+ }
211
+ }
181
212
  };
182
213
 
183
214
  const _singleTenantDb = async (tenantId) => {
@@ -269,30 +300,6 @@ const _calculateOffsetForFirstRun = async () => {
269
300
  return offsetDependingOnLastRun;
270
301
  };
271
302
 
272
- const runEventCombinationForTenant = async (context, type, subType, skipWorkerPool) => {
273
- try {
274
- if (skipWorkerPool) {
275
- return await processEventQueue(context, type, subType);
276
- } else {
277
- const eventConfig = eventQueueConfig.getEventConfig(type, subType);
278
- const label = `${type}_${subType}`;
279
- return await WorkerQueue.instance.addToQueue(
280
- eventConfig.load,
281
- label,
282
- eventConfig.priority,
283
- AsyncResource.bind(async () => await processEventQueue(context, type, subType))
284
- );
285
- }
286
- } catch (err) {
287
- const logger = cds.log(COMPONENT_NAME);
288
- logger.error("error executing event combination for tenant", err, {
289
- tenantId: context.tenant,
290
- type,
291
- subType,
292
- });
293
- }
294
- };
295
-
296
303
  const _multiTenancyDb = async () => {
297
304
  const logger = cds.log(COMPONENT_NAME);
298
305
  try {
@@ -309,14 +316,29 @@ const _multiTenancyPeriodicEvents = async (tenantIds) => {
309
316
  const logger = cds.log(COMPONENT_NAME);
310
317
  try {
311
318
  logger.info("executing event queue update periodic events");
319
+
320
+ if (config.redisEnabled) {
321
+ const dummyContext = new cds.EventContext({});
322
+ const couldAcquireLock = await distributedLock.acquireLock(dummyContext, EVENT_QUEUE_UPDATE_PERIODIC_EVENTS, {
323
+ expiryTime: 60 * 1000, // short living lock --> assume we do not have 2 onboards within 1 minute
324
+ tenantScoped: false,
325
+ });
326
+ if (!couldAcquireLock) {
327
+ return;
328
+ }
329
+ }
330
+
312
331
  tenantIds = tenantIds ?? (await cdsHelper.getAllTenantIds());
313
- return await _executePeriodicEventsAllTenants(tenantIds, EVENT_QUEUE_RUN_PERIODIC_EVENT);
332
+ return await _executePeriodicEventsAllTenants(tenantIds);
314
333
  } catch (err) {
315
334
  logger.error("Couldn't fetch tenant ids for updating periodic event processing!", err);
316
335
  }
317
336
  };
318
337
 
319
- const _checkPeriodicEventsSingleTenant = async (context = {}) => {
338
+ const _checkPeriodicEventsSingleTenantOneTime = () =>
339
+ cds.tx({}, async (tx) => await periodicEvents.checkAndInsertPeriodicEvents(tx.context));
340
+
341
+ const _checkPeriodicEventsSingleTenant = async (context) => {
320
342
  const logger = cds.log(COMPONENT_NAME);
321
343
  if (!eventQueueConfig.updatePeriodicEvents || !eventQueueConfig.periodicEvents.length) {
322
344
  logger.info("updating of periodic events is disabled or no periodic events configured", {
@@ -330,9 +352,7 @@ const _checkPeriodicEventsSingleTenant = async (context = {}) => {
330
352
  tenantId: context.tenant,
331
353
  subdomain: context.http?.req.authInfo.getSubdomain(),
332
354
  });
333
- await cdsHelper.executeInNewTransaction(context, "update-periodic-events", async (tx) => {
334
- await periodicEvents.checkAndInsertPeriodicEvents(tx.context);
335
- });
355
+ await periodicEvents.checkAndInsertPeriodicEvents(context);
336
356
  } catch (err) {
337
357
  logger.error("Couldn't update periodic events for tenant! Next try after defined interval.", err, {
338
358
  tenantId: context.tenant,
@@ -345,7 +365,6 @@ module.exports = {
345
365
  singleTenant,
346
366
  multiTenancyDb,
347
367
  multiTenancyRedis,
348
- runEventCombinationForTenant,
349
368
  __: {
350
369
  _singleTenantDb,
351
370
  _multiTenancyRedis,
@@ -0,0 +1,37 @@
1
+ "use strict";
2
+
3
+ const { AsyncResource } = require("async_hooks");
4
+
5
+ const cds = require("@sap/cds");
6
+
7
+ const { processEventQueue } = require("../processEventQueue");
8
+ const eventQueueConfig = require("../config");
9
+ const WorkerQueue = require("../shared/WorkerQueue");
10
+
11
+ const COMPONENT_NAME = "/eventQueue/runnerHelper";
12
+
13
+ const runEventCombinationForTenant = async (context, type, subType, skipWorkerPool) => {
14
+ try {
15
+ if (skipWorkerPool) {
16
+ return await processEventQueue(context, type, subType);
17
+ } else {
18
+ const eventConfig = eventQueueConfig.getEventConfig(type, subType);
19
+ const label = `${type}_${subType}`;
20
+ return await WorkerQueue.instance.addToQueue(
21
+ eventConfig.load,
22
+ label,
23
+ eventConfig.priority,
24
+ AsyncResource.bind(async () => await processEventQueue(context, type, subType))
25
+ );
26
+ }
27
+ } catch (err) {
28
+ const logger = cds.log(COMPONENT_NAME);
29
+ logger.error("error executing event combination for tenant", err, {
30
+ tenantId: context.tenant,
31
+ type,
32
+ subType,
33
+ });
34
+ }
35
+ };
36
+
37
+ module.exports = { runEventCombinationForTenant };
@@ -129,6 +129,15 @@ const getAllTenantIds = async () => {
129
129
  if (!config.isMultiTenancy) {
130
130
  return null;
131
131
  }
132
+
133
+ // NOTE: tmp workaround until cds-mtxs fixes the connect.to service
134
+ for (let i = 0; i < 10; i++) {
135
+ if (cds.services["saas-registry"]) {
136
+ break;
137
+ }
138
+ await new Promise((resolve) => setTimeout(resolve, 1000));
139
+ }
140
+
132
141
  const ssp = await cds.connect.to("cds.xt.SaasProvisioningService");
133
142
  const response = await ssp.get("/tenant");
134
143
  return response
@@ -54,7 +54,7 @@ const checkLockExistsAndReturnValue = async (context, key, { tenantScoped = true
54
54
  };
55
55
 
56
56
  const _acquireLockRedis = async (context, fullKey, expiryTime, { value = "true", overrideValue = false } = {}) => {
57
- const client = await redis.createMainClientAndConnect();
57
+ const client = await redis.createMainClientAndConnect(config.redisOptions);
58
58
  const result = await client.set(fullKey, value, {
59
59
  PX: expiryTime,
60
60
  ...(overrideValue ? null : { NX: true }),
@@ -63,7 +63,7 @@ const _acquireLockRedis = async (context, fullKey, expiryTime, { value = "true",
63
63
  };
64
64
 
65
65
  const _checkLockExistsRedis = async (context, fullKey) => {
66
- const client = await redis.createMainClientAndConnect();
66
+ const client = await redis.createMainClientAndConnect(config.redisOptions);
67
67
  return await client.get(fullKey);
68
68
  };
69
69
 
@@ -76,7 +76,7 @@ const _checkLockExistsDb = async (context, fullKey) => {
76
76
  };
77
77
 
78
78
  const _releaseLockRedis = async (context, fullKey) => {
79
- const client = await redis.createMainClientAndConnect();
79
+ const client = await redis.createMainClientAndConnect(config.redisOptions);
80
80
  await client.del(fullKey);
81
81
  };
82
82
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  const cds = require("@sap/cds");
4
4
 
5
- const { broadcastEvent } = require("../redisPubSub");
5
+ const redisPub = require("../redis/redisPub");
6
6
  const config = require("./../config");
7
7
 
8
8
  const COMPONENT_NAME = "/eventQueue/shared/eventScheduler";
@@ -26,7 +26,7 @@ class EventScheduler {
26
26
  });
27
27
  setTimeout(() => {
28
28
  delete this.#scheduledEvents[key];
29
- broadcastEvent(tenantId, type, subType).catch((err) => {
29
+ redisPub.broadcastEvent(tenantId, { type, subType }).catch((err) => {
30
30
  cds.log(COMPONENT_NAME).error("could not execute scheduled event", err, {
31
31
  tenantId,
32
32
  type,
@@ -12,7 +12,7 @@ let mainClientPromise;
12
12
  const subscriberChannelClientPromise = {};
13
13
  let lastErrorLog = Date.now();
14
14
 
15
- const createMainClientAndConnect = () => {
15
+ const createMainClientAndConnect = (options) => {
16
16
  if (mainClientPromise) {
17
17
  return mainClientPromise;
18
18
  }
@@ -20,14 +20,14 @@ const createMainClientAndConnect = () => {
20
20
  const errorHandlerCreateClient = (err) => {
21
21
  cds.log(COMPONENT_NAME).error("error from redis client for pub/sub failed", err);
22
22
  mainClientPromise = null;
23
- setTimeout(createMainClientAndConnect, LOG_AFTER_SEC * 1000).unref();
23
+ setTimeout(() => createMainClientAndConnect(options), LOG_AFTER_SEC * 1000).unref();
24
24
  };
25
25
 
26
- mainClientPromise = createClientAndConnect(errorHandlerCreateClient);
26
+ mainClientPromise = createClientAndConnect(options, errorHandlerCreateClient);
27
27
  return mainClientPromise;
28
28
  };
29
29
 
30
- const _createClientBase = () => {
30
+ const _createClientBase = (redisOptions) => {
31
31
  const env = getEnvInstance();
32
32
  try {
33
33
  const credentials = env.getRedisCredentialsFromEnv();
@@ -40,18 +40,19 @@ const _createClientBase = () => {
40
40
  defaults: {
41
41
  password: credentials.password,
42
42
  socket: { tls: credentials.tls },
43
+ ...redisOptions,
43
44
  },
44
45
  });
45
46
  }
46
- return redis.createClient({ url });
47
+ return redis.createClient({ url, ...redisOptions });
47
48
  } catch (err) {
48
49
  throw EventQueueError.redisConnectionFailure(err);
49
50
  }
50
51
  };
51
52
 
52
- const createClientAndConnect = async (errorHandlerCreateClient) => {
53
+ const createClientAndConnect = async (options, errorHandlerCreateClient) => {
53
54
  try {
54
- const client = _createClientBase();
55
+ const client = _createClientBase(options);
55
56
  await client.connect();
56
57
  client.on("error", (err) => {
57
58
  const dateNow = Date.now();
@@ -74,14 +75,14 @@ const createClientAndConnect = async (errorHandlerCreateClient) => {
74
75
  }
75
76
  };
76
77
 
77
- const subscribeRedisChannel = (channel, subscribeHandler) => {
78
+ const subscribeRedisChannel = (options, channel, subscribeHandler) => {
78
79
  const errorHandlerCreateClient = (err) => {
79
80
  cds.log(COMPONENT_NAME).error(`error from redis client for pub/sub failed for channel ${channel}`, err);
80
81
  subscriberChannelClientPromise[channel] = null;
81
- setTimeout(() => subscribeRedisChannel(channel, subscribeHandler), LOG_AFTER_SEC * 1000).unref();
82
+ setTimeout(() => subscribeRedisChannel(options, channel, subscribeHandler), LOG_AFTER_SEC * 1000).unref();
82
83
  };
83
84
 
84
- subscriberChannelClientPromise[channel] = createClientAndConnect(errorHandlerCreateClient)
85
+ subscriberChannelClientPromise[channel] = createClientAndConnect(options, errorHandlerCreateClient)
85
86
  .then((client) => {
86
87
  cds.log(COMPONENT_NAME).info("subscribe redis client connected channel", { channel });
87
88
  client.subscribe(channel, subscribeHandler).catch(errorHandlerCreateClient);
@@ -93,8 +94,8 @@ const subscribeRedisChannel = (channel, subscribeHandler) => {
93
94
  });
94
95
  };
95
96
 
96
- const publishMessage = async (channel, message) => {
97
- const client = await createMainClientAndConnect();
97
+ const publishMessage = async (options, channel, message) => {
98
+ const client = await createMainClientAndConnect(options);
98
99
  return await client.publish(channel, message);
99
100
  };
100
101
 
@@ -116,9 +117,9 @@ const _resilientClientClose = async (client) => {
116
117
  }
117
118
  };
118
119
 
119
- const connectionCheck = async () => {
120
+ const connectionCheck = async (options) => {
120
121
  return new Promise((resolve, reject) => {
121
- createClientAndConnect(reject)
122
+ createClientAndConnect(options, reject)
122
123
  .then((client) => {
123
124
  if (client) {
124
125
  _resilientClientClose(client);
@@ -1,170 +0,0 @@
1
- "use strict";
2
-
3
- const { promisify } = require("util");
4
-
5
- const cds = require("@sap/cds");
6
-
7
- const redis = require("./shared/redis");
8
- const { checkLockExistsAndReturnValue } = require("./shared/distributedLock");
9
- const config = require("./config");
10
- const runner = require("./runner");
11
- const { getSubdomainForTenantId } = require("./shared/cdsHelper");
12
-
13
- const EVENT_MESSAGE_CHANNEL = "EVENT_QUEUE_MESSAGE_CHANNEL";
14
- const COMPONENT_NAME = "/eventQueue/redisPubSub";
15
- const TRIES_FOR_PUBLISH_PERIODIC_EVENT = 10;
16
- const SLEEP_TIME_FOR_PUBLISH_PERIODIC_EVENT = 30 * 1000;
17
-
18
- const wait = promisify(setTimeout);
19
- let subscriberClientPromise;
20
-
21
- const initEventQueueRedisSubscribe = () => {
22
- if (subscriberClientPromise || !config.redisEnabled) {
23
- return;
24
- }
25
- redis.subscribeRedisChannel(EVENT_MESSAGE_CHANNEL, _messageHandlerProcessEvents);
26
- };
27
-
28
- const _messageHandlerProcessEvents = async (messageData) => {
29
- const logger = cds.log(COMPONENT_NAME);
30
- try {
31
- const { tenantId, type, subType } = JSON.parse(messageData);
32
- logger.debug("received redis event", {
33
- tenantId,
34
- type,
35
- subType,
36
- });
37
- if (!config.isEventQueueActive) {
38
- cds.log(COMPONENT_NAME).info("Skipping processing because runner is deactivated!", {
39
- type,
40
- subType,
41
- });
42
- return;
43
- }
44
-
45
- const subdomain = await getSubdomainForTenantId(tenantId);
46
- const user = new cds.User.Privileged(config.userId);
47
- const tenantContext = {
48
- tenant: tenantId,
49
- user,
50
- // NOTE: we need this because of logging otherwise logs would not contain the subdomain
51
- http: { req: { authInfo: { getSubdomain: () => subdomain } } },
52
- };
53
-
54
- if (!config.getEventConfig(type, subType)) {
55
- if (config.isCapOutboxEvent(type)) {
56
- try {
57
- const service = await cds.connect.to(subType);
58
- cds.outboxed(service);
59
- } catch (err) {
60
- logger.error("could not connect to outboxed service", err, {
61
- type,
62
- subType,
63
- });
64
- return;
65
- }
66
- } else {
67
- logger.error("cannot find configuration for published event. Event won't be processed", {
68
- type,
69
- subType,
70
- });
71
- return;
72
- }
73
- }
74
-
75
- return await cds.tx(tenantContext, async ({ context }) => {
76
- return await runner.runEventCombinationForTenant(context, type, subType);
77
- });
78
- } catch (err) {
79
- logger.error("could not parse event information", {
80
- messageData,
81
- });
82
- }
83
- };
84
-
85
- const broadcastEvent = async (tenantId, type, subType) => {
86
- const logger = cds.log(COMPONENT_NAME);
87
- try {
88
- if (!config.isEventQueueActive) {
89
- cds.log(COMPONENT_NAME).info("Skipping processing because runner is deactivated!", {
90
- type,
91
- subType,
92
- });
93
- return;
94
- }
95
- if (!config.redisEnabled) {
96
- if (config.registerAsEventProcessor) {
97
- let context = {};
98
- if (tenantId) {
99
- const subdomain = await getSubdomainForTenantId(tenantId);
100
- const user = new cds.User.Privileged(config.userId);
101
- context = {
102
- // NOTE: we need this because of logging otherwise logs would not contain the subdomain
103
- tenant: tenantId,
104
- user,
105
- http: { req: { authInfo: { getSubdomain: () => subdomain } } },
106
- };
107
- }
108
-
109
- return await cds.tx(context, async ({ context }) => {
110
- return await runner.runEventCombinationForTenant(context, type, subType);
111
- });
112
- }
113
- return;
114
- }
115
- const eventConfig = config.getEventConfig(type, subType);
116
- for (let i = 0; i < TRIES_FOR_PUBLISH_PERIODIC_EVENT; i++) {
117
- const result = await checkLockExistsAndReturnValue(
118
- new cds.EventContext({ tenant: tenantId }),
119
- [type, subType].join("##")
120
- );
121
- if (result) {
122
- logger.debug("skip publish redis event as no lock is available", {
123
- type,
124
- subType,
125
- index: i,
126
- isPeriodic: eventConfig.isPeriodic,
127
- waitInterval: SLEEP_TIME_FOR_PUBLISH_PERIODIC_EVENT,
128
- });
129
- if (!eventConfig.isPeriodic) {
130
- break;
131
- }
132
- await wait(SLEEP_TIME_FOR_PUBLISH_PERIODIC_EVENT);
133
- continue;
134
- }
135
- logger.debug("publishing redis event", {
136
- tenantId,
137
- type,
138
- subType,
139
- });
140
- await redis.publishMessage(EVENT_MESSAGE_CHANNEL, JSON.stringify({ tenantId, type, subType }));
141
- break;
142
- }
143
- } catch (err) {
144
- logger.error("publish event failed!", err, {
145
- tenantId,
146
- type,
147
- subType,
148
- });
149
- }
150
- };
151
-
152
- const closeSubscribeClient = async () => {
153
- try {
154
- const client = await subscriberClientPromise;
155
- if (client?.quit) {
156
- await client.quit();
157
- }
158
- } catch (err) {
159
- // ignore errors during shutdown
160
- }
161
- };
162
-
163
- module.exports = {
164
- initEventQueueRedisSubscribe,
165
- broadcastEvent,
166
- closeSubscribeClient,
167
- __: {
168
- _messageHandlerProcessEvents,
169
- },
170
- };