@cap-js-community/event-queue 2.0.1 → 2.0.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": "2.0.1",
3
+ "version": "2.0.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",
@@ -56,10 +56,11 @@
56
56
  "devDependencies": {
57
57
  "@actions/core": "^1.11.1",
58
58
  "@cap-js/cds-test": "^0.4.0",
59
+ "@cap-js/db-service": "^2.6.0",
59
60
  "@cap-js/hana": "^2.3.4",
60
- "@cap-js/sqlite": "^2.0.4",
61
+ "@cap-js/sqlite": "^2.1.0",
61
62
  "@opentelemetry/api": "^1.9.0",
62
- "@sap/cds": "^9.4.4",
63
+ "@sap/cds": "^9.4.5",
63
64
  "@sap/cds-dk": "^9.4.2",
64
65
  "eslint": "^8.57.0",
65
66
  "eslint-config-prettier": "^9.1.0",
@@ -27,12 +27,16 @@ const ERROR_CODES = {
27
27
  APP_NAMES_FORMAT: "APP_NAMES_FORMAT",
28
28
  APP_INSTANCES_FORMAT: "APP_INSTANCES_FORMAT",
29
29
  MULTI_INSTANCE_PROCESSING_NOT_ALLOWED: "MULTI_INSTANCE_PROCESSING_NOT_ALLOWED",
30
+ INVALID_CLUSTER_HANDLER_RESULT: "INVALID_CLUSTER_HANDLER_RESULT",
30
31
  };
31
32
 
32
33
  const ERROR_CODES_META = {
33
34
  [ERROR_CODES.WRONG_TX_USAGE]: {
34
35
  message: "Usage of this.tx|this.context is not allowed if parallel event processing is enabled",
35
36
  },
37
+ [ERROR_CODES.INVALID_CLUSTER_HANDLER_RESULT]: {
38
+ message: "A cluster handler returned an invalid result. Cluster handlers must return the clustered payload data.",
39
+ },
36
40
  [ERROR_CODES.UNKNOWN_EVENT_TYPE]: {
37
41
  message: "The event type and subType configuration is not configured! Maintain the combination in the config file.",
38
42
  },
@@ -371,6 +375,17 @@ class EventQueueError extends VError {
371
375
  );
372
376
  }
373
377
 
378
+ static invalidClusterHandlerResult(clusterKey, propertyName) {
379
+ const { message } = ERROR_CODES_META[ERROR_CODES.INVALID_CLUSTER_HANDLER_RESULT];
380
+ return new EventQueueError(
381
+ {
382
+ name: ERROR_CODES.INVALID_CLUSTER_HANDLER_RESULT,
383
+ info: { clusterKey, propertyName },
384
+ },
385
+ message
386
+ );
387
+ }
388
+
374
389
  static isRedisConnectionFailure(err) {
375
390
  return err instanceof VError && err.name === ERROR_CODES.REDIS_CREATE_CLIENT;
376
391
  }
@@ -62,6 +62,7 @@ class EventQueueProcessorBase {
62
62
  this.#eventType = eventType;
63
63
  this.#eventSubType = eventSubType;
64
64
  this.#eventConfig = config ?? {};
65
+ this.#eventConfig.selectedDelayedEventIds ??= [];
65
66
  this.__parallelEventProcessing = this.#eventConfig.parallelEventProcessing ?? DEFAULT_PARALLEL_EVENT_PROCESSING;
66
67
  if (this.__parallelEventProcessing > LIMIT_PARALLEL_EVENT_PROCESSING) {
67
68
  this.__parallelEventProcessing = LIMIT_PARALLEL_EVENT_PROCESSING;
@@ -664,45 +665,49 @@ class EventQueueProcessorBase {
664
665
  const baseDate = Date.now();
665
666
  const refDateStartAfter = new Date(baseDate + this.#config.runInterval * 1.2);
666
667
  await executeInNewTransaction(this.__baseContext, "eventQueue-getQueueEntriesAndSetToInProgress", async (tx) => {
667
- const entries = await tx.run(
668
- SELECT.from(this.#config.tableNameEventQueue)
669
- .forUpdate({ wait: this.#config.forUpdateTimeout })
670
- .limit(this.selectMaxChunkSize)
671
- .where(
672
- "type =",
673
- this.#eventType,
674
- "AND subType=",
675
- this.#eventSubType,
676
- "AND namespace =",
677
- this.#namespace,
678
- "AND ( startAfter IS NULL OR startAfter <=",
679
- refDateStartAfter.toISOString(),
680
- " ) AND ( status =",
681
- EventProcessingStatus.Open,
682
- "AND ( lastAttemptTimestamp <=",
683
- this.startTime.toISOString(),
684
- ...(this.isPeriodicEvent
685
- ? [
686
- "OR lastAttemptTimestamp IS NULL ) OR ( status =",
687
- EventProcessingStatus.InProgress,
688
- "AND lastAttemptTimestamp <=",
689
- new Date(baseDate - this.#eventConfig.keepAliveMaxInProgressTime * 1000).toISOString(),
690
- ") )",
691
- ]
692
- : [
693
- "OR lastAttemptTimestamp IS NULL ) OR ( status =",
694
- EventProcessingStatus.Error,
695
- "AND lastAttemptTimestamp <=",
696
- this.startTime.toISOString(),
697
- ") OR ( status =",
698
- EventProcessingStatus.InProgress,
699
- "AND lastAttemptTimestamp <=",
700
- new Date(baseDate - this.#eventConfig.keepAliveMaxInProgressTime * 1000).toISOString(),
701
- ") )",
702
- ])
703
- )
704
- .orderBy("createdAt", "ID")
705
- );
668
+ const cqn = SELECT.from(this.#config.tableNameEventQueue)
669
+ .forUpdate({ wait: this.#config.forUpdateTimeout })
670
+ .limit(this.selectMaxChunkSize)
671
+ .where(
672
+ "type =",
673
+ this.#eventType,
674
+ "AND subType=",
675
+ this.#eventSubType,
676
+ "AND namespace =",
677
+ this.#namespace,
678
+ "AND ( startAfter IS NULL OR startAfter <=",
679
+ refDateStartAfter.toISOString(),
680
+ " ) AND ( status =",
681
+ EventProcessingStatus.Open,
682
+ "AND ( lastAttemptTimestamp <=",
683
+ this.startTime.toISOString(),
684
+ ...(this.isPeriodicEvent
685
+ ? [
686
+ "OR lastAttemptTimestamp IS NULL ) OR ( status =",
687
+ EventProcessingStatus.InProgress,
688
+ "AND lastAttemptTimestamp <=",
689
+ new Date(baseDate - this.#eventConfig.keepAliveMaxInProgressTime * 1000).toISOString(),
690
+ ") ) ",
691
+ ]
692
+ : [
693
+ "OR lastAttemptTimestamp IS NULL ) OR ( status =",
694
+ EventProcessingStatus.Error,
695
+ "AND lastAttemptTimestamp <=",
696
+ this.startTime.toISOString(),
697
+ ") OR ( status =",
698
+ EventProcessingStatus.InProgress,
699
+ "AND lastAttemptTimestamp <=",
700
+ new Date(baseDate - this.#eventConfig.keepAliveMaxInProgressTime * 1000).toISOString(),
701
+ ") )",
702
+ ])
703
+ )
704
+ .orderBy("createdAt", "ID");
705
+
706
+ if (this.#eventConfig.selectedDelayedEventIds) {
707
+ cqn.where("ID NOT IN", this.#eventConfig.selectedDelayedEventIds);
708
+ }
709
+
710
+ const entries = await tx.run(cqn);
706
711
 
707
712
  if (!entries.length) {
708
713
  this.logger.debug("no entries available for processing", {
@@ -756,7 +761,9 @@ class EventQueueProcessorBase {
756
761
  }
757
762
 
758
763
  if (!eventsForProcessing.length) {
759
- this.__emptyChunkSelected = true;
764
+ if (!entries.length) {
765
+ this.__emptyChunkSelected = true;
766
+ }
760
767
  return;
761
768
  }
762
769
 
@@ -806,8 +813,11 @@ class EventQueueProcessorBase {
806
813
  return entry.lastAttemptsTs;
807
814
  }
808
815
 
809
- #handleDelayedEvents(delayedEvents) {
816
+ #handleDelayedEvents(delayedEvents, { skipExcludeDelayedEventIds = false } = {}) {
810
817
  for (const delayedEvent of delayedEvents) {
818
+ if (!skipExcludeDelayedEventIds) {
819
+ this.#eventConfig.selectedDelayedEventIds.push(delayedEvent.ID);
820
+ }
811
821
  this.#eventSchedulerInstance.scheduleEvent(
812
822
  this.__context.tenant,
813
823
  this.#eventType,
@@ -1107,6 +1117,7 @@ class EventQueueProcessorBase {
1107
1117
  }
1108
1118
 
1109
1119
  const newEvent = {
1120
+ ID: cds.utils.uuid(),
1110
1121
  type: this.#eventType,
1111
1122
  subType: this.#eventSubType,
1112
1123
  namespace: this.#eventConfig.namespace,
@@ -1131,16 +1142,16 @@ class EventQueueProcessorBase {
1131
1142
  });
1132
1143
  }
1133
1144
 
1134
- this.tx._skipEventQueueBroadcase = true;
1145
+ this.tx._skipEventQueueBroadcast = true;
1135
1146
  await this.tx.run(
1136
1147
  INSERT.into(this.#config.tableNameEventQueue).entries({
1137
1148
  ...newEvent,
1138
1149
  startAfter: newEvent.startAfter.toISOString(),
1139
1150
  })
1140
1151
  );
1141
- this.tx._skipEventQueueBroadcase = false;
1152
+ this.tx._skipEventQueueBroadcast = false;
1142
1153
  if (intervalInMs < this.#config.runInterval * 1.5) {
1143
- this.#handleDelayedEvents([newEvent]);
1154
+ this.#handleDelayedEvents([newEvent], { skipExcludeDelayedEventIds: true });
1144
1155
  const { relative: relativeAfterSchedule } = this.#eventSchedulerInstance.calculateOffset(
1145
1156
  this.#eventType,
1146
1157
  this.#eventSubType,
package/src/config.js CHANGED
@@ -252,9 +252,8 @@ class Config {
252
252
  this.#unsubscribeHandlers.push(cb);
253
253
  }
254
254
 
255
- addCAPOutboxEventBase(serviceName, config) {
255
+ addCAPOutboxEventBase(serviceName, config, currentEvents) {
256
256
  const namespace = config.namespace ?? this.#namespace;
257
- delete this.#eventMap[this.generateKey(namespace, CAP_EVENT_TYPE, serviceName)];
258
257
 
259
258
  // NOTE: CAP outbox defaults are injected by cds.requires.outbox // cds.requires.queue
260
259
  const eventConfig = this.#sanitizeParamsAdHocEvent({
@@ -262,44 +261,45 @@ class Config {
262
261
  subType: serviceName,
263
262
  impl: "./outbox/EventQueueGenericOutboxHandler",
264
263
  kind: config.kind ?? "persistent-queue",
265
- selectMaxChunkSize: config.selectMaxChunkSize ?? config.chunkSize,
266
- parallelEventProcessing: config.parallelEventProcessing ?? (config.parallel && CAP_PARALLEL_DEFAULT),
267
- retryAttempts: config.retryAttempts ?? config.maxAttempts ?? CAP_MAX_ATTEMPTS_DEFAULT,
264
+ ...this.#mixCAPPropertyNamesWithEventQueueNames(config),
268
265
  namespace,
269
266
  ...config,
270
267
  });
268
+ eventConfig.retryAttempts ??= CAP_MAX_ATTEMPTS_DEFAULT;
271
269
  eventConfig.internalEvent = true;
272
270
 
273
271
  this.#basicEventTransformation(eventConfig);
274
- this.#validateAdHocEvents(this.#eventMap, eventConfig, false);
272
+ this.#validateAdHocEvents(currentEvents, eventConfig, false);
275
273
  this.#basicEventTransformationAfterValidate(eventConfig);
276
- this.#eventMap[this.generateKey(namespace, CAP_EVENT_TYPE, serviceName)] = eventConfig;
274
+ return [this.generateKey(namespace, CAP_EVENT_TYPE, serviceName), eventConfig];
277
275
  }
278
276
 
279
- addCAPOutboxEventSpecificAction(serviceName, actionName) {
280
- const subType = [serviceName, actionName].join(".");
277
+ #mixCAPPropertyNamesWithEventQueueNames(config) {
278
+ return this.#cleanUndefined({
279
+ selectMaxChunkSize: config.selectMaxChunkSize ?? config.chunkSize,
280
+ parallelEventProcessing: config.parallelEventProcessing ?? (config.parallel && CAP_PARALLEL_DEFAULT),
281
+ retryAttempts: config.retryAttempts ?? config.maxAttempts,
282
+ ...config,
283
+ });
284
+ }
281
285
 
282
- const specific = this.#findBaseCAPServiceWithoutNamespace(subType);
283
- if (specific) {
284
- delete this.#eventMap[this.generateKey(specific.namespace, CAP_EVENT_TYPE, subType)];
285
- }
286
+ addCAPOutboxEventSpecificAction(baseServiceConfig, serviceName, actionName, currentEvents) {
287
+ const subType = [serviceName, actionName].join(".");
286
288
 
287
- const base = this.#findBaseCAPServiceWithoutNamespace(serviceName);
288
289
  const eventConfig = this.#sanitizeParamsAdHocEvent({
289
- ...this.getEventConfig(CAP_EVENT_TYPE, serviceName, base.namespace),
290
+ ...baseServiceConfig,
290
291
  ...this.getCdsOutboxEventSpecificConfig(serviceName, actionName),
291
292
  subType,
292
293
  });
293
294
  eventConfig.internalEvent = true;
294
295
 
295
296
  this.#basicEventTransformation(eventConfig);
296
- this.#validateAdHocEvents(this.#eventMap, eventConfig, false);
297
+ this.#validateAdHocEvents(currentEvents, eventConfig, false);
297
298
  this.#basicEventTransformationAfterValidate(eventConfig);
298
- this.#eventMap[this.generateKey(eventConfig.namespace, CAP_EVENT_TYPE, subType)] = eventConfig;
299
- return eventConfig;
299
+ return [this.generateKey(eventConfig.namespace, CAP_EVENT_TYPE, subType), eventConfig];
300
300
  }
301
301
 
302
- #findBaseCAPServiceWithoutNamespace(serviceName) {
302
+ findBaseCAPServiceWithoutNamespace(serviceName) {
303
303
  return Object.values(this.#eventMap).find(
304
304
  (event) => event.type === CAP_EVENT_TYPE && event.subType === serviceName
305
305
  );
@@ -318,22 +318,39 @@ class Config {
318
318
  fileContent.periodicEvents ??= [];
319
319
  const events = this.#configEvents ?? {};
320
320
  const periodicEvents = this.#configPeriodicEvents ?? {};
321
- const periodicCapServiceEvents = this.#cdsPeriodicOutboxServicesFromEnv();
322
- fileContent.events = fileContent.events.concat(this.#mapEnvEvents(events));
321
+ const { adHoc: adHocCAP, periodicEvents: periodicEventsCAP } = this.#cdsQueueEventsFromEnv();
322
+ fileContent.events = fileContent.events.concat(this.#mapEnvEvents(events)).concat(Object.values(adHocCAP));
323
323
  fileContent.periodicEvents = fileContent.periodicEvents
324
324
  .concat(this.#mapEnvEvents(periodicEvents))
325
- .concat(this.#mapCapOutboxPeriodicEvent(periodicCapServiceEvents));
325
+ .concat(Object.values(periodicEventsCAP));
326
326
  this.fileContent = fileContent;
327
327
  }
328
328
 
329
- #mapCapOutboxPeriodicEvent(periodicEventMap) {
330
- return Object.values(periodicEventMap);
329
+ #getBasicCapOutboxEventConfig(serviceName) {
330
+ return Object.assign(
331
+ {},
332
+ (typeof cds.requires.outbox === "object" && cds.requires.outbox) || {},
333
+ (typeof cds.requires.queue === "object" && cds.requires.queue) || {},
334
+ (typeof cds.env.requires[serviceName]?.outbox === "object" && cds.env.requires[serviceName].outbox) || {},
335
+ (typeof cds.env.requires[serviceName]?.queued === "object" && cds.env.requires[serviceName].queued) || {}
336
+ );
331
337
  }
332
338
 
333
- #cdsPeriodicOutboxServicesFromEnv() {
334
- return Object.entries(cds.env.requires).reduce((result, [name, value]) => {
335
- const config = value.outbox ?? value.queued;
336
- if (config?.events) {
339
+ #cdsQueueEventsFromEnv() {
340
+ return Object.entries(cds.env.requires).reduce(
341
+ (result, [name, value]) => {
342
+ const config = value.outbox ?? value.queued;
343
+
344
+ if (!config) {
345
+ return result;
346
+ }
347
+
348
+ // make sure service is known in general
349
+ // only if not known
350
+ const basicServiceConfig = this.#getBasicCapOutboxEventConfig(name);
351
+ const [key, srvConfig] = this.addCAPOutboxEventBase(name, basicServiceConfig, result.adHoc);
352
+ result.adHoc[key] = srvConfig;
353
+
337
354
  for (const fnName in config.events) {
338
355
  const base = { ...config };
339
356
  const fnConfig = config.events[fnName];
@@ -348,7 +365,7 @@ class Config {
348
365
  }
349
366
 
350
367
  const subType = `${name}.${fnName}`;
351
- result[subType] = Object.assign(
368
+ result.periodicEvents[subType] = Object.assign(
352
369
  {
353
370
  type: CAP_EVENT_TYPE,
354
371
  subType,
@@ -358,18 +375,38 @@ class Config {
358
375
  base,
359
376
  fnConfig
360
377
  );
378
+ } else {
379
+ const [key, specificEventConfig] = this.addCAPOutboxEventSpecificAction(
380
+ srvConfig,
381
+ name,
382
+ fnName,
383
+ result.adHoc
384
+ );
385
+ result.adHoc[key] = specificEventConfig;
361
386
  }
362
387
  }
363
- }
364
- return result;
365
- }, {});
388
+ return result;
389
+ },
390
+ { adHoc: {}, periodicEvents: {} }
391
+ );
392
+ }
393
+
394
+ addCAPServiceWithoutEnvConfig(serviceName, srv, customOpts = {}) {
395
+ const queueOptions = srv.options.queued ?? srv.options.outbox ?? {};
396
+ const basicServiceConfig = this.#getBasicCapOutboxEventConfig(serviceName);
397
+ const [key, srvConfig] = this.addCAPOutboxEventBase(
398
+ serviceName,
399
+ { ...basicServiceConfig, ...queueOptions, ...customOpts },
400
+ this.#eventMap
401
+ );
402
+ this.#eventMap[key] = srvConfig;
366
403
  }
367
404
 
368
405
  getCdsOutboxEventSpecificConfig(serviceName, action) {
369
406
  const srv = cds.env.requires[serviceName];
370
407
  const config = srv?.outbox ?? srv?.queued;
371
408
  if (config?.events?.[action]) {
372
- return config.events[action];
409
+ return this.#mixCAPPropertyNamesWithEventQueueNames(config.events[action]);
373
410
  } else {
374
411
  return null;
375
412
  }
@@ -877,6 +914,15 @@ class Config {
877
914
  this.#processingNamespaces = value;
878
915
  }
879
916
 
917
+ #cleanUndefined(input) {
918
+ return Object.entries(input).reduce((acc, [key, value]) => {
919
+ if (value !== undefined) {
920
+ acc[key] = value;
921
+ }
922
+ return acc;
923
+ }, {});
924
+ }
925
+
880
926
  /**
881
927
  @return { Config }
882
928
  **/
package/src/dbHandler.js CHANGED
@@ -19,7 +19,7 @@ const registerEventQueueDbHandler = (dbService) => {
19
19
  registeredHandlers.eventQueueDbHandler = true;
20
20
  const def = dbService.model.definitions[config.tableNameEventQueue];
21
21
  dbService.after("CREATE", def, (_, req) => {
22
- if (req.tx._skipEventQueueBroadcase) {
22
+ if (req.tx._skipEventQueueBroadcast) {
23
23
  return;
24
24
  }
25
25
  req.tx._ = req.tx._ ?? {};
@@ -6,6 +6,7 @@ const EventQueueBaseClass = require("../EventQueueProcessorBase");
6
6
  const { EventProcessingStatus } = require("../constants");
7
7
  const common = require("../shared/common");
8
8
  const config = require("../config");
9
+ const EventQueueError = require("../EventQueueError");
9
10
 
10
11
  const COMPONENT_NAME = "/eventQueue/outbox/generic";
11
12
 
@@ -164,7 +165,7 @@ class EventQueueGenericOutboxHandler extends EventQueueBaseClass {
164
165
  clusterData.queueEntries.map((entry) => entry.payload.data)
165
166
  );
166
167
  if (!clusterResult) {
167
- throw new Error("hmm??");
168
+ throw EventQueueError.invalidClusterHandlerResult(clustersKey, propertyName);
168
169
  }
169
170
  clusterData.payload.data = clusterResult;
170
171
  }
@@ -20,16 +20,6 @@ function outboxed(srv, customOpts) {
20
20
  }
21
21
 
22
22
  const logger = cds.log(COMPONENT_NAME);
23
- let outboxOpts = Object.assign(
24
- {},
25
- (typeof cds.requires.outbox === "object" && cds.requires.outbox) || {},
26
- (typeof cds.requires.queue === "object" && cds.requires.queue) || {},
27
- (typeof srv.options?.outbox === "object" && srv.options.outbox) || {},
28
- (typeof srv.options?.queued === "object" && srv.options.queued) || {},
29
- customOpts || {}
30
- );
31
-
32
- config.addCAPOutboxEventBase(srv.name, outboxOpts);
33
23
 
34
24
  const originalSrv = srv[UNBOXED] || srv;
35
25
  const outboxedSrv = Object.create(originalSrv);
@@ -42,25 +32,19 @@ function outboxed(srv, customOpts) {
42
32
  }
43
33
  outboxedSrv.handle = async function (req) {
44
34
  const context = req.context || cds.context;
45
- outboxOpts = Object.assign(
46
- {},
47
- (typeof cds.requires.outbox === "object" && cds.requires.outbox) || {},
48
- (typeof cds.requires.queue === "object" && cds.requires.queue) || {},
49
- (typeof srv.options?.outbox === "object" && srv.options.outbox) || {},
50
- (typeof srv.options?.queued === "object" && srv.options.queued) || {},
51
- customOpts || {}
52
- );
53
- config.addCAPOutboxEventBase(srv.name, outboxOpts);
54
35
  const hasSpecificSettings = !!config.getCdsOutboxEventSpecificConfig(srv.name, req.event);
55
- if (hasSpecificSettings) {
56
- outboxOpts = config.addCAPOutboxEventSpecificAction(srv.name, req.event);
57
- }
58
36
  const subType = hasSpecificSettings ? [srv.name, req.event].join(".") : srv.name;
59
- const namespace = outboxOpts.namespace ?? config.namespace;
60
- outboxOpts = config.getEventConfig(CDS_EVENT_TYPE, subType, namespace);
37
+ let srvConfig = config.findBaseCAPServiceWithoutNamespace(subType);
38
+ // NOTE: service is outboxed without config in cds.env.requires[srv]
39
+ if (!srvConfig) {
40
+ config.addCAPServiceWithoutEnvConfig(subType, srv, customOpts);
41
+ srvConfig = config.findBaseCAPServiceWithoutNamespace(subType);
42
+ }
43
+
44
+ const outboxOpts = config.getEventConfig(CDS_EVENT_TYPE, subType, srvConfig.namespace);
61
45
  const eventHeaders = getPropagatedHeaders(outboxOpts, req);
62
46
  if (["persistent-outbox", "persistent-queue"].includes(outboxOpts.kind)) {
63
- await _mapToEventAndPublish(context, subType, req, eventHeaders, namespace);
47
+ await _mapToEventAndPublish(context, subType, req, eventHeaders, srvConfig.namespace);
64
48
  return;
65
49
  }
66
50
  context.on("succeeded", async () => {
@@ -171,9 +171,9 @@ const _insertPeriodEvents = async (tx, events, now) => {
171
171
  counter++;
172
172
  });
173
173
 
174
- tx._skipEventQueueBroadcase = true;
174
+ tx._skipEventQueueBroadcast = true;
175
175
  await tx.run(INSERT.into(eventConfig.tableNameEventQueue).entries(eventsToBeInserted));
176
- tx._skipEventQueueBroadcase = false;
176
+ tx._skipEventQueueBroadcast = false;
177
177
  };
178
178
 
179
179
  const _generateKey = ({ type, subType, namespace }) => [namespace, type, subType].join("##");
@@ -137,6 +137,7 @@ const processPeriodicEvent = async (context, eventTypeInstance) => {
137
137
  eventTypeInstance.processEventContext = tx.context;
138
138
  const queueEntries = await eventTypeInstance.getQueueEntriesAndSetToInProgress();
139
139
  if (!queueEntries.length) {
140
+ processNext = false;
140
141
  return;
141
142
  }
142
143
  if (queueEntries.length > 1) {
@@ -77,17 +77,17 @@ const publishEvent = async (
77
77
  }
78
78
  }
79
79
  if (config.insertEventsBeforeCommit && !skipInsertEventsBeforeCommit) {
80
- _registerHandlerAndAddEvents(tx, events);
80
+ _registerHandlerAndAddEvents(tx, events, skipBroadcast);
81
81
  } else {
82
82
  let result;
83
- tx._skipEventQueueBroadcase = skipBroadcast;
83
+ tx._skipEventQueueBroadcast = skipBroadcast;
84
84
  result = await tx.run(INSERT.into(config.tableNameEventQueue).entries(events));
85
- tx._skipEventQueueBroadcase = false;
85
+ tx._skipEventQueueBroadcast = false;
86
86
  return result;
87
87
  }
88
88
  };
89
89
 
90
- const _registerHandlerAndAddEvents = (tx, events) => {
90
+ const _registerHandlerAndAddEvents = (tx, events, skipBroadcast) => {
91
91
  tx._eventQueue ??= { events: [], handlerRegistered: false };
92
92
  tx._eventQueue.events = tx._eventQueue.events.concat(events);
93
93
 
@@ -99,7 +99,9 @@ const _registerHandlerAndAddEvents = (tx, events) => {
99
99
  if (!tx._eventQueue.events?.length) {
100
100
  return;
101
101
  }
102
+ tx._skipEventQueueBroadcast = skipBroadcast;
102
103
  await tx.run(INSERT.into(config.tableNameEventQueue).entries(tx._eventQueue.events));
104
+ tx._skipEventQueueBroadcast = false;
103
105
  tx._eventQueue = null;
104
106
  });
105
107
  };
@@ -48,17 +48,20 @@ const _messageHandlerProcessEvents = async (messageData) => {
48
48
  if (config.isCapOutboxEvent(type)) {
49
49
  try {
50
50
  const service = await cds.connect.to(srvName);
51
- cds.outboxed(service);
52
- if (actionName) {
53
- const specificSettings = config.getCdsOutboxEventSpecificConfig(srvName, actionName);
54
- if (specificSettings) {
55
- config.addCAPOutboxEventSpecificAction(srvName, actionName);
56
- }
51
+ if (!service || actionName) {
52
+ logger.warn("could not find CAP Service configuration to process event!", {
53
+ type,
54
+ subType,
55
+ namespace,
56
+ });
57
+ return;
57
58
  }
59
+ config.addCAPServiceWithoutEnvConfig(subType, service);
58
60
  } catch (err) {
59
61
  logger.warn("could not connect to outboxed service", err, {
60
62
  type,
61
63
  subType,
64
+ namespace,
62
65
  });
63
66
  return;
64
67
  }
@@ -1,8 +1,5 @@
1
1
  "use strict";
2
2
 
3
- const cds = require("@sap/cds");
4
-
5
- const eventConfig = require("../config");
6
3
  const { EventProcessingStatus } = require("../constants");
7
4
  const config = require("../config");
8
5
 
@@ -10,9 +7,9 @@ const MS_IN_DAYS = 24 * 60 * 60 * 1000;
10
7
 
11
8
  const getOpenQueueEntries = async (tx, filterAppSpecificEvents = true) => {
12
9
  const startTime = new Date();
13
- const refDateStartAfter = new Date(startTime.getTime() + eventConfig.runInterval * 1.2);
10
+ const refDateStartAfter = new Date(startTime.getTime() + config.runInterval * 1.2);
14
11
  const entries = await tx.run(
15
- SELECT.from(eventConfig.tableNameEventQueue)
12
+ SELECT.from(config.tableNameEventQueue)
16
13
  .where(
17
14
  "namespace IN",
18
15
  config.processingNamespaces,
@@ -25,7 +22,7 @@ const getOpenQueueEntries = async (tx, filterAppSpecificEvents = true) => {
25
22
  ") OR ( status =",
26
23
  EventProcessingStatus.InProgress,
27
24
  "AND lastAttemptTimestamp <=",
28
- new Date(startTime.getTime() - eventConfig.globalTxTimeout).toISOString(),
25
+ new Date(startTime.getTime() - config.globalTxTimeout).toISOString(),
29
26
  ") ) AND (createdAt >=",
30
27
  new Date(startTime.getTime() - 30 * MS_IN_DAYS).toISOString(),
31
28
  " OR startAfter >=",
@@ -38,19 +35,19 @@ const getOpenQueueEntries = async (tx, filterAppSpecificEvents = true) => {
38
35
 
39
36
  const result = [];
40
37
  for (const { type, subType, namespace } of entries) {
41
- if (eventConfig.isCapOutboxEvent(type)) {
38
+ if (config.isCapOutboxEvent(type)) {
42
39
  const { srvName, actionName } = config.normalizeSubType(type, subType);
43
40
  try {
44
- const service = await cds.connect.to(srvName);
45
41
  if (filterAppSpecificEvents) {
46
- if (!service) {
47
- continue;
48
- }
49
- cds.outboxed(service);
50
- if (actionName) {
51
- config.addCAPOutboxEventSpecificAction(srvName, actionName);
42
+ const eventConfig = config.getEventConfig(type, subType, namespace);
43
+ if (!eventConfig) {
44
+ const service = await cds.connect.to(srvName);
45
+ if (!service || actionName) {
46
+ continue;
47
+ }
48
+ config.addCAPServiceWithoutEnvConfig(subType, service);
52
49
  }
53
- if (eventConfig.shouldBeProcessedInThisApplication(type, subType, namespace)) {
50
+ if (config.shouldBeProcessedInThisApplication(type, subType, namespace)) {
54
51
  result.push({ namespace, type, subType });
55
52
  }
56
53
  }
@@ -64,8 +61,8 @@ const getOpenQueueEntries = async (tx, filterAppSpecificEvents = true) => {
64
61
  } else {
65
62
  if (filterAppSpecificEvents) {
66
63
  if (
67
- eventConfig.getEventConfig(type, subType, namespace) &&
68
- eventConfig.shouldBeProcessedInThisApplication(type, subType, namespace)
64
+ config.getEventConfig(type, subType, namespace) &&
65
+ config.shouldBeProcessedInThisApplication(type, subType, namespace)
69
66
  ) {
70
67
  result.push({ namespace, type, subType });
71
68
  }
@@ -4,7 +4,7 @@ const { AsyncResource } = require("async_hooks");
4
4
 
5
5
  const cds = require("@sap/cds");
6
6
 
7
- const { processEventQueue } = require("../processEventQueue");
7
+ const processor = require("../processEventQueue");
8
8
  const eventQueueConfig = require("../config");
9
9
  const WorkerQueue = require("../shared/WorkerQueue");
10
10
  const distributedLock = require("../shared/distributedLock");
@@ -21,7 +21,7 @@ const runEventCombinationForTenant = async (
21
21
  ) => {
22
22
  try {
23
23
  if (skipWorkerPool) {
24
- return await processEventQueue(context, type, subType, namespace);
24
+ return await processor.processEventQueue(context, type, subType, namespace);
25
25
  } else {
26
26
  const eventConfig = eventQueueConfig.getEventConfig(type, subType, namespace);
27
27
  const label = `${type}_${subType}`;
@@ -39,7 +39,7 @@ const runEventCombinationForTenant = async (
39
39
  }
40
40
  }
41
41
 
42
- await processEventQueue(context, type, subType, namespace);
42
+ await processor.processEventQueue(context, type, subType, namespace);
43
43
  };
44
44
  if (shouldTrace) {
45
45
  return await trace(context, label, _exec, { newRootSpan: true });
@@ -26,7 +26,7 @@ const CONCURRENCY_AUTH_INFO = 3;
26
26
  async function executeInNewTransaction(context = {}, transactionTag, fn, args, { info = {} } = {}) {
27
27
  const parameters = Array.isArray(args) ? args : [args];
28
28
  const logger = cds.log(COMPONENT_NAME);
29
- let transactionRollbackPromise = Promise.resolve(false);
29
+ let transactionRollbackPromise = Promise.resolve(true);
30
30
  try {
31
31
  const authInfo = await common.getAuthContext(context.tenant);
32
32
  const user = new cds.User.Privileged({ id: config.userId, authInfo, tokenInfo: authInfo?.token });
@@ -216,14 +216,15 @@ const getAllLocksRedis = async () => {
216
216
  // NOTE: use SCAN because KEYS is not supported for cluster clients
217
217
  for (const client of clients) {
218
218
  for await (const key of client.scanIterator({ MATCH: "EVENT*", COUNT: 1000 })) {
219
- const [, tenant, guidOrType, subType] = key.split("##");
219
+ const [, namespace, tenant, guidOrType, subType] = key.split("##");
220
220
  if (!subType) {
221
221
  continue;
222
222
  }
223
223
 
224
224
  const pipeline = client.multi();
225
225
  output.push({
226
- tenant: tenant,
226
+ namespace,
227
+ tenant,
227
228
  type: guidOrType,
228
229
  subType: subType,
229
230
  });
@@ -9,14 +9,9 @@ service EventQueueAdminService {
9
9
  @cds.persistence.skip
10
10
  entity Event as projection on db.Event {
11
11
  null as tenant: String,
12
- null as landscape: String,
13
- null as space: String,
14
12
  *
15
13
  } actions {
16
14
  action setStatusAndAttempts(
17
- // TODO: remove tenant as soon as CAP issue is fixed https://github.tools.sap/cap/issues/issues/18445
18
- @mandatory
19
- tenant: String,
20
15
  status: db.Status,
21
16
  @assert.range: [0,100]
22
17
  attempts: Integer) returns Event;
@@ -25,18 +20,14 @@ service EventQueueAdminService {
25
20
  @cds.persistence.skip
26
21
  @readonly
27
22
  entity Lock {
23
+ key namespace: String;
28
24
  key tenant: String;
29
25
  key type: String;
30
26
  key subType: String;
31
- landscape: String;
32
- space: String;
33
27
  ttl: Integer;
34
28
  createdAt: Integer;
35
29
  } actions {
36
30
  action releaseLock(
37
- // TODO: remove tenant as soon as CAP issue is fixed https://github.tools.sap/cap/issues/issues/18445
38
- @mandatory
39
- tenant: String,
40
31
  @mandatory
41
32
  type: String,
42
33
  @mandatory
@@ -44,6 +35,8 @@ service EventQueueAdminService {
44
35
  }
45
36
 
46
37
  action publishEvent(
38
+ @mandatory
39
+ namespace: String,
47
40
  @mandatory
48
41
  tenants: array of String,
49
42
  @mandatory
@@ -52,7 +45,7 @@ service EventQueueAdminService {
52
45
  subType: String,
53
46
  referenceEntity: String,
54
47
  referenceEntityKey: String,
55
- @open
56
- payload: {},
57
- startAfter: String);
48
+ payload: String,
49
+ startAfter: String,
50
+ );
58
51
  }
@@ -15,7 +15,6 @@ module.exports = class AdminService extends cds.ApplicationService {
15
15
  const { Event: EventService, Lock: LockService } = this.entities;
16
16
  const { Event: EventDb } = cds.db.entities("sap.eventqueue");
17
17
  const { publishEvent } = this.actions;
18
- const { landscape, space } = this.getLandscapeAndSpace();
19
18
 
20
19
  this.before("*", (req) => {
21
20
  if (!eventQueue.config.enableAdminService) {
@@ -26,6 +25,10 @@ module.exports = class AdminService extends cds.ApplicationService {
26
25
  return;
27
26
  }
28
27
 
28
+ if (req.target === LockService) {
29
+ return;
30
+ }
31
+
29
32
  const headers = Object.assign({}, req.headers, req.req?.headers);
30
33
  const tenant = headers["z-id"] ?? req.data.tenant;
31
34
 
@@ -46,8 +49,6 @@ module.exports = class AdminService extends cds.ApplicationService {
46
49
  }
47
50
  const events = await tx.run(req.query);
48
51
  return events?.map((event) => {
49
- event.landscape = landscape;
50
- event.space = space;
51
52
  event.tenant = tenant;
52
53
  return event;
53
54
  });
@@ -58,12 +59,7 @@ module.exports = class AdminService extends cds.ApplicationService {
58
59
  if (!eventQueue.config.redisEnabled) {
59
60
  return [];
60
61
  }
61
- const locks = await distributedLock.getAllLocksRedis();
62
- return locks.map((lock) => ({
63
- ...lock,
64
- landscape: landscape,
65
- space: space,
66
- }));
62
+ return await distributedLock.getAllLocksRedis();
67
63
  });
68
64
 
69
65
  this.on("setStatusAndAttempts", async (req) => {
@@ -97,17 +93,20 @@ module.exports = class AdminService extends cds.ApplicationService {
97
93
  });
98
94
 
99
95
  this.on("releaseLock", async (req) => {
96
+ const tenant = req.headers["z-id"];
100
97
  cds.log("eventQueue").info("Releasing event-queue lock", req.data);
101
- const { tenant, type, subType } = req.data;
98
+ const { type, subType, namespace } = req.data;
102
99
  return await cds.tx({ tenant }, async (tx) => {
103
- return await distributedLock.releaseLock(tx.context, [type, subType].join("##"));
100
+ return await distributedLock.releaseLock(tx.context, [namespace, type, subType].join("##"), {
101
+ skipNamespace: true,
102
+ });
104
103
  });
105
104
  });
106
105
 
107
106
  this.on(publishEvent, async (req) => {
108
107
  const logger = cds.log(COMPONENT_NAME);
109
108
  try {
110
- const { type, subType, referenceEntity, referenceEntityKey, payload, startAfter } = req.data;
109
+ const { type, subType, referenceEntity, referenceEntityKey, payload, startAfter, namespace } = req.data;
111
110
  const tenants = await publishEventHelper.resolveTenantInfos(req);
112
111
  const eventOptions = commonHelper.cleanUndefined({
113
112
  type,
@@ -116,8 +115,9 @@ module.exports = class AdminService extends cds.ApplicationService {
116
115
  referenceEntityKey,
117
116
  payload,
118
117
  startAfter,
118
+ namespace,
119
119
  });
120
- const publishInfo = { count: tenants.length, type, subType, tenants: req.data.tenants };
120
+ const publishInfo = { count: tenants.length, type, subType, namespace, tenants: req.data.tenants };
121
121
  logger.info("publishing event for tenant(s)", publishInfo);
122
122
  for (const tenant of tenants) {
123
123
  await cds.tx({ tenant }, async (tx) => {
@@ -127,25 +127,10 @@ module.exports = class AdminService extends cds.ApplicationService {
127
127
  logger.info("finished publishing event for tenant(s)", publishInfo);
128
128
  } catch (err) {
129
129
  logger.error("error publishing event", err);
130
+ req.reject(400, "Error publishing event: " + err.toString());
130
131
  }
131
132
  });
132
133
 
133
134
  await super.init();
134
135
  }
135
-
136
- getLandscapeAndSpace() {
137
- const url = cds.requires.db.credentials.url;
138
- if (!url) {
139
- return { landscape: "eu10-canary", space: "local-dev" };
140
- }
141
- const match = url.match(/https?:\/\/[^.]+\.authentication\.([^.]+)\.hana\.ondemand\.com/);
142
- const landscape = (match?.[1] ?? "sap") === "sap" ? "eu10-canary" : match?.[1];
143
- let space = "local-dev";
144
- try {
145
- space = JSON.parse(process.env.VCAP_APPLICATION)?.space_name;
146
- } catch {
147
- /* empty */
148
- }
149
- return { landscape, space };
150
- }
151
136
  };