@cap-js-community/event-queue 1.11.0 → 2.0.0-beta.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.
@@ -84,7 +84,7 @@ const _acquireLockRedis = async (
84
84
  expiryTime,
85
85
  { value = Date.now(), overrideValue = false, keepTrackOfLock } = {}
86
86
  ) => {
87
- const client = await redis.createMainClientAndConnect(config.redisOptions);
87
+ const client = await redis.createMainClientAndConnect();
88
88
  const result = await client.set(fullKey, value, {
89
89
  PX: Math.round(expiryTime),
90
90
  ...(overrideValue ? null : { NX: true }),
@@ -97,7 +97,7 @@ const _acquireLockRedis = async (
97
97
  };
98
98
 
99
99
  const _renewLockRedis = async (context, fullKey, expiryTime, { value = "true" } = {}) => {
100
- const client = await redis.createMainClientAndConnect(config.redisOptions);
100
+ const client = await redis.createMainClientAndConnect();
101
101
  let result = await client.set(fullKey, value, {
102
102
  PX: Math.round(expiryTime),
103
103
  XX: true,
@@ -116,7 +116,7 @@ const _renewLockRedis = async (context, fullKey, expiryTime, { value = "true" }
116
116
  };
117
117
 
118
118
  const _getLockValueRedis = async (context, fullKey) => {
119
- const client = await redis.createMainClientAndConnect(config.redisOptions);
119
+ const client = await redis.createMainClientAndConnect();
120
120
  return await client.get(fullKey);
121
121
  };
122
122
 
@@ -129,7 +129,7 @@ const _getLockValueDb = async (context, fullKey) => {
129
129
  };
130
130
 
131
131
  const _releaseLockRedis = async (context, fullKey) => {
132
- const client = await redis.createMainClientAndConnect(config.redisOptions);
132
+ const client = await redis.createMainClientAndConnect();
133
133
  const result = await client.del(fullKey);
134
134
  delete existingLocks[fullKey];
135
135
  return result === 1;
@@ -195,14 +195,14 @@ const _acquireLockDB = async (
195
195
  };
196
196
 
197
197
  const _generateKey = (context, tenantScoped, key) => {
198
- const keyParts = [config.redisOptions.redisNamespace];
198
+ const keyParts = [config.redisNamespace];
199
199
  tenantScoped && keyParts.push(context.tenant);
200
200
  keyParts.push(key);
201
201
  return `${keyParts.join("##")}`;
202
202
  };
203
203
 
204
204
  const getAllLocksRedis = async () => {
205
- const clientOrCluster = await redis.createMainClientAndConnect(config.redisOptions);
205
+ const clientOrCluster = await redis.createMainClientAndConnect();
206
206
  const output = [];
207
207
  const results = [];
208
208
 
@@ -15,9 +15,9 @@ class EventScheduler {
15
15
  config.attachUnsubscribeHandler(this.clearForTenant.bind(this));
16
16
  }
17
17
 
18
- scheduleEvent(tenantId, type, subType, startAfter) {
18
+ scheduleEvent(tenantId, type, subType, namespace, startAfter) {
19
19
  const { date, relative } = this.calculateOffset(type, subType, startAfter);
20
- const key = [tenantId, type, subType, date.toISOString()].join("##");
20
+ const key = [tenantId, type, subType, namespace, date.toISOString()].join("##");
21
21
  if (this.#scheduledEvents[key]) {
22
22
  return; // event combination already scheduled
23
23
  }
@@ -34,7 +34,7 @@ class EventScheduler {
34
34
  clearTimeout(timeout);
35
35
  delete this.#eventsByTenants[tenantId][timeout];
36
36
  delete this.#scheduledEvents[key];
37
- redisPub.broadcastEvent(tenantId, { type, subType }).catch((err) => {
37
+ redisPub.broadcastEvent(tenantId, { type, subType, namespace }).catch((err) => {
38
38
  cds.log(COMPONENT_NAME).error("could not execute scheduled event", err, {
39
39
  tenantId,
40
40
  type,
@@ -0,0 +1,46 @@
1
+ "use strict";
2
+
3
+ const { RedisClient } = require("@cap-js-community/common");
4
+
5
+ const config = require("../../config");
6
+
7
+ const REDIS_CLIENT_NAME = "eventQueue";
8
+
9
+ const createMainClientAndConnect = async () => {
10
+ const redisClient = RedisClient.create(REDIS_CLIENT_NAME);
11
+ return await redisClient.createMainClientAndConnect(config.redisOptions);
12
+ };
13
+
14
+ const subscribeRedisChannel = async (channel, subscribeHandler) => {
15
+ const redisClient = RedisClient.create(REDIS_CLIENT_NAME);
16
+ const channelWithNamespace = [config.redisNamespace, channel].join("##");
17
+ return await redisClient.subscribeChannel(config.redisOptions, channelWithNamespace, subscribeHandler);
18
+ };
19
+
20
+ const publishMessage = async (channel, message) => {
21
+ const redisClient = RedisClient.create(REDIS_CLIENT_NAME);
22
+ const channelWithNamespace = [config.redisNamespace, channel].join("##");
23
+ return await redisClient.publishMessage(config.redisOptions, channelWithNamespace, message);
24
+ };
25
+
26
+ const connectionCheck = async () => {
27
+ const redisClient = RedisClient.create(REDIS_CLIENT_NAME);
28
+ return await redisClient.connectionCheck(config.redisOptions);
29
+ };
30
+
31
+ const isClusterMode = () => {
32
+ return RedisClient.create(REDIS_CLIENT_NAME).isCluster;
33
+ };
34
+
35
+ const registerShutdownHandler = (cb) => {
36
+ RedisClient.create(REDIS_CLIENT_NAME).beforeCloseHandler = cb;
37
+ };
38
+
39
+ module.exports = {
40
+ createMainClientAndConnect,
41
+ subscribeRedisChannel,
42
+ publishMessage,
43
+ connectionCheck,
44
+ isClusterMode,
45
+ registerShutdownHandler,
46
+ };
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+
3
+ const cds = require("@sap/cds");
4
+
5
+ const client = require("./client");
6
+ const config = require("../../config");
7
+
8
+ const COMPONENT_NAME = "/eventQueue/redis";
9
+ const REDIS_OFFBOARD_TENANT_CHANNEL = "REDIS_OFFBOARD_TENANT_CHANNEL";
10
+
11
+ const attachRedisUnsubscribeHandler = () => {
12
+ cds.log(COMPONENT_NAME).info("attached redis handle for unsubscribe events");
13
+ client
14
+ .subscribeRedisChannel(REDIS_OFFBOARD_TENANT_CHANNEL, (messageData) => {
15
+ try {
16
+ const { tenantId } = JSON.parse(messageData);
17
+ cds.log(COMPONENT_NAME).info("received unsubscribe broadcast event", { tenantId });
18
+ this.executeUnsubscribeHandlers(tenantId);
19
+ } catch (err) {
20
+ cds.log(COMPONENT_NAME).error("could not parse unsubscribe broadcast event", err, {
21
+ messageData,
22
+ });
23
+ }
24
+ })
25
+ .catch((err) => _errorHandlerSubscribeChannel(REDIS_OFFBOARD_TENANT_CHANNEL, err));
26
+ };
27
+
28
+ const handleUnsubscribe = (tenantId) => {
29
+ if (config.redisEnabled) {
30
+ client.publishMessage(REDIS_OFFBOARD_TENANT_CHANNEL, JSON.stringify({ tenantId })).catch((error) => {
31
+ cds.log(COMPONENT_NAME).error(`publishing tenant unsubscribe failed. tenantId: ${tenantId}`, error);
32
+ });
33
+ } else {
34
+ config.executeUnsubscribeHandlers(tenantId);
35
+ }
36
+ };
37
+
38
+ const _errorHandlerSubscribeChannel = (channelName, err) =>
39
+ cds.log(COMPONENT_NAME).error("error subscribing to channel", err, { channelName });
40
+
41
+ module.exports = {
42
+ ...client,
43
+ attachRedisUnsubscribeHandler,
44
+ handleUnsubscribe,
45
+ };
@@ -8,7 +8,7 @@ const redisPub = require("../../src/redis/redisPub");
8
8
 
9
9
  module.exports = class AdminService extends cds.ApplicationService {
10
10
  async init() {
11
- const { Event: EventService, Lock: LockService } = this.entities();
11
+ const { Event: EventService, Lock: LockService } = this.entities;
12
12
  const { Event: EventDb } = cds.db.entities("sap.eventqueue");
13
13
  const { landscape, space } = this.getLandscapeAndSpace();
14
14
 
@@ -1,199 +0,0 @@
1
- "use strict";
2
-
3
- const redis = require("redis");
4
-
5
- const { getEnvInstance } = require("./env");
6
- const EventQueueError = require("../EventQueueError");
7
-
8
- const COMPONENT_NAME = "/eventQueue/shared/redis";
9
- const LOG_AFTER_SEC = 5;
10
-
11
- let mainClientPromise;
12
- let subscriberClientPromise;
13
- const subscribedChannels = {};
14
- let lastErrorLog = Date.now();
15
-
16
- const createMainClientAndConnect = (options) => {
17
- if (mainClientPromise) {
18
- return mainClientPromise;
19
- }
20
-
21
- const errorHandlerCreateClient = (err) => {
22
- mainClientPromise?.then?.(_resilientClientClose);
23
- cds.log(COMPONENT_NAME).error("error from redis main client:", err);
24
- mainClientPromise = null;
25
- setTimeout(() => createMainClientAndConnect(options), LOG_AFTER_SEC * 1000).unref();
26
- };
27
-
28
- mainClientPromise = createClientAndConnect(options, errorHandlerCreateClient);
29
- return mainClientPromise;
30
- };
31
-
32
- const _createClientBase = (redisOptions = {}) => {
33
- const env = getEnvInstance();
34
- try {
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
- delete socketOptions.redisNamespace;
50
- if (credentials.cluster_mode) {
51
- return redis.createCluster({
52
- rootNodes: [socketOptions],
53
- defaults: socketOptions,
54
- });
55
- }
56
- return redis.createClient(socketOptions);
57
- } catch (err) {
58
- throw EventQueueError.redisConnectionFailure(err);
59
- }
60
- };
61
-
62
- const createClientAndConnect = async (options, errorHandlerCreateClient, isConnectionCheck) => {
63
- try {
64
- const client = _createClientBase(options);
65
- if (!isConnectionCheck) {
66
- client.on("error", (err) => {
67
- const dateNow = Date.now();
68
- if (dateNow - lastErrorLog > LOG_AFTER_SEC * 1000) {
69
- cds.log(COMPONENT_NAME).error("error redis client:", err);
70
- lastErrorLog = dateNow;
71
- }
72
- });
73
-
74
- client.on("reconnecting", () => {
75
- const dateNow = Date.now();
76
- if (dateNow - lastErrorLog > LOG_AFTER_SEC * 1000) {
77
- cds.log(COMPONENT_NAME).info("redis client trying reconnect...");
78
- lastErrorLog = dateNow;
79
- }
80
- });
81
- }
82
- await client.connect();
83
- return client;
84
- } catch (err) {
85
- errorHandlerCreateClient(err);
86
- }
87
- };
88
-
89
- const subscribeRedisChannel = (options, channel, subscribeHandler) => {
90
- subscribedChannels[channel] = subscribeHandler;
91
- const errorHandlerCreateClient = (err) => {
92
- cds.log(COMPONENT_NAME).error(`error from redis client for pub/sub failed for channel ${channel}`, err);
93
- subscriberClientPromise?.then?.(_resilientClientClose);
94
- subscriberClientPromise = null;
95
- setTimeout(() => _subscribeChannels(options, subscribedChannels, subscribeHandler), LOG_AFTER_SEC * 1000).unref();
96
- };
97
-
98
- _subscribeChannels(options, { [channel]: subscribeHandler }, errorHandlerCreateClient);
99
- };
100
-
101
- const _subscribeChannels = (options, subscribedChannels, errorHandlerCreateClient) => {
102
- subscriberClientPromise = createClientAndConnect(options, errorHandlerCreateClient)
103
- .then((client) => {
104
- for (const channel in subscribedChannels) {
105
- const fn = subscribedChannels[channel];
106
- client._subscribedChannels ??= {};
107
- if (client._subscribedChannels[channel]) {
108
- continue;
109
- }
110
- const prefixedChannelName = [options.redisNamespace, channel].join("_");
111
- cds.log(COMPONENT_NAME).info("subscribe redis client connected channel", { channel: prefixedChannelName });
112
- client
113
- .subscribe(prefixedChannelName, fn)
114
- .then(() => {
115
- client._subscribedChannels ??= {};
116
- client._subscribedChannels[channel] = 1;
117
- })
118
- .catch(() => {
119
- cds.log(COMPONENT_NAME).error("error subscribe to channel - retrying...");
120
- setTimeout(() => _subscribeChannels(options, [channel], fn), LOG_AFTER_SEC * 1000).unref();
121
- });
122
- }
123
- })
124
- .catch((err) => {
125
- cds
126
- .log(COMPONENT_NAME)
127
- .error(
128
- `error from redis client for pub/sub failed during startup - trying to reconnect - ${Object.keys(
129
- subscribedChannels
130
- ).join(", ")}`,
131
- err
132
- );
133
- });
134
- };
135
-
136
- const publishMessage = async (options, channel, message) => {
137
- const client = await createMainClientAndConnect(options);
138
- return await client.publish([options.redisNamespace, channel].join("_"), message);
139
- };
140
-
141
- const closeMainClient = async () => {
142
- await _resilientClientClose(await mainClientPromise);
143
- cds.log(COMPONENT_NAME).info("main redis client closed!");
144
- };
145
-
146
- const closeSubscribeClient = async () => {
147
- await _resilientClientClose(await subscriberClientPromise);
148
- cds.log(COMPONENT_NAME).info("subscribe redis client closed!");
149
- };
150
-
151
- const _resilientClientClose = async (client) => {
152
- try {
153
- if (client?.quit) {
154
- await client.quit();
155
- }
156
- } catch (err) {
157
- cds.log(COMPONENT_NAME).info("error during redis close - continuing...", err);
158
- }
159
- };
160
-
161
- const connectionCheck = async (options) => {
162
- return new Promise((resolve, reject) => {
163
- createClientAndConnect(options, reject, true)
164
- .then((client) => {
165
- if (client) {
166
- _resilientClientClose(client);
167
- resolve();
168
- } else {
169
- reject(new Error());
170
- }
171
- })
172
- .catch(reject);
173
- })
174
- .then(() => true)
175
- .catch((err) => {
176
- cds.log(COMPONENT_NAME).error("Redis connection check failed! Falling back to NO_REDIS mode", err);
177
- return false;
178
- });
179
- };
180
-
181
- const isClusterMode = () => {
182
- if (!("__clusterMode" in isClusterMode)) {
183
- const env = getEnvInstance();
184
- const { credentials } = env.redisRequires;
185
- isClusterMode.__clusterMode = credentials.cluster_mode;
186
- }
187
- return isClusterMode.__clusterMode;
188
- };
189
-
190
- module.exports = {
191
- createClientAndConnect,
192
- createMainClientAndConnect,
193
- subscribeRedisChannel,
194
- publishMessage,
195
- closeMainClient,
196
- closeSubscribeClient,
197
- connectionCheck,
198
- isClusterMode,
199
- };