@cap-js-community/event-queue 1.10.5 → 1.10.7

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/db/Lock.cds CHANGED
@@ -1,6 +1,5 @@
1
1
  namespace sap.eventqueue;
2
2
 
3
- using cuid from '@sap/cds/common';
4
3
  using managed from '@sap/cds/common';
5
4
 
6
5
  @assert.unique.semanticKey: [code]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cap-js-community/event-queue",
3
- "version": "1.10.5",
3
+ "version": "1.10.7",
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",
@@ -55,10 +55,10 @@
55
55
  },
56
56
  "devDependencies": {
57
57
  "@cap-js/cds-test": "^0.3.0",
58
- "@cap-js/hana": "^1.8.0",
59
- "@cap-js/sqlite": "^1.10.0",
60
- "@sap/cds": "^8.9.0",
61
- "@sap/cds-dk": "^8.8.0",
58
+ "@cap-js/hana": "^2.1.0",
59
+ "@cap-js/sqlite": "^2.0.1",
60
+ "@sap/cds": "^9.0.3",
61
+ "@sap/cds-dk": "^9.0.4",
62
62
  "eslint": "^8.57.0",
63
63
  "eslint-config-prettier": "^9.1.0",
64
64
  "eslint-plugin-jest": "^28.6.0",
@@ -1016,7 +1016,7 @@ class EventQueueProcessorBase {
1016
1016
 
1017
1017
  // NOTE: do not pass current date as we always want to calc. a future date
1018
1018
  const cronExpression = CronExpressionParser.parse(this.#eventConfig.cron, {
1019
- tz: eventConfig.tz,
1019
+ tz: this.#eventConfig.tz,
1020
1020
  });
1021
1021
  return cronExpression.next();
1022
1022
  }
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;
@@ -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, {
@@ -52,7 +52,7 @@ function outboxed(srv, customOpts) {
52
52
  outboxOpts = config.addCAPOutboxEventSpecificAction(srv.name, req.event);
53
53
  }
54
54
 
55
- if (outboxOpts.kind === "persistent-outbox") {
55
+ if (["persistent-outbox", "persistent-queue"].includes(outboxOpts.kind)) {
56
56
  await _mapToEventAndPublish(context, srv.name, req, !!specificSettings);
57
57
  return;
58
58
  }
@@ -7,9 +7,12 @@ const { EventProcessingStatus } = require("./constants");
7
7
  const { processChunkedSync } = require("./shared/common");
8
8
  const eventConfig = require("./config");
9
9
 
10
- const COMPONENT_NAME = "/eventQueue/periodicEvents";
11
10
  const CHUNK_SIZE_INSERT_PERIODIC_EVENTS = 4;
12
11
 
12
+ const ALLOWED_PERIODIC_SEC_DIFF = 30;
13
+
14
+ const COMPONENT_NAME = "/eventQueue/periodicEvents";
15
+
13
16
  const checkAndInsertPeriodicEvents = async (context) => {
14
17
  const now = new Date();
15
18
  const tx = cds.tx(context);
@@ -117,11 +120,16 @@ const _determineChangedCron = (existingEventsCron) => {
117
120
  const config = eventConfig.getEventConfig(event.type, event.subType);
118
121
  const eventStartAfter = new Date(event.startAfter);
119
122
  const eventCreatedAt = new Date(event.createdAt);
123
+ const randomOffset = config.randomOffset ?? eventConfig.randomOffsetPeriodicEvents ?? 0;
120
124
  const cronExpression = CronExpressionParser.parse(config.cron, {
121
125
  currentDate: eventCreatedAt,
122
126
  tz: config.tz,
123
127
  });
124
- return Math.abs(cronExpression.next().getTime() - eventStartAfter.getTime()) > 30 * 1000; // report as changed if diff created than 30 seconds
128
+ // report as changed if diff created than ALLOWED_PERIODIC_SEC_DIFF + the random event offset seconds
129
+ return (
130
+ Math.abs(cronExpression.next().getTime() - eventStartAfter.getTime()) >
131
+ (ALLOWED_PERIODIC_SEC_DIFF + randomOffset) * 1000
132
+ );
125
133
  });
126
134
  };
127
135
 
@@ -318,8 +318,11 @@ const _checkEventIsBlocked = async (baseInstance) => {
318
318
 
319
319
  const _processEvent = async (eventTypeInstance, processContext, key, queueEntries, payload) => {
320
320
  let traceContext;
321
- if (queueEntries.length === 1 && eventTypeInstance.inheritTraceContext) {
322
- traceContext = queueEntries[0].context?.traceContext;
321
+ if (eventTypeInstance.inheritTraceContext) {
322
+ const uniqueTraceContext = [...new Set(queueEntries.map((entry) => entry.context?.traceContext).filter((a) => a))];
323
+ if (uniqueTraceContext.length === 1) {
324
+ traceContext = uniqueTraceContext[0];
325
+ }
323
326
  }
324
327
 
325
328
  return await trace(
@@ -138,8 +138,12 @@ const _executeEventsAllTenantsRedis = async (tenantIds) => {
138
138
  if (!couldAcquireLock) {
139
139
  return;
140
140
  }
141
+ } catch (err) {
142
+ logger.error("executing event queue run for multi instance and tenant failed", err);
143
+ }
141
144
 
142
- for (const tenantId of tenantIds) {
145
+ for (const tenantId of tenantIds) {
146
+ try {
143
147
  await cds.tx({ tenant: tenantId }, async (tx) => {
144
148
  await trace(
145
149
  tx.context,
@@ -168,9 +172,9 @@ const _executeEventsAllTenantsRedis = async (tenantIds) => {
168
172
  { newRootSpan: true }
169
173
  );
170
174
  });
175
+ } catch (err) {
176
+ logger.error("broadcasting events for tenant failed", { tenantId }, err);
171
177
  }
172
- } catch (err) {
173
- logger.info("executing event queue run for multi instance and tenant failed", err);
174
178
  }
175
179
  };
176
180
 
@@ -179,23 +183,29 @@ const _executeEventsAllTenants = async (tenantIds, runId) => {
179
183
  for (const tenantId of tenantIds) {
180
184
  const id = cds.utils.uuid();
181
185
  let tenantContext;
182
- const events = await trace(
183
- { id, tenant: tenantId },
184
- "fetch-openEvents-and-tokenInfo",
185
- async () => {
186
- const user = await cds.tx({ tenant: tenantId }, async () => {
187
- return new cds.User.Privileged({ id: config.userId, tokenInfo: await common.getTokenInfo(tenantId) });
188
- });
189
- tenantContext = {
190
- tenant: tenantId,
191
- user,
192
- };
193
- return await cds.tx(tenantContext, async (tx) => {
194
- return await openEvents.getOpenQueueEntries(tx);
195
- });
196
- },
197
- { newRootSpan: true }
198
- );
186
+ let events;
187
+ try {
188
+ events = await trace(
189
+ { id, tenant: tenantId },
190
+ "fetch-openEvents-and-tokenInfo",
191
+ async () => {
192
+ const user = await cds.tx({ tenant: tenantId }, async () => {
193
+ return new cds.User.Privileged({ id: config.userId, tokenInfo: await common.getTokenInfo(tenantId) });
194
+ });
195
+ tenantContext = {
196
+ tenant: tenantId,
197
+ user,
198
+ };
199
+ return await cds.tx(tenantContext, async (tx) => {
200
+ return await openEvents.getOpenQueueEntries(tx);
201
+ });
202
+ },
203
+ { newRootSpan: true }
204
+ );
205
+ } catch (err) {
206
+ cds.log(COMPONENT_NAME).error("fetching open events for tenant failed", { tenantId }, err);
207
+ continue;
208
+ }
199
209
 
200
210
  if (!events.length) {
201
211
  continue;
@@ -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) => {
@@ -191,43 +191,45 @@ const _generateKey = (context, tenantScoped, key) => {
191
191
  };
192
192
 
193
193
  const getAllLocksRedis = async () => {
194
- const client = await redis.createMainClientAndConnect(config.redisOptions);
195
- const keys = await client.keys(`${config.redisOptions.redisNamespace}*`);
196
- const relevantKeys = {};
197
- for (const key of keys) {
198
- const [, tenant, guidOrType, subType] = key.split("##");
199
- if (subType) {
200
- relevantKeys[key] = { tenant, guidOrType, subType };
201
- }
202
- }
194
+ const clientOrCluster = await redis.createMainClientAndConnect(config.redisOptions);
195
+ const output = [];
196
+ const results = [];
203
197
 
204
- if (!Object.keys(relevantKeys)) {
205
- return [];
198
+ let clients;
199
+ if (redis.isClusterMode()) {
200
+ clients = clientOrCluster.masters.map((master) => master.client);
201
+ } else {
202
+ clients = [clientOrCluster];
206
203
  }
207
204
 
208
- const pipeline = client.multi();
209
- for (const key in relevantKeys) {
210
- pipeline.ttl(key);
211
- pipeline.get(key);
212
- }
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
+ }
213
212
 
214
- const replies = await pipeline.exec();
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
+ }
215
224
 
216
225
  let counter = 0;
217
- const result = [];
218
- for (const value of Object.values(relevantKeys)) {
219
- const ttl = replies[counter];
220
- const createdAt = replies[counter + 1];
221
- result.push({
222
- tenant: value.tenant,
223
- type: value.guidOrType,
224
- subType: value.subType,
225
- ttl,
226
- createdAt,
227
- });
226
+ for (const row of output) {
227
+ const ttl = results[counter];
228
+ const createdAt = results[counter + 1];
229
+ Object.assign(row, { ttl, createdAt });
228
230
  counter = counter + 2;
229
231
  }
230
- return result;
232
+ return output;
231
233
  };
232
234
 
233
235
  const shutdownHandler = async () => {
@@ -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,7 +22,8 @@ service EventQueueAdminService {
22
22
  attempts: Integer) returns Event;
23
23
  }
24
24
 
25
- @cds.persistence.exists
25
+ @cds.persistence.skip
26
+ @readonly
26
27
  entity Lock {
27
28
  key tenant: String;
28
29
  key type: String;