@cap-js-community/event-queue 1.3.3 → 1.3.5

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.3",
3
+ "version": "1.3.5",
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": [
@@ -44,7 +44,7 @@
44
44
  "dependencies": {
45
45
  "redis": "4.6.13",
46
46
  "verror": "1.10.1",
47
- "yaml": "2.4.0"
47
+ "yaml": "2.4.1"
48
48
  },
49
49
  "devDependencies": {
50
50
  "@cap-js/hana": "^0.0.6",
@@ -67,6 +67,16 @@
67
67
  "url": "https://github.com/cap-js-community/event-queue.git"
68
68
  },
69
69
  "cds": {
70
+ "eventQueue": {
71
+ "[production]": {
72
+ "disableRedis": false
73
+ },
74
+ "[test]": {
75
+ "registerAsEventProcessor": false,
76
+ "isRunnerDeactivated": true,
77
+ "updatePeriodicEvents": false
78
+ }
79
+ },
70
80
  "requires": {
71
81
  "event-queue": {
72
82
  "model": "@cap-js-community/event-queue"
@@ -10,6 +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
14
 
14
15
  const IMPLEMENT_ERROR_MESSAGE = "needs to be reimplemented";
15
16
  const COMPONENT_NAME = "/eventQueue/EventQueueProcessorBase";
@@ -1002,11 +1003,11 @@ class EventQueueProcessorBase {
1002
1003
  }
1003
1004
 
1004
1005
  async scheduleNextPeriodEvent(queueEntry) {
1005
- const intervalInSec = this.#eventConfig.interval * 1000;
1006
+ const intervalInMs = this.#eventConfig.interval * 1000;
1006
1007
  const newEvent = {
1007
1008
  type: this.#eventType,
1008
1009
  subType: this.#eventSubType,
1009
- startAfter: new Date(new Date(queueEntry.startAfter).getTime() + intervalInSec),
1010
+ startAfter: new Date(new Date(queueEntry.startAfter).getTime() + intervalInMs),
1010
1011
  };
1011
1012
  const { relative } = this.#eventSchedulerInstance.calculateOffset(
1012
1013
  this.#eventType,
@@ -1015,11 +1016,13 @@ class EventQueueProcessorBase {
1015
1016
  );
1016
1017
 
1017
1018
  // more than one interval behind - shift tick to keep up
1018
- if (relative < 0 && Math.abs(relative) >= intervalInSec) {
1019
+ if (relative < 0 && Math.abs(relative) >= intervalInMs) {
1020
+ const plannedStartAfter = newEvent.startAfter;
1019
1021
  newEvent.startAfter = new Date(Date.now() + 5 * 1000);
1020
1022
  this.logger.info("interval adjusted because shifted more than one interval", {
1021
1023
  eventType: this.#eventType,
1022
1024
  eventSubType: this.#eventSubType,
1025
+ plannedStartAfter,
1023
1026
  newStartAfter: newEvent.startAfter,
1024
1027
  });
1025
1028
  }
@@ -1027,7 +1030,7 @@ class EventQueueProcessorBase {
1027
1030
  this.tx._skipEventQueueBroadcase = true;
1028
1031
  await this.tx.run(INSERT.into(this.#config.tableNameEventQueue).entries({ ...newEvent }));
1029
1032
  this.tx._skipEventQueueBroadcase = false;
1030
- if (intervalInSec < this.#config.runInterval * 1.5) {
1033
+ if (intervalInMs < this.#config.runInterval * 1.5) {
1031
1034
  this.#handleDelayedEvents([newEvent]);
1032
1035
  const { relative: relativeAfterSchedule } = this.#eventSchedulerInstance.calculateOffset(
1033
1036
  this.#eventType,
@@ -1108,6 +1111,18 @@ class EventQueueProcessorBase {
1108
1111
  this.__processTx = null;
1109
1112
  }
1110
1113
 
1114
+ broadCastEvent() {
1115
+ setTimeout(() => {
1116
+ broadcastEvent(this.__baseContext.tenant, this.#eventType, this.#eventSubType).catch((err) => {
1117
+ this.logger.error("could not execute scheduled event", err, {
1118
+ tenantId: this.__baseContext.tenant,
1119
+ type: this.#eventType,
1120
+ subType: this.#eventSubType,
1121
+ });
1122
+ });
1123
+ }, 1000).unref();
1124
+ }
1125
+
1111
1126
  get logger() {
1112
1127
  return this.__logger ?? this.__baseLogger;
1113
1128
  }
package/src/config.js CHANGED
@@ -99,7 +99,8 @@ class Config {
99
99
  }
100
100
 
101
101
  checkRedisEnabled() {
102
- this.#redisEnabled = !this.#disableRedis && this._checkRedisIsBound() && this.#env.isOnCF;
102
+ this.#redisEnabled = !this.#disableRedis && this._checkRedisIsBound();
103
+ return this.#redisEnabled;
103
104
  }
104
105
 
105
106
  attachConfigChangeHandler() {
package/src/initialize.js CHANGED
@@ -12,7 +12,7 @@ const runner = require("./runner");
12
12
  const dbHandler = require("./dbHandler");
13
13
  const config = require("./config");
14
14
  const { initEventQueueRedisSubscribe, closeSubscribeClient } = require("./redisPubSub");
15
- const { closeMainClient } = require("./shared/redis");
15
+ const redis = require("./shared/redis");
16
16
  const eventQueueAsOutbox = require("./outbox/eventQueueAsOutbox");
17
17
  const { getAllTenantIds } = require("./shared/cdsHelper");
18
18
  const { EventProcessingStatus } = require("./constants");
@@ -33,7 +33,7 @@ const CONFIG_VARS = [
33
33
  ["runInterval", 25 * 60 * 1000],
34
34
  ["tableNameEventQueue", BASE_TABLES.EVENT],
35
35
  ["tableNameEventLock", BASE_TABLES.LOCK],
36
- ["disableRedis", false],
36
+ ["disableRedis", true],
37
37
  ["skipCsnCheck", false],
38
38
  ["updatePeriodicEvents", true],
39
39
  ["thresholdLoggingEventProcessing", 50],
@@ -84,14 +84,19 @@ const initialize = async ({
84
84
  );
85
85
 
86
86
  const logger = cds.log(COMPONENT);
87
- config.checkRedisEnabled();
87
+ const redisEnabled = config.checkRedisEnabled();
88
+ let resolveFn;
89
+ let initFinished = new Promise((resolve) => (resolveFn = resolve));
88
90
  cds.on("connect", (service) => {
89
91
  if (service.name === "db") {
90
92
  config.processEventsAfterPublish && dbHandler.registerEventQueueDbHandler(service);
91
93
  config.cleanupLocksAndEventsForDev && registerCleanupForDevDb().catch(() => {});
92
- registerEventProcessors();
94
+ initFinished.then(registerEventProcessors);
93
95
  }
94
96
  });
97
+ if (redisEnabled) {
98
+ config.redisEnabled = await redis.connectionCheck();
99
+ }
95
100
  config.fileContent = await readConfigFromFile(config.configFilePath);
96
101
 
97
102
  !config.skipCsnCheck && (await csnCheck());
@@ -105,6 +110,7 @@ const initialize = async ({
105
110
  redisEnabled: config.redisEnabled,
106
111
  runInterval: config.runInterval,
107
112
  });
113
+ resolveFn();
108
114
  };
109
115
 
110
116
  const readConfigFromFile = async (configFilepath) => {
@@ -219,7 +225,7 @@ const mixConfigVarsWithEnv = (...args) => {
219
225
 
220
226
  const registerCdsShutdown = () => {
221
227
  cds.on("shutdown", async () => {
222
- await Promise.allSettled([closeMainClient(), closeSubscribeClient()]);
228
+ await Promise.allSettled([redis.closeMainClient(), closeSubscribeClient()]);
223
229
  });
224
230
  };
225
231
 
@@ -117,6 +117,7 @@ const reevaluateShouldContinue = (eventTypeInstance, iterationCounter, startTime
117
117
  if (new Date(startTime.getTime() + config.runInterval) > new Date()) {
118
118
  return true;
119
119
  }
120
+
120
121
  eventTypeInstance.logTimeExceeded(iterationCounter);
121
122
  return false;
122
123
  };
@@ -1,5 +1,7 @@
1
1
  "use strict";
2
2
 
3
+ const { promisify } = require("util");
4
+
3
5
  const cds = require("@sap/cds");
4
6
 
5
7
  const redis = require("./shared/redis");
@@ -10,7 +12,10 @@ const { getSubdomainForTenantId } = require("./shared/cdsHelper");
10
12
 
11
13
  const EVENT_MESSAGE_CHANNEL = "EVENT_QUEUE_MESSAGE_CHANNEL";
12
14
  const COMPONENT_NAME = "/eventQueue/redisPubSub";
15
+ const TRIES_FOR_PUBLISH_PERIODIC_EVENT = 10;
16
+ const SLEEP_TIME_FOR_PUBLISH_PERIODIC_EVENT = 30 * 1000;
13
17
 
18
+ const wait = promisify(setTimeout);
14
19
  let subscriberClientPromise;
15
20
 
16
21
  const initEventQueueRedisSubscribe = () => {
@@ -107,23 +112,34 @@ const broadcastEvent = async (tenantId, type, subType) => {
107
112
  }
108
113
  return;
109
114
  }
110
- const result = await checkLockExistsAndReturnValue(
111
- new cds.EventContext({ tenant: tenantId }),
112
- [type, subType].join("##")
113
- );
114
- if (result) {
115
- logger.debug("skip publish redis event as no lock is available", {
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,
116
137
  type,
117
138
  subType,
118
139
  });
119
- return;
140
+ await redis.publishMessage(EVENT_MESSAGE_CHANNEL, JSON.stringify({ tenantId, type, subType }));
141
+ break;
120
142
  }
121
- logger.debug("publishing redis event", {
122
- tenantId,
123
- type,
124
- subType,
125
- });
126
- await redis.publishMessage(EVENT_MESSAGE_CHANNEL, JSON.stringify({ tenantId, type, subType }));
127
143
  } catch (err) {
128
144
  logger.error("publish event failed!", err, {
129
145
  tenantId,
package/src/shared/env.js CHANGED
@@ -4,12 +4,9 @@ let instance;
4
4
 
5
5
  class Env {
6
6
  #isLocal;
7
- #isOnCF;
8
7
  #vcapServices;
9
8
 
10
9
  constructor() {
11
- this.#isLocal = process.env.USER !== "vcap";
12
- this.#isOnCF = !this.#isLocal;
13
10
  try {
14
11
  this.#vcapServices = JSON.parse(process.env.VCAP_SERVICES);
15
12
  } catch {
@@ -28,13 +25,6 @@ class Env {
28
25
  return this.#isLocal;
29
26
  }
30
27
 
31
- set isOnCF(value) {
32
- this.#isOnCF = value;
33
- }
34
- get isOnCF() {
35
- return this.#isOnCF;
36
- }
37
-
38
28
  set vcapServices(value) {
39
29
  this.#vcapServices = value;
40
30
  }
@@ -26,29 +26,23 @@ const createMainClientAndConnect = () => {
26
26
 
27
27
  const _createClientBase = () => {
28
28
  const env = getEnvInstance();
29
- if (env.isOnCF) {
30
- try {
31
- const credentials = env.getRedisCredentialsFromEnv();
32
- const redisIsCluster = credentials.cluster_mode;
33
- const url = credentials.uri.replace(/(?<=rediss:\/\/)[\w-]+?(?=:)/, "");
34
- if (redisIsCluster) {
35
- return redis.createCluster({
36
- rootNodes: [{ url }],
37
- // https://github.com/redis/node-redis/issues/1782
38
- defaults: {
39
- password: credentials.password,
40
- socket: { tls: credentials.tls },
41
- },
42
- });
43
- }
44
- return redis.createClient({ url });
45
- } catch (err) {
46
- throw EventQueueError.redisConnectionFailure(err);
29
+ try {
30
+ const credentials = env.getRedisCredentialsFromEnv();
31
+ const redisIsCluster = credentials.cluster_mode;
32
+ const url = credentials.uri.replace(/(?<=rediss:\/\/)[\w-]+?(?=:)/, "");
33
+ if (redisIsCluster) {
34
+ return redis.createCluster({
35
+ rootNodes: [{ url }],
36
+ // https://github.com/redis/node-redis/issues/1782
37
+ defaults: {
38
+ password: credentials.password,
39
+ socket: { tls: credentials.tls },
40
+ },
41
+ });
47
42
  }
48
- } else {
49
- return redis.createClient({
50
- socket: { reconnectStrategy: _localReconnectStrategy },
51
- });
43
+ return redis.createClient({ url });
44
+ } catch (err) {
45
+ throw EventQueueError.redisConnectionFailure(err);
52
46
  }
53
47
  };
54
48
 
@@ -93,11 +87,16 @@ const publishMessage = async (channel, message) => {
93
87
  return await client.publish(channel, message);
94
88
  };
95
89
 
96
- const _localReconnectStrategy = () => EventQueueError.redisNoReconnect();
97
-
98
90
  const closeMainClient = async () => {
99
91
  try {
100
- const client = await mainClientPromise;
92
+ await _resilientClientClose(await mainClientPromise);
93
+ } catch (err) {
94
+ // ignore errors during shutdown
95
+ }
96
+ };
97
+
98
+ const _resilientClientClose = async (client) => {
99
+ try {
101
100
  if (client?.quit) {
102
101
  await client.quit();
103
102
  }
@@ -106,10 +105,28 @@ const closeMainClient = async () => {
106
105
  }
107
106
  };
108
107
 
108
+ const connectionCheck = async () => {
109
+ return new Promise((resolve, reject) => {
110
+ createClientAndConnect(reject)
111
+ .then((client) => {
112
+ if (client) {
113
+ _resilientClientClose(client);
114
+ resolve();
115
+ } else {
116
+ reject(new Error());
117
+ }
118
+ })
119
+ .catch(reject);
120
+ })
121
+ .then(() => true)
122
+ .catch(() => false);
123
+ };
124
+
109
125
  module.exports = {
110
126
  createClientAndConnect,
111
127
  createMainClientAndConnect,
112
128
  subscribeRedisChannel,
113
129
  publishMessage,
114
130
  closeMainClient,
131
+ connectionCheck,
115
132
  };