@cap-js-community/event-queue 1.2.5 → 1.3.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cap-js-community/event-queue",
3
- "version": "1.2.5",
3
+ "version": "1.3.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
  "files": [
@@ -17,7 +17,9 @@ const ERROR_CODES = {
17
17
  DUPLICATE_EVENT_REGISTRATION: "DUPLICATE_EVENT_REGISTRATION",
18
18
  NO_MANUEL_INSERT_OF_PERIODIC: "NO_MANUEL_INSERT_OF_PERIODIC",
19
19
  LOAD_HIGHER_THAN_LIMIT: "LOAD_HIGHER_THAN_LIMIT",
20
+ NOT_ALLOWED_PRIORITY: "NOT_ALLOWED_PRIORITY",
20
21
  SCHEMA_TENANT_MISMATCH: "SCHEMA_TENANT_MISMATCH",
22
+ GLOBAL_CDS_CONTEXT_MISMATCH: "GLOBAL_CDS_CONTEXT_MISMATCH",
21
23
  };
22
24
 
23
25
  const ERROR_CODES_META = {
@@ -64,9 +66,15 @@ const ERROR_CODES_META = {
64
66
  [ERROR_CODES.LOAD_HIGHER_THAN_LIMIT]: {
65
67
  message: "The defined load of an event is higher than the maximum defined limit. Check your configuration!",
66
68
  },
69
+ [ERROR_CODES.NOT_ALLOWED_PRIORITY]: {
70
+ message: "The supplied priority is not allowed. Only LOW, MEDIUM, HIGH is allowed!",
71
+ },
67
72
  [ERROR_CODES.SCHEMA_TENANT_MISMATCH]: {
68
73
  message: "The db client associated to the tenant context does not match! Processing will be skipped.",
69
74
  },
75
+ [ERROR_CODES.GLOBAL_CDS_CONTEXT_MISMATCH]: {
76
+ message: "The global cds context does not match the local cds context.",
77
+ },
70
78
  };
71
79
 
72
80
  class EventQueueError extends VError {
@@ -225,6 +233,17 @@ class EventQueueError extends VError {
225
233
  );
226
234
  }
227
235
 
236
+ static priorityNotAllowed(priority, label) {
237
+ const { message } = ERROR_CODES_META[ERROR_CODES.NOT_ALLOWED_PRIORITY];
238
+ return new EventQueueError(
239
+ {
240
+ name: ERROR_CODES.NOT_ALLOWED_PRIORITY,
241
+ info: { priority, label },
242
+ },
243
+ message
244
+ );
245
+ }
246
+
228
247
  static dbClientSchemaMismatch(tenantId, dbClientSchema, serviceManagerSchema) {
229
248
  const { message } = ERROR_CODES_META[ERROR_CODES.SCHEMA_TENANT_MISMATCH];
230
249
  return new EventQueueError(
@@ -235,6 +254,17 @@ class EventQueueError extends VError {
235
254
  message
236
255
  );
237
256
  }
257
+
258
+ static globalCdsContextNotMatchingLocal(globalProperties, localProperties) {
259
+ const { message } = ERROR_CODES_META[ERROR_CODES.GLOBAL_CDS_CONTEXT_MISMATCH];
260
+ return new EventQueueError(
261
+ {
262
+ name: ERROR_CODES.GLOBAL_CDS_CONTEXT_MISMATCH,
263
+ info: { globalProperties, localProperties },
264
+ },
265
+ message
266
+ );
267
+ }
238
268
  }
239
269
 
240
270
  module.exports = EventQueueError;
@@ -70,6 +70,8 @@ class EventQueueProcessorBase {
70
70
  this.__txMap = {};
71
71
  this.__txRollback = {};
72
72
  this.__queueEntries = [];
73
+
74
+ this.#checkGlobalContextToLocalContext();
73
75
  }
74
76
 
75
77
  /**
@@ -533,7 +535,9 @@ class EventQueueProcessorBase {
533
535
  async getQueueEntriesAndSetToInProgress() {
534
536
  let result = [];
535
537
  const refDateStartAfter = new Date(Date.now() + this.#config.runInterval * 1.2);
538
+ this.#checkGlobalContextToLocalContext();
536
539
  await executeInNewTransaction(this.__baseContext, "eventQueue-getQueueEntriesAndSetToInProgress", async (tx) => {
540
+ this.#checkGlobalContextToLocalContext();
537
541
  await this.checkTxConsistency(tx);
538
542
  const entries = await tx.run(
539
543
  SELECT.from(this.#config.tableNameEventQueue)
@@ -692,7 +696,7 @@ class EventQueueProcessorBase {
692
696
  } catch (err) {
693
697
  errorHandler(err);
694
698
  }
695
- if (txSchema !== serviceManagerSchema) {
699
+ if (serviceManagerSchema && txSchema !== serviceManagerSchema) {
696
700
  const err = EventQueueError.dbClientSchemaMismatch(tx.context.tenant, txSchema, serviceManagerSchema);
697
701
  errorHandler(err);
698
702
  throw err;
@@ -724,6 +728,48 @@ class EventQueueProcessorBase {
724
728
  return entry.lastAttemptsTs;
725
729
  }
726
730
 
731
+ #checkGlobalContextToLocalContext() {
732
+ if (!this.#config.enableTxConsistencyCheck) {
733
+ return;
734
+ }
735
+ if (this.__context.tenant !== cds.context.tenant) {
736
+ throw EventQueueError.globalCdsContextNotMatchingLocal(
737
+ JSON.stringify(
738
+ {
739
+ correlationId: cds.context.id,
740
+ tenantId: cds.context.tenant,
741
+ timestamp: cds.context.timestamp,
742
+ base: JSON.stringify(
743
+ {
744
+ correlationId: cds.context.context?.id,
745
+ tenantId: cds.context.context?.tenant,
746
+ timestamp: cds.context.context?.timestamp,
747
+ },
748
+ null,
749
+ 2
750
+ ),
751
+ },
752
+ null,
753
+ 2
754
+ ),
755
+ JSON.stringify(
756
+ {
757
+ correlationId: this.__context.id,
758
+ tenantId: this.__context.tenant,
759
+ timestamp: this.__context.timestamp,
760
+ base: JSON.stringify({
761
+ correlationId: this.__context.context?.id,
762
+ tenantId: this.__context.context?.tenant,
763
+ timestamp: this.__context.context?.timestamp,
764
+ }),
765
+ },
766
+ null,
767
+ 2
768
+ )
769
+ );
770
+ }
771
+ }
772
+
727
773
  #handleDelayedEvents(delayedEvents) {
728
774
  for (const delayedEvent of delayedEvents) {
729
775
  this.#eventSchedulerInstance.scheduleEvent(
package/src/config.js CHANGED
@@ -5,6 +5,7 @@ const cds = require("@sap/cds");
5
5
  const { getEnvInstance } = require("./shared/env");
6
6
  const redis = require("./shared/redis");
7
7
  const EventQueueError = require("./EventQueueError");
8
+ const { Priorities } = require("./constants");
8
9
 
9
10
  const FOR_UPDATE_TIMEOUT = 10;
10
11
  const GLOBAL_TX_TIMEOUT = 30 * 60 * 1000;
@@ -13,6 +14,7 @@ const REDIS_CONFIG_BLOCKLIST_CHANNEL = "REDIS_CONFIG_BLOCKLIST_CHANNEL";
13
14
  const COMPONENT_NAME = "/eventQueue/config";
14
15
  const MIN_INTERVAL_SEC = 10;
15
16
  const DEFAULT_LOAD = 1;
17
+ const DEFAULT_PRIORITY = Priorities.Medium;
16
18
  const SUFFIX_PERIODIC = "_PERIODIC";
17
19
  const COMMAND_BLOCK = "EVENT_QUEUE_EVENT_BLOCK";
18
20
  const COMMAND_UNBLOCK = "EVENT_QUEUE_EVENT_UNBLOCK";
@@ -24,6 +26,7 @@ const BASE_PERIODIC_EVENTS = [
24
26
  {
25
27
  type: "EVENT_QUEUE_BASE",
26
28
  subType: "DELETE_EVENTS",
29
+ priority: Priorities.Low,
27
30
  impl: "./housekeeping/EventQueueDeleteEvents",
28
31
  load: 1,
29
32
  interval: 86400, // 1 day,
@@ -51,8 +54,8 @@ class Config {
51
54
  #env;
52
55
  #eventMap;
53
56
  #updatePeriodicEvents;
54
- #blockedPeriodicEvents;
55
- #isPeriodicEventBlockedCb;
57
+ #blockedEvents;
58
+ #isEventBlockedCb;
56
59
  #thresholdLoggingEventProcessing;
57
60
  #useAsCAPOutbox;
58
61
  #userId;
@@ -76,7 +79,7 @@ class Config {
76
79
  this.#skipCsnCheck = null;
77
80
  this.#disableRedis = null;
78
81
  this.#env = getEnvInstance();
79
- this.#blockedPeriodicEvents = {};
82
+ this.#blockedEvents = {};
80
83
  }
81
84
 
82
85
  getEventConfig(type, subType) {
@@ -100,7 +103,7 @@ class Config {
100
103
  }
101
104
 
102
105
  attachConfigChangeHandler() {
103
- this.#attachBlocklistChangeHandler();
106
+ this.#attachBlockListChangeHandler();
104
107
  redis.subscribeRedisChannel(REDIS_CONFIG_CHANNEL, (messageData) => {
105
108
  try {
106
109
  const { key, value } = JSON.parse(messageData);
@@ -126,14 +129,14 @@ class Config {
126
129
  });
127
130
  }
128
131
 
129
- #attachBlocklistChangeHandler() {
132
+ #attachBlockListChangeHandler() {
130
133
  redis.subscribeRedisChannel(REDIS_CONFIG_BLOCKLIST_CHANNEL, (messageData) => {
131
134
  try {
132
135
  const { command, key, tenant } = JSON.parse(messageData);
133
136
  if (command === COMMAND_BLOCK) {
134
- this.#blockPeriodicEventLocalState(key, tenant);
137
+ this.#blockEventLocalState(key, tenant);
135
138
  } else {
136
- this.#unblockPeriodicEventLocalState(key, tenant);
139
+ this.#unblockEventLocalState(key, tenant);
137
140
  }
138
141
  } catch (err) {
139
142
  this.#logger.error("could not parse event blocklist change", err, {
@@ -143,14 +146,14 @@ class Config {
143
146
  });
144
147
  }
145
148
 
146
- blockPeriodicEvent(type, subType, tenant = "*") {
147
- const typeWithSuffix = `${type}${SUFFIX_PERIODIC}`;
149
+ blockEvent(type, subType, isPeriodic, tenant = "*") {
150
+ const typeWithSuffix = `${type}${isPeriodic ? SUFFIX_PERIODIC : ""}`;
148
151
  const config = this.getEventConfig(typeWithSuffix, subType);
149
152
  if (!config) {
150
153
  return;
151
154
  }
152
155
  const key = this.generateKey(typeWithSuffix, subType);
153
- this.#blockPeriodicEventLocalState(key, tenant);
156
+ this.#blockEventLocalState(key, tenant);
154
157
  if (!this.redisEnabled) {
155
158
  return;
156
159
  }
@@ -162,24 +165,24 @@ class Config {
162
165
  });
163
166
  }
164
167
 
165
- #blockPeriodicEventLocalState(key, tenant) {
166
- this.#blockedPeriodicEvents[key] ??= {};
167
- this.#blockedPeriodicEvents[key][tenant] = true;
168
+ #blockEventLocalState(key, tenant) {
169
+ this.#blockedEvents[key] ??= {};
170
+ this.#blockedEvents[key][tenant] = true;
168
171
  return key;
169
172
  }
170
173
 
171
174
  clearPeriodicEventBlockList() {
172
- this.#blockedPeriodicEvents = {};
175
+ this.#blockedEvents = {};
173
176
  }
174
177
 
175
- unblockPeriodicEvent(type, subType, tenant = "*") {
176
- const typeWithSuffix = `${type}${SUFFIX_PERIODIC}`;
178
+ unblockEvent(type, subType, isPeriodic, tenant = "*") {
179
+ const typeWithSuffix = `${type}${isPeriodic ? SUFFIX_PERIODIC : ""}`;
177
180
  const key = this.generateKey(typeWithSuffix, subType);
178
181
  const config = this.getEventConfig(typeWithSuffix, subType);
179
182
  if (!config) {
180
183
  return;
181
184
  }
182
- this.#unblockPeriodicEventLocalState(key, tenant);
185
+ this.#unblockEventLocalState(key, tenant);
183
186
  if (!this.redisEnabled) {
184
187
  return;
185
188
  }
@@ -215,17 +218,17 @@ class Config {
215
218
  this.#eventMap[this.generateKey(CAP_EVENT_TYPE, serviceName)] = eventConfig;
216
219
  }
217
220
 
218
- #unblockPeriodicEventLocalState(key, tenant) {
219
- const map = this.#blockedPeriodicEvents[key];
221
+ #unblockEventLocalState(key, tenant) {
222
+ const map = this.#blockedEvents[key];
220
223
  if (!map) {
221
224
  return;
222
225
  }
223
- this.#blockedPeriodicEvents[key][tenant] = false;
226
+ this.#blockedEvents[key][tenant] = false;
224
227
  return key;
225
228
  }
226
229
 
227
- isPeriodicEventBlocked(type, subType, tenant) {
228
- const map = this.#blockedPeriodicEvents[this.generateKey(type, subType)];
230
+ isEventBlocked(type, subType, tenant) {
231
+ const map = this.#blockedEvents[this.generateKey(type, subType)];
229
232
  if (!map) {
230
233
  return false;
231
234
  }
@@ -248,12 +251,14 @@ class Config {
248
251
  config.periodicEvents = (config.periodicEvents ?? []).concat(BASE_PERIODIC_EVENTS.map((event) => ({ ...event })));
249
252
  this.#eventMap = config.events.reduce((result, event) => {
250
253
  event.load = event.load ?? DEFAULT_LOAD;
254
+ event.priority = event.priority ?? DEFAULT_PRIORITY;
251
255
  this.validateAdHocEvents(result, event);
252
256
  result[this.generateKey(event.type, event.subType)] = event;
253
257
  return result;
254
258
  }, {});
255
259
  this.#eventMap = config.periodicEvents.reduce((result, event) => {
256
260
  event.load = event.load ?? DEFAULT_LOAD;
261
+ event.priority = event.priority ?? DEFAULT_PRIORITY;
257
262
  event.type = `${event.type}${SUFFIX_PERIODIC}`;
258
263
  event.isPeriodic = true;
259
264
  this.validatePeriodicConfig(result, event);
@@ -371,12 +376,12 @@ class Config {
371
376
  this.#instanceLoadLimit = value;
372
377
  }
373
378
 
374
- get isPeriodicEventBlockedCb() {
375
- return this.#isPeriodicEventBlockedCb;
379
+ get isEventBlockedCb() {
380
+ return this.#isEventBlockedCb;
376
381
  }
377
382
 
378
- set isPeriodicEventBlockedCb(value) {
379
- this.#isPeriodicEventBlockedCb = value;
383
+ set isEventBlockedCb(value) {
384
+ this.#isEventBlockedCb = value;
380
385
  }
381
386
 
382
387
  get tableNameEventQueue() {
package/src/constants.js CHANGED
@@ -13,4 +13,10 @@ module.exports = {
13
13
  alwaysCommit: "alwaysCommit",
14
14
  alwaysRollback: "alwaysRollback",
15
15
  },
16
+ Priorities: {
17
+ Low: "low",
18
+ Medium: "medium",
19
+ High: "high",
20
+ VeryHigh: "veryHigh",
21
+ },
16
22
  };
@@ -29,6 +29,10 @@ const processEventQueue = async (context, eventType, eventSubType, startTime = n
29
29
  return;
30
30
  }
31
31
  baseInstance = new EventTypeClass(context, eventType, eventSubType, eventConfig);
32
+ if (await _checkEventIsBlocked(baseInstance)) {
33
+ return;
34
+ }
35
+
32
36
  const continueProcessing = await baseInstance.acquireDistributedLock();
33
37
  if (!continueProcessing) {
34
38
  return;
@@ -119,31 +123,6 @@ const reevaluateShouldContinue = (eventTypeInstance, iterationCounter, startTime
119
123
  };
120
124
 
121
125
  const processPeriodicEvent = async (context, eventTypeInstance) => {
122
- const isPeriodicEventBlockedCb = config.isPeriodicEventBlockedCb;
123
- const params = [eventTypeInstance.eventType, eventTypeInstance.eventSubType, eventTypeInstance.context.tenant];
124
- let eventBlocked = false;
125
- if (isPeriodicEventBlockedCb) {
126
- try {
127
- eventBlocked = await isPeriodicEventBlockedCb(...params);
128
- } catch (err) {
129
- eventBlocked = true;
130
- eventTypeInstance.logger.error("skipping run because periodic event blocked check failed!", err, {
131
- type: eventTypeInstance.eventType,
132
- subType: eventTypeInstance.eventSubType,
133
- });
134
- }
135
- } else {
136
- eventBlocked = config.isPeriodicEventBlocked(...params);
137
- }
138
-
139
- if (eventBlocked) {
140
- eventTypeInstance.logger.info("skipping run because periodic event is blocked by configuration", {
141
- type: eventTypeInstance.eventType,
142
- subType: eventTypeInstance.eventSubType,
143
- });
144
- return;
145
- }
146
-
147
126
  try {
148
127
  let queueEntry;
149
128
  let processNext = true;
@@ -268,6 +247,41 @@ const processEventMap = async (eventTypeInstance) => {
268
247
  eventTypeInstance.endPerformanceTracerEvents();
269
248
  };
270
249
 
250
+ const _checkEventIsBlocked = async (baseInstance) => {
251
+ const isEventBlockedCb = config.isEventBlockedCb;
252
+ let eventBlocked;
253
+ if (isEventBlockedCb) {
254
+ try {
255
+ eventBlocked = await isEventBlockedCb(
256
+ baseInstance.eventType,
257
+ baseInstance.eventSubType,
258
+ baseInstance.isPeriodicEvent,
259
+ baseInstance.context.tenant
260
+ );
261
+ } catch (err) {
262
+ eventBlocked = true;
263
+ baseInstance.logger.error("skipping run because periodic event blocked check failed!", err, {
264
+ type: baseInstance.eventType,
265
+ subType: baseInstance.eventSubType,
266
+ });
267
+ }
268
+ } else {
269
+ eventBlocked = config.isEventBlocked(
270
+ baseInstance.eventType,
271
+ baseInstance.eventSubType,
272
+ baseInstance.context.tenant
273
+ );
274
+ }
275
+
276
+ if (eventBlocked) {
277
+ baseInstance.logger.info("skipping run because periodic event is blocked by configuration", {
278
+ type: baseInstance.eventType,
279
+ subType: baseInstance.eventSubType,
280
+ });
281
+ }
282
+ return eventBlocked;
283
+ };
284
+
271
285
  const _processEvent = async (eventTypeInstance, processContext, key, queueEntries, payload) => {
272
286
  try {
273
287
  const eventOutdated = await eventTypeInstance.isOutdatedAndKeepalive(queueEntries);
package/src/runner.js CHANGED
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
 
3
3
  const { randomUUID } = require("crypto");
4
+ const { AsyncResource } = require("async_hooks");
4
5
 
5
6
  const cds = require("@sap/cds");
6
7
 
@@ -14,6 +15,7 @@ const { getSubdomainForTenantId } = require("./shared/cdsHelper");
14
15
  const periodicEvents = require("./periodicEvents");
15
16
  const { hashStringTo32Bit } = require("./shared/common");
16
17
  const config = require("./config");
18
+ const { Priorities } = require("./constants");
17
19
 
18
20
  const COMPONENT_NAME = "/eventQueue/runner";
19
21
  const EVENT_QUEUE_RUN_ID = "EVENT_QUEUE_RUN_ID";
@@ -26,9 +28,9 @@ let singleRunDone;
26
28
 
27
29
  const singleTenant = () => _scheduleFunction(_checkPeriodicEventsSingleTenant, _singleTenantDb);
28
30
 
29
- const multiTenancyDb = () => _scheduleFunction(_multiTenancyPeriodicEvents, _multiTenancyDb);
31
+ const multiTenancyDb = () => _scheduleFunction(async () => {}, _multiTenancyDb);
30
32
 
31
- const multiTenancyRedis = () => _scheduleFunction(_multiTenancyPeriodicEvents, _multiTenancyRedis);
33
+ const multiTenancyRedis = () => _scheduleFunction(async () => {}, _multiTenancyRedis);
32
34
 
33
35
  const _scheduleFunction = async (singleRunFn, periodicFn) => {
34
36
  const logger = cds.log(COMPONENT_NAME);
@@ -69,7 +71,7 @@ const _multiTenancyRedis = async () => {
69
71
  const emptyContext = new cds.EventContext({});
70
72
  logger.info("executing event queue run for multi instance and tenant");
71
73
  const tenantIds = await cdsHelper.getAllTenantIds();
72
- _checkAndTriggerPeriodicEventUpdate(tenantIds);
74
+ await _checkPeriodicEventUpdate(tenantIds);
73
75
 
74
76
  const runId = await _acquireRunId(emptyContext);
75
77
 
@@ -78,22 +80,21 @@ const _multiTenancyRedis = async () => {
78
80
  return;
79
81
  }
80
82
 
81
- return _executeEventsAllTenants(tenantIds, runId);
83
+ return await _executeEventsAllTenants(tenantIds, runId);
82
84
  };
83
85
 
84
- const _checkAndTriggerPeriodicEventUpdate = (tenantIds) => {
86
+ const _checkPeriodicEventUpdate = async (tenantIds) => {
85
87
  const hash = hashStringTo32Bit(JSON.stringify(tenantIds));
86
88
  if (!tenantIdHash) {
87
89
  tenantIdHash = hash;
88
- _multiTenancyPeriodicEvents().catch((err) => {
90
+ return await _multiTenancyPeriodicEvents(tenantIds).catch((err) => {
89
91
  cds.log(COMPONENT_NAME).error("Error during triggering updating periodic events!", err);
90
92
  });
91
- return;
92
93
  }
93
94
  if (tenantIdHash && tenantIdHash !== hash) {
94
95
  tenantIdHash = hash;
95
96
  cds.log(COMPONENT_NAME).info("tenant id hash changed, triggering updating periodic events!");
96
- _multiTenancyPeriodicEvents().catch((err) => {
97
+ return await _multiTenancyPeriodicEvents(tenantIds).catch((err) => {
97
98
  cds.log(COMPONENT_NAME).error("Error during triggering updating periodic events!", err);
98
99
  });
99
100
  }
@@ -108,91 +109,103 @@ const _executeEventsAllTenants = (tenantIds, runId) => {
108
109
  return result;
109
110
  }, []);
110
111
 
111
- return product.map(async ([tenantId, event]) => {
112
- const subdomain = await getSubdomainForTenantId(tenantId);
113
- const user = new cds.User.Privileged(config.userId);
114
- const tenantContext = {
115
- tenant: tenantId,
116
- user,
117
- // NOTE: we need this because of logging otherwise logs would not contain the subdomain
118
- http: { req: { authInfo: { getSubdomain: () => subdomain } } },
119
- };
120
- return await cds.tx(tenantContext, async ({ context }) => {
121
- const label = `${event.type}_${event.subType}`;
122
- return await WorkerQueue.instance.addToQueue(event.load, label, async () => {
112
+ return Promise.allSettled(
113
+ product.map(async ([tenantId, eventConfig]) => {
114
+ const subdomain = await getSubdomainForTenantId(tenantId);
115
+ const user = new cds.User.Privileged(config.userId);
116
+ const tenantContext = {
117
+ tenant: tenantId,
118
+ user,
119
+ // NOTE: we need this because of logging otherwise logs would not contain the subdomain
120
+ http: { req: { authInfo: { getSubdomain: () => subdomain } } },
121
+ };
122
+ const label = `${eventConfig.type}_${eventConfig.subType}`;
123
+ return await WorkerQueue.instance.addToQueue(eventConfig.load, label, eventConfig.priority, async () => {
124
+ return await cds.tx(tenantContext, async ({ context }) => {
125
+ try {
126
+ const lockId = `${runId}_${label}`;
127
+ const couldAcquireLock = await distributedLock.acquireLock(context, lockId, {
128
+ expiryTime: eventQueueConfig.runInterval * 0.95,
129
+ });
130
+ if (!couldAcquireLock) {
131
+ return;
132
+ }
133
+ await runEventCombinationForTenant(context, eventConfig.type, eventConfig.subType, true);
134
+ } catch (err) {
135
+ cds.log(COMPONENT_NAME).error("executing event-queue run for tenant failed", {
136
+ tenantId,
137
+ });
138
+ }
139
+ });
140
+ });
141
+ })
142
+ );
143
+ };
144
+
145
+ const _executePeriodicEventsAllTenants = async (tenantIds, runId) => {
146
+ return await Promise.allSettled(
147
+ tenantIds.map(async (tenantId) => {
148
+ const label = `UPDATE_PERIODIC_EVENTS_${tenantId}`;
149
+ return await WorkerQueue.instance.addToQueue(1, label, Priorities.Low, async () => {
123
150
  try {
124
- const lockId = `${runId}_${label}`;
125
- const couldAcquireLock = await distributedLock.acquireLock(context, lockId, {
126
- expiryTime: eventQueueConfig.runInterval * 0.95,
151
+ const subdomain = await getSubdomainForTenantId(tenantId);
152
+ const user = new cds.User.Privileged(config.userId);
153
+ const tenantContext = {
154
+ tenant: tenantId,
155
+ user,
156
+ // NOTE: we need this because of logging otherwise logs would not contain the subdomain
157
+ http: { req: { authInfo: { getSubdomain: () => subdomain } } },
158
+ };
159
+
160
+ return await cds.tx(tenantContext, async ({ context }) => {
161
+ const couldAcquireLock = await distributedLock.acquireLock(context, runId, {
162
+ expiryTime: eventQueueConfig.runInterval * 0.95,
163
+ });
164
+ if (!couldAcquireLock) {
165
+ return;
166
+ }
167
+ await _checkPeriodicEventsSingleTenant(context);
127
168
  });
128
- if (!couldAcquireLock) {
129
- return;
130
- }
131
- await runEventCombinationForTenant(context, event.type, event.subType, true);
132
169
  } catch (err) {
133
170
  cds.log(COMPONENT_NAME).error("executing event-queue run for tenant failed", {
134
171
  tenantId,
135
172
  });
136
173
  }
137
174
  });
138
- });
139
- });
175
+ })
176
+ );
140
177
  };
141
178
 
142
- const _executePeriodicEventsAllTenants = (tenantIds, runId) => {
143
- tenantIds.forEach((tenantId) => {
144
- const label = `UPDATE_PERIODIC_EVENTS_${tenantId}`;
145
- WorkerQueue.instance.addToQueue(1, label, async () => {
146
- try {
147
- const subdomain = await getSubdomainForTenantId(tenantId);
148
- const user = new cds.User.Privileged(config.userId);
149
- const tenantContext = {
150
- tenant: tenantId,
151
- user,
152
- // NOTE: we need this because of logging otherwise logs would not contain the subdomain
153
- http: { req: { authInfo: { getSubdomain: () => subdomain } } },
154
- };
155
-
179
+ const _singleTenantDb = async (tenantId) => {
180
+ return Promise.allSettled(
181
+ eventQueueConfig.allEvents.map(async (eventConfig) => {
182
+ const label = `${eventConfig.type}_${eventConfig.subType}`;
183
+ const user = new cds.User.Privileged(config.userId);
184
+ const tenantContext = {
185
+ tenant: tenantId,
186
+ user,
187
+ };
188
+ return await WorkerQueue.instance.addToQueue(eventConfig.load, label, eventConfig.priority, async () => {
156
189
  return await cds.tx(tenantContext, async ({ context }) => {
157
- const couldAcquireLock = await distributedLock.acquireLock(context, runId, {
158
- expiryTime: eventQueueConfig.runInterval * 0.95,
159
- });
160
- if (!couldAcquireLock) {
161
- return;
190
+ try {
191
+ const lockId = `${EVENT_QUEUE_RUN_ID}_${label}`;
192
+ const couldAcquireLock = await distributedLock.acquireLock(context, lockId, {
193
+ expiryTime: eventQueueConfig.runInterval * 0.95,
194
+ });
195
+ if (!couldAcquireLock) {
196
+ return;
197
+ }
198
+ await runEventCombinationForTenant(context, eventConfig.type, eventConfig.subType, true);
199
+ } catch (err) {
200
+ cds.log(COMPONENT_NAME).error("executing event-queue run for tenant failed", {
201
+ tenantId,
202
+ redisEnabled: eventQueueConfig.redisEnabled,
203
+ });
162
204
  }
163
- await _checkPeriodicEventsSingleTenant(context);
164
- });
165
- } catch (err) {
166
- cds.log(COMPONENT_NAME).error("executing event-queue run for tenant failed", {
167
- tenantId,
168
205
  });
169
- }
170
- });
171
- });
172
- };
173
-
174
- const _singleTenantDb = async (tenantId) => {
175
- return eventQueueConfig.allEvents.map((event) => {
176
- const label = `${event.type}_${event.subType}`;
177
- return WorkerQueue.instance.addToQueue(event.load, label, async () => {
178
- try {
179
- const context = new cds.EventContext({ tenant: tenantId });
180
- const lockId = `${EVENT_QUEUE_RUN_ID}_${label}`;
181
- const couldAcquireLock = await distributedLock.acquireLock(context, lockId, {
182
- expiryTime: eventQueueConfig.runInterval * 0.95,
183
- });
184
- if (!couldAcquireLock) {
185
- return;
186
- }
187
- await runEventCombinationForTenant(context, event.type, event.subType, true);
188
- } catch (err) {
189
- cds.log(COMPONENT_NAME).error("executing event-queue run for tenant failed", {
190
- tenantId,
191
- redisEnabled: eventQueueConfig.redisEnabled,
192
- });
193
- }
194
- });
195
- });
206
+ });
207
+ })
208
+ );
196
209
  };
197
210
 
198
211
  const _acquireRunId = async (context) => {
@@ -257,12 +270,13 @@ const runEventCombinationForTenant = async (context, type, subType, skipWorkerPo
257
270
  if (skipWorkerPool) {
258
271
  return await processEventQueue(context, type, subType);
259
272
  } else {
260
- const config = eventQueueConfig.getEventConfig(type, subType);
273
+ const eventConfig = eventQueueConfig.getEventConfig(type, subType);
261
274
  const label = `${type}_${subType}`;
262
275
  return await WorkerQueue.instance.addToQueue(
263
- config.load,
276
+ eventConfig.load,
264
277
  label,
265
- async () => await processEventQueue(context, type, subType)
278
+ eventConfig.priority,
279
+ AsyncResource.bind(async () => await processEventQueue(context, type, subType))
266
280
  );
267
281
  }
268
282
  } catch (err) {
@@ -280,19 +294,19 @@ const _multiTenancyDb = async () => {
280
294
  try {
281
295
  logger.info("executing event queue run for single instance and multi tenant");
282
296
  const tenantIds = await cdsHelper.getAllTenantIds();
283
- _checkAndTriggerPeriodicEventUpdate(tenantIds);
284
- return _executeEventsAllTenants(tenantIds, EVENT_QUEUE_RUN_ID);
297
+ await _checkPeriodicEventUpdate(tenantIds);
298
+ return await _executeEventsAllTenants(tenantIds, EVENT_QUEUE_RUN_ID);
285
299
  } catch (err) {
286
300
  logger.error("Couldn't fetch tenant ids for event queue processing! Next try after defined interval.", err);
287
301
  }
288
302
  };
289
303
 
290
- const _multiTenancyPeriodicEvents = async () => {
304
+ const _multiTenancyPeriodicEvents = async (tenantIds) => {
291
305
  const logger = cds.log(COMPONENT_NAME);
292
306
  try {
293
307
  logger.info("executing event queue update periodic events");
294
- const tenantIds = await cdsHelper.getAllTenantIds();
295
- _executePeriodicEventsAllTenants(tenantIds, EVENT_QUEUE_RUN_PERIODIC_EVENT);
308
+ tenantIds = tenantIds ?? (await cdsHelper.getAllTenantIds());
309
+ return await _executePeriodicEventsAllTenants(tenantIds, EVENT_QUEUE_RUN_PERIODIC_EVENT);
296
310
  } catch (err) {
297
311
  logger.error("Couldn't fetch tenant ids for updating periodic event processing!", err);
298
312
  }
@@ -4,15 +4,30 @@ const cds = require("@sap/cds");
4
4
 
5
5
  const config = require("../config");
6
6
  const EventQueueError = require("../EventQueueError");
7
+ const { Priorities } = require("../constants");
8
+ const SetIntervalDriftSafe = require("./SetIntervalDriftSafe");
9
+
10
+ const PRIORITIES = Object.values(Priorities).reverse();
11
+ const PRIORITY_MULTIPLICATOR = PRIORITIES.reduce((result, element, index) => {
12
+ result[element] = index + 1;
13
+ return result;
14
+ }, {});
7
15
 
8
16
  const COMPONENT_NAME = "/eventQueue/WorkerQueue";
9
17
  const NANO_TO_MS = 1e6;
18
+ const MIN_TO_MS = 60 * 1000;
19
+ const INCREASE_PRIORITY_AFTER = 3;
20
+
21
+ let lastLogTs;
22
+
10
23
  const THRESHOLD = {
11
24
  INFO: 35 * 1000,
12
25
  WARN: 55 * 1000,
13
26
  ERROR: 75 * 1000,
14
27
  };
15
28
 
29
+ const CHECK_INTERVAL_QUEUE = 60 * 1000;
30
+
16
31
  class WorkerQueue {
17
32
  #concurrencyLimit;
18
33
  #runningPromises;
@@ -28,24 +43,53 @@ class WorkerQueue {
28
43
  }
29
44
  this.#runningPromises = [];
30
45
  this.#runningLoad = 0;
31
- this.#queue = [];
46
+ this.#queue = PRIORITIES.reduce((result, priority) => {
47
+ result[priority] = [];
48
+ return result;
49
+ }, {});
50
+
51
+ const runner = new SetIntervalDriftSafe(CHECK_INTERVAL_QUEUE);
52
+ runner.run(this.#adjustPriority.bind(this));
32
53
  }
33
54
 
34
- addToQueue(load, label, cb) {
55
+ addToQueue(load, label, priority = Priorities.Medium, cb) {
35
56
  if (load > this.#concurrencyLimit) {
36
57
  throw EventQueueError.loadHigherThanLimit(load, label);
37
58
  }
38
59
 
60
+ if (!PRIORITIES.includes(priority)) {
61
+ throw EventQueueError.priorityNotAllowed(priority, label);
62
+ }
63
+
39
64
  const startTime = process.hrtime.bigint();
40
65
  const p = new Promise((resolve, reject) => {
41
- this.#queue.push([load, label, cb, resolve, reject, startTime]);
66
+ this.#queue[priority].push([load, label, cb, resolve, reject, startTime]);
42
67
  });
43
- this._checkForNext();
68
+ this.#checkForNext();
44
69
  return p;
45
70
  }
46
71
 
47
- _executeFunction(load, label, cb, resolve, reject, startTime) {
48
- this.checkAndLogWaitingTime(startTime, label);
72
+ #adjustPriority() {
73
+ const checkTime = process.hrtime.bigint();
74
+ const priorityValues = Object.values(Priorities);
75
+
76
+ for (let i = 0; i < priorityValues.length - 1; i++) {
77
+ const priority = priorityValues[i];
78
+ const nextPriority = priorityValues[i + 1];
79
+ for (let i = 0; i < this.queue[priority].length; i++) {
80
+ const queueEntry = this.queue[priority][i];
81
+ const startTime = queueEntry[6] ?? queueEntry[5];
82
+ if (Math.round(Number(checkTime - startTime) / NANO_TO_MS) > INCREASE_PRIORITY_AFTER * MIN_TO_MS) {
83
+ const [entry] = this.queue[priority].splice(i, 1);
84
+ entry.push(checkTime);
85
+ this.queue[nextPriority].push(entry);
86
+ }
87
+ }
88
+ }
89
+ }
90
+
91
+ _executeFunction(load, label, cb, resolve, reject, startTime, priority) {
92
+ this.#checkAndLogWaitingTime(startTime, label, priority);
49
93
  const promise = Promise.resolve().then(() => cb());
50
94
  this.#runningPromises.push(promise);
51
95
  this.#runningLoad = this.#runningLoad + load;
@@ -53,7 +97,7 @@ class WorkerQueue {
53
97
  .finally(() => {
54
98
  this.#runningLoad = this.#runningLoad - load;
55
99
  this.#runningPromises.splice(this.#runningPromises.indexOf(promise), 1);
56
- this._checkForNext();
100
+ this.#checkForNext();
57
101
  })
58
102
  .then((...results) => {
59
103
  resolve(...results);
@@ -62,15 +106,32 @@ class WorkerQueue {
62
106
  cds.log(COMPONENT_NAME).error("Error happened in WorkQueue. Errors should be caught before!", err, { label });
63
107
  reject(err);
64
108
  });
109
+
110
+ if (this.#runningLoad !== this.#concurrencyLimit) {
111
+ this.#checkForNext();
112
+ }
65
113
  }
66
114
 
67
- _checkForNext() {
68
- const load = this.#queue[0]?.[0];
69
- if (!this.#queue.length || this.#runningLoad + load > this.#concurrencyLimit) {
115
+ #checkForNext() {
116
+ if (!this.#queue.length && this.#runningLoad === this.#concurrencyLimit) {
70
117
  return;
71
118
  }
72
- const args = this.#queue.shift();
73
- this._executeFunction(...args);
119
+
120
+ let entryFound = false;
121
+ for (const priority of PRIORITIES) {
122
+ for (let i = 0; i < this.#queue[priority].length; i++) {
123
+ const [load] = this.#queue[priority][i];
124
+ if (this.#runningLoad + load <= this.#concurrencyLimit) {
125
+ const [args] = this.#queue[priority].splice(i, 1);
126
+ this._executeFunction(...args, priority);
127
+ entryFound = true;
128
+ break;
129
+ }
130
+ }
131
+ if (entryFound) {
132
+ break;
133
+ }
134
+ }
74
135
  }
75
136
 
76
137
  get runningPromises() {
@@ -87,14 +148,24 @@ class WorkerQueue {
87
148
  return WorkerQueue.#instance;
88
149
  }
89
150
 
90
- checkAndLogWaitingTime(startTime, label) {
151
+ get queue() {
152
+ return this.#queue;
153
+ }
154
+
155
+ #checkAndLogWaitingTime(startTime, label, priority) {
156
+ const ts = Date.now();
157
+ if (ts - lastLogTs <= 1000) {
158
+ return;
159
+ }
160
+ lastLogTs = ts;
91
161
  const diffMs = Math.round(Number(process.hrtime.bigint() - startTime) / NANO_TO_MS);
162
+ const priorityMultiplication = PRIORITY_MULTIPLICATOR[priority];
92
163
  let logLevel;
93
- if (diffMs >= THRESHOLD.ERROR) {
164
+ if (diffMs >= THRESHOLD.ERROR * priorityMultiplication) {
94
165
  logLevel = "error";
95
- } else if (diffMs >= THRESHOLD.WARN) {
166
+ } else if (diffMs >= THRESHOLD.WARN * priorityMultiplication) {
96
167
  logLevel = "warn";
97
- } else if (diffMs >= THRESHOLD.INFO) {
168
+ } else if (diffMs >= THRESHOLD.INFO * priorityMultiplication) {
98
169
  logLevel = "info";
99
170
  } else {
100
171
  logLevel = "debug";