@cap-js-community/event-queue 1.5.2 → 1.6.0

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/cds-plugin.js CHANGED
@@ -5,6 +5,6 @@ const cds = require("@sap/cds");
5
5
  const eventQueue = require("./src");
6
6
  const COMPONENT_NAME = "/eventQueue/plugin";
7
7
 
8
- if (!cds.build?.register && cds.env.eventQueue) {
8
+ if (!cds.build?.register && Object.keys(cds.env.eventQueue ?? {}).length) {
9
9
  module.exports = eventQueue.initialize().catch((err) => cds.log(COMPONENT_NAME).error(err));
10
10
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cap-js-community/event-queue",
3
- "version": "1.5.2",
3
+ "version": "1.6.0",
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",
@@ -44,12 +44,12 @@
44
44
  },
45
45
  "dependencies": {
46
46
  "@sap/xssec": "^3.6.1",
47
- "redis": "^4.6.14",
47
+ "redis": "^4.6.15",
48
48
  "verror": "^1.10.1",
49
49
  "yaml": "^2.4.2"
50
50
  },
51
51
  "devDependencies": {
52
- "@cap-js/hana": "^1.0.0",
52
+ "@cap-js/hana": "^1.1.0",
53
53
  "@cap-js/sqlite": "^1.7.2",
54
54
  "@sap/cds": "^7.9.2",
55
55
  "@sap/cds-dk": "^7.8.0",
@@ -18,8 +18,7 @@ const ERROR_CODES = {
18
18
  NO_MANUEL_INSERT_OF_PERIODIC: "NO_MANUEL_INSERT_OF_PERIODIC",
19
19
  LOAD_HIGHER_THAN_LIMIT: "LOAD_HIGHER_THAN_LIMIT",
20
20
  NOT_ALLOWED_PRIORITY: "NOT_ALLOWED_PRIORITY",
21
- SCHEMA_TENANT_MISMATCH: "SCHEMA_TENANT_MISMATCH",
22
- GLOBAL_CDS_CONTEXT_MISMATCH: "GLOBAL_CDS_CONTEXT_MISMATCH",
21
+ APP_NAMES_FORMAT: "APP_NAMES_FORMAT",
23
22
  };
24
23
 
25
24
  const ERROR_CODES_META = {
@@ -69,11 +68,8 @@ const ERROR_CODES_META = {
69
68
  [ERROR_CODES.NOT_ALLOWED_PRIORITY]: {
70
69
  message: "The supplied priority is not allowed. Only LOW, MEDIUM, HIGH is allowed!",
71
70
  },
72
- [ERROR_CODES.SCHEMA_TENANT_MISMATCH]: {
73
- message: "The db client associated to the tenant context does not match! Processing will be skipped.",
74
- },
75
- [ERROR_CODES.GLOBAL_CDS_CONTEXT_MISMATCH]: {
76
- message: "The global cds context does not match the local cds context.",
71
+ [ERROR_CODES.APP_NAMES_FORMAT]: {
72
+ message: "The app names property must be an array and only contain strings.",
77
73
  },
78
74
  };
79
75
 
@@ -244,23 +240,12 @@ class EventQueueError extends VError {
244
240
  );
245
241
  }
246
242
 
247
- static dbClientSchemaMismatch(tenantId, dbClientSchema, serviceManagerSchema) {
248
- const { message } = ERROR_CODES_META[ERROR_CODES.SCHEMA_TENANT_MISMATCH];
249
- return new EventQueueError(
250
- {
251
- name: ERROR_CODES.SCHEMA_TENANT_MISMATCH,
252
- info: { tenantId, dbClientSchema, serviceManagerSchema },
253
- },
254
- message
255
- );
256
- }
257
-
258
- static globalCdsContextNotMatchingLocal(globalProperties, localProperties) {
259
- const { message } = ERROR_CODES_META[ERROR_CODES.GLOBAL_CDS_CONTEXT_MISMATCH];
243
+ static appNamesFormat(type, subType, appNames) {
244
+ const { message } = ERROR_CODES_META[ERROR_CODES.APP_NAMES_FORMAT];
260
245
  return new EventQueueError(
261
246
  {
262
- name: ERROR_CODES.GLOBAL_CDS_CONTEXT_MISMATCH,
263
- info: { globalProperties, localProperties },
247
+ name: ERROR_CODES.APP_NAMES_FORMAT,
248
+ info: { type, subType, appNames },
264
249
  },
265
250
  message
266
251
  );
@@ -1108,6 +1108,10 @@ class EventQueueProcessorBase {
1108
1108
  return this.#eventType.replace(SUFFIX_PERIODIC, "");
1109
1109
  }
1110
1110
 
1111
+ get rawEventType() {
1112
+ return this.#eventType;
1113
+ }
1114
+
1111
1115
  get eventSubType() {
1112
1116
  return this.#eventSubType;
1113
1117
  }
package/src/config.js CHANGED
@@ -105,7 +105,12 @@ class Config {
105
105
  }
106
106
 
107
107
  _checkRedisIsBound() {
108
- return !!this.#env.getRedisCredentialsFromEnv();
108
+ return !!this.#env.redisCredentialsFromEnv;
109
+ }
110
+
111
+ shouldBeProcessedInThisApplication(type, subType) {
112
+ const config = this.#eventMap[this.generateKey(type, subType)];
113
+ return !config._appNameMap || config._appNameMap[this.#env.applicationName];
109
114
  }
110
115
 
111
116
  checkRedisEnabled() {
@@ -276,8 +281,12 @@ class Config {
276
281
  eventOutdatedCheck: config.eventOutdatedCheck,
277
282
  checkForNextChunk: config.checkForNextChunk,
278
283
  deleteFinishedEventsAfterDays: config.deleteFinishedEventsAfterDays,
284
+ appNames: config.appNames,
279
285
  internalEvent: true,
280
286
  };
287
+ eventConfig._appNameMap = eventConfig.appNames
288
+ ? Object.fromEntries(new Map(eventConfig.appNames.map((a) => [a, true])))
289
+ : null;
281
290
  this.#config.events.push(eventConfig);
282
291
  this.#eventMap[this.generateKey(CAP_EVENT_TYPE, serviceName)] = eventConfig;
283
292
  }
@@ -317,6 +326,7 @@ class Config {
317
326
  event.load = event.load ?? DEFAULT_LOAD;
318
327
  event.priority = event.priority ?? DEFAULT_PRIORITY;
319
328
  this.validateAdHocEvents(result, event);
329
+ event._appNameMap = event.appNames ? Object.fromEntries(new Map(event.appNames.map((a) => [a, true]))) : null;
320
330
  result[this.generateKey(event.type, event.subType)] = event;
321
331
  return result;
322
332
  }, {});
@@ -326,6 +336,7 @@ class Config {
326
336
  event.type = `${event.type}${SUFFIX_PERIODIC}`;
327
337
  event.isPeriodic = true;
328
338
  this.validatePeriodicConfig(result, event);
339
+ event._appNameMap = event.appNames ? Object.fromEntries(new Map(event.appNames.map((a) => [a, true]))) : null;
329
340
  result[this.generateKey(event.type, event.subType)] = event;
330
341
  return result;
331
342
  }, this.#eventMap);
@@ -355,6 +366,12 @@ class Config {
355
366
  if (!config.impl) {
356
367
  throw EventQueueError.missingImpl(config.type, config.subType);
357
368
  }
369
+
370
+ if (config.appNames) {
371
+ if (!Array.isArray(config.appNames) || config.appNames.some((appName) => typeof appName !== "string")) {
372
+ throw EventQueueError.appNamesFormat(config.type, config.subType, config.appNames);
373
+ }
374
+ }
358
375
  }
359
376
 
360
377
  generateKey(type, subType) {
package/src/initialize.js CHANGED
@@ -131,22 +131,7 @@ const readConfigFromFile = async (configFilepath) => {
131
131
  };
132
132
 
133
133
  const registerEventProcessors = () => {
134
- cds.on("listening", () => {
135
- cds.connect
136
- .to("cds.xt.DeploymentService")
137
- .then((ds) => {
138
- cds.log(COMPONENT).info("event-queue unsubscribe handler registered", {
139
- redisEnabled: config.redisEnabled,
140
- });
141
- ds.after("unsubscribe", async (_, req) => {
142
- const { tenant } = req.data;
143
- config.handleUnsubscribe(tenant);
144
- });
145
- })
146
- .catch(
147
- () => {} // ignore errors as the DeploymentService is most of the time only available in the mtx sidecar
148
- );
149
- });
134
+ _registerUnsubscribe();
150
135
  config.redisEnabled && config.attachRedisUnsubscribeHandler();
151
136
 
152
137
  if (!config.registerAsEventProcessor) {
@@ -215,6 +200,25 @@ const registerCleanupForDevDb = async () => {
215
200
  }
216
201
  };
217
202
 
203
+ const _registerUnsubscribe = () => {
204
+ cds.on("listening", () => {
205
+ cds.connect
206
+ .to("cds.xt.DeploymentService")
207
+ .then((ds) => {
208
+ cds.log(COMPONENT).info("event-queue unsubscribe handler registered", {
209
+ redisEnabled: config.redisEnabled,
210
+ });
211
+ ds.after("unsubscribe", async (_, req) => {
212
+ const { tenant } = req.data;
213
+ config.handleUnsubscribe(tenant);
214
+ });
215
+ })
216
+ .catch(
217
+ () => {} // ignore errors as the DeploymentService is most of the time only available in the mtx sidecar
218
+ );
219
+ });
220
+ };
221
+
218
222
  module.exports = {
219
223
  initialize,
220
224
  };
@@ -15,9 +15,19 @@ const COMPONENT_NAME = "/eventQueue/eventQueueAsOutbox";
15
15
  function outboxed(srv, customOpts) {
16
16
  // outbox max. once
17
17
  const logger = cds.log(COMPONENT_NAME);
18
+ const outboxOpts = Object.assign(
19
+ {},
20
+ (typeof cds.requires.outbox === "object" && cds.requires.outbox) || {},
21
+ (typeof srv.options?.outbox === "object" && srv.options.outbox) || {},
22
+ customOpts || {}
23
+ );
24
+
18
25
  if (!new.target) {
19
26
  const former = srv[OUTBOXED];
20
27
  if (former) {
28
+ if (outboxOpts.kind === "persistent-outbox") {
29
+ config.addCAPOutboxEvent(srv.name, outboxOpts);
30
+ }
21
31
  return former;
22
32
  }
23
33
  }
@@ -30,14 +40,9 @@ function outboxed(srv, customOpts) {
30
40
  Object.defineProperty(srv, OUTBOXED, { value: outboxedSrv });
31
41
  }
32
42
 
33
- const outboxOpts = Object.assign(
34
- {},
35
- (typeof cds.requires.outbox === "object" && cds.requires.outbox) || {},
36
- (typeof srv.options?.outbox === "object" && srv.options.outbox) || {},
37
- customOpts || {}
38
- );
39
-
40
- config.addCAPOutboxEvent(srv.name, outboxOpts);
43
+ if (outboxOpts.kind === "persistent-outbox") {
44
+ config.addCAPOutboxEvent(srv.name, outboxOpts);
45
+ }
41
46
  outboxedSrv.handle = async function (req) {
42
47
  const context = req.context || cds.context;
43
48
  if (outboxOpts.kind === "persistent-outbox") {
@@ -269,6 +269,7 @@ const _checkEventIsBlocked = async (baseInstance) => {
269
269
  });
270
270
  }
271
271
  } else {
272
+ // TODO: we should be able to get rid of baseInstance.isPeriodicEvent with rawEventType
272
273
  eventBlocked = config.isEventBlocked(
273
274
  baseInstance.eventType,
274
275
  baseInstance.eventSubType,
@@ -283,11 +284,16 @@ const _checkEventIsBlocked = async (baseInstance) => {
283
284
 
284
285
  if (eventBlocked) {
285
286
  baseInstance.logger.info("skipping run because event is blocked by configuration", {
286
- type: baseInstance.eventType,
287
+ type: baseInstance.rawEventType,
287
288
  subType: baseInstance.eventSubType,
288
289
  tenantUnsubscribed: config.isTenantUnsubscribed(baseInstance.context.tenant),
289
290
  });
290
291
  }
292
+
293
+ if (!eventBlocked) {
294
+ eventBlocked = !config.shouldBeProcessedInThisApplication(baseInstance.rawEventType, baseInstance.eventSubType);
295
+ }
296
+
291
297
  return eventBlocked;
292
298
  };
293
299
 
@@ -87,11 +87,11 @@ const _processLocalWithoutRedis = async (tenantId, events) => {
87
87
  };
88
88
  }
89
89
 
90
- return await cds.tx(context, async ({ context }) => {
91
- for (const { type, subType } of events) {
90
+ for (const { type, subType } of events) {
91
+ await cds.tx(context, async ({ context }) => {
92
92
  await runEventCombinationForTenant(context, type, subType, { shouldTrace: true });
93
- }
94
- });
93
+ });
94
+ }
95
95
  }
96
96
  };
97
97
 
@@ -35,14 +35,6 @@ const _messageHandlerProcessEvents = async (messageData) => {
35
35
  return;
36
36
  }
37
37
 
38
- const user = await cds.tx({ tenant: tenantId }, async () => {
39
- return new cds.User.Privileged({ id: config.userId, authInfo: await common.getAuthInfo(tenantId) });
40
- });
41
- const tenantContext = {
42
- tenant: tenantId,
43
- user,
44
- };
45
-
46
38
  if (!config.getEventConfig(type, subType)) {
47
39
  if (config.isCapOutboxEvent(type)) {
48
40
  try {
@@ -64,6 +56,23 @@ const _messageHandlerProcessEvents = async (messageData) => {
64
56
  }
65
57
  }
66
58
 
59
+ if (!(config.getEventConfig(type, subType) && config.shouldBeProcessedInThisApplication(type, subType))) {
60
+ logger.debug("event is not configured to be processed on this app-name", {
61
+ tenantId,
62
+ type,
63
+ subType,
64
+ });
65
+ return;
66
+ }
67
+
68
+ const user = await cds.tx({ tenant: tenantId }, async () => {
69
+ return new cds.User.Privileged({ id: config.userId, authInfo: await common.getAuthInfo(tenantId) });
70
+ });
71
+ const tenantContext = {
72
+ tenant: tenantId,
73
+ user,
74
+ };
75
+
67
76
  return await cds.tx(tenantContext, async ({ context }) => {
68
77
  return await runnerHelper.runEventCombinationForTenant(context, type, subType, { lockId, shouldTrace: true });
69
78
  });
@@ -29,18 +29,21 @@ const getOpenQueueEntries = async (tx) => {
29
29
 
30
30
  const result = [];
31
31
  for (const { type, subType } of entries) {
32
- if (type.startsWith("CAP_OUTBOX")) {
33
- if (cds.requires[subType]) {
34
- await cds.connect.to(subType).catch(() => {});
35
- result.push({ type, subType });
36
- } else {
37
- const service = await cds.connect.to(subType).catch(() => {});
38
- if (service) {
39
- result.push({ type, subType });
40
- }
41
- }
32
+ if (eventConfig.isCapOutboxEvent(type)) {
33
+ await cds.connect
34
+ .to(subType)
35
+ .then((service) => {
36
+ if (!service) {
37
+ return;
38
+ }
39
+ cds.outboxed(service);
40
+ if (eventConfig.shouldBeProcessedInThisApplication(type, subType)) {
41
+ result.push({ type, subType });
42
+ }
43
+ })
44
+ .catch(() => {});
42
45
  } else {
43
- if (eventConfig.getEventConfig(type, subType)) {
46
+ if (eventConfig.getEventConfig(type, subType) && eventConfig.shouldBeProcessedInThisApplication(type, subType)) {
44
47
  result.push({ type, subType });
45
48
  }
46
49
  }
@@ -22,7 +22,7 @@ const EVENT_QUEUE_RUN_ID = "EVENT_QUEUE_RUN_ID";
22
22
  const EVENT_QUEUE_RUN_TS = "EVENT_QUEUE_RUN_TS";
23
23
  const EVENT_QUEUE_RUN_REDIS_CHECK = "EVENT_QUEUE_RUN_REDIS_CHECK";
24
24
  const EVENT_QUEUE_UPDATE_PERIODIC_EVENTS = "EVENT_QUEUE_UPDATE_PERIODIC_EVENTS";
25
- const OFFSET_FIRST_RUN = 10 * 1000;
25
+ let OFFSET_FIRST_RUN = 10 * 1000;
26
26
 
27
27
  let tenantIdHash;
28
28
  let singleRunDone;
@@ -160,7 +160,6 @@ const _executeEventsAllTenantsRedis = async (tenantIds) => {
160
160
 
161
161
  const _executeEventsAllTenants = async (tenantIds, runId) => {
162
162
  const promises = [];
163
-
164
163
  for (const tenantId of tenantIds) {
165
164
  const id = cds.utils.uuid();
166
165
  let tenantContext;
@@ -254,32 +253,46 @@ const _executePeriodicEventsAllTenants = async (tenantIds) => {
254
253
  }
255
254
  };
256
255
 
257
- const _singleTenantDb = async (tenantId) => {
258
- return Promise.allSettled(
259
- eventQueueConfig.allEvents.map(async (eventConfig) => {
256
+ const _singleTenantDb = async () => {
257
+ const id = cds.utils.uuid();
258
+ const events = await trace(
259
+ { id },
260
+ "fetch-openEvents-and-authInfo",
261
+ async () => {
262
+ return await cds.tx({}, async (tx) => {
263
+ return await openEvents.getOpenQueueEntries(tx);
264
+ });
265
+ },
266
+ { newRootSpan: true }
267
+ );
268
+
269
+ return await Promise.allSettled(
270
+ events.map(async (openEvent) => {
271
+ const eventConfig = config.getEventConfig(openEvent.type, openEvent.subType);
260
272
  const label = `${eventConfig.type}_${eventConfig.subType}`;
261
- const user = new cds.User.Privileged(config.userId);
262
- const tenantContext = {
263
- tenant: tenantId,
264
- user,
265
- };
266
273
  return await WorkerQueue.instance.addToQueue(eventConfig.load, label, eventConfig.priority, async () => {
267
- return await cds.tx(tenantContext, async ({ context }) => {
268
- try {
269
- const lockId = `${EVENT_QUEUE_RUN_ID}_${label}`;
270
- const couldAcquireLock = await distributedLock.acquireLock(context, lockId, {
271
- expiryTime: eventQueueConfig.runInterval * 0.95,
272
- });
273
- if (!couldAcquireLock) {
274
- return;
275
- }
276
- await runEventCombinationForTenant(context, eventConfig.type, eventConfig.subType, true);
277
- } catch (err) {
278
- cds.log(COMPONENT_NAME).error("executing event-queue run for tenant failed", {
279
- tenantId,
280
- redisEnabled: eventQueueConfig.redisEnabled,
281
- });
282
- }
274
+ return await cds.tx({}, async ({ context }) => {
275
+ await trace(
276
+ context,
277
+ label,
278
+ async () => {
279
+ try {
280
+ const lockId = `${label}`;
281
+ const couldAcquireLock = await distributedLock.acquireLock(context, lockId, {
282
+ expiryTime: eventQueueConfig.runInterval * 0.95,
283
+ });
284
+ if (!couldAcquireLock) {
285
+ return;
286
+ }
287
+ await runEventCombinationForTenant(context, eventConfig.type, eventConfig.subType, {
288
+ skipWorkerPool: true,
289
+ });
290
+ } catch (err) {
291
+ cds.log(COMPONENT_NAME).error("executing event-queue run for tenant failed");
292
+ }
293
+ },
294
+ { newRootSpan: true }
295
+ );
283
296
  });
284
297
  });
285
298
  })
@@ -387,7 +400,8 @@ const _multiTenancyPeriodicEvents = async (tenantIds) => {
387
400
  }
388
401
  };
389
402
 
390
- const _checkPeriodicEventsSingleTenantOneTime = () =>
403
+ const _checkPeriodicEventsSingleTenantOneTime = async () =>
404
+ eventQueueConfig.updatePeriodicEvents &&
391
405
  cds.tx({}, async (tx) => await periodicEvents.checkAndInsertPeriodicEvents(tx.context));
392
406
 
393
407
  const _checkPeriodicEventsSingleTenant = async (context) => {
@@ -425,5 +439,6 @@ module.exports = {
425
439
  _acquireRunId,
426
440
  EVENT_QUEUE_RUN_TS,
427
441
  clearHash: () => (tenantIdHash = null),
442
+ setOffsetFirstRun: (value) => (OFFSET_FIRST_RUN = value),
428
443
  },
429
444
  };
@@ -113,7 +113,7 @@ class WorkerQueue {
113
113
  }
114
114
 
115
115
  #checkForNext() {
116
- if (!this.#queue.length && this.#runningLoad === this.#concurrencyLimit) {
116
+ if (!Object.values(this.#queue).some((queue) => queue.length) || this.#runningLoad === this.#concurrencyLimit) {
117
117
  return;
118
118
  }
119
119
 
package/src/shared/env.js CHANGED
@@ -3,34 +3,42 @@
3
3
  let instance;
4
4
 
5
5
  class Env {
6
- #isLocal;
7
6
  #vcapServices;
7
+ #vcapApplication;
8
8
 
9
9
  constructor() {
10
10
  try {
11
11
  this.#vcapServices = JSON.parse(process.env.VCAP_SERVICES);
12
+ this.#vcapApplication = JSON.parse(process.env.VCAP_APPLICATION);
12
13
  } catch {
13
14
  this.#vcapServices = {};
15
+ this.#vcapApplication = {};
14
16
  }
15
17
  }
16
18
 
17
- getRedisCredentialsFromEnv() {
19
+ get redisCredentialsFromEnv() {
18
20
  return this.#vcapServices["redis-cache"]?.[0]?.credentials;
19
21
  }
20
22
 
21
- set isLocal(value) {
22
- this.#isLocal = value;
23
- }
24
- get isLocal() {
25
- return this.#isLocal;
23
+ get applicationName() {
24
+ return this.#vcapApplication.application_name;
26
25
  }
27
26
 
28
27
  set vcapServices(value) {
29
28
  this.#vcapServices = value;
30
29
  }
30
+
31
31
  get vcapServices() {
32
32
  return this.#vcapServices;
33
33
  }
34
+
35
+ set vcapApplication(value) {
36
+ this.#vcapApplication = value;
37
+ }
38
+
39
+ get vcapApplication() {
40
+ return this.#vcapApplication;
41
+ }
34
42
  }
35
43
 
36
44
  module.exports = {
@@ -47,7 +47,7 @@ class EventScheduler {
47
47
  }
48
48
 
49
49
  clearForTenant(tenantId) {
50
- Object.values(this.#eventsByTenants[tenantId]).forEach((timeoutId) => clearTimeout(timeoutId));
50
+ Object.keys(this.#eventsByTenants[tenantId] ?? []).forEach((timeoutId) => clearTimeout(timeoutId));
51
51
  }
52
52
 
53
53
  calculateOffset(type, subType, startAfter) {
@@ -67,6 +67,14 @@ class EventScheduler {
67
67
  clearScheduledEvents() {
68
68
  this.#scheduledEvents = {};
69
69
  }
70
+
71
+ clearEventsByTenants() {
72
+ this.#eventsByTenants = {};
73
+ }
74
+
75
+ get eventsByTenants() {
76
+ return this.#eventsByTenants;
77
+ }
70
78
  }
71
79
 
72
80
  module.exports = {
@@ -30,7 +30,7 @@ const createMainClientAndConnect = (options) => {
30
30
  const _createClientBase = (redisOptions) => {
31
31
  const env = getEnvInstance();
32
32
  try {
33
- const credentials = env.getRedisCredentialsFromEnv();
33
+ const credentials = env.redisCredentialsFromEnv;
34
34
  const redisIsCluster = credentials.cluster_mode;
35
35
  const url = credentials.uri.replace(/(?<=rediss:\/\/)[\w-]+?(?=:)/, "");
36
36
  if (redisIsCluster) {