@cap-js-community/event-queue 0.1.49 → 0.1.51

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/src/runner.js CHANGED
@@ -3,11 +3,11 @@
3
3
  const uuid = require("uuid");
4
4
 
5
5
  const eventQueueConfig = require("./config");
6
- const cdsHelper = require("./shared/cdsHelper");
7
6
  const { eventQueueRunner } = require("./processEventQueue");
8
- const distributedLock = require("./shared/distributedLock");
9
7
  const { getWorkerPoolInstance } = require("./shared/WorkerQueue");
10
- const { getConfigInstance } = require("./config");
8
+ const cdsHelper = require("./shared/cdsHelper");
9
+ const distributedLock = require("./shared/distributedLock");
10
+ const SetIntervalDriftSafe = require("./shared/SetIntervalDriftSafe");
11
11
 
12
12
  const COMPONENT_NAME = "eventQueue/runner";
13
13
  const EVENT_QUEUE_RUN_ID = "EVENT_QUEUE_RUN_ID";
@@ -26,17 +26,13 @@ const _scheduleFunction = async (fn) => {
26
26
  const configInstance = eventQueueConfig.getConfigInstance();
27
27
  const eventsForAutomaticRun = configInstance.events;
28
28
  if (!eventsForAutomaticRun.length) {
29
- LOGGER.warn(
30
- "no events for automatic run are configured - skipping runner registration"
31
- );
29
+ LOGGER.warn("no events for automatic run are configured - skipping runner registration");
32
30
  return;
33
31
  }
34
32
 
35
33
  const fnWithRunningCheck = () => {
36
34
  if (configInstance.isRunnerDeactivated) {
37
- LOGGER.info(
38
- "runner is deactivated via config variable. Skipping this run."
39
- );
35
+ LOGGER.info("runner is deactivated via config variable. Skipping this run.");
40
36
  return;
41
37
  }
42
38
  return fn();
@@ -45,14 +41,13 @@ const _scheduleFunction = async (fn) => {
45
41
  const offsetDependingOnLastRun = await _calculateOffsetForFirstRun();
46
42
 
47
43
  LOGGER.info("first event-queue run scheduled", {
48
- firstRunScheduledFor: new Date(
49
- Date.now() + offsetDependingOnLastRun
50
- ).toISOString(),
44
+ firstRunScheduledFor: new Date(Date.now() + offsetDependingOnLastRun).toISOString(),
51
45
  });
52
46
 
53
47
  setTimeout(() => {
54
48
  fnWithRunningCheck();
55
- setInterval(fnWithRunningCheck, configInstance.runInterval).unref();
49
+ const intervalRunner = new SetIntervalDriftSafe(configInstance.runInterval);
50
+ intervalRunner.run(fnWithRunningCheck);
56
51
  }, offsetDependingOnLastRun).unref();
57
52
  };
58
53
 
@@ -72,9 +67,7 @@ const _multiTenancyRedis = async () => {
72
67
 
73
68
  const _multiTenancyDb = async () => {
74
69
  try {
75
- LOGGER.info(
76
- "executing event queue run for single instance and multi tenant"
77
- );
70
+ LOGGER.info("executing event queue run for single instance and multi tenant");
78
71
  const tenantIds = await cdsHelper.getAllTenantIds();
79
72
  _executeAllTenants(tenantIds, EVENT_QUEUE_RUN_ID);
80
73
  } catch (err) {
@@ -91,13 +84,9 @@ const _executeAllTenants = (tenantIds, runId) => {
91
84
  workerQueueInstance.addToQueue(async () => {
92
85
  try {
93
86
  const tenantContext = new cds.EventContext({ tenant: tenantId });
94
- const couldAcquireLock = await distributedLock.acquireLock(
95
- tenantContext,
96
- runId,
97
- {
98
- expiryTime: configInstance.runInterval * 0.95,
99
- }
100
- );
87
+ const couldAcquireLock = await distributedLock.acquireLock(tenantContext, runId, {
88
+ expiryTime: configInstance.runInterval * 0.95,
89
+ });
101
90
  if (!couldAcquireLock) {
102
91
  return;
103
92
  }
@@ -129,55 +118,38 @@ const _executeRunForTenant = async (tenantId, runId) => {
129
118
  });
130
119
  await eventQueueRunner(context, eventsForAutomaticRun);
131
120
  } catch (err) {
132
- LOGGER.error(
133
- `Couldn't process eventQueue for tenant! Next try after defined interval. Error: ${err}`,
134
- {
135
- tenantId,
136
- redisEnabled: configInstance.redisEnabled,
137
- }
138
- );
121
+ LOGGER.error(`Couldn't process eventQueue for tenant! Next try after defined interval. Error: ${err}`, {
122
+ tenantId,
123
+ redisEnabled: configInstance.redisEnabled,
124
+ });
139
125
  }
140
126
  };
141
127
 
142
128
  const _acquireRunId = async (context) => {
143
129
  const configInstance = eventQueueConfig.getConfigInstance();
144
130
  let runId = uuid.v4();
145
- const couldSetValue = await distributedLock.setValueWithExpire(
146
- context,
147
- EVENT_QUEUE_RUN_ID,
148
- runId,
149
- {
150
- tenantScoped: false,
151
- expiryTime: configInstance.runInterval * 0.95,
152
- }
153
- );
131
+ const couldSetValue = await distributedLock.setValueWithExpire(context, EVENT_QUEUE_RUN_ID, runId, {
132
+ tenantScoped: false,
133
+ expiryTime: configInstance.runInterval * 0.95,
134
+ });
154
135
 
155
136
  if (couldSetValue) {
156
- await distributedLock.setValueWithExpire(
157
- context,
158
- EVENT_QUEUE_RUN_TS,
159
- new Date().toISOString(),
160
- {
161
- tenantScoped: false,
162
- expiryTime: configInstance.runInterval,
163
- overrideValue: true,
164
- }
165
- );
137
+ await distributedLock.setValueWithExpire(context, EVENT_QUEUE_RUN_TS, new Date().toISOString(), {
138
+ tenantScoped: false,
139
+ expiryTime: configInstance.runInterval,
140
+ overrideValue: true,
141
+ });
166
142
  } else {
167
- runId = await distributedLock.checkLockExistsAndReturnValue(
168
- context,
169
- EVENT_QUEUE_RUN_ID,
170
- {
171
- tenantScoped: false,
172
- }
173
- );
143
+ runId = await distributedLock.checkLockExistsAndReturnValue(context, EVENT_QUEUE_RUN_ID, {
144
+ tenantScoped: false,
145
+ });
174
146
  }
175
147
 
176
148
  return runId;
177
149
  };
178
150
 
179
151
  const _calculateOffsetForFirstRun = async () => {
180
- const configInstance = getConfigInstance();
152
+ const configInstance = eventQueueConfig.getConfigInstance();
181
153
  let offsetDependingOnLastRun = OFFSET_FIRST_RUN;
182
154
  const now = Date.now();
183
155
  // NOTE: this is only supported with Redis, because this is a tenant agnostic information
@@ -185,34 +157,24 @@ const _calculateOffsetForFirstRun = async () => {
185
157
  try {
186
158
  if (configInstance.redisEnabled) {
187
159
  const dummyContext = new cds.EventContext({});
188
- let lastRunTs = await distributedLock.checkLockExistsAndReturnValue(
189
- dummyContext,
190
- EVENT_QUEUE_RUN_TS,
191
- { tenantScoped: false }
192
- );
160
+ let lastRunTs = await distributedLock.checkLockExistsAndReturnValue(dummyContext, EVENT_QUEUE_RUN_TS, {
161
+ tenantScoped: false,
162
+ });
193
163
  if (!lastRunTs) {
194
164
  const ts = new Date(now).toISOString();
195
- const couldSetValue = await distributedLock.setValueWithExpire(
196
- dummyContext,
197
- EVENT_QUEUE_RUN_TS,
198
- ts,
199
- {
200
- tenantScoped: false,
201
- expiryTime: configInstance.runInterval,
202
- }
203
- );
165
+ const couldSetValue = await distributedLock.setValueWithExpire(dummyContext, EVENT_QUEUE_RUN_TS, ts, {
166
+ tenantScoped: false,
167
+ expiryTime: configInstance.runInterval,
168
+ });
204
169
  if (couldSetValue) {
205
170
  lastRunTs = ts;
206
171
  } else {
207
- lastRunTs = await distributedLock.checkLockExistsAndReturnValue(
208
- dummyContext,
209
- EVENT_QUEUE_RUN_TS,
210
- { tenantScoped: false }
211
- );
172
+ lastRunTs = await distributedLock.checkLockExistsAndReturnValue(dummyContext, EVENT_QUEUE_RUN_TS, {
173
+ tenantScoped: false,
174
+ });
212
175
  }
213
176
  }
214
- offsetDependingOnLastRun =
215
- new Date(lastRunTs).getTime() + configInstance.runInterval - now;
177
+ offsetDependingOnLastRun = new Date(lastRunTs).getTime() + configInstance.runInterval - now;
216
178
  }
217
179
  } catch (err) {
218
180
  LOGGER.error(
@@ -36,9 +36,7 @@ class PerformanceTracer {
36
36
  //determine, if an options object was provided as first argument
37
37
  if (
38
38
  typeof args?.[0] === "object" &&
39
- (args[0].quantity >= 0 ||
40
- args[0].threshold > 0 ||
41
- args[0].additionalQuantityThreshold > 0)
39
+ (args[0].quantity >= 0 || args[0].threshold > 0 || args[0].additionalQuantityThreshold > 0)
42
40
  ) {
43
41
  options = args.shift();
44
42
  }
@@ -0,0 +1,37 @@
1
+ "use strict";
2
+
3
+ const COMPONENT = "eventQueue/SetIntervalDriftSafe";
4
+
5
+ class SetIntervalDriftSafe {
6
+ #adjustedInterval;
7
+ #interval;
8
+ #expectedCycleTime = 0;
9
+ #nextTickScheduledFor;
10
+ #logger;
11
+
12
+ constructor(interval) {
13
+ this.#interval = interval;
14
+ this.#adjustedInterval = interval;
15
+ this.#logger = cds.log(COMPONENT);
16
+ }
17
+
18
+ run(fn) {
19
+ const now = Date.now();
20
+ if (this.#expectedCycleTime === 0) {
21
+ this.#expectedCycleTime = now + this.#interval;
22
+ } else if (now + this.#interval - this.#nextTickScheduledFor < this.#interval) {
23
+ this.#logger.info("overlapping ticks, skipping this run");
24
+ return;
25
+ } else {
26
+ this.#adjustedInterval = this.#interval - (now - this.#expectedCycleTime);
27
+ this.#expectedCycleTime += this.#interval;
28
+ }
29
+ this.#nextTickScheduledFor = now + this.#adjustedInterval;
30
+ setTimeout(() => {
31
+ this.run(fn);
32
+ fn();
33
+ }, this.#adjustedInterval);
34
+ }
35
+ }
36
+
37
+ module.exports = SetIntervalDriftSafe;
@@ -32,31 +32,20 @@ class WorkerQueue {
32
32
  this.__runningPromises.push(promise);
33
33
  promise
34
34
  .finally(() => {
35
- this.__runningPromises.splice(
36
- this.__runningPromises.indexOf(promise),
37
- 1
38
- );
35
+ this.__runningPromises.splice(this.__runningPromises.indexOf(promise), 1);
39
36
  this._checkForNext();
40
37
  })
41
38
  .then((...results) => {
42
39
  resolve(...results);
43
40
  })
44
41
  .catch((err) => {
45
- cds
46
- .log(COMPONENT_NAME)
47
- .error(
48
- "Error happened in WorkQueue. Errors should be caught before! Error:",
49
- err
50
- );
42
+ cds.log(COMPONENT_NAME).error("Error happened in WorkQueue. Errors should be caught before! Error:", err);
51
43
  reject(err);
52
44
  });
53
45
  }
54
46
 
55
47
  _checkForNext() {
56
- if (
57
- !this.__queue.length ||
58
- this.__runningPromises.length >= this.__concurrencyLimit
59
- ) {
48
+ if (!this.__queue.length || this.__runningPromises.length >= this.__concurrencyLimit) {
60
49
  return;
61
50
  }
62
51
  const [cb, resolve, reject] = this.__queue.shift();
@@ -20,13 +20,7 @@ const COMPONENT_NAME = "eventQueue/cdsHelper";
20
20
  * @param info {object} Additional information object attached to logging
21
21
  * @returns {Promise<boolean>} Promise resolving to true if everything worked fine / false if an error occurred
22
22
  */
23
- async function executeInNewTransaction(
24
- context = {},
25
- transactionTag,
26
- fn,
27
- args,
28
- { info = {} } = {}
29
- ) {
23
+ async function executeInNewTransaction(context = {}, transactionTag, fn, args, { info = {} } = {}) {
30
24
  const parameters = Array.isArray(args) ? args : [args];
31
25
  const logger = cds.log(COMPONENT_NAME);
32
26
  try {
@@ -48,10 +42,7 @@ async function executeInNewTransaction(
48
42
  } else {
49
43
  const contextTx = cds.tx(context);
50
44
  const contextTxState = contextTx.ready;
51
- if (
52
- !contextTxState ||
53
- ["committed", "rolled back"].includes(contextTxState)
54
- ) {
45
+ if (!contextTxState || ["committed", "rolled back"].includes(contextTxState)) {
55
46
  await cds.tx(
56
47
  {
57
48
  id: context.id,
@@ -56,10 +56,7 @@ class Funnel {
56
56
  }
57
57
 
58
58
  // map function call to promise
59
- const p =
60
- f.constructor.name === "AsyncFunction"
61
- ? f(...args)
62
- : Promise.resolve().then(() => f(...args));
59
+ const p = f.constructor.name === "AsyncFunction" ? f(...args) : Promise.resolve().then(() => f(...args));
63
60
 
64
61
  // create promise for book keeping
65
62
  const workload = p.finally(() => {
@@ -100,9 +97,7 @@ const limiter = async (limit, payloads, iterator) => {
100
97
  returnPromises.push(p);
101
98
 
102
99
  if (limit <= payloads.length) {
103
- const e = p
104
- .catch(() => {})
105
- .finally(() => runningPromises.splice(runningPromises.indexOf(e), 1));
100
+ const e = p.catch(() => {}).finally(() => runningPromises.splice(runningPromises.indexOf(e), 1));
106
101
  runningPromises.push(e);
107
102
  if (limit <= runningPromises.length) {
108
103
  await Promise.race(runningPromises);
@@ -8,10 +8,7 @@ const { getConfigInstance } = require("../config");
8
8
  const acquireLock = async (
9
9
  context,
10
10
  key,
11
- {
12
- tenantScoped = true,
13
- expiryTime = config.getConfigInstance().globalTxTimeout,
14
- } = {}
11
+ { tenantScoped = true, expiryTime = config.getConfigInstance().globalTxTimeout } = {}
15
12
  ) => {
16
13
  const fullKey = _generateKey(context, tenantScoped, key);
17
14
  if (config.getConfigInstance().redisEnabled) {
@@ -25,11 +22,7 @@ const setValueWithExpire = async (
25
22
  context,
26
23
  key,
27
24
  value,
28
- {
29
- tenantScoped = true,
30
- expiryTime = config.getConfigInstance().globalTxTimeout,
31
- overrideValue = false,
32
- } = {}
25
+ { tenantScoped = true, expiryTime = config.getConfigInstance().globalTxTimeout, overrideValue = false } = {}
33
26
  ) => {
34
27
  const fullKey = _generateKey(context, tenantScoped, key);
35
28
  if (config.getConfigInstance().redisEnabled) {
@@ -54,11 +47,7 @@ const releaseLock = async (context, key, { tenantScoped = true } = {}) => {
54
47
  }
55
48
  };
56
49
 
57
- const checkLockExistsAndReturnValue = async (
58
- context,
59
- key,
60
- { tenantScoped = true } = {}
61
- ) => {
50
+ const checkLockExistsAndReturnValue = async (context, key, { tenantScoped = true } = {}) => {
62
51
  const fullKey = _generateKey(context, tenantScoped, key);
63
52
  if (config.getConfigInstance().redisEnabled) {
64
53
  return await _checkLockExistsRedis(context, fullKey);
@@ -67,12 +56,7 @@ const checkLockExistsAndReturnValue = async (
67
56
  }
68
57
  };
69
58
 
70
- const _acquireLockRedis = async (
71
- context,
72
- fullKey,
73
- expiryTime,
74
- { value = "true", overrideValue = false } = {}
75
- ) => {
59
+ const _acquireLockRedis = async (context, fullKey, expiryTime, { value = "true", overrideValue = false } = {}) => {
76
60
  const client = await redis.createMainClientAndConnect();
77
61
  const result = await client.set(fullKey, value, {
78
62
  PX: expiryTime,
@@ -89,17 +73,9 @@ const _checkLockExistsRedis = async (context, fullKey) => {
89
73
  const _checkLockExistsDb = async (context, fullKey) => {
90
74
  let result;
91
75
  const configInstance = getConfigInstance();
92
- await cdsHelper.executeInNewTransaction(
93
- context,
94
- "distributedLock-checkExists",
95
- async (tx) => {
96
- result = await tx.run(
97
- SELECT.one
98
- .from(configInstance.tableNameEventLock)
99
- .where("code =", fullKey)
100
- );
101
- }
102
- );
76
+ await cdsHelper.executeInNewTransaction(context, "distributedLock-checkExists", async (tx) => {
77
+ result = await tx.run(SELECT.one.from(configInstance.tableNameEventLock).where("code =", fullKey));
78
+ });
103
79
  return result?.value;
104
80
  };
105
81
 
@@ -110,69 +86,49 @@ const _releaseLockRedis = async (context, fullKey) => {
110
86
 
111
87
  const _releaseLockDb = async (context, fullKey) => {
112
88
  const configInstance = getConfigInstance();
113
- await cdsHelper.executeInNewTransaction(
114
- context,
115
- "distributedLock-release",
116
- async (tx) => {
117
- await tx.run(
118
- DELETE.from(configInstance.tableNameEventLock).where("code =", fullKey)
119
- );
120
- }
121
- );
89
+ await cdsHelper.executeInNewTransaction(context, "distributedLock-release", async (tx) => {
90
+ await tx.run(DELETE.from(configInstance.tableNameEventLock).where("code =", fullKey));
91
+ });
122
92
  };
123
93
 
124
- const _acquireLockDB = async (
125
- context,
126
- fullKey,
127
- expiryTime,
128
- { value = "true", overrideValue = false } = {}
129
- ) => {
94
+ const _acquireLockDB = async (context, fullKey, expiryTime, { value = "true", overrideValue = false } = {}) => {
130
95
  let result;
131
96
  const configInstance = getConfigInstance();
132
- await cdsHelper.executeInNewTransaction(
133
- context,
134
- "distributedLock-acquire",
135
- async (tx) => {
136
- try {
97
+ await cdsHelper.executeInNewTransaction(context, "distributedLock-acquire", async (tx) => {
98
+ try {
99
+ await tx.run(
100
+ INSERT.into(configInstance.tableNameEventLock).entries({
101
+ code: fullKey,
102
+ value,
103
+ })
104
+ );
105
+ result = true;
106
+ } catch (err) {
107
+ let currentEntry;
108
+
109
+ if (!overrideValue) {
110
+ currentEntry = await tx.run(
111
+ SELECT.one
112
+ .from(configInstance.tableNameEventLock)
113
+ .forUpdate({ wait: config.getConfigInstance().forUpdateTimeout })
114
+ .where("code =", fullKey)
115
+ );
116
+ }
117
+ if (overrideValue || (currentEntry && new Date(currentEntry.createdAt).getTime() + expiryTime <= Date.now())) {
137
118
  await tx.run(
138
- INSERT.into(configInstance.tableNameEventLock).entries({
139
- code: fullKey,
140
- value,
141
- })
119
+ UPDATE.entity(configInstance.tableNameEventLock)
120
+ .set({
121
+ createdAt: new Date().toISOString(),
122
+ value,
123
+ })
124
+ .where("code =", currentEntry.code)
142
125
  );
143
126
  result = true;
144
- } catch (err) {
145
- let currentEntry;
146
-
147
- if (!overrideValue) {
148
- currentEntry = await tx.run(
149
- SELECT.one
150
- .from(configInstance.tableNameEventLock)
151
- .forUpdate({ wait: config.getConfigInstance().forUpdateTimeout })
152
- .where("code =", fullKey)
153
- );
154
- }
155
- if (
156
- overrideValue ||
157
- (currentEntry &&
158
- new Date(currentEntry.createdAt).getTime() + expiryTime <=
159
- Date.now())
160
- ) {
161
- await tx.run(
162
- UPDATE.entity(configInstance.tableNameEventLock)
163
- .set({
164
- createdAt: new Date().toISOString(),
165
- value,
166
- })
167
- .where("code =", currentEntry.code)
168
- );
169
- result = true;
170
- } else {
171
- result = false;
172
- }
127
+ } else {
128
+ result = false;
173
129
  }
174
130
  }
175
- );
131
+ });
176
132
  return result;
177
133
  };
178
134
 
@@ -15,9 +15,7 @@ const createMainClientAndConnect = () => {
15
15
  }
16
16
 
17
17
  const errorHandlerCreateClient = (err) => {
18
- cds
19
- .log(COMPONENT_NAME)
20
- .error("error from redis client for pub/sub failed", err);
18
+ cds.log(COMPONENT_NAME).error("error from redis client for pub/sub failed", err);
21
19
  subscriberClientPromise = null;
22
20
  setTimeout(createMainClientAndConnect, 5 * 1000).unref();
23
21
  };