@cap-js-community/event-queue 1.11.0-beta.1 → 1.11.0-beta.2

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.11.0-beta.1",
3
+ "version": "1.11.0-beta.2",
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",
@@ -76,7 +76,7 @@ class EventQueueProcessorBase {
76
76
  this.__txMap = {};
77
77
  this.__txRollback = {};
78
78
  this.__queueEntries = [];
79
- this.#keepAliveRunner = new SetIntervalDriftSafe(this.#eventConfig.keepAliveInterval);
79
+ this.#keepAliveRunner = new SetIntervalDriftSafe(this.#eventConfig.keepAliveInterval * 1000);
80
80
  }
81
81
 
82
82
  /**
@@ -617,7 +617,7 @@ class EventQueueProcessorBase {
617
617
  "OR lastAttemptTimestamp IS NULL ) OR ( status =",
618
618
  EventProcessingStatus.InProgress,
619
619
  "AND lastAttemptTimestamp <=",
620
- new Date(baseDate - this.#eventConfig.keepAliveMaxInProgressTime).toISOString(),
620
+ new Date(baseDate - this.#eventConfig.keepAliveMaxInProgressTime * 1000).toISOString(),
621
621
  ") )",
622
622
  ]
623
623
  : [
@@ -628,7 +628,7 @@ class EventQueueProcessorBase {
628
628
  ") OR ( status =",
629
629
  EventProcessingStatus.InProgress,
630
630
  "AND lastAttemptTimestamp <=",
631
- new Date(baseDate - this.#eventConfig.keepAliveMaxInProgressTime).toISOString(),
631
+ new Date(baseDate - this.#eventConfig.keepAliveMaxInProgressTime * 1000).toISOString(),
632
632
  ") )",
633
633
  ])
634
634
  )
@@ -868,7 +868,7 @@ class EventQueueProcessorBase {
868
868
  }
869
869
 
870
870
  continuesKeepAlive() {
871
- if (Date.now() - this.lockAcquiredTime.getTime() >= this.#eventConfig.keepAliveInterval) {
871
+ if (Date.now() - this.lockAcquiredTime.getTime() >= this.#eventConfig.keepAliveInterval * 1000) {
872
872
  trace(this.baseContext, "keepAlive-between-iterations", async () => {
873
873
  await this.#renewDistributedLock();
874
874
  }).catch((err) => this.logger.error("renewing lock between intervals failed!", err));
@@ -961,7 +961,7 @@ class EventQueueProcessorBase {
961
961
  const lockAcquired = await distributedLock.acquireLock(
962
962
  this.__context,
963
963
  [this.#eventType, this.#eventSubType].join("##"),
964
- { keepTrackOfLock: true, expiryTime: this.#eventConfig.keepAliveMaxInProgressTime }
964
+ { keepTrackOfLock: true, expiryTime: this.#eventConfig.keepAliveMaxInProgressTime * 1000 }
965
965
  );
966
966
  if (!lockAcquired) {
967
967
  this.logger.debug("no lock available, exit processing", {
@@ -983,7 +983,7 @@ class EventQueueProcessorBase {
983
983
  const lockAcquired = await distributedLock.renewLock(
984
984
  this.__context,
985
985
  [this.#eventType, this.#eventSubType].join("##"),
986
- { expiryTime: this.#eventConfig.keepAliveMaxInProgressTime }
986
+ { expiryTime: this.#eventConfig.keepAliveMaxInProgressTime * 1000 }
987
987
  );
988
988
  if (!lockAcquired) {
989
989
  this.logger.error("renewing distributed lock failed!", {
package/src/config.js CHANGED
@@ -550,7 +550,7 @@ class Config {
550
550
  event.load = event.load ?? DEFAULT_LOAD;
551
551
  event.priority = event.priority ?? DEFAULT_PRIORITY;
552
552
  event.increasePriorityOverTime = event.increasePriorityOverTime ?? DEFAULT_INCREASE_PRIORITY;
553
- event.keepAliveInterval = (event.keepAliveInterval ?? DEFAULT_KEEP_ALIVE_INTERVAL) * 1000;
553
+ event.keepAliveInterval = event.keepAliveInterval ?? DEFAULT_KEEP_ALIVE_INTERVAL;
554
554
  event.keepAliveMaxInProgressTime = event.keepAliveInterval * DEFAULT_MAX_FACTOR_STUCK_2_KEEP_ALIVE_INTERVAL;
555
555
  event.checkForNextChunk = event.checkForNextChunk ?? DEFAULT_CHECK_FOR_NEXT_CHUNK;
556
556
  }
package/src/initialize.js CHANGED
@@ -202,9 +202,6 @@ const mixConfigVarsWithEnv = (options) => {
202
202
  };
203
203
 
204
204
  const registerCdsShutdown = () => {
205
- if (!config.developmentMode) {
206
- return;
207
- }
208
205
  cds.on("shutdown", async () => {
209
206
  return await new Promise((resolve) => {
210
207
  let timeoutRef;
@@ -17,7 +17,7 @@ const acquireLock = async (
17
17
  if (config.redisEnabled) {
18
18
  return await _acquireLockRedis(context, fullKey, expiryTime, { keepTrackOfLock });
19
19
  } else {
20
- return await _acquireLockDB(context, fullKey, expiryTime);
20
+ return await _acquireLockDB(context, fullKey, expiryTime, { keepTrackOfLock });
21
21
  }
22
22
  };
23
23
 
@@ -73,7 +73,7 @@ const _acquireLockRedis = async (
73
73
  context,
74
74
  fullKey,
75
75
  expiryTime,
76
- { value = "true", overrideValue = false, keepTrackOfLock } = {}
76
+ { value = Date.now(), overrideValue = false, keepTrackOfLock } = {}
77
77
  ) => {
78
78
  const client = await redis.createMainClientAndConnect(config.redisOptions);
79
79
  const result = await client.set(fullKey, value, {
@@ -82,7 +82,7 @@ const _acquireLockRedis = async (
82
82
  });
83
83
  const isOk = result === REDIS_COMMAND_OK;
84
84
  if (isOk && keepTrackOfLock) {
85
- existingLocks[fullKey] = 1;
85
+ existingLocks[fullKey] = context.tenant;
86
86
  }
87
87
  return isOk;
88
88
  };
@@ -129,9 +129,15 @@ const _releaseLockDb = async (context, fullKey) => {
129
129
  await cdsHelper.executeInNewTransaction(context, "distributedLock-release", async (tx) => {
130
130
  await tx.run(DELETE.from(config.tableNameEventLock).where("code =", fullKey));
131
131
  });
132
+ delete existingLocks[fullKey];
132
133
  };
133
134
 
134
- const _acquireLockDB = async (context, fullKey, expiryTime, { value = "true", overrideValue = false } = {}) => {
135
+ const _acquireLockDB = async (
136
+ context,
137
+ fullKey,
138
+ expiryTime,
139
+ { value = "true", overrideValue = false, keepTrackOfLock } = {}
140
+ ) => {
135
141
  let result;
136
142
  await cdsHelper.executeInNewTransaction(context, "distributedLock-acquire", async (tx) => {
137
143
  try {
@@ -171,6 +177,9 @@ const _acquireLockDB = async (context, fullKey, expiryTime, { value = "true", ov
171
177
  }
172
178
  }
173
179
  });
180
+ if (result && keepTrackOfLock) {
181
+ existingLocks[fullKey] = context.tenant;
182
+ }
174
183
  return result;
175
184
  };
176
185
 
@@ -181,14 +190,63 @@ const _generateKey = (context, tenantScoped, key) => {
181
190
  return `${keyParts.join("##")}`;
182
191
  };
183
192
 
193
+ const getAllLocksRedis = async () => {
194
+ const client = await redis.createMainClientAndConnect(config.redisOptions);
195
+ const batchSize = 500;
196
+ const results = [];
197
+ let pipeline = client.multi();
198
+ const output = [];
199
+ let count = 0;
200
+
201
+ // NOTE: use SCAN because KEYS is not supported for cluster clients
202
+ for await (const key of client.scanIterator({ MATCH: "EVENT*", COUNT: 1000 })) {
203
+ const [, tenant, guidOrType, subType] = key.split("##");
204
+ if (!subType) {
205
+ continue;
206
+ }
207
+
208
+ output.push({
209
+ tenant: tenant,
210
+ type: guidOrType,
211
+ subType: subType,
212
+ });
213
+ pipeline.ttl(key).get(key);
214
+ count++;
215
+
216
+ if (count >= batchSize) {
217
+ const replies = await pipeline.exec();
218
+ results.push(...replies);
219
+ pipeline = client.multi();
220
+ count = 0;
221
+ }
222
+ }
223
+ if (count > 0) {
224
+ const replies = await pipeline.exec();
225
+ results.push(...replies);
226
+ }
227
+
228
+ let counter = 0;
229
+ for (const row of output) {
230
+ const ttl = results[counter];
231
+ const createdAt = results[counter + 1];
232
+ Object.assign(row, { ttl, createdAt });
233
+ counter = counter + 2;
234
+ }
235
+ return output;
236
+ };
237
+
184
238
  const shutdownHandler = async () => {
185
239
  const logger = cds.log(COMPONENT_NAME);
186
240
  logger.info("received shutdown event, trying to release all locks", {
187
241
  numberOfLocks: Object.keys(existingLocks).length,
188
242
  });
189
243
  const result = await Promise.allSettled(
190
- Object.keys(existingLocks).map(async (key) => {
191
- await _releaseLockRedis(null, key);
244
+ Object.entries(existingLocks).map(async ([key, tenant]) => {
245
+ if (config.redisEnabled) {
246
+ await _releaseLockRedis({ tenant }, key);
247
+ } else {
248
+ await _releaseLockDb({ tenant }, key);
249
+ }
192
250
  logger.info("lock released", { key });
193
251
  })
194
252
  );
@@ -206,4 +264,5 @@ module.exports = {
206
264
  setValueWithExpire,
207
265
  shutdownHandler,
208
266
  renewLock,
267
+ getAllLocksRedis,
209
268
  };
@@ -22,6 +22,18 @@ service EventQueueAdminService {
22
22
  attempts: Integer) returns Event;
23
23
  }
24
24
 
25
+ @cds.persistence.skip
26
+ @readonly
27
+ entity Lock {
28
+ key tenant: String;
29
+ key type: String;
30
+ key subType: String;
31
+ landscape: String;
32
+ space: String;
33
+ ttl: Integer;
34
+ createdAt: Integer;
35
+ }
36
+
25
37
  @readonly
26
38
  @cds.persistence.skip
27
39
  entity Tenant {
@@ -4,10 +4,11 @@ const cds = require("@sap/cds");
4
4
  const cdsHelper = require("../../src/shared/cdsHelper");
5
5
  const { EventProcessingStatus } = require("../../src");
6
6
  const config = require("../../src/config");
7
+ const distributedLock = require("../../src/shared/distributedLock");
7
8
 
8
9
  module.exports = class AdminService extends cds.ApplicationService {
9
10
  async init() {
10
- const { Event: EventService, Tenant } = this.entities();
11
+ const { Event: EventService, Tenant, Lock: LockService } = this.entities();
11
12
  const { Event: EventDb } = cds.db.entities("sap.eventqueue");
12
13
  const { landscape, space } = this.getLandscapeAndSpace();
13
14
 
@@ -47,6 +48,18 @@ module.exports = class AdminService extends cds.ApplicationService {
47
48
  });
48
49
  });
49
50
 
51
+ this.on("READ", LockService, async () => {
52
+ if (!config.redisEnabled) {
53
+ return [];
54
+ }
55
+ const locks = await distributedLock.getAllLocksRedis();
56
+ return locks.map((lock) => ({
57
+ ...lock,
58
+ landscape: landscape,
59
+ space: space,
60
+ }));
61
+ });
62
+
50
63
  this.on("READ", Tenant, async () => {
51
64
  const tenants = await cdsHelper.getAllTenantWithSubdomain();
52
65
  return tenants ?? [];