@cap-js-community/event-queue 0.1.49

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.
@@ -0,0 +1,297 @@
1
+ "use strict";
2
+
3
+ const pathLib = require("path");
4
+
5
+ const cds = require("@sap/cds");
6
+
7
+ const { getConfigInstance } = require("./config");
8
+ const { limiter, Funnel } = require("./shared/common");
9
+
10
+ const EventQueueBase = require("./EventQueueProcessorBase");
11
+ const {
12
+ executeInNewTransaction,
13
+ TriggerRollback,
14
+ } = require("./shared/cdsHelper");
15
+
16
+ const COMPONENT_NAME = "eventQueue/processEventQueue";
17
+ const MAX_EXECUTION_TIME = 5 * 60 * 1000;
18
+
19
+ const eventQueueRunner = async (context, events) => {
20
+ const startTime = new Date();
21
+ const funnel = new Funnel();
22
+ await Promise.allSettled(
23
+ events.map((event) =>
24
+ funnel.run(event.load, async () =>
25
+ processEventQueue(context, event.type, event.subType, startTime)
26
+ )
27
+ )
28
+ );
29
+ };
30
+
31
+ const processEventQueue = async (
32
+ context,
33
+ eventType,
34
+ eventSubType,
35
+ startTime = new Date()
36
+ ) => {
37
+ let iterationCounter = 0;
38
+ let shouldContinue = true;
39
+ let baseInstance;
40
+ try {
41
+ let eventTypeInstance;
42
+ const eventConfig = getConfigInstance().getEventConfig(
43
+ eventType,
44
+ eventSubType
45
+ );
46
+ const [err, EventTypeClass] = resilientRequire(eventConfig?.impl);
47
+ if (
48
+ !eventConfig ||
49
+ err ||
50
+ !(typeof EventTypeClass.constructor === "function")
51
+ ) {
52
+ await EventQueueBase.handleMissingTypeImplementation(
53
+ context,
54
+ eventType,
55
+ eventSubType
56
+ );
57
+ return;
58
+ }
59
+ baseInstance = new EventTypeClass(
60
+ context,
61
+ eventType,
62
+ eventSubType,
63
+ eventConfig
64
+ );
65
+ const continueProcessing = await baseInstance.handleDistributedLock();
66
+ if (!continueProcessing) {
67
+ return;
68
+ }
69
+ eventConfig.startTime = startTime;
70
+ while (shouldContinue) {
71
+ iterationCounter++;
72
+ await executeInNewTransaction(
73
+ context,
74
+ `eventQueue-pre-processing-${eventType}##${eventSubType}`,
75
+ async (tx) => {
76
+ eventTypeInstance = new EventTypeClass(
77
+ tx.context,
78
+ eventType,
79
+ eventSubType,
80
+ eventConfig
81
+ );
82
+ const queueEntries =
83
+ await eventTypeInstance.getQueueEntriesAndSetToInProgress();
84
+ eventTypeInstance.startPerformanceTracerPreprocessing();
85
+ for (const queueEntry of queueEntries) {
86
+ try {
87
+ eventTypeInstance.modifyQueueEntry(queueEntry);
88
+ const payload =
89
+ await eventTypeInstance.checkEventAndGeneratePayload(
90
+ queueEntry
91
+ );
92
+ if (payload === null) {
93
+ eventTypeInstance.setStatusToDone(queueEntry);
94
+ continue;
95
+ }
96
+ if (payload === undefined) {
97
+ eventTypeInstance.handleInvalidPayloadReturned(queueEntry);
98
+ continue;
99
+ }
100
+ eventTypeInstance.addEventWithPayloadForProcessing(
101
+ queueEntry,
102
+ payload
103
+ );
104
+ } catch (err) {
105
+ eventTypeInstance.handleErrorDuringProcessing(err, queueEntry);
106
+ }
107
+ }
108
+ throw new TriggerRollback();
109
+ }
110
+ );
111
+ eventTypeInstance.exceededEvents.length &&
112
+ (await executeInNewTransaction(
113
+ context,
114
+ `eventQueue-handleExceededEvents-${eventType}##${eventSubType}`,
115
+ async (tx) => {
116
+ eventTypeInstance.processEventContext = tx.context;
117
+ await eventTypeInstance.handleExceededEvents(
118
+ eventTypeInstance.exceededEvents
119
+ );
120
+ }
121
+ ));
122
+ if (!eventTypeInstance) {
123
+ return;
124
+ }
125
+ eventTypeInstance.endPerformanceTracerPreprocessing();
126
+ if (Object.keys(eventTypeInstance.queueEntriesWithPayloadMap).length) {
127
+ await executeInNewTransaction(
128
+ context,
129
+ `eventQueue-processing-${eventType}##${eventSubType}`,
130
+ async (tx) => {
131
+ eventTypeInstance.processEventContext = tx.context;
132
+ try {
133
+ eventTypeInstance.clusterQueueEntries();
134
+ await processEventMap(eventTypeInstance);
135
+ } catch (err) {
136
+ eventTypeInstance.handleErrorDuringClustering(err);
137
+ }
138
+ if (
139
+ eventTypeInstance.shouldTriggerRollback ||
140
+ Object.entries(eventTypeInstance.eventProcessingMap).some(
141
+ ([key]) => eventTypeInstance.shouldRollbackTransaction(key)
142
+ )
143
+ ) {
144
+ throw new TriggerRollback();
145
+ }
146
+ }
147
+ );
148
+ }
149
+ await executeInNewTransaction(
150
+ context,
151
+ `eventQueue-persistStatus-${eventType}##${eventSubType}`,
152
+ async (tx) => {
153
+ await eventTypeInstance.persistEventStatus(tx);
154
+ }
155
+ );
156
+ shouldContinue = reevaluateShouldContinue(
157
+ eventTypeInstance,
158
+ iterationCounter,
159
+ startTime
160
+ );
161
+ }
162
+ } catch (err) {
163
+ cds
164
+ .log(COMPONENT_NAME)
165
+ .error(
166
+ "Processing event queue failed with unexpected error. Error:",
167
+ err,
168
+ {
169
+ eventType,
170
+ eventSubType,
171
+ }
172
+ );
173
+ } finally {
174
+ await baseInstance?.handleReleaseLock();
175
+ }
176
+ };
177
+
178
+ const reevaluateShouldContinue = (
179
+ eventTypeInstance,
180
+ iterationCounter,
181
+ startTime
182
+ ) => {
183
+ if (!eventTypeInstance.getSelectNextChunk()) {
184
+ return false; // no select next chunk configured for this event
185
+ }
186
+ if (eventTypeInstance.emptyChunkSelected) {
187
+ return false; // the last selected chunk was empty - no more data for processing
188
+ }
189
+ if (new Date(startTime.getTime() + MAX_EXECUTION_TIME) > new Date()) {
190
+ return true;
191
+ }
192
+ eventTypeInstance.logTimeExceeded(iterationCounter);
193
+ return false;
194
+ };
195
+
196
+ const processEventMap = async (eventTypeInstance) => {
197
+ eventTypeInstance.startPerformanceTracerEvents();
198
+ await eventTypeInstance.beforeProcessingEvents();
199
+ if (eventTypeInstance.commitOnEventLevel) {
200
+ eventTypeInstance.txUsageAllowed = false;
201
+ }
202
+ await limiter(
203
+ eventTypeInstance.parallelEventProcessing,
204
+ Object.entries(eventTypeInstance.eventProcessingMap),
205
+ async ([key, { queueEntries, payload }]) => {
206
+ if (eventTypeInstance.commitOnEventLevel) {
207
+ let statusMap;
208
+ await executeInNewTransaction(
209
+ eventTypeInstance.baseContext,
210
+ `eventQueue-processEvent-${eventTypeInstance.eventType}##${eventTypeInstance.eventSubType}`,
211
+ async (tx) => {
212
+ statusMap = await _processEvent(
213
+ eventTypeInstance,
214
+ tx.context,
215
+ key,
216
+ queueEntries,
217
+ payload
218
+ );
219
+ if (
220
+ eventTypeInstance.statusMapContainsError(statusMap) ||
221
+ eventTypeInstance.shouldRollbackTransaction(key)
222
+ ) {
223
+ throw new TriggerRollback();
224
+ }
225
+ }
226
+ );
227
+ await executeInNewTransaction(
228
+ eventTypeInstance.baseContext,
229
+ `eventQueue-persistStatus-${eventTypeInstance.eventType}##${eventTypeInstance.eventSubType}`,
230
+ async (tx) => {
231
+ eventTypeInstance.processEventContext = tx.context;
232
+ await eventTypeInstance.persistEventStatus(tx, {
233
+ skipChecks: true,
234
+ statusMap,
235
+ });
236
+ }
237
+ );
238
+ } else {
239
+ await _processEvent(
240
+ eventTypeInstance,
241
+ eventTypeInstance.context,
242
+ key,
243
+ queueEntries,
244
+ payload
245
+ );
246
+ }
247
+ }
248
+ ).finally(() => {
249
+ eventTypeInstance.clearEventProcessingContext();
250
+ if (eventTypeInstance.commitOnEventLevel) {
251
+ eventTypeInstance.txUsageAllowed = true;
252
+ }
253
+ });
254
+ eventTypeInstance.endPerformanceTracerEvents();
255
+ };
256
+
257
+ const _processEvent = async (
258
+ eventTypeInstance,
259
+ processContext,
260
+ key,
261
+ queueEntries,
262
+ payload
263
+ ) => {
264
+ try {
265
+ eventTypeInstance.logStartMessage(queueEntries);
266
+ const eventOutdated = await eventTypeInstance.isOutdatedAndKeepalive(
267
+ queueEntries
268
+ );
269
+ if (eventOutdated) {
270
+ return;
271
+ }
272
+ eventTypeInstance.setTxForEventProcessing(key, cds.tx(processContext));
273
+ const statusTuple = await eventTypeInstance.processEvent(
274
+ processContext,
275
+ key,
276
+ queueEntries,
277
+ payload
278
+ );
279
+ return eventTypeInstance.setEventStatus(queueEntries, statusTuple);
280
+ } catch (err) {
281
+ return eventTypeInstance.handleErrorDuringProcessing(err, queueEntries);
282
+ }
283
+ };
284
+
285
+ const resilientRequire = (path) => {
286
+ try {
287
+ const module = require(pathLib.join(process.cwd(), path));
288
+ return [null, module];
289
+ } catch (err) {
290
+ return [err, null];
291
+ }
292
+ };
293
+
294
+ module.exports = {
295
+ processEventQueue,
296
+ eventQueueRunner,
297
+ };
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+
3
+ const config = require("./config");
4
+ const EventQueueError = require("./EventQueueError");
5
+
6
+ const publishEvent = async (tx, events) => {
7
+ const configInstance = config.getConfigInstance();
8
+ if (!configInstance.initialized) {
9
+ throw EventQueueError.notInitialized();
10
+ }
11
+ const eventsForProcessing = Array.isArray(events) ? events : [events];
12
+ for (const { type, subType } of eventsForProcessing) {
13
+ const eventConfig = configInstance.getEventConfig(type, subType);
14
+ if (!eventConfig) {
15
+ throw EventQueueError.unknownEventType(type, subType);
16
+ }
17
+ }
18
+ await tx.run(
19
+ INSERT.into(configInstance.tableNameEventQueue).entries(eventsForProcessing)
20
+ );
21
+ };
22
+
23
+ module.exports = {
24
+ publishEvent,
25
+ };
@@ -0,0 +1,143 @@
1
+ "use strict";
2
+
3
+ const redis = require("./shared/redis");
4
+ const { processEventQueue } = require("./processEventQueue");
5
+ const { getSubdomainForTenantId } = require("./shared/cdsHelper");
6
+ const { checkLockExistsAndReturnValue } = require("./shared/distributedLock");
7
+ const config = require("./config");
8
+ const { getWorkerPoolInstance } = require("./shared/WorkerQueue");
9
+
10
+ const MESSAGE_CHANNEL = "cdsEventQueue";
11
+ const COMPONENT_NAME = "eventQueue/redisPubSub";
12
+
13
+ let publishClient;
14
+ let subscriberClientPromise;
15
+
16
+ const initEventQueueRedisSubscribe = () => {
17
+ if (subscriberClientPromise || !config.getConfigInstance().redisEnabled) {
18
+ return;
19
+ }
20
+ subscribeRedisClient();
21
+ };
22
+
23
+ const subscribeRedisClient = () => {
24
+ const errorHandlerCreateClient = (err) => {
25
+ cds
26
+ .log(COMPONENT_NAME)
27
+ .error("error from redis client for pub/sub failed", err);
28
+ subscriberClientPromise = null;
29
+ setTimeout(subscribeRedisClient, 5 * 1000).unref();
30
+ };
31
+ subscriberClientPromise = redis.createClientAndConnect(
32
+ errorHandlerCreateClient
33
+ );
34
+ subscriberClientPromise
35
+ .then((client) => {
36
+ cds.log(COMPONENT_NAME).info("subscribe redis client connected");
37
+ client.subscribe(MESSAGE_CHANNEL, messageHandlerProcessEvents);
38
+ })
39
+ .catch((err) => {
40
+ cds
41
+ .log(COMPONENT_NAME)
42
+ .error(
43
+ "error from redis client for pub/sub failed during startup - trying to reconnect",
44
+ err
45
+ );
46
+ });
47
+ };
48
+
49
+ const messageHandlerProcessEvents = async (messageData) => {
50
+ let tenantId, type, subType;
51
+ try {
52
+ ({ tenantId, type, subType } = JSON.parse(messageData));
53
+ } catch (err) {
54
+ cds.log(COMPONENT_NAME).error("could not parse event information", {
55
+ messageData,
56
+ });
57
+ return;
58
+ }
59
+ const subdomain = await getSubdomainForTenantId(tenantId);
60
+ const context = new cds.EventContext({
61
+ tenant: tenantId,
62
+ // NOTE: we need this because of logging otherwise logs would not contain the subdomain
63
+ http: { req: { authInfo: { getSubdomain: () => subdomain } } },
64
+ });
65
+ cds.context = context;
66
+ cds.log(COMPONENT_NAME).debug("received redis event", {
67
+ tenantId,
68
+ type,
69
+ subType,
70
+ });
71
+ getWorkerPoolInstance().addToQueue(async () =>
72
+ processEventQueue(context, type, subType)
73
+ );
74
+ };
75
+
76
+ const publishEvent = async (tenantId, type, subType) => {
77
+ const configInstance = config.getConfigInstance();
78
+ if (!configInstance.redisEnabled) {
79
+ await _handleEventInternally(tenantId, type, subType);
80
+ return;
81
+ }
82
+
83
+ const logger = cds.log(COMPONENT_NAME);
84
+ const errorHandlerCreateClient = (err) => {
85
+ logger.error("error from redis client for pub/sub failed", {
86
+ err,
87
+ });
88
+ publishClient = null;
89
+ };
90
+ try {
91
+ if (!publishClient) {
92
+ publishClient = await redis.createClientAndConnect(
93
+ errorHandlerCreateClient
94
+ );
95
+ logger.info("publish redis client connected");
96
+ }
97
+
98
+ const result = await checkLockExistsAndReturnValue(
99
+ new cds.EventContext({ tenant: tenantId }),
100
+ [type, subType].join("##")
101
+ );
102
+ if (result) {
103
+ logger.info("skip publish redis event as no lock is available");
104
+ return;
105
+ }
106
+ logger.debug("publishing redis event", {
107
+ tenantId,
108
+ type,
109
+ subType,
110
+ });
111
+ await publishClient.publish(
112
+ MESSAGE_CHANNEL,
113
+ JSON.stringify({ tenantId, type, subType })
114
+ );
115
+ } catch (err) {
116
+ logger.error(`publish event failed with error: ${err.toString()}`, {
117
+ tenantId,
118
+ type,
119
+ subType,
120
+ });
121
+ }
122
+ };
123
+
124
+ const _handleEventInternally = async (tenantId, type, subType) => {
125
+ const logger = cds.log(COMPONENT_NAME);
126
+ logger.info("processEventQueue internally", {
127
+ tenantId,
128
+ type,
129
+ subType,
130
+ });
131
+ const subdomain = await getSubdomainForTenantId(tenantId);
132
+ const context = new cds.EventContext({
133
+ tenant: tenantId,
134
+ // NOTE: we need this because of logging otherwise logs would not contain the subdomain
135
+ http: { req: { authInfo: { getSubdomain: () => subdomain } } },
136
+ });
137
+ processEventQueue(context, type, subType);
138
+ };
139
+
140
+ module.exports = {
141
+ initEventQueueRedisSubscribe,
142
+ publishEvent,
143
+ };
package/src/runner.js ADDED
@@ -0,0 +1,237 @@
1
+ "use strict";
2
+
3
+ const uuid = require("uuid");
4
+
5
+ const eventQueueConfig = require("./config");
6
+ const cdsHelper = require("./shared/cdsHelper");
7
+ const { eventQueueRunner } = require("./processEventQueue");
8
+ const distributedLock = require("./shared/distributedLock");
9
+ const { getWorkerPoolInstance } = require("./shared/WorkerQueue");
10
+ const { getConfigInstance } = require("./config");
11
+
12
+ const COMPONENT_NAME = "eventQueue/runner";
13
+ const EVENT_QUEUE_RUN_ID = "EVENT_QUEUE_RUN_ID";
14
+ const EVENT_QUEUE_RUN_TS = "EVENT_QUEUE_RUN_TS";
15
+ const OFFSET_FIRST_RUN = 10 * 1000;
16
+
17
+ const LOGGER = cds.log(COMPONENT_NAME);
18
+
19
+ const singleTenant = () => _scheduleFunction(_executeRunForTenant);
20
+
21
+ const multiTenancyDb = () => _scheduleFunction(_multiTenancyDb);
22
+
23
+ const multiTenancyRedis = () => _scheduleFunction(_multiTenancyRedis);
24
+
25
+ const _scheduleFunction = async (fn) => {
26
+ const configInstance = eventQueueConfig.getConfigInstance();
27
+ const eventsForAutomaticRun = configInstance.events;
28
+ if (!eventsForAutomaticRun.length) {
29
+ LOGGER.warn(
30
+ "no events for automatic run are configured - skipping runner registration"
31
+ );
32
+ return;
33
+ }
34
+
35
+ const fnWithRunningCheck = () => {
36
+ if (configInstance.isRunnerDeactivated) {
37
+ LOGGER.info(
38
+ "runner is deactivated via config variable. Skipping this run."
39
+ );
40
+ return;
41
+ }
42
+ return fn();
43
+ };
44
+
45
+ const offsetDependingOnLastRun = await _calculateOffsetForFirstRun();
46
+
47
+ LOGGER.info("first event-queue run scheduled", {
48
+ firstRunScheduledFor: new Date(
49
+ Date.now() + offsetDependingOnLastRun
50
+ ).toISOString(),
51
+ });
52
+
53
+ setTimeout(() => {
54
+ fnWithRunningCheck();
55
+ setInterval(fnWithRunningCheck, configInstance.runInterval).unref();
56
+ }, offsetDependingOnLastRun).unref();
57
+ };
58
+
59
+ const _multiTenancyRedis = async () => {
60
+ const emptyContext = new cds.EventContext({});
61
+ LOGGER.info("executing event queue run for multi instance and tenant");
62
+ const tenantIds = await cdsHelper.getAllTenantIds();
63
+ const runId = await _acquireRunId(emptyContext);
64
+
65
+ if (!runId) {
66
+ LOGGER.error("could not acquire runId, skip processing events!");
67
+ return;
68
+ }
69
+
70
+ _executeAllTenants(tenantIds, runId);
71
+ };
72
+
73
+ const _multiTenancyDb = async () => {
74
+ try {
75
+ LOGGER.info(
76
+ "executing event queue run for single instance and multi tenant"
77
+ );
78
+ const tenantIds = await cdsHelper.getAllTenantIds();
79
+ _executeAllTenants(tenantIds, EVENT_QUEUE_RUN_ID);
80
+ } catch (err) {
81
+ LOGGER.error(
82
+ `Couldn't fetch tenant ids for event queue processing! Next try after defined interval. Error: ${err}`
83
+ );
84
+ }
85
+ };
86
+
87
+ const _executeAllTenants = (tenantIds, runId) => {
88
+ const configInstance = eventQueueConfig.getConfigInstance();
89
+ const workerQueueInstance = getWorkerPoolInstance();
90
+ tenantIds.forEach((tenantId) => {
91
+ workerQueueInstance.addToQueue(async () => {
92
+ try {
93
+ const tenantContext = new cds.EventContext({ tenant: tenantId });
94
+ const couldAcquireLock = await distributedLock.acquireLock(
95
+ tenantContext,
96
+ runId,
97
+ {
98
+ expiryTime: configInstance.runInterval * 0.95,
99
+ }
100
+ );
101
+ if (!couldAcquireLock) {
102
+ return;
103
+ }
104
+ await _executeRunForTenant(tenantId, runId);
105
+ } catch (err) {
106
+ LOGGER.error("executing event-queue run for tenant failed", {
107
+ tenantId,
108
+ });
109
+ }
110
+ });
111
+ });
112
+ };
113
+
114
+ const _executeRunForTenant = async (tenantId, runId) => {
115
+ const configInstance = eventQueueConfig.getConfigInstance();
116
+ try {
117
+ const eventsForAutomaticRun = configInstance.events;
118
+ const subdomain = await cdsHelper.getSubdomainForTenantId(tenantId);
119
+ const context = new cds.EventContext({
120
+ tenant: tenantId,
121
+ // NOTE: we need this because of logging otherwise logs would not contain the subdomain
122
+ http: { req: { authInfo: { getSubdomain: () => subdomain } } },
123
+ });
124
+ cds.context = context;
125
+ LOGGER.info("executing eventQueue run", {
126
+ tenantId,
127
+ subdomain,
128
+ ...(runId ? { runId } : null),
129
+ });
130
+ await eventQueueRunner(context, eventsForAutomaticRun);
131
+ } catch (err) {
132
+ LOGGER.error(
133
+ `Couldn't process eventQueue for tenant! Next try after defined interval. Error: ${err}`,
134
+ {
135
+ tenantId,
136
+ redisEnabled: configInstance.redisEnabled,
137
+ }
138
+ );
139
+ }
140
+ };
141
+
142
+ const _acquireRunId = async (context) => {
143
+ const configInstance = eventQueueConfig.getConfigInstance();
144
+ let runId = uuid.v4();
145
+ const couldSetValue = await distributedLock.setValueWithExpire(
146
+ context,
147
+ EVENT_QUEUE_RUN_ID,
148
+ runId,
149
+ {
150
+ tenantScoped: false,
151
+ expiryTime: configInstance.runInterval * 0.95,
152
+ }
153
+ );
154
+
155
+ if (couldSetValue) {
156
+ await distributedLock.setValueWithExpire(
157
+ context,
158
+ EVENT_QUEUE_RUN_TS,
159
+ new Date().toISOString(),
160
+ {
161
+ tenantScoped: false,
162
+ expiryTime: configInstance.runInterval,
163
+ overrideValue: true,
164
+ }
165
+ );
166
+ } else {
167
+ runId = await distributedLock.checkLockExistsAndReturnValue(
168
+ context,
169
+ EVENT_QUEUE_RUN_ID,
170
+ {
171
+ tenantScoped: false,
172
+ }
173
+ );
174
+ }
175
+
176
+ return runId;
177
+ };
178
+
179
+ const _calculateOffsetForFirstRun = async () => {
180
+ const configInstance = getConfigInstance();
181
+ let offsetDependingOnLastRun = OFFSET_FIRST_RUN;
182
+ const now = Date.now();
183
+ // NOTE: this is only supported with Redis, because this is a tenant agnostic information
184
+ // currently there is no proper place to store this information beside t0 schema
185
+ try {
186
+ if (configInstance.redisEnabled) {
187
+ const dummyContext = new cds.EventContext({});
188
+ let lastRunTs = await distributedLock.checkLockExistsAndReturnValue(
189
+ dummyContext,
190
+ EVENT_QUEUE_RUN_TS,
191
+ { tenantScoped: false }
192
+ );
193
+ if (!lastRunTs) {
194
+ const ts = new Date(now).toISOString();
195
+ const couldSetValue = await distributedLock.setValueWithExpire(
196
+ dummyContext,
197
+ EVENT_QUEUE_RUN_TS,
198
+ ts,
199
+ {
200
+ tenantScoped: false,
201
+ expiryTime: configInstance.runInterval,
202
+ }
203
+ );
204
+ if (couldSetValue) {
205
+ lastRunTs = ts;
206
+ } else {
207
+ lastRunTs = await distributedLock.checkLockExistsAndReturnValue(
208
+ dummyContext,
209
+ EVENT_QUEUE_RUN_TS,
210
+ { tenantScoped: false }
211
+ );
212
+ }
213
+ }
214
+ offsetDependingOnLastRun =
215
+ new Date(lastRunTs).getTime() + configInstance.runInterval - now;
216
+ }
217
+ } catch (err) {
218
+ LOGGER.error(
219
+ "calculating offset for first run failed, falling back to default. Runs might be out-of-sync. Error:",
220
+ err
221
+ );
222
+ }
223
+ return offsetDependingOnLastRun;
224
+ };
225
+
226
+ module.exports = {
227
+ singleTenant,
228
+ multiTenancyDb,
229
+ multiTenancyRedis,
230
+ _: {
231
+ _multiTenancyRedis,
232
+ _multiTenancyDb,
233
+ _calculateOffsetForFirstRun,
234
+ _acquireRunId,
235
+ EVENT_QUEUE_RUN_TS,
236
+ },
237
+ };