@cap-js-community/event-queue 1.11.0-beta.5 → 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.
@@ -165,38 +165,7 @@ const getAllTenantIds = async () => {
165
165
  }, []);
166
166
  };
167
167
 
168
- const TENANT_COLUMNS = ["subscribedSubdomain", "createdAt", "modifiedAt"];
169
-
170
- const getAllTenantWithMetadata = async () => {
171
- const response = await _getAllTenantBase();
172
- if (!response) {
173
- return null;
174
- }
175
-
176
- return response.reduce(async (result, row) => {
177
- const tenantId = row.subscribedTenantId ?? row.tenant;
178
- result = await result;
179
- if (await common.isTenantIdValidCb(TenantIdCheckTypes.eventProcessing, tenantId)) {
180
- const data = Object.entries(row).reduce(
181
- (result, [key, value]) => {
182
- if (TENANT_COLUMNS.includes(key)) {
183
- result[key] = value;
184
- } else {
185
- result.metadata[key] = value;
186
- }
187
- return result;
188
- },
189
- { metadata: {} }
190
- );
191
- data.metadata = JSON.stringify(data.metadata);
192
- result.push(data);
193
- }
194
- return result;
195
- }, []);
196
- };
197
-
198
168
  module.exports = {
199
169
  executeInNewTransaction,
200
170
  getAllTenantIds,
201
- getAllTenantWithMetadata,
202
171
  };
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
 
3
+ const { AsyncResource } = require("async_hooks");
3
4
  const crypto = require("crypto");
4
5
 
5
6
  const cds = require("@sap/cds");
@@ -92,7 +93,7 @@ const hashStringTo32Bit = (value) => crypto.createHash("sha256").update(String(v
92
93
  const _getNewAuthContext = async (tenantId) => {
93
94
  try {
94
95
  if (!_getNewAuthContext._xsuaaService) {
95
- _getNewAuthContext._xsuaaService = new xssec.XsuaaService(cds.requires.auth.credentials);
96
+ _getNewAuthContext._xsuaaService = new xssec.XsuaaService(cds.requires["xsuaa-eventQueue"]?.credentials);
96
97
  }
97
98
  const authService = _getNewAuthContext._xsuaaService;
98
99
  const token = await authService.fetchClientCredentialsToken({ zid: tenantId });
@@ -104,6 +105,7 @@ const _getNewAuthContext = async (tenantId) => {
104
105
  err: err.message,
105
106
  responseCode: err.responseCode,
106
107
  responseText: err.responseText,
108
+ tenantId,
107
109
  });
108
110
 
109
111
  if (err.responseCode === 404) {
@@ -131,7 +133,10 @@ const getAuthContext = async (tenantId, { returnError = false } = {}) => {
131
133
  }
132
134
 
133
135
  getAuthContext._cache = getAuthContext._cache ?? new ExpiringLazyCache();
134
- const result = await getAuthContext._cache.getSetCb(tenantId, async () => _getNewAuthContext(tenantId));
136
+ const result = await getAuthContext._cache.getSetCb(
137
+ tenantId,
138
+ AsyncResource.bind(async () => _getNewAuthContext(tenantId))
139
+ );
135
140
  if (returnError) {
136
141
  return result;
137
142
  } else {
@@ -60,12 +60,21 @@ const releaseLock = async (context, key, { tenantScoped = true } = {}) => {
60
60
  }
61
61
  };
62
62
 
63
- const checkLockExistsAndReturnValue = async (context, key, { tenantScoped = true } = {}) => {
63
+ const checkLockExists = async (context, key, { tenantScoped = true } = {}) => {
64
64
  const fullKey = _generateKey(context, tenantScoped, key);
65
65
  if (config.redisEnabled) {
66
- return await _checkLockExistsRedis(context, fullKey);
66
+ return !!(await _getLockValueRedis(context, fullKey));
67
67
  } else {
68
- return await _checkLockExistsDb(context, fullKey);
68
+ return !!(await _getLockValueDb(context, fullKey));
69
+ }
70
+ };
71
+
72
+ const getValue = async (context, key, { tenantScoped = true } = {}) => {
73
+ const fullKey = _generateKey(context, tenantScoped, key);
74
+ if (config.redisEnabled) {
75
+ return await _getLockValueRedis(context, fullKey);
76
+ } else {
77
+ return await _getLockValueDb(context, fullKey);
69
78
  }
70
79
  };
71
80
 
@@ -75,7 +84,7 @@ const _acquireLockRedis = async (
75
84
  expiryTime,
76
85
  { value = Date.now(), overrideValue = false, keepTrackOfLock } = {}
77
86
  ) => {
78
- const client = await redis.createMainClientAndConnect(config.redisOptions);
87
+ const client = await redis.createMainClientAndConnect();
79
88
  const result = await client.set(fullKey, value, {
80
89
  PX: Math.round(expiryTime),
81
90
  ...(overrideValue ? null : { NX: true }),
@@ -88,7 +97,7 @@ const _acquireLockRedis = async (
88
97
  };
89
98
 
90
99
  const _renewLockRedis = async (context, fullKey, expiryTime, { value = "true" } = {}) => {
91
- const client = await redis.createMainClientAndConnect(config.redisOptions);
100
+ const client = await redis.createMainClientAndConnect();
92
101
  let result = await client.set(fullKey, value, {
93
102
  PX: Math.round(expiryTime),
94
103
  XX: true,
@@ -106,12 +115,12 @@ const _renewLockRedis = async (context, fullKey, expiryTime, { value = "true" }
106
115
  return result === REDIS_COMMAND_OK;
107
116
  };
108
117
 
109
- const _checkLockExistsRedis = async (context, fullKey) => {
110
- const client = await redis.createMainClientAndConnect(config.redisOptions);
111
- return await client.exists(fullKey);
118
+ const _getLockValueRedis = async (context, fullKey) => {
119
+ const client = await redis.createMainClientAndConnect();
120
+ return await client.get(fullKey);
112
121
  };
113
122
 
114
- const _checkLockExistsDb = async (context, fullKey) => {
123
+ const _getLockValueDb = async (context, fullKey) => {
115
124
  let result;
116
125
  await cdsHelper.executeInNewTransaction(context, "distributedLock-checkExists", async (tx) => {
117
126
  result = await tx.run(SELECT.one.from(config.tableNameEventLock).where("code =", fullKey));
@@ -120,7 +129,7 @@ const _checkLockExistsDb = async (context, fullKey) => {
120
129
  };
121
130
 
122
131
  const _releaseLockRedis = async (context, fullKey) => {
123
- const client = await redis.createMainClientAndConnect(config.redisOptions);
132
+ const client = await redis.createMainClientAndConnect();
124
133
  const result = await client.del(fullKey);
125
134
  delete existingLocks[fullKey];
126
135
  return result === 1;
@@ -186,14 +195,14 @@ const _acquireLockDB = async (
186
195
  };
187
196
 
188
197
  const _generateKey = (context, tenantScoped, key) => {
189
- const keyParts = [config.redisOptions.redisNamespace];
198
+ const keyParts = [config.redisNamespace];
190
199
  tenantScoped && keyParts.push(context.tenant);
191
200
  keyParts.push(key);
192
201
  return `${keyParts.join("##")}`;
193
202
  };
194
203
 
195
204
  const getAllLocksRedis = async () => {
196
- const clientOrCluster = await redis.createMainClientAndConnect(config.redisOptions);
205
+ const clientOrCluster = await redis.createMainClientAndConnect();
197
206
  const output = [];
198
207
  const results = [];
199
208
 
@@ -259,7 +268,8 @@ const shutdownHandler = async () => {
259
268
  module.exports = {
260
269
  acquireLock,
261
270
  releaseLock,
262
- checkLockExistsAndReturnValue,
271
+ checkLockExists,
272
+ getValue,
263
273
  setValueWithExpire,
264
274
  shutdownHandler,
265
275
  renewLock,
@@ -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
+ };
@@ -42,12 +42,4 @@ service EventQueueAdminService {
42
42
  @mandatory
43
43
  subType: String) returns Boolean;
44
44
  }
45
-
46
- @readonly
47
- @cds.persistence.skip
48
- entity Tenant {
49
- Key ID: String;
50
- subdomain: String;
51
- metadata: String;
52
- }
53
45
  }
@@ -1,7 +1,6 @@
1
1
  "use strict";
2
2
 
3
3
  const cds = require("@sap/cds");
4
- const cdsHelper = require("../../src/shared/cdsHelper");
5
4
  const { EventProcessingStatus } = require("../../src");
6
5
  const config = require("../../src/config");
7
6
  const distributedLock = require("../../src/shared/distributedLock");
@@ -9,7 +8,7 @@ const redisPub = require("../../src/redis/redisPub");
9
8
 
10
9
  module.exports = class AdminService extends cds.ApplicationService {
11
10
  async init() {
12
- const { Event: EventService, Tenant, Lock: LockService } = this.entities();
11
+ const { Event: EventService, Lock: LockService } = this.entities;
13
12
  const { Event: EventDb } = cds.db.entities("sap.eventqueue");
14
13
  const { landscape, space } = this.getLandscapeAndSpace();
15
14
 
@@ -18,9 +17,6 @@ module.exports = class AdminService extends cds.ApplicationService {
18
17
  req.reject(403, "Admin service is disabled by configuration");
19
18
  }
20
19
 
21
- if (req.target.name === Tenant.name) {
22
- return;
23
- }
24
20
  const headers = Object.assign({}, req.headers, req.req?.headers);
25
21
  const tenant = headers["z-id"] ?? req.data.tenant;
26
22
 
@@ -61,11 +57,6 @@ module.exports = class AdminService extends cds.ApplicationService {
61
57
  }));
62
58
  });
63
59
 
64
- this.on("READ", Tenant, async () => {
65
- const tenants = await cdsHelper.getAllTenantWithMetadata();
66
- return tenants ?? [];
67
- });
68
-
69
60
  this.on("setStatusAndAttempts", async (req) => {
70
61
  const tenant = req.headers["z-id"];
71
62
  cds.log("eventQueue").info("Restarting processing for event queue");
@@ -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
- };