@cap-js-community/event-queue 2.0.0-beta.8 → 2.0.1

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.0-beta.8",
3
+ "version": "2.0.1",
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",
@@ -296,7 +296,7 @@ class EventQueueProcessorBase {
296
296
  const statusMap = this.commitOnEventLevel || returnMap ? {} : this.__statusMap;
297
297
  const errorHandler = (error) => {
298
298
  queueEntries.forEach((queueEntry) =>
299
- this.#determineAndAddEventStatusToMap(queueEntry.ID, EventProcessingStatus.Error, statusMap)
299
+ this.#determineAndAddEventStatusToMap(queueEntry.ID, EventProcessingStatus.Error, { statusMap })
300
300
  );
301
301
  this.logger.error(
302
302
  "The supplied status tuple doesn't have the required structure. Setting all entries to error.",
@@ -317,7 +317,7 @@ class EventQueueProcessorBase {
317
317
 
318
318
  try {
319
319
  queueEntryProcessingStatusTuple.forEach(([id, processingStatus]) =>
320
- this.#determineAndAddEventStatusToMap(id, processingStatus, statusMap)
320
+ this.#determineAndAddEventStatusToMap(id, processingStatus, { statusMap })
321
321
  );
322
322
  } catch (error) {
323
323
  errorHandler(error);
@@ -344,11 +344,15 @@ class EventQueueProcessorBase {
344
344
  }
345
345
  }
346
346
 
347
- #determineAndAddEventStatusToMap(id, statusOrUpdateData, statusMap = this.__statusMap) {
347
+ #determineAndAddEventStatusToMap(id, statusOrUpdateData, { statusMap = this.__statusMap, error } = {}) {
348
348
  if (typeof statusOrUpdateData === "number") {
349
349
  statusOrUpdateData = { status: statusOrUpdateData };
350
350
  }
351
351
 
352
+ if (error) {
353
+ statusOrUpdateData.error = error;
354
+ }
355
+
352
356
  if (!statusMap[id]) {
353
357
  statusMap[id] = statusOrUpdateData;
354
358
  return;
@@ -376,9 +380,17 @@ class EventQueueProcessorBase {
376
380
  }
377
381
  );
378
382
  queueEntries.forEach((queueEntry) =>
379
- this.#determineAndAddEventStatusToMap(queueEntry.ID, EventProcessingStatus.Error)
383
+ this.#determineAndAddEventStatusToMap(queueEntry.ID, EventProcessingStatus.Error, { error })
384
+ );
385
+ return Object.fromEntries(
386
+ queueEntries.map((queueEntry) => [
387
+ queueEntry.ID,
388
+ {
389
+ status: EventProcessingStatus.Error,
390
+ error,
391
+ },
392
+ ])
380
393
  );
381
- return Object.fromEntries(queueEntries.map((queueEntry) => [queueEntry.ID, EventProcessingStatus.Error]));
382
394
  }
383
395
 
384
396
  handleErrorDuringPeriodicEventProcessing(error, queueEntry) {
@@ -389,11 +401,12 @@ class EventQueueProcessorBase {
389
401
  });
390
402
  }
391
403
 
392
- async setPeriodicEventStatus(queueEntryIds, status) {
404
+ async setPeriodicEventStatus(queueEntryIds, status, error) {
393
405
  await this.tx.run(
394
406
  UPDATE.entity(this.#config.tableNameEventQueue)
395
407
  .set({
396
408
  status: status,
409
+ ...(error && { error: this.#error2String(error) }),
397
410
  })
398
411
  .where({
399
412
  ID: queueEntryIds,
@@ -1018,7 +1031,7 @@ class EventQueueProcessorBase {
1018
1031
  const lockAcquired = await distributedLock.acquireLock(
1019
1032
  this.__context,
1020
1033
  [this.#namespace, this.#eventType, this.#eventSubType].join("##"),
1021
- { keepTrackOfLock: true, expiryTime: this.#eventConfig.keepAliveMaxInProgressTime * 1000 }
1034
+ { keepTrackOfLock: true, expiryTime: this.#eventConfig.keepAliveMaxInProgressTime * 1000, skipNamespace: true }
1022
1035
  );
1023
1036
  if (!lockAcquired) {
1024
1037
  this.logger.debug("no lock available, exit processing", {
@@ -1061,7 +1074,8 @@ class EventQueueProcessorBase {
1061
1074
  await trace(this.baseContext, "release-lock", async () => {
1062
1075
  await distributedLock.releaseLock(
1063
1076
  this.context,
1064
- [this.#namespace, this.#eventType, this.#eventSubType].join("##")
1077
+ [this.#namespace, this.#eventType, this.#eventSubType].join("##"),
1078
+ { skipNamespace: true }
1065
1079
  );
1066
1080
  });
1067
1081
  } catch (err) {
@@ -154,6 +154,7 @@ const processPeriodicEvent = async (context, eventTypeInstance) => {
154
154
  }
155
155
 
156
156
  let status = EventProcessingStatus.Done;
157
+ let error;
157
158
  await executeInNewTransaction(
158
159
  eventTypeInstance.context,
159
160
  `eventQueue-periodic-process-${eventTypeInstance.eventType}##${eventTypeInstance.eventSubType}`,
@@ -166,6 +167,7 @@ const processPeriodicEvent = async (context, eventTypeInstance) => {
166
167
  eventTypeInstance.startPerformanceTracerPeriodicEvents();
167
168
  await eventTypeInstance.processPeriodicEvent(tx.context, queueEntry.ID, queueEntry);
168
169
  } catch (err) {
170
+ error = err;
169
171
  status = EventProcessingStatus.Error;
170
172
  eventTypeInstance.handleErrorDuringPeriodicEventProcessing(err, queueEntry);
171
173
  await tx.rollback();
@@ -190,7 +192,7 @@ const processPeriodicEvent = async (context, eventTypeInstance) => {
190
192
  async (tx) => {
191
193
  await trace(eventTypeInstance.context, "periodic-event-set-status", async () => {
192
194
  eventTypeInstance.processEventContext = tx.context;
193
- await eventTypeInstance.setPeriodicEventStatus(queueEntry.ID, status);
195
+ await eventTypeInstance.setPeriodicEventStatus(queueEntry.ID, status, error);
194
196
  });
195
197
  }
196
198
  );
@@ -180,7 +180,7 @@ const _executeEventsAllTenantsRedis = async (tenantIds) => {
180
180
  }
181
181
  };
182
182
 
183
- const _executeEventsAllTenants = async (tenantIds, runId) => {
183
+ const _executeEventsAllTenants = async (tenantIds) => {
184
184
  const promises = [];
185
185
  for (const tenantId of tenantIds) {
186
186
  const id = cds.utils.uuid();
@@ -230,13 +230,6 @@ const _executeEventsAllTenants = async (tenantIds, runId) => {
230
230
  label,
231
231
  async () => {
232
232
  try {
233
- const lockId = `${runId}_${label}`;
234
- const couldAcquireLock = await distributedLock.acquireLock(context, lockId, {
235
- expiryTime: eventQueueConfig.runInterval * 0.95,
236
- });
237
- if (!couldAcquireLock) {
238
- return;
239
- }
240
233
  await runEventCombinationForTenant(
241
234
  context,
242
235
  eventConfig.type,
@@ -308,50 +301,40 @@ const _singleTenantDb = async () => {
308
301
  { newRootSpan: true }
309
302
  );
310
303
 
311
- return await Promise.allSettled(
312
- events.map(async (openEvent) => {
313
- const eventConfig = config.getEventConfig(openEvent.type, openEvent.subType, openEvent.namespace);
314
- const label = [openEvent.namespace, eventConfig.type, eventConfig.subType].join("##");
315
- return await WorkerQueue.instance.addToQueue(
316
- eventConfig.load,
317
- label,
318
- eventConfig.priority,
319
- eventConfig.increasePriorityOverTime,
320
- async () => {
321
- return await cds.tx({}, async ({ context }) => {
322
- await trace(
323
- context,
324
- label,
325
- async () => {
326
- try {
327
- const couldAcquireLock = eventConfig.multiInstanceProcessing
328
- ? true
329
- : await distributedLock.acquireLock(context, label, {
330
- expiryTime: eventQueueConfig.runInterval * 0.95,
331
- });
332
- if (!couldAcquireLock) {
333
- return;
304
+ for (const openEvent of events) {
305
+ const eventConfig = config.getEventConfig(openEvent.type, openEvent.subType, openEvent.namespace);
306
+ const label = [openEvent.namespace, eventConfig.type, eventConfig.subType].join("##");
307
+ await WorkerQueue.instance.addToQueue(
308
+ eventConfig.load,
309
+ label,
310
+ eventConfig.priority,
311
+ eventConfig.increasePriorityOverTime,
312
+ async () => {
313
+ return await cds.tx({}, async ({ context }) => {
314
+ await trace(
315
+ context,
316
+ label,
317
+ async () => {
318
+ try {
319
+ await runEventCombinationForTenant(
320
+ context,
321
+ eventConfig.type,
322
+ eventConfig.subType,
323
+ openEvent.namespace,
324
+ {
325
+ skipWorkerPool: true,
334
326
  }
335
- await runEventCombinationForTenant(
336
- context,
337
- eventConfig.type,
338
- eventConfig.subType,
339
- openEvent.namespace,
340
- {
341
- skipWorkerPool: true,
342
- }
343
- );
344
- } catch (err) {
345
- cds.log(COMPONENT_NAME).error("executing event-queue run for tenant failed");
346
- }
347
- },
348
- { newRootSpan: true }
349
- );
350
- });
351
- }
352
- );
353
- })
354
- );
327
+ );
328
+ } catch (err) {
329
+ cds.log(COMPONENT_NAME).error("executing event-queue run for tenant failed");
330
+ }
331
+ },
332
+ { newRootSpan: true }
333
+ );
334
+ });
335
+ }
336
+ );
337
+ }
355
338
  };
356
339
 
357
340
  const _singleTenantRedis = async () => {
@@ -467,7 +450,7 @@ const _multiTenancyDb = async () => {
467
450
  logger.info("executing event queue run for single instance and multi tenant");
468
451
  const tenantIds = await cdsHelper.getAllTenantIds();
469
452
  await _checkPeriodicEventUpdate(tenantIds);
470
- return await _executeEventsAllTenants(tenantIds, EVENT_QUEUE_RUN_ID);
453
+ return await _executeEventsAllTenants(tenantIds);
471
454
  } catch (err) {
472
455
  logger.error("Couldn't fetch tenant ids for event queue processing! Next try after defined interval.", err);
473
456
  }
@@ -165,6 +165,14 @@ const isTenantIdValidCb = async (checkType, tenantId) => {
165
165
  }
166
166
  };
167
167
 
168
+ const cleanUndefined = (input) =>
169
+ Object.entries(input).reduce((acc, [key, value]) => {
170
+ if (value !== undefined) {
171
+ acc[key] = value;
172
+ }
173
+ return acc;
174
+ }, {});
175
+
168
176
  module.exports = {
169
177
  arrayToFlatMap,
170
178
  limiter,
@@ -174,6 +182,7 @@ module.exports = {
174
182
  getAuthContext,
175
183
  isTenantIdValidCb,
176
184
  promiseAllDone,
185
+ cleanUndefined,
177
186
  __: {
178
187
  clearAuthContextCache: () => getAuthContext._cache?.clear(),
179
188
  },
@@ -11,9 +11,9 @@ const COMPONENT_NAME = "/eventQueue/distributedLock";
11
11
  const acquireLock = async (
12
12
  context,
13
13
  key,
14
- { tenantScoped = true, expiryTime = config.globalTxTimeout, keepTrackOfLock = false } = {}
14
+ { tenantScoped = true, expiryTime = config.globalTxTimeout, keepTrackOfLock = false, skipNamespace = false } = {}
15
15
  ) => {
16
- const fullKey = _generateKey(context, tenantScoped, key);
16
+ const fullKey = _generateKey(context, tenantScoped, key, skipNamespace);
17
17
  if (config.redisEnabled) {
18
18
  return await _acquireLockRedis(context, fullKey, expiryTime, { keepTrackOfLock });
19
19
  } else {
@@ -51,8 +51,8 @@ const setValueWithExpire = async (
51
51
  }
52
52
  };
53
53
 
54
- const releaseLock = async (context, key, { tenantScoped = true } = {}) => {
55
- const fullKey = _generateKey(context, tenantScoped, key);
54
+ const releaseLock = async (context, key, { tenantScoped = true, skipNamespace = false } = {}) => {
55
+ const fullKey = _generateKey(context, tenantScoped, key, skipNamespace);
56
56
  if (config.redisEnabled) {
57
57
  return await _releaseLockRedis(context, fullKey);
58
58
  } else {
@@ -194,8 +194,8 @@ const _acquireLockDB = async (
194
194
  return result;
195
195
  };
196
196
 
197
- const _generateKey = (context, tenantScoped, key) => {
198
- const keyParts = [config.redisNamespace()];
197
+ const _generateKey = (context, tenantScoped, key, skipNamespace) => {
198
+ const keyParts = [config.redisNamespace(!skipNamespace)];
199
199
  tenantScoped && keyParts.push(context.tenant);
200
200
  keyParts.push(key);
201
201
  return `${keyParts.join("##")}`;
@@ -42,4 +42,17 @@ service EventQueueAdminService {
42
42
  @mandatory
43
43
  subType: String) returns Boolean;
44
44
  }
45
+
46
+ action publishEvent(
47
+ @mandatory
48
+ tenants: array of String,
49
+ @mandatory
50
+ type: String,
51
+ @mandatory
52
+ subType: String,
53
+ referenceEntity: String,
54
+ referenceEntityKey: String,
55
+ @open
56
+ payload: {},
57
+ startAfter: String);
45
58
  }
@@ -1,26 +1,35 @@
1
1
  "use strict";
2
2
 
3
3
  const cds = require("@sap/cds");
4
- const { EventProcessingStatus } = require("../../src");
5
- const config = require("../../src/config");
4
+
5
+ const eventQueue = require("../../src");
6
6
  const distributedLock = require("../../src/shared/distributedLock");
7
7
  const redisPub = require("../../src/redis/redisPub");
8
+ const publishEventHelper = require("./publishEventHelper");
9
+ const commonHelper = require("../../src/shared/common");
10
+
11
+ const COMPONENT_NAME = "/eventQueue/admin";
8
12
 
9
13
  module.exports = class AdminService extends cds.ApplicationService {
10
14
  async init() {
11
15
  const { Event: EventService, Lock: LockService } = this.entities;
12
16
  const { Event: EventDb } = cds.db.entities("sap.eventqueue");
17
+ const { publishEvent } = this.actions;
13
18
  const { landscape, space } = this.getLandscapeAndSpace();
14
19
 
15
20
  this.before("*", (req) => {
16
- if (!config.enableAdminService) {
21
+ if (!eventQueue.config.enableAdminService) {
17
22
  req.reject(403, "Admin service is disabled by configuration");
18
23
  }
19
24
 
25
+ if (req.event === publishEvent.name.split(".")[1]) {
26
+ return;
27
+ }
28
+
20
29
  const headers = Object.assign({}, req.headers, req.req?.headers);
21
30
  const tenant = headers["z-id"] ?? req.data.tenant;
22
31
 
23
- if (config.isMultiTenancy && tenant == null) {
32
+ if (eventQueue.config.isMultiTenancy && tenant == null) {
24
33
  req.reject(400, "Missing tenant ID in request header (z-id)");
25
34
  }
26
35
  req.headers ??= {};
@@ -46,7 +55,7 @@ module.exports = class AdminService extends cds.ApplicationService {
46
55
  });
47
56
 
48
57
  this.on("READ", LockService, async () => {
49
- if (!config.redisEnabled) {
58
+ if (!eventQueue.config.redisEnabled) {
50
59
  return [];
51
60
  }
52
61
  const locks = await distributedLock.getAllLocksRedis();
@@ -59,14 +68,14 @@ module.exports = class AdminService extends cds.ApplicationService {
59
68
 
60
69
  this.on("setStatusAndAttempts", async (req) => {
61
70
  const tenant = req.headers["z-id"];
62
- cds.log("eventQueue").info("Restarting processing for event queue");
71
+ cds.log(COMPONENT_NAME).info("Restarting processing for event queue");
63
72
  const updateData = {};
64
73
 
65
74
  if (Number.isInteger(req.data.attempts)) {
66
75
  updateData.attempts = req.data.attempts;
67
76
  }
68
77
 
69
- if (Object.values(EventProcessingStatus).includes(req.data.status)) {
78
+ if (Object.values(eventQueue.EventProcessingStatus).includes(req.data.status)) {
70
79
  updateData.status = req.data.status;
71
80
  }
72
81
 
@@ -95,6 +104,32 @@ module.exports = class AdminService extends cds.ApplicationService {
95
104
  });
96
105
  });
97
106
 
107
+ this.on(publishEvent, async (req) => {
108
+ const logger = cds.log(COMPONENT_NAME);
109
+ try {
110
+ const { type, subType, referenceEntity, referenceEntityKey, payload, startAfter } = req.data;
111
+ const tenants = await publishEventHelper.resolveTenantInfos(req);
112
+ const eventOptions = commonHelper.cleanUndefined({
113
+ type,
114
+ subType,
115
+ referenceEntity,
116
+ referenceEntityKey,
117
+ payload,
118
+ startAfter,
119
+ });
120
+ const publishInfo = { count: tenants.length, type, subType, tenants: req.data.tenants };
121
+ logger.info("publishing event for tenant(s)", publishInfo);
122
+ for (const tenant of tenants) {
123
+ await cds.tx({ tenant }, async (tx) => {
124
+ await eventQueue.publishEvent(tx, { ...eventOptions });
125
+ });
126
+ }
127
+ logger.info("finished publishing event for tenant(s)", publishInfo);
128
+ } catch (err) {
129
+ logger.error("error publishing event", err);
130
+ }
131
+ });
132
+
98
133
  await super.init();
99
134
  }
100
135
 
@@ -0,0 +1,52 @@
1
+ "use strict";
2
+
3
+ const cdsHelper = require("../../src/shared/cdsHelper");
4
+
5
+ /**
6
+ * @typedef TenantInfo
7
+ * @type object
8
+ * @property {string} tenantId
9
+ * @property {string} subdomain
10
+ */
11
+ /**
12
+ * For service action interfaces, with a tenants = [...] data field this code
13
+ * resolves the inputs into a tenantInfos array.
14
+ *
15
+ * Inputs:
16
+ * - ["all"] give me all tenants
17
+ * - ["self"] give me just the context tenant
18
+ * - [] give me no tenant
19
+ * - ["<tenantId1>", "<tenantId2>", "<subdomain3>", "<subdomain4>",...]
20
+ * give me those tenants where either tenantId or subdomain corresponds to the given inputs
21
+ *
22
+ * @returns {Promise<Array<TenantInfo>>} returns an array of {@link TenantInfo} objects.
23
+ */
24
+ const resolveTenantInfos = async (context, { sortByTenantId = true } = {}) => {
25
+ let result = await _resolveTenantInfos(context);
26
+ if (sortByTenantId) {
27
+ result = result.sort(_orderByTenantId);
28
+ }
29
+ return result;
30
+ };
31
+
32
+ const _resolveTenantInfos = async (context) => {
33
+ if (!Array.isArray(context.data.tenants)) {
34
+ return [];
35
+ }
36
+ for (const tenant of context.data.tenants) {
37
+ if (tenant === "self") {
38
+ return [context.tenant];
39
+ }
40
+ if (["all", "*"].includes(tenant)) {
41
+ return await cdsHelper.getAllTenantIds();
42
+ }
43
+ }
44
+ const contextTenantsMap = Object.fromEntries(context.data.tenants.map((tenant) => [tenant, true]));
45
+ return (await cdsHelper.getAllTenantIds()).filter((tenantId) => contextTenantsMap[tenantId]);
46
+ };
47
+
48
+ const _orderByTenantId = (a, b) => a.localeCompare(b);
49
+
50
+ module.exports = {
51
+ resolveTenantInfos,
52
+ };