@cap-js-community/event-queue 0.2.2 → 0.2.3

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": "0.2.2",
3
+ "version": "0.2.3",
4
4
  "description": "An event queue that enables secure transactional processing of asynchronous 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": [
@@ -8,7 +8,7 @@ const distributedLock = require("./shared/distributedLock");
8
8
  const EventQueueError = require("./EventQueueError");
9
9
  const { arrayToFlatMap } = require("./shared/common");
10
10
  const eventScheduler = require("./shared/eventScheduler");
11
- const eventQueueConfig = require("./config");
11
+ const eventConfig = require("./config");
12
12
  const PerformanceTracer = require("./shared/PerformanceTracer");
13
13
 
14
14
  const IMPLEMENT_ERROR_MESSAGE = "needs to be reimplemented";
@@ -31,37 +31,43 @@ class EventQueueProcessorBase {
31
31
  #eventType = null;
32
32
  #eventSubType = null;
33
33
  #config = null;
34
+ #eventSchedulerInstance = null;
35
+ #eventConfig;
36
+ #isPeriodic;
34
37
 
35
38
  constructor(context, eventType, eventSubType, config) {
36
39
  this.__context = context;
37
40
  this.__baseContext = context;
38
41
  this.__tx = cds.tx(context);
39
42
  this.__baseLogger = cds.log(COMPONENT_NAME);
43
+ this.#eventSchedulerInstance = eventScheduler.getInstance();
44
+ this.#config = eventConfig;
45
+ this.#isPeriodic = this.#config.isPeriodicEvent(eventType, eventSubType);
40
46
  this.__logger = null;
41
47
  this.__eventProcessingMap = {};
42
48
  this.__statusMap = {};
43
49
  this.__commitedStatusMap = {};
44
50
  this.#eventType = eventType;
45
51
  this.#eventSubType = eventSubType;
46
- this.__eventConfig = config ?? {};
47
- this.__parallelEventProcessing = this.__eventConfig.parallelEventProcessing ?? DEFAULT_PARALLEL_EVENT_PROCESSING;
52
+ this.#eventConfig = config ?? {};
53
+ this.__parallelEventProcessing = this.#eventConfig.parallelEventProcessing ?? DEFAULT_PARALLEL_EVENT_PROCESSING;
48
54
  if (this.__parallelEventProcessing > LIMIT_PARALLEL_EVENT_PROCESSING) {
49
55
  this.__parallelEventProcessing = LIMIT_PARALLEL_EVENT_PROCESSING;
50
56
  }
51
57
  // NOTE: keep the feature, this might be needed again
52
58
  this.__concurrentEventProcessing = false;
53
- this.__startTime = this.__eventConfig.startTime ?? new Date();
54
- this.__retryAttempts = this.__eventConfig.retryAttempts ?? DEFAULT_RETRY_ATTEMPTS;
55
- this.__selectMaxChunkSize = this.__eventConfig.selectMaxChunkSize ?? SELECT_LIMIT_EVENTS_PER_TICK;
56
- this.__selectNextChunk = !!this.__eventConfig.checkForNextChunk;
59
+ this.__startTime = this.#eventConfig.startTime ?? new Date();
60
+ this.__retryAttempts = this.#eventConfig.retryAttempts ?? DEFAULT_RETRY_ATTEMPTS;
61
+ this.__selectMaxChunkSize = this.#eventConfig.selectMaxChunkSize ?? SELECT_LIMIT_EVENTS_PER_TICK;
62
+ this.__selectNextChunk = !!this.#eventConfig.checkForNextChunk;
57
63
  this.__keepalivePromises = {};
58
- this.__outdatedCheckEnabled = this.__eventConfig.eventOutdatedCheck ?? true;
59
- this.__transactionMode = this.__eventConfig.transactionMode ?? TransactionMode.isolated;
60
- if (this.__eventConfig.deleteFinishedEventsAfterDays) {
64
+ this.__outdatedCheckEnabled = this.#eventConfig.eventOutdatedCheck ?? true;
65
+ this.__transactionMode = this.#eventConfig.transactionMode ?? TransactionMode.isolated;
66
+ if (this.#eventConfig.deleteFinishedEventsAfterDays) {
61
67
  this.__deleteFinishedEventsAfter =
62
- Number.isInteger(this.__eventConfig.deleteFinishedEventsAfterDays) &&
63
- this.__eventConfig.deleteFinishedEventsAfterDays > 0
64
- ? this.__eventConfig.deleteFinishedEventsAfterDays
68
+ Number.isInteger(this.#eventConfig.deleteFinishedEventsAfterDays) &&
69
+ this.#eventConfig.deleteFinishedEventsAfterDays > 0
70
+ ? this.#eventConfig.deleteFinishedEventsAfterDays
65
71
  : DEFAULT_DELETE_FINISHED_EVENTS_AFTER;
66
72
  } else {
67
73
  this.__deleteFinishedEventsAfter = DEFAULT_DELETE_FINISHED_EVENTS_AFTER;
@@ -71,7 +77,6 @@ class EventQueueProcessorBase {
71
77
  this.__txUsageAllowed = true;
72
78
  this.__txMap = {};
73
79
  this.__txRollback = {};
74
- this.#config = eventQueueConfig.getConfigInstance();
75
80
  this.__queueEntries = [];
76
81
  }
77
82
 
@@ -610,9 +615,8 @@ class EventQueueProcessorBase {
610
615
  }
611
616
 
612
617
  #handleDelayedEvents(delayedEvents) {
613
- const eventSchedulerInstance = eventScheduler.getInstance();
614
618
  for (const delayedEvent of delayedEvents) {
615
- eventSchedulerInstance.scheduleEvent(
619
+ this.#eventSchedulerInstance.scheduleEvent(
616
620
  this.__context.tenant,
617
621
  this.#eventType,
618
622
  this.#eventSubType,
@@ -836,17 +840,47 @@ class EventQueueProcessorBase {
836
840
  }
837
841
 
838
842
  async scheduleNextPeriodEvent(queueEntry) {
839
- const interval = this.__eventConfig.interval;
843
+ const interval = this.#eventConfig.interval;
840
844
  const newEvent = {
841
845
  type: this.#eventType,
842
846
  subType: this.#eventSubType,
843
847
  startAfter: new Date(new Date(queueEntry.startAfter).getTime() + interval * 1000),
844
848
  };
849
+ const { relative } = this.#eventSchedulerInstance.calculateOffset(
850
+ this.#eventType,
851
+ this.#eventSubType,
852
+ newEvent.startAfter
853
+ );
854
+
855
+ // more than one interval behind - shift tick to keep up
856
+ if (relative < 0 && Math.abs(relative) >= this.#eventConfig.interval * 1000) {
857
+ newEvent.startAfter = new Date(Date.now() + 5 * 1000);
858
+ this.logger.info("interval adjusted because shifted more than one interval", {
859
+ eventType: this.#eventType,
860
+ eventSubType: this.#eventSubType,
861
+ newStartAfter: newEvent.startAfter,
862
+ });
863
+ }
864
+
845
865
  this.tx._skipEventQueueBroadcase = true;
846
866
  await this.tx.run(INSERT.into(this.#config.tableNameEventQueue).entries({ ...newEvent }));
847
867
  this.tx._skipEventQueueBroadcase = false;
848
868
  if (interval < this.#config.runInterval) {
849
869
  this.#handleDelayedEvents([newEvent]);
870
+ const { relative: relativeAfterSchedule } = this.#eventSchedulerInstance.calculateOffset(
871
+ this.#eventType,
872
+ this.#eventSubType,
873
+ newEvent.startAfter
874
+ );
875
+ // next tick is already behind schedule --> execute direct
876
+ if (relativeAfterSchedule <= 0) {
877
+ this.logger.info("running behind schedule - executing next tick immediately", {
878
+ eventType: this.#eventType,
879
+ eventSubType: this.#eventSubType,
880
+ newStartAfter: newEvent.startAfter,
881
+ });
882
+ return true;
883
+ }
850
884
  }
851
885
  }
852
886
 
@@ -986,7 +1020,7 @@ class EventQueueProcessorBase {
986
1020
  }
987
1021
 
988
1022
  get isPeriodicEvent() {
989
- return this.__eventConfig.isPeriodic;
1023
+ return this.#eventConfig.isPeriodic;
990
1024
  }
991
1025
  }
992
1026
 
package/src/config.js CHANGED
@@ -6,8 +6,6 @@ const { getEnvInstance } = require("./shared/env");
6
6
  const redis = require("./shared/redis");
7
7
  const EventQueueError = require("./EventQueueError");
8
8
 
9
- let instance;
10
-
11
9
  const FOR_UPDATE_TIMEOUT = 10;
12
10
  const GLOBAL_TX_TIMEOUT = 30 * 60 * 1000;
13
11
  const REDIS_CONFIG_CHANNEL = "EVENT_QUEUE_CONFIG_CHANNEL";
@@ -33,6 +31,7 @@ class Config {
33
31
  #env;
34
32
  #eventMap;
35
33
  #updatePeriodicEvents;
34
+ static #instance;
36
35
  constructor() {
37
36
  this.#logger = cds.log(COMPONENT_NAME);
38
37
  this.#config = null;
@@ -112,15 +111,18 @@ class Config {
112
111
  return result;
113
112
  }, {});
114
113
  this.#eventMap = config.periodicEvents.reduce((result, event) => {
115
- this.validatePeriodicConfig(result, event);
114
+ const SUFFIX_PERIODIC = "_PERIODIC";
115
+ event.type = `${event.type}${SUFFIX_PERIODIC}`;
116
116
  event.isPeriodic = true;
117
+ this.validatePeriodicConfig(result, event);
117
118
  result[[event.type, event.subType].join("##")] = event;
118
119
  return result;
119
120
  }, this.#eventMap);
120
121
  }
121
122
 
122
123
  validatePeriodicConfig(eventMap, config) {
123
- if (eventMap[this.generateKey(config.type, config.subType)]) {
124
+ const key = this.generateKey(config.type, config.subType);
125
+ if (eventMap[key] && eventMap[key].isPeriodic) {
124
126
  throw EventQueueError.duplicateEventRegistration(config.type, config.subType);
125
127
  }
126
128
 
@@ -134,7 +136,8 @@ class Config {
134
136
  }
135
137
 
136
138
  validateAdHocEvents(eventMap, config) {
137
- if (eventMap[this.generateKey(config.type, config.subType)]) {
139
+ const key = this.generateKey(config.type, config.subType);
140
+ if (eventMap[key] && !eventMap[key].isPeriodic) {
138
141
  throw EventQueueError.duplicateEventRegistration(config.type, config.subType);
139
142
  }
140
143
 
@@ -274,15 +277,18 @@ class Config {
274
277
  get isMultiTenancy() {
275
278
  return !!cds.requires.multitenancy;
276
279
  }
277
- }
278
280
 
279
- const getConfigInstance = () => {
280
- if (!instance) {
281
- instance = new Config();
281
+ /**
282
+ @return { Config }
283
+ **/
284
+ static get instance() {
285
+ if (!Config.#instance) {
286
+ Config.#instance = new Config();
287
+ }
288
+ return Config.#instance;
282
289
  }
283
- return instance;
284
- };
290
+ }
291
+
292
+ const instance = Config.instance;
285
293
 
286
- module.exports = {
287
- getConfigInstance,
288
- };
294
+ module.exports = instance;
package/src/dbHandler.js CHANGED
@@ -8,8 +8,7 @@ const config = require("./config");
8
8
  const COMPONENT_NAME = "eventQueue/dbHandler";
9
9
 
10
10
  const registerEventQueueDbHandler = (dbService) => {
11
- const configInstance = config.getConfigInstance();
12
- const def = dbService.model.definitions[configInstance.tableNameEventQueue];
11
+ const def = dbService.model.definitions[config.tableNameEventQueue];
13
12
  dbService.after("CREATE", def, (_, req) => {
14
13
  if (req.tx._skipEventQueueBroadcase) {
15
14
  return;
@@ -21,7 +20,7 @@ const registerEventQueueDbHandler = (dbService) => {
21
20
  const eventCombinations = Object.keys(
22
21
  data.reduce((result, event) => {
23
22
  const key = [event.type, event.subType].join("##");
24
- if (!configInstance.hasEventAfterCommitFlag(event.type, event.subType) || eventQueuePublishEvents[key]) {
23
+ if (!config.hasEventAfterCommitFlag(event.type, event.subType) || eventQueuePublishEvents[key]) {
25
24
  return result;
26
25
  }
27
26
  eventQueuePublishEvents[key] = true;
package/src/index.js CHANGED
@@ -11,7 +11,7 @@
11
11
 
12
12
  module.exports = {
13
13
  ...require("./initialize"),
14
- ...require("./config"),
14
+ config: require("./config"),
15
15
  ...require("./processEventQueue"),
16
16
  ...require("./dbHandler"),
17
17
  ...require("./constants"),
package/src/initialize.js CHANGED
@@ -11,7 +11,7 @@ const VError = require("verror");
11
11
  const EventQueueError = require("./EventQueueError");
12
12
  const runner = require("./runner");
13
13
  const dbHandler = require("./dbHandler");
14
- const { getConfigInstance } = require("./config");
14
+ const config = require("./config");
15
15
  const { initEventQueueRedisSubscribe, closeSubscribeClient } = require("./redisPubSub");
16
16
  const { closeMainClient } = require("./shared/redis");
17
17
 
@@ -54,11 +54,10 @@ const initialize = async ({
54
54
  // - content of yaml check
55
55
  // - betweenRuns and parallelTenantProcessing
56
56
 
57
- const configInstance = getConfigInstance();
58
- if (configInstance.initialized) {
57
+ if (config.initialized) {
59
58
  return;
60
59
  }
61
- configInstance.initialized = true;
60
+ config.initialized = true;
62
61
 
63
62
  mixConfigVarsWithEnv(
64
63
  configFilePath,
@@ -75,24 +74,24 @@ const initialize = async ({
75
74
  );
76
75
 
77
76
  const logger = cds.log(COMPONENT);
78
- configInstance.fileContent = await readConfigFromFile(configInstance.configFilePath);
79
- configInstance.checkRedisEnabled();
77
+ config.fileContent = await readConfigFromFile(config.configFilePath);
78
+ config.checkRedisEnabled();
80
79
 
81
80
  const dbService = await cds.connect.to("db");
82
81
  await (cds.model ? Promise.resolve() : new Promise((resolve) => cds.on("serving", resolve)));
83
- !configInstance.skipCsnCheck && (await csnCheck());
84
- if (configInstance.processEventsAfterPublish) {
82
+ !config.skipCsnCheck && (await csnCheck());
83
+ if (config.processEventsAfterPublish) {
85
84
  dbHandler.registerEventQueueDbHandler(dbService);
86
85
  }
87
86
 
88
87
  registerEventProcessors();
89
88
  registerCdsShutdown();
90
89
  logger.info("event queue initialized", {
91
- registerAsEventProcessor: configInstance.registerAsEventProcessor,
92
- multiTenancyEnabled: configInstance.isMultiTenancy,
93
- redisEnabled: configInstance.redisEnabled,
94
- runInterval: configInstance.runInterval,
95
- parallelTenantProcessing: configInstance.parallelTenantProcessing,
90
+ registerAsEventProcessor: config.registerAsEventProcessor,
91
+ multiTenancyEnabled: config.isMultiTenancy,
92
+ redisEnabled: config.redisEnabled,
93
+ runInterval: config.runInterval,
94
+ config: config.parallelTenantProcessing,
96
95
  });
97
96
  };
98
97
 
@@ -114,22 +113,20 @@ const readConfigFromFile = async (configFilepath) => {
114
113
  };
115
114
 
116
115
  const registerEventProcessors = () => {
117
- const configInstance = getConfigInstance();
118
-
119
- if (!configInstance.registerAsEventProcessor) {
116
+ if (!config.registerAsEventProcessor) {
120
117
  return;
121
118
  }
122
119
 
123
120
  const errorHandler = (err) => cds.log(COMPONENT).error("error during init runner", err);
124
121
 
125
- if (!configInstance.isMultiTenancy) {
122
+ if (!config.isMultiTenancy) {
126
123
  runner.singleTenant().catch(errorHandler);
127
124
  return;
128
125
  }
129
126
 
130
- if (configInstance.redisEnabled) {
127
+ if (config.redisEnabled) {
131
128
  initEventQueueRedisSubscribe();
132
- configInstance.attachConfigChangeHandler();
129
+ config.attachConfigChangeHandler();
133
130
  runner.multiTenancyRedis().catch(errorHandler);
134
131
  } else {
135
132
  runner.multiTenancyDb().catch(errorHandler);
@@ -137,21 +134,17 @@ const registerEventProcessors = () => {
137
134
  };
138
135
 
139
136
  const csnCheck = async () => {
140
- const configInstance = getConfigInstance();
141
- const eventCsn = cds.model.definitions[configInstance.tableNameEventQueue];
137
+ const eventCsn = cds.model.definitions[config.tableNameEventQueue];
142
138
  if (!eventCsn) {
143
- throw EventQueueError.missingTableInCsn(configInstance.tableNameEventQueue);
139
+ throw EventQueueError.missingTableInCsn(config.tableNameEventQueue);
144
140
  }
145
141
 
146
- const lockCsn = cds.model.definitions[configInstance.tableNameEventLock];
142
+ const lockCsn = cds.model.definitions[config.tableNameEventLock];
147
143
  if (!lockCsn) {
148
- throw EventQueueError.missingTableInCsn(configInstance.tableNameEventLock);
144
+ throw EventQueueError.missingTableInCsn(config.tableNameEventLock);
149
145
  }
150
146
 
151
- if (
152
- configInstance.tableNameEventQueue === BASE_TABLES.EVENT &&
153
- configInstance.tableNameEventLock === BASE_TABLES.LOCK
154
- ) {
147
+ if (config.tableNameEventQueue === BASE_TABLES.EVENT && config.tableNameEventLock === BASE_TABLES.LOCK) {
155
148
  return; // no need to check base tables
156
149
  }
157
150
 
@@ -181,10 +174,9 @@ const checkCustomTable = (baseCsn, customCsn) => {
181
174
  };
182
175
 
183
176
  const mixConfigVarsWithEnv = (...args) => {
184
- const configInstance = getConfigInstance();
185
177
  CONFIG_VARS.forEach(([configName, defaultValue], index) => {
186
178
  const configValue = args[index];
187
- configInstance[configName] = configValue ?? cds.env.eventQueue?.[configName] ?? defaultValue;
179
+ config[configName] = configValue ?? cds.env.eventQueue?.[configName] ?? defaultValue;
188
180
  });
189
181
  };
190
182
 
@@ -4,19 +4,18 @@ const cds = require("@sap/cds");
4
4
 
5
5
  const { EventProcessingStatus } = require("./constants");
6
6
  const { processChunkedSync } = require("./shared/common");
7
- const { getConfigInstance } = require("./config");
7
+ const eventConfig = require("./config");
8
8
 
9
9
  const COMPONENT_NAME = "eventQueue/periodicEvents";
10
10
 
11
11
  const checkAndInsertPeriodicEvents = async (context) => {
12
12
  const tx = cds.tx(context);
13
- const configInstance = getConfigInstance();
14
- const baseCqn = SELECT.from(configInstance.tableNameEventQueue)
13
+ const baseCqn = SELECT.from(eventConfig.tableNameEventQueue)
15
14
  .where([
16
15
  { list: [{ ref: ["type"] }, { ref: ["subType"] }] },
17
16
  "IN",
18
17
  {
19
- list: configInstance.periodicEvents.map((periodicEvent) => ({
18
+ list: eventConfig.periodicEvents.map((periodicEvent) => ({
20
19
  list: [{ val: periodicEvent.type }, { val: periodicEvent.subType }],
21
20
  })),
22
21
  },
@@ -30,7 +29,7 @@ const checkAndInsertPeriodicEvents = async (context) => {
30
29
 
31
30
  if (!currentPeriodEvents.length) {
32
31
  // fresh insert all
33
- return await insertPeriodEvents(tx, configInstance.periodicEvents);
32
+ return await insertPeriodEvents(tx, eventConfig.periodicEvents);
34
33
  }
35
34
 
36
35
  const exitingEventMap = currentPeriodEvents.reduce((result, current) => {
@@ -39,7 +38,7 @@ const checkAndInsertPeriodicEvents = async (context) => {
39
38
  return result;
40
39
  }, {});
41
40
 
42
- const { newEvents, existingEvents } = configInstance.periodicEvents.reduce(
41
+ const { newEvents, existingEvents } = eventConfig.periodicEvents.reduce(
43
42
  (result, event) => {
44
43
  if (exitingEventMap[_generateKey(event)]) {
45
44
  result.existingEvents.push(exitingEventMap[_generateKey(event)]);
@@ -53,7 +52,7 @@ const checkAndInsertPeriodicEvents = async (context) => {
53
52
 
54
53
  const currentDate = new Date();
55
54
  const exitingWithNotMatchingInterval = existingEvents.filter((existingEvent) => {
56
- const config = configInstance.getEventConfig(existingEvent.type, existingEvent.subType);
55
+ const config = eventConfig.getEventConfig(existingEvent.type, existingEvent.subType);
57
56
  const eventStartAfter = new Date(existingEvent.startAfter);
58
57
  // check if to far in future
59
58
  const dueInWithNewInterval = new Date(currentDate.getTime() + config.interval * 1000);
@@ -65,7 +64,7 @@ const checkAndInsertPeriodicEvents = async (context) => {
65
64
  changedEvents: exitingWithNotMatchingInterval.map(({ type, subType }) => ({ type, subType })),
66
65
  });
67
66
  await tx.run(
68
- DELETE.from(configInstance.tableNameEventQueue).where(
67
+ DELETE.from(eventConfig.tableNameEventQueue).where(
69
68
  "ID IN",
70
69
  exitingWithNotMatchingInterval.map(({ ID }) => ID)
71
70
  )
@@ -82,11 +81,10 @@ const checkAndInsertPeriodicEvents = async (context) => {
82
81
 
83
82
  const insertPeriodEvents = async (tx, events) => {
84
83
  const startAfter = new Date();
85
- const configInstance = getConfigInstance();
86
84
  processChunkedSync(events, 4, (chunk) => {
87
85
  cds.log(COMPONENT_NAME).info("inserting changed or new periodic events", {
88
86
  events: chunk.map(({ type, subType }) => {
89
- const { interval } = configInstance.getEventConfig(type, subType);
87
+ const { interval } = eventConfig.getEventConfig(type, subType);
90
88
  return { type, subType, interval };
91
89
  }),
92
90
  });
@@ -98,7 +96,7 @@ const insertPeriodEvents = async (tx, events) => {
98
96
  }));
99
97
 
100
98
  tx._skipEventQueueBroadcase = true;
101
- await tx.run(INSERT.into(configInstance.tableNameEventQueue).entries(periodEventsInsert));
99
+ await tx.run(INSERT.into(eventConfig.tableNameEventQueue).entries(periodEventsInsert));
102
100
  tx._skipEventQueueBroadcase = false;
103
101
  };
104
102
 
@@ -4,7 +4,7 @@ const pathLib = require("path");
4
4
 
5
5
  const cds = require("@sap/cds");
6
6
 
7
- const { getConfigInstance } = require("./config");
7
+ const config = require("./config");
8
8
  const { TransactionMode } = require("./constants");
9
9
  const { limiter, Funnel } = require("./shared/common");
10
10
 
@@ -29,7 +29,7 @@ const processEventQueue = async (context, eventType, eventSubType, startTime = n
29
29
  let baseInstance;
30
30
  try {
31
31
  let eventTypeInstance;
32
- const eventConfig = getConfigInstance().getEventConfig(eventType, eventSubType);
32
+ const eventConfig = config.getEventConfig(eventType, eventSubType);
33
33
  const [err, EventTypeClass] = resilientRequire(eventConfig?.impl);
34
34
  if (!eventConfig || err || !(typeof EventTypeClass.constructor === "function")) {
35
35
  cds.log(COMPONENT_NAME).error("No Implementation found in the provided configuration file.", {
@@ -137,58 +137,67 @@ const reevaluateShouldContinue = (eventTypeInstance, iterationCounter, startTime
137
137
  // TODO: don't forget to release lock
138
138
  const processPeriodicEvent = async (eventTypeInstance) => {
139
139
  let queueEntry;
140
+ let processNext = true;
141
+
140
142
  try {
141
- await executeInNewTransaction(
142
- eventTypeInstance.context,
143
- `eventQueue-periodic-scheduleNext-${eventTypeInstance.eventType}##${eventTypeInstance.eventSubType}`,
144
- async (tx) => {
145
- eventTypeInstance.processEventContext = tx.context;
146
- const queueEntries = await eventTypeInstance.getQueueEntriesAndSetToInProgress();
147
- if (!queueEntries.length) {
148
- return;
149
- }
150
- if (queueEntries.length > 1) {
151
- queueEntry = await eventTypeInstance.handleDuplicatedPeriodicEventEntry(queueEntries);
152
- } else {
153
- queueEntry = queueEntries[0];
143
+ while (processNext) {
144
+ await executeInNewTransaction(
145
+ eventTypeInstance.context,
146
+ `eventQueue-periodic-scheduleNext-${eventTypeInstance.eventType}##${eventTypeInstance.eventSubType}`,
147
+ async (tx) => {
148
+ eventTypeInstance.processEventContext = tx.context;
149
+ const queueEntries = await eventTypeInstance.getQueueEntriesAndSetToInProgress();
150
+ if (!queueEntries.length) {
151
+ return;
152
+ }
153
+ if (queueEntries.length > 1) {
154
+ queueEntry = await eventTypeInstance.handleDuplicatedPeriodicEventEntry(queueEntries);
155
+ } else {
156
+ queueEntry = queueEntries[0];
157
+ }
158
+ processNext = await eventTypeInstance.scheduleNextPeriodEvent(queueEntry);
154
159
  }
155
- await eventTypeInstance.scheduleNextPeriodEvent(queueEntry);
156
- }
157
- );
160
+ );
158
161
 
159
- if (!queueEntry) {
160
- return;
161
- }
162
+ if (!queueEntry) {
163
+ return;
164
+ }
162
165
 
163
- await executeInNewTransaction(
164
- eventTypeInstance.context,
165
- `eventQueue-periodic-process-${eventTypeInstance.eventType}##${eventTypeInstance.eventSubType}`,
166
- async (tx) => {
167
- eventTypeInstance.processEventContext = tx.context;
168
- eventTypeInstance.setTxForEventProcessing(queueEntry.ID, cds.tx(tx.context));
169
- try {
170
- await eventTypeInstance.processEvent(tx.context, queueEntry.ID, [queueEntry]);
171
- } catch (err) {
172
- eventTypeInstance.handleErrorDuringPeriodicEventProcessing(err, queueEntry);
173
- throw new TriggerRollback();
174
- }
175
- if (
176
- eventTypeInstance.transactionMode !== TransactionMode.alwaysCommit ||
177
- eventTypeInstance.shouldRollbackTransaction(queueEntry.ID)
178
- ) {
179
- throw new TriggerRollback();
166
+ await executeInNewTransaction(
167
+ eventTypeInstance.context,
168
+ `eventQueue-periodic-process-${eventTypeInstance.eventType}##${eventTypeInstance.eventSubType}`,
169
+ async (tx) => {
170
+ eventTypeInstance.processEventContext = tx.context;
171
+ eventTypeInstance.setTxForEventProcessing(queueEntry.ID, cds.tx(tx.context));
172
+ try {
173
+ await eventTypeInstance.processEvent(tx.context, queueEntry.ID, [queueEntry]);
174
+ } catch (err) {
175
+ eventTypeInstance.handleErrorDuringPeriodicEventProcessing(err, queueEntry);
176
+ throw new TriggerRollback();
177
+ }
178
+ if (
179
+ eventTypeInstance.transactionMode !== TransactionMode.alwaysCommit ||
180
+ eventTypeInstance.shouldRollbackTransaction(queueEntry.ID)
181
+ ) {
182
+ throw new TriggerRollback();
183
+ }
180
184
  }
181
- }
182
- );
185
+ );
183
186
 
184
- await executeInNewTransaction(
185
- eventTypeInstance.context,
186
- `eventQueue-periodic-setStatus-${eventTypeInstance.eventType}##${eventTypeInstance.eventSubType}`,
187
- async (tx) => {
188
- eventTypeInstance.processEventContext = tx.context;
189
- await eventTypeInstance.setPeriodicEventStatus(queueEntry.ID);
190
- }
191
- );
187
+ await executeInNewTransaction(
188
+ eventTypeInstance.context,
189
+ `eventQueue-periodic-setStatus-${eventTypeInstance.eventType}##${eventTypeInstance.eventSubType}`,
190
+ async (tx) => {
191
+ eventTypeInstance.processEventContext = tx.context;
192
+ await eventTypeInstance.setPeriodicEventStatus(queueEntry.ID);
193
+ }
194
+ );
195
+ }
196
+ } catch (err) {
197
+ cds.log(COMPONENT_NAME).error("Processing periodic events failed with unexpected error. Error:", err, {
198
+ eventType: eventTypeInstance?.eventType,
199
+ eventSubType: eventTypeInstance?.eventSubType,
200
+ });
192
201
  } finally {
193
202
  await eventTypeInstance?.handleReleaseLock();
194
203
  }
@@ -28,13 +28,12 @@ const EventQueueError = require("./EventQueueError");
28
28
  * @returns {Promise} Returns a promise which resolves to the result of the database insert operation.
29
29
  */
30
30
  const publishEvent = async (tx, events, skipBroadcast = false) => {
31
- const configInstance = config.getConfigInstance();
32
- if (!configInstance.initialized) {
31
+ if (!config.initialized) {
33
32
  throw EventQueueError.notInitialized();
34
33
  }
35
34
  const eventsForProcessing = Array.isArray(events) ? events : [events];
36
35
  for (const { type, subType, startAfter } of eventsForProcessing) {
37
- const eventConfig = configInstance.getEventConfig(type, subType);
36
+ const eventConfig = config.getEventConfig(type, subType);
38
37
  if (!eventConfig) {
39
38
  throw EventQueueError.unknownEventType(type, subType);
40
39
  }
@@ -47,7 +46,7 @@ const publishEvent = async (tx, events, skipBroadcast = false) => {
47
46
  }
48
47
  }
49
48
  tx._skipEventQueueBroadcase = skipBroadcast;
50
- const result = await tx.run(INSERT.into(configInstance.tableNameEventQueue).entries(eventsForProcessing));
49
+ const result = await tx.run(INSERT.into(config.tableNameEventQueue).entries(eventsForProcessing));
51
50
  tx._skipEventQueueBroadcase = false;
52
51
  return result;
53
52
  };
@@ -11,7 +11,7 @@ const COMPONENT_NAME = "eventQueue/redisPubSub";
11
11
  let subscriberClientPromise;
12
12
 
13
13
  const initEventQueueRedisSubscribe = () => {
14
- if (subscriberClientPromise || !config.getConfigInstance().redisEnabled) {
14
+ if (subscriberClientPromise || !config.redisEnabled) {
15
15
  return;
16
16
  }
17
17
  redis.subscribeRedisChannel(EVENT_MESSAGE_CHANNEL, messageHandlerProcessEvents);
@@ -36,9 +36,8 @@ const messageHandlerProcessEvents = async (messageData) => {
36
36
 
37
37
  const broadcastEvent = async (tenantId, type, subType) => {
38
38
  const logger = cds.log(COMPONENT_NAME);
39
- const configInstance = config.getConfigInstance();
40
- if (!configInstance.redisEnabled) {
41
- if (configInstance.registerAsEventProcessor) {
39
+ if (!config.redisEnabled) {
40
+ if (config.registerAsEventProcessor) {
42
41
  await runEventCombinationForTenant(tenantId, type, subType);
43
42
  }
44
43
  return;
package/src/runner.js CHANGED
@@ -19,6 +19,7 @@ const EVENT_QUEUE_RUN_PERIODIC_EVENT = "EVENT_QUEUE_RUN_PERIODIC_EVENT";
19
19
  const OFFSET_FIRST_RUN = 10 * 1000;
20
20
 
21
21
  let tenantIdHash;
22
+ let singleRunDone;
22
23
 
23
24
  const singleTenant = () => _scheduleFunction(_checkPeriodicEventsSingleTenant, _executeRunForTenant);
24
25
 
@@ -28,8 +29,7 @@ const multiTenancyRedis = () => _scheduleFunction(_multiTenancyPeriodicEvents, _
28
29
 
29
30
  const _scheduleFunction = async (singleRunFn, periodicFn) => {
30
31
  const logger = cds.log(COMPONENT_NAME);
31
- const configInstance = eventQueueConfig.getConfigInstance();
32
- const eventsForAutomaticRun = configInstance.allEvents;
32
+ const eventsForAutomaticRun = eventQueueConfig.allEvents;
33
33
  if (!eventsForAutomaticRun.length) {
34
34
  logger.warn("no events for automatic run are configured - skipping runner registration");
35
35
  return;
@@ -37,10 +37,14 @@ const _scheduleFunction = async (singleRunFn, periodicFn) => {
37
37
 
38
38
  const fnWithRunningCheck = () => {
39
39
  const logger = cds.log(COMPONENT_NAME);
40
- if (configInstance.isRunnerDeactivated) {
40
+ if (eventQueueConfig.isRunnerDeactivated) {
41
41
  logger.info("runner is deactivated via config variable. Skipping this run.");
42
42
  return;
43
43
  }
44
+ if (!singleRunDone) {
45
+ singleRunDone = true;
46
+ singleRunFn().catch(() => (singleRunDone = false));
47
+ }
44
48
  return periodicFn();
45
49
  };
46
50
 
@@ -51,9 +55,8 @@ const _scheduleFunction = async (singleRunFn, periodicFn) => {
51
55
  });
52
56
 
53
57
  setTimeout(() => {
54
- singleRunFn();
55
58
  fnWithRunningCheck();
56
- const intervalRunner = new SetIntervalDriftSafe(configInstance.runInterval);
59
+ const intervalRunner = new SetIntervalDriftSafe(eventQueueConfig.runInterval);
57
60
  intervalRunner.run(fnWithRunningCheck);
58
61
  }, offsetDependingOnLastRun).unref();
59
62
  };
@@ -63,7 +66,7 @@ const _multiTenancyRedis = async () => {
63
66
  const emptyContext = new cds.EventContext({});
64
67
  logger.info("executing event queue run for multi instance and tenant");
65
68
  const tenantIds = await cdsHelper.getAllTenantIds();
66
- _checkAndTriggerPriodicEventUpdate(tenantIds);
69
+ _checkAndTriggerPeriodicEventUpdate(tenantIds);
67
70
 
68
71
  const runId = await _acquireRunId(emptyContext);
69
72
 
@@ -75,7 +78,7 @@ const _multiTenancyRedis = async () => {
75
78
  _executeAllTenants(tenantIds, runId);
76
79
  };
77
80
 
78
- const _checkAndTriggerPriodicEventUpdate = (tenantIds) => {
81
+ const _checkAndTriggerPeriodicEventUpdate = (tenantIds) => {
79
82
  const hash = hashStringTo32Bit(JSON.stringify(tenantIds));
80
83
  if (!tenantIdHash) {
81
84
  tenantIdHash = hash;
@@ -90,14 +93,13 @@ const _checkAndTriggerPriodicEventUpdate = (tenantIds) => {
90
93
  };
91
94
 
92
95
  const _executeAllTenantsGeneric = (tenantIds, runId, fn) => {
93
- const configInstance = eventQueueConfig.getConfigInstance();
94
96
  const workerQueueInstance = getWorkerPoolInstance();
95
97
  tenantIds.forEach((tenantId) => {
96
98
  workerQueueInstance.addToQueue(async () => {
97
99
  try {
98
100
  const tenantContext = new cds.EventContext({ tenant: tenantId });
99
101
  const couldAcquireLock = await distributedLock.acquireLock(tenantContext, runId, {
100
- expiryTime: configInstance.runInterval * 0.95,
102
+ expiryTime: eventQueueConfig.runInterval * 0.95,
101
103
  });
102
104
  if (!couldAcquireLock) {
103
105
  return;
@@ -119,9 +121,8 @@ const _executePeriodicEventsAllTenants = (tenantIds, runId) =>
119
121
 
120
122
  const _executeRunForTenant = async (tenantId, runId) => {
121
123
  const logger = cds.log(COMPONENT_NAME);
122
- const configInstance = eventQueueConfig.getConfigInstance();
123
124
  try {
124
- const eventsForAutomaticRun = configInstance.allEvents;
125
+ const eventsForAutomaticRun = eventQueueConfig.allEvents;
125
126
  const subdomain = await cdsHelper.getSubdomainForTenantId(tenantId);
126
127
  const context = new cds.EventContext({
127
128
  tenant: tenantId,
@@ -138,23 +139,22 @@ const _executeRunForTenant = async (tenantId, runId) => {
138
139
  } catch (err) {
139
140
  logger.error(`Couldn't process eventQueue for tenant! Next try after defined interval. Error: ${err}`, {
140
141
  tenantId,
141
- redisEnabled: configInstance.redisEnabled,
142
+ redisEnabled: eventQueueConfig.redisEnabled,
142
143
  });
143
144
  }
144
145
  };
145
146
 
146
147
  const _acquireRunId = async (context) => {
147
- const configInstance = eventQueueConfig.getConfigInstance();
148
148
  let runId = randomUUID();
149
149
  const couldSetValue = await distributedLock.setValueWithExpire(context, EVENT_QUEUE_RUN_ID, runId, {
150
150
  tenantScoped: false,
151
- expiryTime: configInstance.runInterval * 0.95,
151
+ expiryTime: eventQueueConfig.runInterval * 0.95,
152
152
  });
153
153
 
154
154
  if (couldSetValue) {
155
155
  await distributedLock.setValueWithExpire(context, EVENT_QUEUE_RUN_TS, new Date().toISOString(), {
156
156
  tenantScoped: false,
157
- expiryTime: configInstance.runInterval,
157
+ expiryTime: eventQueueConfig.runInterval,
158
158
  overrideValue: true,
159
159
  });
160
160
  } else {
@@ -167,13 +167,12 @@ const _acquireRunId = async (context) => {
167
167
  };
168
168
 
169
169
  const _calculateOffsetForFirstRun = async () => {
170
- const configInstance = eventQueueConfig.getConfigInstance();
171
170
  let offsetDependingOnLastRun = OFFSET_FIRST_RUN;
172
171
  const now = Date.now();
173
172
  // NOTE: this is only supported with Redis, because this is a tenant agnostic information
174
173
  // currently there is no proper place to store this information beside t0 schema
175
174
  try {
176
- if (configInstance.redisEnabled) {
175
+ if (eventQueueConfig.redisEnabled) {
177
176
  const dummyContext = new cds.EventContext({});
178
177
  let lastRunTs = await distributedLock.checkLockExistsAndReturnValue(dummyContext, EVENT_QUEUE_RUN_TS, {
179
178
  tenantScoped: false,
@@ -182,7 +181,7 @@ const _calculateOffsetForFirstRun = async () => {
182
181
  const ts = new Date(now).toISOString();
183
182
  const couldSetValue = await distributedLock.setValueWithExpire(dummyContext, EVENT_QUEUE_RUN_TS, ts, {
184
183
  tenantScoped: false,
185
- expiryTime: configInstance.runInterval,
184
+ expiryTime: eventQueueConfig.runInterval,
186
185
  });
187
186
  if (couldSetValue) {
188
187
  lastRunTs = ts;
@@ -192,7 +191,7 @@ const _calculateOffsetForFirstRun = async () => {
192
191
  });
193
192
  }
194
193
  }
195
- offsetDependingOnLastRun = new Date(lastRunTs).getTime() + configInstance.runInterval - now;
194
+ offsetDependingOnLastRun = new Date(lastRunTs).getTime() + eventQueueConfig.runInterval - now;
196
195
  }
197
196
  } catch (err) {
198
197
  cds
@@ -230,7 +229,7 @@ const _multiTenancyDb = async () => {
230
229
  try {
231
230
  logger.info("executing event queue run for single instance and multi tenant");
232
231
  const tenantIds = await cdsHelper.getAllTenantIds();
233
- _checkAndTriggerPriodicEventUpdate(tenantIds);
232
+ _checkAndTriggerPeriodicEventUpdate(tenantIds);
234
233
  _executeAllTenants(tenantIds, EVENT_QUEUE_RUN_ID);
235
234
  } catch (err) {
236
235
  logger.error(
@@ -252,8 +251,7 @@ const _multiTenancyPeriodicEvents = async () => {
252
251
 
253
252
  const _checkPeriodicEventsSingleTenant = async (tenantId) => {
254
253
  const logger = cds.log(COMPONENT_NAME);
255
- const configInstance = eventQueueConfig.getConfigInstance();
256
- if (!configInstance.updatePeriodicEvents) {
254
+ if (!eventQueueConfig.updatePeriodicEvents) {
257
255
  logger.info("updating of periodic events is disabled");
258
256
  }
259
257
  try {
@@ -274,7 +272,7 @@ const _checkPeriodicEventsSingleTenant = async (tenantId) => {
274
272
  } catch (err) {
275
273
  logger.error(`Couldn't process eventQueue for tenant! Next try after defined interval. Error: ${err}`, {
276
274
  tenantId,
277
- redisEnabled: configInstance.redisEnabled,
275
+ redisEnabled: eventQueueConfig.redisEnabled,
278
276
  });
279
277
  }
280
278
  };
@@ -2,7 +2,7 @@
2
2
 
3
3
  const cds = require("@sap/cds");
4
4
 
5
- const { getConfigInstance } = require("../config");
5
+ const config = require("../config");
6
6
 
7
7
  const COMPONENT_NAME = "eventQueue/WorkerQueue";
8
8
 
@@ -56,8 +56,7 @@ class WorkerQueue {
56
56
  module.exports = {
57
57
  getWorkerPoolInstance: () => {
58
58
  if (!instance) {
59
- const configInstance = getConfigInstance();
60
- instance = new WorkerQueue(configInstance.parallelTenantProcessing);
59
+ instance = new WorkerQueue(config.parallelTenantProcessing);
61
60
  }
62
61
  return instance;
63
62
  },
@@ -94,7 +94,7 @@ class TriggerRollback extends VError {
94
94
  }
95
95
 
96
96
  const getSubdomainForTenantId = async (tenantId) => {
97
- if (!config.getConfigInstance().isMultiTenancy) {
97
+ if (!config.isMultiTenancy) {
98
98
  return null;
99
99
  }
100
100
  if (subdomainCache[tenantId]) {
@@ -111,7 +111,7 @@ const getSubdomainForTenantId = async (tenantId) => {
111
111
  };
112
112
 
113
113
  const getAllTenantIds = async () => {
114
- if (!config.getConfigInstance().isMultiTenancy) {
114
+ if (!config.isMultiTenancy) {
115
115
  return null;
116
116
  }
117
117
  const ssp = await cds.connect.to("cds.xt.SaasProvisioningService");
@@ -3,15 +3,10 @@
3
3
  const redis = require("./redis");
4
4
  const config = require("../config");
5
5
  const cdsHelper = require("./cdsHelper");
6
- const { getConfigInstance } = require("../config");
7
6
 
8
- const acquireLock = async (
9
- context,
10
- key,
11
- { tenantScoped = true, expiryTime = config.getConfigInstance().globalTxTimeout } = {}
12
- ) => {
7
+ const acquireLock = async (context, key, { tenantScoped = true, expiryTime = config.globalTxTimeout } = {}) => {
13
8
  const fullKey = _generateKey(context, tenantScoped, key);
14
- if (config.getConfigInstance().redisEnabled) {
9
+ if (config.redisEnabled) {
15
10
  return await _acquireLockRedis(context, fullKey, expiryTime);
16
11
  } else {
17
12
  return await _acquireLockDB(context, fullKey, expiryTime);
@@ -22,10 +17,10 @@ const setValueWithExpire = async (
22
17
  context,
23
18
  key,
24
19
  value,
25
- { tenantScoped = true, expiryTime = config.getConfigInstance().globalTxTimeout, overrideValue = false } = {}
20
+ { tenantScoped = true, expiryTime = config.globalTxTimeout, overrideValue = false } = {}
26
21
  ) => {
27
22
  const fullKey = _generateKey(context, tenantScoped, key);
28
- if (config.getConfigInstance().redisEnabled) {
23
+ if (config.redisEnabled) {
29
24
  return await _acquireLockRedis(context, fullKey, expiryTime, {
30
25
  value,
31
26
  overrideValue,
@@ -40,7 +35,7 @@ const setValueWithExpire = async (
40
35
 
41
36
  const releaseLock = async (context, key, { tenantScoped = true } = {}) => {
42
37
  const fullKey = _generateKey(context, tenantScoped, key);
43
- if (config.getConfigInstance().redisEnabled) {
38
+ if (config.redisEnabled) {
44
39
  return await _releaseLockRedis(context, fullKey);
45
40
  } else {
46
41
  return await _releaseLockDb(context, fullKey);
@@ -49,7 +44,7 @@ const releaseLock = async (context, key, { tenantScoped = true } = {}) => {
49
44
 
50
45
  const checkLockExistsAndReturnValue = async (context, key, { tenantScoped = true } = {}) => {
51
46
  const fullKey = _generateKey(context, tenantScoped, key);
52
- if (config.getConfigInstance().redisEnabled) {
47
+ if (config.redisEnabled) {
53
48
  return await _checkLockExistsRedis(context, fullKey);
54
49
  } else {
55
50
  return await _checkLockExistsDb(context, fullKey);
@@ -72,9 +67,8 @@ const _checkLockExistsRedis = async (context, fullKey) => {
72
67
 
73
68
  const _checkLockExistsDb = async (context, fullKey) => {
74
69
  let result;
75
- const configInstance = getConfigInstance();
76
70
  await cdsHelper.executeInNewTransaction(context, "distributedLock-checkExists", async (tx) => {
77
- result = await tx.run(SELECT.one.from(configInstance.tableNameEventLock).where("code =", fullKey));
71
+ result = await tx.run(SELECT.one.from(config.tableNameEventLock).where("code =", fullKey));
78
72
  });
79
73
  return result?.value;
80
74
  };
@@ -85,19 +79,17 @@ const _releaseLockRedis = async (context, fullKey) => {
85
79
  };
86
80
 
87
81
  const _releaseLockDb = async (context, fullKey) => {
88
- const configInstance = getConfigInstance();
89
82
  await cdsHelper.executeInNewTransaction(context, "distributedLock-release", async (tx) => {
90
- await tx.run(DELETE.from(configInstance.tableNameEventLock).where("code =", fullKey));
83
+ await tx.run(DELETE.from(config.tableNameEventLock).where("code =", fullKey));
91
84
  });
92
85
  };
93
86
 
94
87
  const _acquireLockDB = async (context, fullKey, expiryTime, { value = "true", overrideValue = false } = {}) => {
95
88
  let result;
96
- const configInstance = getConfigInstance();
97
89
  await cdsHelper.executeInNewTransaction(context, "distributedLock-acquire", async (tx) => {
98
90
  try {
99
91
  await tx.run(
100
- INSERT.into(configInstance.tableNameEventLock).entries({
92
+ INSERT.into(config.tableNameEventLock).entries({
101
93
  code: fullKey,
102
94
  value,
103
95
  })
@@ -109,14 +101,14 @@ const _acquireLockDB = async (context, fullKey, expiryTime, { value = "true", ov
109
101
  if (!overrideValue) {
110
102
  currentEntry = await tx.run(
111
103
  SELECT.one
112
- .from(configInstance.tableNameEventLock)
113
- .forUpdate({ wait: config.getConfigInstance().forUpdateTimeout })
104
+ .from(config.tableNameEventLock)
105
+ .forUpdate({ wait: config.forUpdateTimeout })
114
106
  .where("code =", fullKey)
115
107
  );
116
108
  }
117
109
  if (overrideValue || (currentEntry && new Date(currentEntry.createdAt).getTime() + expiryTime <= Date.now())) {
118
110
  await tx.run(
119
- UPDATE.entity(configInstance.tableNameEventLock)
111
+ UPDATE.entity(config.tableNameEventLock)
120
112
  .set({
121
113
  createdAt: new Date().toISOString(),
122
114
  value,
@@ -13,11 +13,8 @@ class EventScheduler {
13
13
  constructor() {}
14
14
 
15
15
  scheduleEvent(tenantId, type, subType, startAfter) {
16
- const configInstance = config.getConfigInstance();
17
- const eventConfig = configInstance.getEventConfig(type, subType);
18
- const scheduleWithoutDelay = configInstance.isPeriodicEvent(type, subType) && eventConfig.interval < 30 * 1000;
19
- const roundUpDate = scheduleWithoutDelay ? startAfter : this.calculateFutureTime(startAfter, 10);
20
- const key = [tenantId, type, subType, roundUpDate.toISOString()].join("##");
16
+ const { date, relative } = this.calculateOffset(type, subType, startAfter);
17
+ const key = [tenantId, type, subType, date.toISOString()].join("##");
21
18
  if (this.#scheduledEvents[key]) {
22
19
  return; // event combination already scheduled
23
20
  }
@@ -25,7 +22,7 @@ class EventScheduler {
25
22
  cds.log(COMPONENT_NAME).info("scheduling event queue run for delayed event", {
26
23
  type,
27
24
  subType,
28
- delaySeconds: (roundUpDate.getTime() - Date.now()) / 1000,
25
+ delaySeconds: (date.getTime() - Date.now()) / 1000,
29
26
  });
30
27
  setTimeout(() => {
31
28
  delete this.#scheduledEvents[key];
@@ -34,10 +31,18 @@ class EventScheduler {
34
31
  tenantId,
35
32
  type,
36
33
  subType,
37
- scheduledFor: roundUpDate.toISOString(),
34
+ scheduledFor: date.toISOString(),
38
35
  });
39
36
  });
40
- }, roundUpDate.getTime() - Date.now()).unref();
37
+ }, relative).unref();
38
+ }
39
+
40
+ calculateOffset(type, subType, startAfter) {
41
+ const eventConfig = config.getEventConfig(type, subType);
42
+ const scheduleWithoutDelay = config.isPeriodicEvent(type, subType) && eventConfig.interval < 30 * 1000;
43
+ const date = scheduleWithoutDelay ? startAfter : this.calculateFutureTime(startAfter, 10);
44
+
45
+ return { date, relative: date.getTime() - Date.now() };
41
46
  }
42
47
 
43
48
  calculateFutureTime(date, seoncds) {