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

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.3",
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
@@ -28,6 +28,7 @@ const DEFAULT_CHECK_FOR_NEXT_CHUNK = true;
28
28
  const SUFFIX_PERIODIC = "_PERIODIC";
29
29
  const CAP_EVENT_TYPE = "CAP_OUTBOX";
30
30
  const CAP_PARALLEL_DEFAULT = 5;
31
+ const CAP_MAX_ATTEMPTS_DEFAULT = 5;
31
32
  const DELETE_TENANT_BLOCK_AFTER_MS = 5 * 60 * 1000;
32
33
  const PRIORITIES = Object.values(Priorities);
33
34
  const UTC_DEFAULT = false;
@@ -387,7 +388,7 @@ class Config {
387
388
  kind: config.kind ?? "persistent-outbox",
388
389
  selectMaxChunkSize: config.selectMaxChunkSize ?? config.chunkSize,
389
390
  parallelEventProcessing: config.parallelEventProcessing ?? (config.parallel && CAP_PARALLEL_DEFAULT),
390
- retryAttempts: config.retryAttempts ?? config.maxAttempts,
391
+ retryAttempts: config.retryAttempts ?? config.maxAttempts ?? CAP_MAX_ATTEMPTS_DEFAULT,
391
392
  ...config,
392
393
  });
393
394
  eventConfig.internalEvent = true;
@@ -550,7 +551,7 @@ class Config {
550
551
  event.load = event.load ?? DEFAULT_LOAD;
551
552
  event.priority = event.priority ?? DEFAULT_PRIORITY;
552
553
  event.increasePriorityOverTime = event.increasePriorityOverTime ?? DEFAULT_INCREASE_PRIORITY;
553
- event.keepAliveInterval = (event.keepAliveInterval ?? DEFAULT_KEEP_ALIVE_INTERVAL) * 1000;
554
+ event.keepAliveInterval = event.keepAliveInterval ?? DEFAULT_KEEP_ALIVE_INTERVAL;
554
555
  event.keepAliveMaxInProgressTime = event.keepAliveInterval * DEFAULT_MAX_FACTOR_STUCK_2_KEEP_ALIVE_INTERVAL;
555
556
  event.checkForNextChunk = event.checkForNextChunk ?? DEFAULT_CHECK_FOR_NEXT_CHUNK;
556
557
  }
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;
@@ -61,7 +61,7 @@ class EventQueueGenericOutboxHandler extends EventQueueBaseClass {
61
61
  }
62
62
  } else {
63
63
  for (const actionName in genericClusterEvents) {
64
- const msg = new cds.Request({
64
+ const reg = new cds.Request({
65
65
  event: EVENT_QUEUE_ACTIONS.CLUSTER,
66
66
  user: this.context.user,
67
67
  eventQueue: {
@@ -74,14 +74,14 @@ class EventQueueGenericOutboxHandler extends EventQueueBaseClass {
74
74
  this.#clusterByDataProperty(actionName, genericClusterEvents[actionName], propertyName, cb),
75
75
  },
76
76
  });
77
- const clusterResult = await this.__srvUnboxed.tx(this.context).send(msg);
77
+ const clusterResult = await this.__srvUnboxed.tx(this.context).send(reg);
78
78
  if (this.#validateCluster(clusterResult)) {
79
79
  Object.assign(clusterMap, clusterResult);
80
80
  } else {
81
81
  this.logger.error(
82
82
  "cluster result of handler is not valid. Check the documentation for the expected structure. Continuing without clustering!",
83
83
  {
84
- handler: msg.event,
84
+ handler: reg.event,
85
85
  clusterResult: JSON.stringify(clusterResult),
86
86
  }
87
87
  );
@@ -92,7 +92,7 @@ class EventQueueGenericOutboxHandler extends EventQueueBaseClass {
92
92
  }
93
93
 
94
94
  for (const actionName in specificClusterEvents) {
95
- const msg = new cds.Request({
95
+ const reg = new cds.Request({
96
96
  event: `${EVENT_QUEUE_ACTIONS.CLUSTER}.${actionName}`,
97
97
  user: this.context.user,
98
98
  eventQueue: {
@@ -105,14 +105,14 @@ class EventQueueGenericOutboxHandler extends EventQueueBaseClass {
105
105
  this.#clusterByDataProperty(actionName, specificClusterEvents[actionName], propertyName, cb),
106
106
  },
107
107
  });
108
- const clusterResult = await this.__srvUnboxed.tx(this.context).send(msg);
108
+ const clusterResult = await this.__srvUnboxed.tx(this.context).send(reg);
109
109
  if (this.#validateCluster(clusterResult)) {
110
110
  Object.assign(clusterMap, clusterResult);
111
111
  } else {
112
112
  this.logger.error(
113
113
  "cluster result of handler is not valid. Check the documentation for the expected structure. Continuing without clustering!",
114
114
  {
115
- handler: msg.event,
115
+ handler: reg.event,
116
116
  clusterResult: JSON.stringify(clusterResult),
117
117
  }
118
118
  );
@@ -264,12 +264,12 @@ class EventQueueGenericOutboxHandler extends EventQueueBaseClass {
264
264
  return payload;
265
265
  }
266
266
 
267
- const { msg, userId } = this.#buildDispatchData(this.context, payload, {
267
+ const { reg, userId } = this.#buildDispatchData(this.context, payload, {
268
268
  queueEntries: [queueEntry],
269
269
  });
270
- msg.event = handlerName;
271
- await this.#setContextUser(this.context, userId, msg);
272
- const data = await this.__srvUnboxed.tx(this.context).send(msg);
270
+ reg.event = handlerName;
271
+ await this.#setContextUser(this.context, userId, reg);
272
+ const data = await this.__srvUnboxed.tx(this.context).send(reg);
273
273
  if (data) {
274
274
  payload.data = data;
275
275
  return payload;
@@ -285,12 +285,12 @@ class EventQueueGenericOutboxHandler extends EventQueueBaseClass {
285
285
  return await super.hookForExceededEvents(exceededEvent);
286
286
  }
287
287
 
288
- const { msg, userId } = this.#buildDispatchData(this.context, exceededEvent.payload, {
288
+ const { reg, userId } = this.#buildDispatchData(this.context, exceededEvent.payload, {
289
289
  queueEntries: [exceededEvent],
290
290
  });
291
- await this.#setContextUser(this.context, userId, msg);
292
- msg.event = handlerName;
293
- await this.__srvUnboxed.tx(this.context).send(msg);
291
+ await this.#setContextUser(this.context, userId, reg);
292
+ reg.event = handlerName;
293
+ await this.__srvUnboxed.tx(this.context).send(reg);
294
294
  }
295
295
 
296
296
  // NOTE: Currently not exposed to CAP service; we wait for a valid use case
@@ -310,37 +310,37 @@ class EventQueueGenericOutboxHandler extends EventQueueBaseClass {
310
310
 
311
311
  async processPeriodicEvent(processContext, key, queueEntry) {
312
312
  const [, action] = this.eventSubType.split(".");
313
- const msg = new cds.Event({ event: action, eventQueue: { processor: this, key, queueEntries: [queueEntry] } });
314
- await this.#setContextUser(processContext, config.userId, msg);
315
- await this.__srvUnboxed.tx(processContext).emit(msg);
313
+ const reg = new cds.Event({ event: action, eventQueue: { processor: this, key, queueEntries: [queueEntry] } });
314
+ await this.#setContextUser(processContext, config.userId, reg);
315
+ await this.__srvUnboxed.tx(processContext).emit(reg);
316
316
  }
317
317
 
318
318
  #buildDispatchData(context, payload, { key, queueEntries } = {}) {
319
319
  const { useEventQueueUser } = this.eventConfig;
320
320
  const userId = useEventQueueUser ? config.userId : payload.contextUser;
321
- const msg = payload._fromSend ? new cds.Request(payload) : new cds.Event(payload);
321
+ const reg = payload._fromSend ? new cds.Request(payload) : new cds.Event(payload);
322
322
  const invocationFn = payload._fromSend ? "send" : "emit";
323
- delete msg._fromSend; // TODO: this changes the source object --> check after multiple invocations
324
- delete msg.contextUser;
325
- msg.eventQueue = { processor: this, key, queueEntries, payload };
326
- return { msg, userId, invocationFn };
323
+ delete reg._fromSend;
324
+ delete reg.contextUser;
325
+ reg.eventQueue = { processor: this, key, queueEntries, payload };
326
+ return { reg, userId, invocationFn };
327
327
  }
328
328
 
329
- async #setContextUser(context, userId, data) {
329
+ async #setContextUser(context, userId, reg) {
330
330
  context.user = new cds.User.Privileged({
331
331
  id: userId,
332
332
  tokenInfo: await common.getTokenInfo(this.baseContext.tenant),
333
333
  });
334
- if (data) {
335
- data.user = context.user;
334
+ if (reg) {
335
+ reg.user = context.user;
336
336
  }
337
337
  }
338
338
 
339
339
  async processEvent(processContext, key, queueEntries, payload) {
340
340
  try {
341
- const { userId, invocationFn, msg } = this.#buildDispatchData(processContext, payload, { key, queueEntries });
342
- await this.#setContextUser(processContext, userId, msg);
343
- const result = await this.__srvUnboxed.tx(processContext)[invocationFn](msg);
341
+ const { userId, invocationFn, reg } = this.#buildDispatchData(processContext, payload, { key, queueEntries });
342
+ await this.#setContextUser(processContext, userId, reg);
343
+ const result = await this.__srvUnboxed.tx(processContext)[invocationFn](reg);
344
344
  return this.#determineResultStatus(result, queueEntries);
345
345
  } catch (err) {
346
346
  this.logger.error("error processing outboxed service call", err, {
@@ -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
  };
@@ -108,7 +108,7 @@ const _renewLockRedis = async (context, fullKey, expiryTime, { value = "true" }
108
108
 
109
109
  const _checkLockExistsRedis = async (context, fullKey) => {
110
110
  const client = await redis.createMainClientAndConnect(config.redisOptions);
111
- return await client.get(fullKey);
111
+ return await client.exists(fullKey);
112
112
  };
113
113
 
114
114
  const _checkLockExistsDb = async (context, fullKey) => {
@@ -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,60 @@ const _generateKey = (context, tenantScoped, key) => {
181
190
  return `${keyParts.join("##")}`;
182
191
  };
183
192
 
193
+ const getAllLocksRedis = async () => {
194
+ const clientOrCluster = await redis.createMainClientAndConnect(config.redisOptions);
195
+ const output = [];
196
+ const results = [];
197
+
198
+ let clients;
199
+ if (redis.isClusterMode()) {
200
+ clients = clientOrCluster.masters.map((master) => master.client);
201
+ } else {
202
+ clients = [clientOrCluster];
203
+ }
204
+
205
+ // NOTE: use SCAN because KEYS is not supported for cluster clients
206
+ for (const client of clients) {
207
+ for await (const key of client.scanIterator({ MATCH: "EVENT*", COUNT: 1000 })) {
208
+ const [, tenant, guidOrType, subType] = key.split("##");
209
+ if (!subType) {
210
+ continue;
211
+ }
212
+
213
+ const pipeline = client.multi();
214
+ output.push({
215
+ tenant: tenant,
216
+ type: guidOrType,
217
+ subType: subType,
218
+ });
219
+ pipeline.ttl(key).get(key);
220
+ const replies = await pipeline.exec();
221
+ results.push(...replies);
222
+ }
223
+ }
224
+
225
+ let counter = 0;
226
+ for (const row of output) {
227
+ const ttl = results[counter];
228
+ const createdAt = results[counter + 1];
229
+ Object.assign(row, { ttl, createdAt });
230
+ counter = counter + 2;
231
+ }
232
+ return output;
233
+ };
234
+
184
235
  const shutdownHandler = async () => {
185
236
  const logger = cds.log(COMPONENT_NAME);
186
237
  logger.info("received shutdown event, trying to release all locks", {
187
238
  numberOfLocks: Object.keys(existingLocks).length,
188
239
  });
189
240
  const result = await Promise.allSettled(
190
- Object.keys(existingLocks).map(async (key) => {
191
- await _releaseLockRedis(null, key);
241
+ Object.entries(existingLocks).map(async ([key, tenant]) => {
242
+ if (config.redisEnabled) {
243
+ await _releaseLockRedis({ tenant }, key);
244
+ } else {
245
+ await _releaseLockDb({ tenant }, key);
246
+ }
192
247
  logger.info("lock released", { key });
193
248
  })
194
249
  );
@@ -206,4 +261,5 @@ module.exports = {
206
261
  setValueWithExpire,
207
262
  shutdownHandler,
208
263
  renewLock,
264
+ getAllLocksRedis,
209
265
  };
@@ -178,6 +178,15 @@ const connectionCheck = async (options) => {
178
178
  });
179
179
  };
180
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
+
181
190
  module.exports = {
182
191
  createClientAndConnect,
183
192
  createMainClientAndConnect,
@@ -186,4 +195,5 @@ module.exports = {
186
195
  closeMainClient,
187
196
  closeSubscribeClient,
188
197
  connectionCheck,
198
+ isClusterMode,
189
199
  };
@@ -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 ?? [];