@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.
- package/LICENSE +201 -0
- package/README.md +160 -0
- package/cds-plugin.js +11 -0
- package/db/Event.cds +24 -0
- package/db/Lock.cds +10 -0
- package/db/index.cds +2 -0
- package/index.cds +1 -0
- package/package.json +55 -0
- package/src/EventQueueError.js +141 -0
- package/src/EventQueueProcessorBase.js +922 -0
- package/src/config.js +196 -0
- package/src/constants.js +11 -0
- package/src/dbHandler.js +40 -0
- package/src/index.js +20 -0
- package/src/initialize.js +216 -0
- package/src/processEventQueue.js +297 -0
- package/src/publishEvent.js +25 -0
- package/src/redisPubSub.js +143 -0
- package/src/runner.js +237 -0
- package/src/shared/PerformanceTracer.js +74 -0
- package/src/shared/WorkerQueue.js +78 -0
- package/src/shared/cdsHelper.js +136 -0
- package/src/shared/common.js +115 -0
- package/src/shared/distributedLock.js +191 -0
- package/src/shared/env.js +9 -0
- package/src/shared/redis.js +70 -0
|
@@ -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
|
+
};
|