@cap-js-community/event-queue 0.1.53 → 0.1.54

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": "0.1.53",
3
+ "version": "0.1.54",
4
4
  "description": "event queue for cds",
5
5
  "main": "src/index.js",
6
6
  "files": [
package/src/config.js CHANGED
@@ -2,30 +2,33 @@
2
2
 
3
3
  const cds = require("@sap/cds");
4
4
 
5
- const env = require("./shared/env");
5
+ const { getEnvInstance: getEnvInstance } = require("./shared/env");
6
+ const redis = require("./shared/redis");
6
7
 
7
8
  let instance;
8
9
 
9
10
  const FOR_UPDATE_TIMEOUT = 10;
10
11
  const GLOBAL_TX_TIMEOUT = 30 * 60 * 1000;
12
+ const REDIS_CONFIG_CHANNEL = "EVENT_QUEUE_CONFIG_CHANNEL";
13
+ const COMPONENT_NAME = "eventQueue/config";
11
14
 
12
15
  class Config {
13
16
  constructor() {
17
+ this.__logger = cds.log(COMPONENT_NAME);
14
18
  this.__config = null;
15
19
  this.__forUpdateTimeout = FOR_UPDATE_TIMEOUT;
16
20
  this.__globalTxTimeout = GLOBAL_TX_TIMEOUT;
17
21
  this.__runInterval = null;
18
22
  this.__redisEnabled = null;
19
- this.__isOnCF = env.isOnCF;
20
23
  this.__initialized = false;
21
24
  this.__parallelTenantProcessing = null;
22
25
  this.__tableNameEventQueue = null;
23
26
  this.__tableNameEventLock = null;
24
- this.__vcapServices = this._parseVcapServices();
25
27
  this.__isRunnerDeactivated = false;
26
28
  this.__configFilePath = null;
27
29
  this.__processEventsAfterPublish = null;
28
30
  this.__skipCsnCheck = null;
31
+ this.__env = getEnvInstance();
29
32
  }
30
33
 
31
34
  getEventConfig(type, subType) {
@@ -37,23 +40,37 @@ class Config {
37
40
  }
38
41
 
39
42
  _checkRedisIsBound() {
40
- return !!this.getRedisCredentialsFromEnv();
43
+ return !!this.__env.getRedisCredentialsFromEnv();
41
44
  }
42
45
 
43
- getRedisCredentialsFromEnv() {
44
- return this.__vcapServices["redis-cache"]?.[0]?.credentials;
46
+ checkRedisEnabled() {
47
+ this.__redisEnabled = this._checkRedisIsBound() && this.__env.isOnCF;
45
48
  }
46
49
 
47
- _parseVcapServices() {
48
- try {
49
- return JSON.parse(process.env.VCAP_SERVICES);
50
- } catch {
51
- return {};
52
- }
50
+ attachConfigChangeHandler() {
51
+ redis.subscribeRedisChannel(REDIS_CONFIG_CHANNEL, (messageData) => {
52
+ try {
53
+ const { key, value } = JSON.parse(messageData);
54
+ if (this[key] !== value) {
55
+ this.__logger.info("received config change", { key, value });
56
+ this[key] = value;
57
+ }
58
+ } catch (err) {
59
+ this.__logger.error("could not parse event config change", {
60
+ messageData,
61
+ });
62
+ }
63
+ });
53
64
  }
54
65
 
55
- calculateIsRedisEnabled() {
56
- this.__redisEnabled = this._checkRedisIsBound() && this.__isOnCF;
66
+ publishConfigChange(key, value) {
67
+ if (!this.redisEnabled) {
68
+ this.__logger.info("redis not connected, config change won't be published", { key, value });
69
+ return;
70
+ }
71
+ redis.publishMessage(REDIS_CONFIG_CHANNEL, JSON.stringify({ key, value })).catch((error) => {
72
+ this.__logger.error(`publishing config change failed key: ${key}, value: ${value}`, error);
73
+ });
57
74
  }
58
75
 
59
76
  get isRunnerDeactivated() {
@@ -112,14 +129,6 @@ class Config {
112
129
  this.__redisEnabled = value;
113
130
  }
114
131
 
115
- get isOnCF() {
116
- return this.__isOnCF;
117
- }
118
-
119
- set isOnCF(value) {
120
- this.__isOnCF = value;
121
- }
122
-
123
132
  get initialized() {
124
133
  return this.__initialized;
125
134
  }
package/src/initialize.js CHANGED
@@ -28,6 +28,7 @@ const initialize = async ({
28
28
  configFilePath,
29
29
  registerAsEventProcessor,
30
30
  processEventsAfterPublish,
31
+ isRunnerDeactivated,
31
32
  runInterval,
32
33
  parallelTenantProcessing,
33
34
  tableNameEventQueue,
@@ -48,6 +49,7 @@ const initialize = async ({
48
49
  configFilePath,
49
50
  registerAsEventProcessor,
50
51
  processEventsAfterPublish,
52
+ isRunnerDeactivated,
51
53
  runInterval,
52
54
  parallelTenantProcessing,
53
55
  tableNameEventQueue,
@@ -57,7 +59,7 @@ const initialize = async ({
57
59
 
58
60
  const logger = cds.log(COMPONENT);
59
61
  configInstance.fileContent = await readConfigFromFile(configInstance.configFilePath);
60
- configInstance.calculateIsRedisEnabled();
62
+ configInstance.checkRedisEnabled();
61
63
 
62
64
  const dbService = await cds.connect.to("db");
63
65
  await (cds.model ? Promise.resolve() : new Promise((resolve) => cds.on("serving", resolve)));
@@ -107,6 +109,7 @@ const registerEventProcessors = () => {
107
109
 
108
110
  if (configInstance.redisEnabled) {
109
111
  initEventQueueRedisSubscribe();
112
+ configInstance.attachConfigChangeHandler();
110
113
  runner.multiTenancyRedis();
111
114
  } else {
112
115
  runner.multiTenancyDb();
@@ -161,6 +164,7 @@ const mixConfigVarsWithEnv = (
161
164
  configFilePath,
162
165
  registerAsEventProcessor,
163
166
  processEventsAfterPublish,
167
+ isRunnerDeactivated,
164
168
  runInterval,
165
169
  parallelTenantProcessing,
166
170
  tableNameEventQueue,
@@ -172,6 +176,7 @@ const mixConfigVarsWithEnv = (
172
176
  configInstance.configFilePath = configFilePath ?? cds.env.eventQueue?.configFilePath;
173
177
  configInstance.registerAsEventProcessor =
174
178
  registerAsEventProcessor ?? cds.env.eventQueue?.registerAsEventProcessor ?? true;
179
+ configInstance.isRunnerDeactivated = isRunnerDeactivated ?? cds.env.eventQueue?.isRunnerDeactivated ?? false;
175
180
  configInstance.processEventsAfterPublish =
176
181
  processEventsAfterPublish ?? cds.env.eventQueue?.processEventsAfterPublish ?? true;
177
182
  configInstance.runInterval = runInterval ?? cds.env.eventQueue?.runInterval ?? 5 * 60 * 1000;
@@ -7,83 +7,50 @@ const { checkLockExistsAndReturnValue } = require("./shared/distributedLock");
7
7
  const config = require("./config");
8
8
  const { getWorkerPoolInstance } = require("./shared/WorkerQueue");
9
9
 
10
- const MESSAGE_CHANNEL = "cdsEventQueue";
10
+ const EVENT_MESSAGE_CHANNEL = "EVENT_QUEUE_MESSAGE_CHANNEL";
11
11
  const COMPONENT_NAME = "eventQueue/redisPubSub";
12
12
 
13
- let publishClient;
14
13
  let subscriberClientPromise;
15
14
 
16
15
  const initEventQueueRedisSubscribe = () => {
17
16
  if (subscriberClientPromise || !config.getConfigInstance().redisEnabled) {
18
17
  return;
19
18
  }
20
- subscribeRedisClient();
21
- };
22
-
23
- const subscribeRedisClient = () => {
24
- const errorHandlerCreateClient = (err) => {
25
- cds.log(COMPONENT_NAME).error("error from redis client for pub/sub failed", err);
26
- subscriberClientPromise = null;
27
- setTimeout(subscribeRedisClient, 5 * 1000).unref();
28
- };
29
- subscriberClientPromise = redis.createClientAndConnect(errorHandlerCreateClient);
30
- subscriberClientPromise
31
- .then((client) => {
32
- cds.log(COMPONENT_NAME).info("subscribe redis client connected");
33
- client.subscribe(MESSAGE_CHANNEL, messageHandlerProcessEvents);
34
- })
35
- .catch((err) => {
36
- cds
37
- .log(COMPONENT_NAME)
38
- .error("error from redis client for pub/sub failed during startup - trying to reconnect", err);
39
- });
19
+ redis.subscribeRedisChannel(EVENT_MESSAGE_CHANNEL, messageHandlerProcessEvents);
40
20
  };
41
21
 
42
22
  const messageHandlerProcessEvents = async (messageData) => {
43
- let tenantId, type, subType;
23
+ const logger = cds.log(COMPONENT_NAME);
44
24
  try {
45
- ({ tenantId, type, subType } = JSON.parse(messageData));
25
+ const { tenantId, type, subType } = JSON.parse(messageData);
26
+ const subdomain = await getSubdomainForTenantId(tenantId);
27
+ const context = new cds.EventContext({
28
+ tenant: tenantId,
29
+ // NOTE: we need this because of logging otherwise logs would not contain the subdomain
30
+ http: { req: { authInfo: { getSubdomain: () => subdomain } } },
31
+ });
32
+ cds.context = context;
33
+ logger.debug("received redis event", {
34
+ tenantId,
35
+ type,
36
+ subType,
37
+ });
38
+ getWorkerPoolInstance().addToQueue(async () => processEventQueue(context, type, subType));
46
39
  } catch (err) {
47
- cds.log(COMPONENT_NAME).error("could not parse event information", {
40
+ logger.error("could not parse event information", {
48
41
  messageData,
49
42
  });
50
- return;
51
43
  }
52
- const subdomain = await getSubdomainForTenantId(tenantId);
53
- const context = new cds.EventContext({
54
- tenant: tenantId,
55
- // NOTE: we need this because of logging otherwise logs would not contain the subdomain
56
- http: { req: { authInfo: { getSubdomain: () => subdomain } } },
57
- });
58
- cds.context = context;
59
- cds.log(COMPONENT_NAME).debug("received redis event", {
60
- tenantId,
61
- type,
62
- subType,
63
- });
64
- getWorkerPoolInstance().addToQueue(async () => processEventQueue(context, type, subType));
65
44
  };
66
45
 
67
46
  const publishEvent = async (tenantId, type, subType) => {
47
+ const logger = cds.log(COMPONENT_NAME);
68
48
  const configInstance = config.getConfigInstance();
69
49
  if (!configInstance.redisEnabled) {
70
50
  await _handleEventInternally(tenantId, type, subType);
71
51
  return;
72
52
  }
73
-
74
- const logger = cds.log(COMPONENT_NAME);
75
- const errorHandlerCreateClient = (err) => {
76
- logger.error("error from redis client for pub/sub failed", {
77
- err,
78
- });
79
- publishClient = null;
80
- };
81
53
  try {
82
- if (!publishClient) {
83
- publishClient = await redis.createClientAndConnect(errorHandlerCreateClient);
84
- logger.info("publish redis client connected");
85
- }
86
-
87
54
  const result = await checkLockExistsAndReturnValue(
88
55
  new cds.EventContext({ tenant: tenantId }),
89
56
  [type, subType].join("##")
@@ -97,7 +64,7 @@ const publishEvent = async (tenantId, type, subType) => {
97
64
  type,
98
65
  subType,
99
66
  });
100
- await publishClient.publish(MESSAGE_CHANNEL, JSON.stringify({ tenantId, type, subType }));
67
+ await redis.publishMessage(EVENT_MESSAGE_CHANNEL, JSON.stringify({ tenantId, type, subType }));
101
68
  } catch (err) {
102
69
  logger.error(`publish event failed with error: ${err.toString()}`, {
103
70
  tenantId,
@@ -108,8 +75,7 @@ const publishEvent = async (tenantId, type, subType) => {
108
75
  };
109
76
 
110
77
  const _handleEventInternally = async (tenantId, type, subType) => {
111
- const logger = cds.log(COMPONENT_NAME);
112
- logger.info("processEventQueue internally", {
78
+ cds.log(COMPONENT_NAME).info("processEventQueue internally", {
113
79
  tenantId,
114
80
  type,
115
81
  subType,
package/src/runner.js CHANGED
@@ -14,8 +14,6 @@ const EVENT_QUEUE_RUN_ID = "EVENT_QUEUE_RUN_ID";
14
14
  const EVENT_QUEUE_RUN_TS = "EVENT_QUEUE_RUN_TS";
15
15
  const OFFSET_FIRST_RUN = 10 * 1000;
16
16
 
17
- const LOGGER = cds.log(COMPONENT_NAME);
18
-
19
17
  const singleTenant = () => _scheduleFunction(_executeRunForTenant);
20
18
 
21
19
  const multiTenancyDb = () => _scheduleFunction(_multiTenancyDb);
@@ -23,16 +21,18 @@ const multiTenancyDb = () => _scheduleFunction(_multiTenancyDb);
23
21
  const multiTenancyRedis = () => _scheduleFunction(_multiTenancyRedis);
24
22
 
25
23
  const _scheduleFunction = async (fn) => {
24
+ const logger = cds.log(COMPONENT_NAME);
26
25
  const configInstance = eventQueueConfig.getConfigInstance();
27
26
  const eventsForAutomaticRun = configInstance.events;
28
27
  if (!eventsForAutomaticRun.length) {
29
- LOGGER.warn("no events for automatic run are configured - skipping runner registration");
28
+ logger.warn("no events for automatic run are configured - skipping runner registration");
30
29
  return;
31
30
  }
32
31
 
33
32
  const fnWithRunningCheck = () => {
33
+ const logger = cds.log(COMPONENT_NAME);
34
34
  if (configInstance.isRunnerDeactivated) {
35
- LOGGER.info("runner is deactivated via config variable. Skipping this run.");
35
+ logger.info("runner is deactivated via config variable. Skipping this run.");
36
36
  return;
37
37
  }
38
38
  return fn();
@@ -40,7 +40,7 @@ const _scheduleFunction = async (fn) => {
40
40
 
41
41
  const offsetDependingOnLastRun = await _calculateOffsetForFirstRun();
42
42
 
43
- LOGGER.info("first event-queue run scheduled", {
43
+ logger.info("first event-queue run scheduled", {
44
44
  firstRunScheduledFor: new Date(Date.now() + offsetDependingOnLastRun).toISOString(),
45
45
  });
46
46
 
@@ -52,13 +52,14 @@ const _scheduleFunction = async (fn) => {
52
52
  };
53
53
 
54
54
  const _multiTenancyRedis = async () => {
55
+ const logger = cds.log(COMPONENT_NAME);
55
56
  const emptyContext = new cds.EventContext({});
56
- LOGGER.info("executing event queue run for multi instance and tenant");
57
+ logger.info("executing event queue run for multi instance and tenant");
57
58
  const tenantIds = await cdsHelper.getAllTenantIds();
58
59
  const runId = await _acquireRunId(emptyContext);
59
60
 
60
61
  if (!runId) {
61
- LOGGER.error("could not acquire runId, skip processing events!");
62
+ logger.error("could not acquire runId, skip processing events!");
62
63
  return;
63
64
  }
64
65
 
@@ -66,12 +67,13 @@ const _multiTenancyRedis = async () => {
66
67
  };
67
68
 
68
69
  const _multiTenancyDb = async () => {
70
+ const logger = cds.log(COMPONENT_NAME);
69
71
  try {
70
- LOGGER.info("executing event queue run for single instance and multi tenant");
72
+ logger.info("executing event queue run for single instance and multi tenant");
71
73
  const tenantIds = await cdsHelper.getAllTenantIds();
72
74
  _executeAllTenants(tenantIds, EVENT_QUEUE_RUN_ID);
73
75
  } catch (err) {
74
- LOGGER.error(
76
+ logger.error(
75
77
  `Couldn't fetch tenant ids for event queue processing! Next try after defined interval. Error: ${err}`
76
78
  );
77
79
  }
@@ -92,7 +94,7 @@ const _executeAllTenants = (tenantIds, runId) => {
92
94
  }
93
95
  await _executeRunForTenant(tenantId, runId);
94
96
  } catch (err) {
95
- LOGGER.error("executing event-queue run for tenant failed", {
97
+ cds.log(COMPONENT_NAME).error("executing event-queue run for tenant failed", {
96
98
  tenantId,
97
99
  });
98
100
  }
@@ -101,6 +103,7 @@ const _executeAllTenants = (tenantIds, runId) => {
101
103
  };
102
104
 
103
105
  const _executeRunForTenant = async (tenantId, runId) => {
106
+ const logger = cds.log(COMPONENT_NAME);
104
107
  const configInstance = eventQueueConfig.getConfigInstance();
105
108
  try {
106
109
  const eventsForAutomaticRun = configInstance.events;
@@ -111,14 +114,14 @@ const _executeRunForTenant = async (tenantId, runId) => {
111
114
  http: { req: { authInfo: { getSubdomain: () => subdomain } } },
112
115
  });
113
116
  cds.context = context;
114
- LOGGER.info("executing eventQueue run", {
117
+ logger.info("executing eventQueue run", {
115
118
  tenantId,
116
119
  subdomain,
117
120
  ...(runId ? { runId } : null),
118
121
  });
119
122
  await eventQueueRunner(context, eventsForAutomaticRun);
120
123
  } catch (err) {
121
- LOGGER.error(`Couldn't process eventQueue for tenant! Next try after defined interval. Error: ${err}`, {
124
+ logger.error(`Couldn't process eventQueue for tenant! Next try after defined interval. Error: ${err}`, {
122
125
  tenantId,
123
126
  redisEnabled: configInstance.redisEnabled,
124
127
  });
@@ -177,10 +180,12 @@ const _calculateOffsetForFirstRun = async () => {
177
180
  offsetDependingOnLastRun = new Date(lastRunTs).getTime() + configInstance.runInterval - now;
178
181
  }
179
182
  } catch (err) {
180
- LOGGER.error(
181
- "calculating offset for first run failed, falling back to default. Runs might be out-of-sync. Error:",
182
- err
183
- );
183
+ cds
184
+ .log(COMPONENT_NAME)
185
+ .error(
186
+ "calculating offset for first run failed, falling back to default. Runs might be out-of-sync. Error:",
187
+ err
188
+ );
184
189
  }
185
190
  return offsetDependingOnLastRun;
186
191
  };
@@ -35,7 +35,7 @@ class SetIntervalDriftSafe {
35
35
  setTimeout(() => {
36
36
  this.run(fn);
37
37
  fn();
38
- }, this.#adjustedInterval);
38
+ }, this.#adjustedInterval).unref();
39
39
  }
40
40
  }
41
41
 
package/src/shared/env.js CHANGED
@@ -1,9 +1,53 @@
1
1
  "use strict";
2
2
 
3
- const isLocal = process.env.USER !== "vcap";
4
- const isOnCF = !isLocal;
3
+ let instance;
4
+
5
+ class Env {
6
+ #isLocal;
7
+ #isOnCF;
8
+ #vcapServices;
9
+
10
+ constructor() {
11
+ this.#isLocal = process.env.USER !== "vcap";
12
+ this.#isOnCF = !this.#isLocal;
13
+ try {
14
+ this.#vcapServices = JSON.parse(process.env.VCAP_SERVICES);
15
+ } catch {
16
+ this.#vcapServices = {};
17
+ }
18
+ }
19
+
20
+ getRedisCredentialsFromEnv() {
21
+ return this.#vcapServices["redis-cache"]?.[0]?.credentials;
22
+ }
23
+
24
+ set isLocal(value) {
25
+ this.#isLocal = value;
26
+ }
27
+ get isLocal() {
28
+ return this.#isLocal;
29
+ }
30
+
31
+ set isOnCF(value) {
32
+ this.#isOnCF = value;
33
+ }
34
+ get isOnCF() {
35
+ return this.#isOnCF;
36
+ }
37
+
38
+ set vcapServices(value) {
39
+ this.#vcapServices = value;
40
+ }
41
+ get vcapServices() {
42
+ return this.#vcapServices;
43
+ }
44
+ }
5
45
 
6
46
  module.exports = {
7
- isOnCF,
8
- isLocal,
47
+ getEnvInstance: () => {
48
+ if (!instance) {
49
+ instance = new Env();
50
+ }
51
+ return instance;
52
+ },
9
53
  };
@@ -2,32 +2,33 @@
2
2
 
3
3
  const redis = require("redis");
4
4
 
5
- const config = require("../config");
5
+ const { getInstance: getEnvInstance } = require("./env");
6
6
  const EventQueueError = require("../EventQueueError");
7
7
 
8
8
  const COMPONENT_NAME = "eventQueue/shared/redis";
9
9
 
10
- let subscriberClientPromise;
10
+ let mainClientPromise;
11
+ const subscriberChannelClientPromise = {};
11
12
 
12
13
  const createMainClientAndConnect = () => {
13
- if (subscriberClientPromise) {
14
- return subscriberClientPromise;
14
+ if (mainClientPromise) {
15
+ return mainClientPromise;
15
16
  }
16
17
 
17
18
  const errorHandlerCreateClient = (err) => {
18
19
  cds.log(COMPONENT_NAME).error("error from redis client for pub/sub failed", err);
19
- subscriberClientPromise = null;
20
+ mainClientPromise = null;
20
21
  setTimeout(createMainClientAndConnect, 5 * 1000).unref();
21
22
  };
22
- subscriberClientPromise = createClientAndConnect(errorHandlerCreateClient);
23
- return subscriberClientPromise;
23
+ mainClientPromise = createClientAndConnect(errorHandlerCreateClient);
24
+ return mainClientPromise;
24
25
  };
25
26
 
26
27
  const _createClientBase = () => {
27
- const configInstance = config.getConfigInstance();
28
- if (configInstance.isOnCF) {
28
+ const env = getEnvInstance();
29
+ if (env.isOnCF) {
29
30
  try {
30
- const credentials = configInstance.getRedisCredentialsFromEnv();
31
+ const credentials = env.getRedisCredentialsFromEnv();
31
32
  // NOTE: settings the user explicitly to empty resolves auth problems, see
32
33
  // https://github.com/go-redis/redis/issues/1343
33
34
  const url = credentials.uri.replace(/(?<=rediss:\/\/)[\w-]+?(?=:)/, "");
@@ -60,9 +61,35 @@ const createClientAndConnect = async (errorHandlerCreateClient) => {
60
61
  return client;
61
62
  };
62
63
 
64
+ const subscribeRedisChannel = (channel, subscribeCb) => {
65
+ const errorHandlerCreateClient = (err) => {
66
+ cds.log(COMPONENT_NAME).error(`error from redis client for pub/sub failed for channel ${channel}`, err);
67
+ subscriberChannelClientPromise[channel] = null;
68
+ setTimeout(() => subscribeRedisChannel(channel, subscribeCb), 5 * 1000).unref();
69
+ };
70
+ subscriberChannelClientPromise[channel] = createClientAndConnect(errorHandlerCreateClient);
71
+ subscriberChannelClientPromise[channel]
72
+ .then((client) => {
73
+ cds.log(COMPONENT_NAME).info("subscribe redis client connected channel", { channel });
74
+ client.subscribe(channel, subscribeCb);
75
+ })
76
+ .catch((err) => {
77
+ cds
78
+ .log(COMPONENT_NAME)
79
+ .error(`error from redis client for pub/sub failed during startup - trying to reconnect - ${channel}`, err);
80
+ });
81
+ };
82
+
83
+ const publishMessage = async (channel, message) => {
84
+ const client = await createMainClientAndConnect();
85
+ return await client.publish(channel, message);
86
+ };
87
+
63
88
  const _localReconnectStrategy = () => EventQueueError.redisNoReconnect();
64
89
 
65
90
  module.exports = {
66
91
  createClientAndConnect,
67
92
  createMainClientAndConnect,
93
+ subscribeRedisChannel,
94
+ publishMessage,
68
95
  };