@cap-js-community/event-queue 1.8.1 → 1.8.3

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.1",
3
+ "version": "1.8.3",
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",
@@ -75,13 +75,19 @@
75
75
  "disableRedis": false
76
76
  },
77
77
  "[test]": {
78
- "isEventQueueActive": true,
78
+ "isEventQueueActive": false,
79
79
  "registerAsEventProcessor": false,
80
80
  "updatePeriodicEvents": false,
81
81
  "insertEventsBeforeCommit": false
82
82
  }
83
83
  },
84
84
  "requires": {
85
+ "redis-eventQueue": {
86
+ "options": {},
87
+ "vcap": {
88
+ "label": "redis-cache"
89
+ }
90
+ },
85
91
  "event-queue": {
86
92
  "model": "@cap-js-community/event-queue"
87
93
  }
package/src/config.js CHANGED
@@ -113,7 +113,7 @@ class Config {
113
113
  }
114
114
 
115
115
  _checkRedisIsBound() {
116
- return !!this.#env.redisCredentialsFromEnv;
116
+ return !!this.#env.redisRequires?.credentials;
117
117
  }
118
118
 
119
119
  shouldBeProcessedInThisApplication(type, subType) {
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],
@@ -148,7 +149,7 @@ const registerEventProcessors = () => {
148
149
  const errorHandler = (err) => cds.log(COMPONENT).error("error during init runner", err);
149
150
 
150
151
  if (config.redisEnabled) {
151
- initEventQueueRedisSubscribe();
152
+ redisSub.initEventQueueRedisSubscribe();
152
153
  config.attachConfigChangeHandler();
153
154
  if (config.isMultiTenancy) {
154
155
  runner.multiTenancyRedis().catch(errorHandler);
@@ -186,9 +187,24 @@ const mixConfigVarsWithEnv = (options) => {
186
187
  };
187
188
 
188
189
  const registerCdsShutdown = () => {
190
+ const isTestProfile = cds.env.profiles.find((profile) => profile.includes("test"));
191
+ if (isTestProfile) {
192
+ return;
193
+ }
189
194
  cds.on("shutdown", async () => {
190
- await distributedLock.shutdownHandler();
191
- await Promise.allSettled([redis.closeMainClient(), closeSubscribeClient()]);
195
+ return await new Promise((resolve) => {
196
+ const timeoutRef = setTimeout(() => {
197
+ clearTimeout(timeoutRef);
198
+ cds.log(COMPONENT).info("shutdown timeout reached - some locks might not have been released!");
199
+ resolve();
200
+ }, TIMEOUT_SHUTDOWN);
201
+ distributedLock.shutdownHandler().then(() =>
202
+ Promise.allSettled([redis.closeMainClient(), redis.closeSubscribeClient()]).then((result) => {
203
+ clearTimeout(timeoutRef);
204
+ resolve(result);
205
+ })
206
+ );
207
+ });
192
208
  });
193
209
  };
194
210
 
@@ -9,12 +9,12 @@ const common = require("../shared/common");
9
9
 
10
10
  const EVENT_MESSAGE_CHANNEL = "EVENT_QUEUE_MESSAGE_CHANNEL";
11
11
  const COMPONENT_NAME = "/eventQueue/redisSub";
12
- let subscriberClientPromise;
13
12
 
14
13
  const initEventQueueRedisSubscribe = () => {
15
- if (subscriberClientPromise || !config.redisEnabled) {
14
+ if (initEventQueueRedisSubscribe._initDone || !config.redisEnabled) {
16
15
  return;
17
16
  }
17
+ initEventQueueRedisSubscribe._initDone = true;
18
18
  redis.subscribeRedisChannel(config.redisOptions, EVENT_MESSAGE_CHANNEL, _messageHandlerProcessEvents);
19
19
  };
20
20
 
@@ -83,20 +83,8 @@ const _messageHandlerProcessEvents = async (messageData) => {
83
83
  }
84
84
  };
85
85
 
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
86
  module.exports = {
98
87
  initEventQueueRedisSubscribe,
99
- closeSubscribeClient,
100
88
  __: {
101
89
  _messageHandlerProcessEvents,
102
90
  },
package/src/shared/env.js CHANGED
@@ -1,25 +1,24 @@
1
1
  "use strict";
2
2
 
3
+ const cds = require("@sap/cds");
4
+
3
5
  let instance;
4
6
 
5
7
  class Env {
6
- #vcapServices;
7
8
  #vcapApplication;
8
9
  #vcapApplicationInstance;
9
10
 
10
11
  constructor() {
11
12
  try {
12
- this.#vcapServices = JSON.parse(process.env.VCAP_SERVICES);
13
13
  this.#vcapApplication = JSON.parse(process.env.VCAP_APPLICATION);
14
14
  } catch {
15
- this.#vcapServices = {};
16
15
  this.#vcapApplication = {};
17
16
  }
18
17
  this.#vcapApplicationInstance = Number(process.env.CF_INSTANCE_INDEX);
19
18
  }
20
19
 
21
- get redisCredentialsFromEnv() {
22
- return this.#vcapServices["redis-cache"]?.[0]?.credentials;
20
+ get redisRequires() {
21
+ return cds.requires["redis-eventQueue"] || cds.requires["redis"];
23
22
  }
24
23
 
25
24
  get applicationName() {
@@ -30,14 +29,6 @@ class Env {
30
29
  return this.#vcapApplicationInstance;
31
30
  }
32
31
 
33
- set vcapServices(value) {
34
- this.#vcapServices = value;
35
- }
36
-
37
- get vcapServices() {
38
- return this.#vcapServices;
39
- }
40
-
41
32
  set applicationInstance(value) {
42
33
  this.#vcapApplicationInstance = value;
43
34
  }
@@ -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
  };
@@ -27,24 +29,30 @@ const createMainClientAndConnect = (options) => {
27
29
  return mainClientPromise;
28
30
  };
29
31
 
30
- const _createClientBase = (redisOptions) => {
32
+ const _createClientBase = (redisOptions = {}) => {
31
33
  const env = getEnvInstance();
32
34
  try {
33
- const credentials = env.redisCredentialsFromEnv;
34
- const redisIsCluster = credentials.cluster_mode;
35
- const url = credentials.uri.replace(/(?<=rediss:\/\/)[\w-]+?(?=:)/, "");
36
- if (redisIsCluster) {
35
+ const { credentials, options } = env.redisRequires;
36
+ const socket = Object.assign(
37
+ {
38
+ host: credentials.hostname,
39
+ tls: !!credentials.tls,
40
+ port: credentials.port,
41
+ },
42
+ options?.socket,
43
+ redisOptions.socket
44
+ );
45
+ const socketOptions = Object.assign({}, options, redisOptions, {
46
+ password: redisOptions.password ?? options.password ?? credentials.password,
47
+ socket,
48
+ });
49
+ if (credentials.cluster_mode) {
37
50
  return redis.createCluster({
38
- rootNodes: [{ url }],
39
- // https://github.com/redis/node-redis/issues/1782
40
- defaults: {
41
- password: credentials.password,
42
- socket: { tls: credentials.tls },
43
- ...redisOptions,
44
- },
51
+ rootNodes: [socketOptions],
52
+ defaults: socketOptions,
45
53
  });
46
54
  }
47
- return redis.createClient({ url, ...redisOptions });
55
+ return redis.createClient(socketOptions);
48
56
  } catch (err) {
49
57
  throw EventQueueError.redisConnectionFailure(err);
50
58
  }
@@ -57,7 +65,7 @@ const createClientAndConnect = async (options, errorHandlerCreateClient, isConne
57
65
  client.on("error", (err) => {
58
66
  const dateNow = Date.now();
59
67
  if (dateNow - lastErrorLog > LOG_AFTER_SEC * 1000) {
60
- cds.log(COMPONENT_NAME).error("error from redis client for pub/sub failed", err);
68
+ cds.log(COMPONENT_NAME).error("error redis client:", err);
61
69
  lastErrorLog = dateNow;
62
70
  }
63
71
  });
@@ -78,21 +86,48 @@ const createClientAndConnect = async (options, errorHandlerCreateClient, isConne
78
86
  };
79
87
 
80
88
  const subscribeRedisChannel = (options, channel, subscribeHandler) => {
89
+ subscribedChannels[channel] = subscribeHandler;
81
90
  const errorHandlerCreateClient = (err) => {
82
91
  cds.log(COMPONENT_NAME).error(`error from redis client for pub/sub failed for channel ${channel}`, err);
83
- subscriberChannelClientPromise[channel] = null;
84
- 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();
85
95
  };
86
96
 
87
- subscriberChannelClientPromise[channel] = createClientAndConnect(options, errorHandlerCreateClient)
97
+ _subscribeChannels(options, { [channel]: subscribeHandler }, errorHandlerCreateClient);
98
+ };
99
+
100
+ const _subscribeChannels = (options, subscribedChannels, errorHandlerCreateClient) => {
101
+ subscriberClientPromise = createClientAndConnect(options, errorHandlerCreateClient)
88
102
  .then((client) => {
89
- cds.log(COMPONENT_NAME).info("subscribe redis client connected channel", { channel });
90
- 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
+ }
91
121
  })
92
122
  .catch((err) => {
93
123
  cds
94
124
  .log(COMPONENT_NAME)
95
- .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
+ );
96
131
  });
97
132
  };
98
133
 
@@ -102,11 +137,13 @@ const publishMessage = async (options, channel, message) => {
102
137
  };
103
138
 
104
139
  const closeMainClient = async () => {
105
- try {
106
- await _resilientClientClose(await mainClientPromise);
107
- } catch (err) {
108
- // ignore errors during shutdown
109
- }
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!");
110
147
  };
111
148
 
112
149
  const _resilientClientClose = async (client) => {
@@ -115,7 +152,7 @@ const _resilientClientClose = async (client) => {
115
152
  await client.quit();
116
153
  }
117
154
  } catch (err) {
118
- // ignore errors during shutdown
155
+ cds.log(COMPONENT_NAME).info("error during redis close - continuing...", err);
119
156
  }
120
157
  };
121
158
 
@@ -145,5 +182,6 @@ module.exports = {
145
182
  subscribeRedisChannel,
146
183
  publishMessage,
147
184
  closeMainClient,
185
+ closeSubscribeClient,
148
186
  connectionCheck,
149
187
  };