@cap-js-community/event-queue 1.3.2 → 1.3.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.3.2",
3
+ "version": "1.3.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
  "files": [
@@ -44,12 +44,12 @@
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
- "@cap-js/hana": "^0.0.5",
50
+ "@cap-js/hana": "^0.0.6",
51
51
  "@cap-js/sqlite": "^1.5.0",
52
- "@sap/cds": "^7.5.3",
52
+ "@sap/cds": "^7.7.0",
53
53
  "@sap/cds-dk": "^7.5.1",
54
54
  "eslint": "^8.56.0",
55
55
  "eslint-config-prettier": "^9.1.0",
@@ -67,6 +67,11 @@
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
+ },
70
75
  "requires": {
71
76
  "event-queue": {
72
77
  "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
@@ -28,7 +28,7 @@ const BASE_PERIODIC_EVENTS = [
28
28
  subType: "DELETE_EVENTS",
29
29
  priority: Priorities.Low,
30
30
  impl: "./housekeeping/EventQueueDeleteEvents",
31
- load: 1,
31
+ load: 20,
32
32
  interval: 86400, // 1 day,
33
33
  internalEvent: true,
34
34
  },
@@ -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");
@@ -30,10 +30,10 @@ const CONFIG_VARS = [
30
30
  ["registerAsEventProcessor", true],
31
31
  ["processEventsAfterPublish", true],
32
32
  ["isEventQueueActive", true],
33
- ["runInterval", 5 * 60 * 1000],
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
 
@@ -11,7 +11,6 @@ const { limiter } = require("./shared/common");
11
11
  const { executeInNewTransaction, TriggerRollback } = require("./shared/cdsHelper");
12
12
 
13
13
  const COMPONENT_NAME = "/eventQueue/processEventQueue";
14
- const MAX_EXECUTION_TIME = 5 * 60 * 1000;
15
14
 
16
15
  const processEventQueue = async (context, eventType, eventSubType, startTime = new Date()) => {
17
16
  let iterationCounter = 0;
@@ -115,9 +114,10 @@ const reevaluateShouldContinue = (eventTypeInstance, iterationCounter, startTime
115
114
  if (eventTypeInstance.emptyChunkSelected) {
116
115
  return false; // the last selected chunk was empty - no more data for processing
117
116
  }
118
- if (new Date(startTime.getTime() + MAX_EXECUTION_TIME) > new Date()) {
117
+ if (new Date(startTime.getTime() + config.runInterval) > new Date()) {
119
118
  return true;
120
119
  }
120
+
121
121
  eventTypeInstance.logTimeExceeded(iterationCounter);
122
122
  return false;
123
123
  };
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
  };