@cap-js-community/event-queue 0.2.1 → 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 +1 -1
- package/src/EventQueueError.js +60 -0
- package/src/EventQueueProcessorBase.js +119 -17
- package/src/config.js +79 -13
- package/src/dbHandler.js +5 -3
- package/src/index.js +1 -1
- package/src/initialize.js +31 -34
- package/src/periodicEvents.js +107 -0
- package/src/processEventQueue.js +74 -2
- package/src/publishEvent.js +12 -5
- package/src/redisPubSub.js +3 -4
- package/src/runner.js +99 -33
- package/src/shared/WorkerQueue.js +2 -3
- package/src/shared/cdsHelper.js +2 -2
- package/src/shared/common.js +15 -1
- package/src/shared/distributedLock.js +12 -20
- package/src/shared/{EventScheduler.js → eventScheduler.js} +21 -8
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const cds = require("@sap/cds");
|
|
4
|
+
|
|
5
|
+
const { EventProcessingStatus } = require("./constants");
|
|
6
|
+
const { processChunkedSync } = require("./shared/common");
|
|
7
|
+
const eventConfig = require("./config");
|
|
8
|
+
|
|
9
|
+
const COMPONENT_NAME = "eventQueue/periodicEvents";
|
|
10
|
+
|
|
11
|
+
const checkAndInsertPeriodicEvents = async (context) => {
|
|
12
|
+
const tx = cds.tx(context);
|
|
13
|
+
const baseCqn = SELECT.from(eventConfig.tableNameEventQueue)
|
|
14
|
+
.where([
|
|
15
|
+
{ list: [{ ref: ["type"] }, { ref: ["subType"] }] },
|
|
16
|
+
"IN",
|
|
17
|
+
{
|
|
18
|
+
list: eventConfig.periodicEvents.map((periodicEvent) => ({
|
|
19
|
+
list: [{ val: periodicEvent.type }, { val: periodicEvent.subType }],
|
|
20
|
+
})),
|
|
21
|
+
},
|
|
22
|
+
"AND",
|
|
23
|
+
{ ref: ["status"] },
|
|
24
|
+
"=",
|
|
25
|
+
{ val: EventProcessingStatus.Open },
|
|
26
|
+
])
|
|
27
|
+
.columns(["ID", "type", "subType", "startAfter"]);
|
|
28
|
+
const currentPeriodEvents = await tx.run(baseCqn);
|
|
29
|
+
|
|
30
|
+
if (!currentPeriodEvents.length) {
|
|
31
|
+
// fresh insert all
|
|
32
|
+
return await insertPeriodEvents(tx, eventConfig.periodicEvents);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const exitingEventMap = currentPeriodEvents.reduce((result, current) => {
|
|
36
|
+
const key = _generateKey(current);
|
|
37
|
+
result[key] = current;
|
|
38
|
+
return result;
|
|
39
|
+
}, {});
|
|
40
|
+
|
|
41
|
+
const { newEvents, existingEvents } = eventConfig.periodicEvents.reduce(
|
|
42
|
+
(result, event) => {
|
|
43
|
+
if (exitingEventMap[_generateKey(event)]) {
|
|
44
|
+
result.existingEvents.push(exitingEventMap[_generateKey(event)]);
|
|
45
|
+
} else {
|
|
46
|
+
result.newEvents.push(event);
|
|
47
|
+
}
|
|
48
|
+
return result;
|
|
49
|
+
},
|
|
50
|
+
{ newEvents: [], existingEvents: [] }
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
const currentDate = new Date();
|
|
54
|
+
const exitingWithNotMatchingInterval = existingEvents.filter((existingEvent) => {
|
|
55
|
+
const config = eventConfig.getEventConfig(existingEvent.type, existingEvent.subType);
|
|
56
|
+
const eventStartAfter = new Date(existingEvent.startAfter);
|
|
57
|
+
// check if to far in future
|
|
58
|
+
const dueInWithNewInterval = new Date(currentDate.getTime() + config.interval * 1000);
|
|
59
|
+
return eventStartAfter >= dueInWithNewInterval;
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
exitingWithNotMatchingInterval.length &&
|
|
63
|
+
cds.log(COMPONENT_NAME).info("deleting periodic events because they have changed", {
|
|
64
|
+
changedEvents: exitingWithNotMatchingInterval.map(({ type, subType }) => ({ type, subType })),
|
|
65
|
+
});
|
|
66
|
+
await tx.run(
|
|
67
|
+
DELETE.from(eventConfig.tableNameEventQueue).where(
|
|
68
|
+
"ID IN",
|
|
69
|
+
exitingWithNotMatchingInterval.map(({ ID }) => ID)
|
|
70
|
+
)
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
const newOrChangedEvents = newEvents.concat(exitingWithNotMatchingInterval);
|
|
74
|
+
|
|
75
|
+
if (!newOrChangedEvents.length) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return await insertPeriodEvents(tx, newOrChangedEvents);
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const insertPeriodEvents = async (tx, events) => {
|
|
83
|
+
const startAfter = new Date();
|
|
84
|
+
processChunkedSync(events, 4, (chunk) => {
|
|
85
|
+
cds.log(COMPONENT_NAME).info("inserting changed or new periodic events", {
|
|
86
|
+
events: chunk.map(({ type, subType }) => {
|
|
87
|
+
const { interval } = eventConfig.getEventConfig(type, subType);
|
|
88
|
+
return { type, subType, interval };
|
|
89
|
+
}),
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
const periodEventsInsert = events.map((periodicEvent) => ({
|
|
93
|
+
type: periodicEvent.type,
|
|
94
|
+
subType: periodicEvent.subType,
|
|
95
|
+
startAfter: startAfter,
|
|
96
|
+
}));
|
|
97
|
+
|
|
98
|
+
tx._skipEventQueueBroadcase = true;
|
|
99
|
+
await tx.run(INSERT.into(eventConfig.tableNameEventQueue).entries(periodEventsInsert));
|
|
100
|
+
tx._skipEventQueueBroadcase = false;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const _generateKey = ({ type, subType }) => [type, subType].join("##");
|
|
104
|
+
|
|
105
|
+
module.exports = {
|
|
106
|
+
checkAndInsertPeriodicEvents,
|
|
107
|
+
};
|
package/src/processEventQueue.js
CHANGED
|
@@ -4,7 +4,7 @@ const pathLib = require("path");
|
|
|
4
4
|
|
|
5
5
|
const cds = require("@sap/cds");
|
|
6
6
|
|
|
7
|
-
const
|
|
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 =
|
|
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.", {
|
|
@@ -43,6 +43,9 @@ const processEventQueue = async (context, eventType, eventSubType, startTime = n
|
|
|
43
43
|
if (!continueProcessing) {
|
|
44
44
|
return;
|
|
45
45
|
}
|
|
46
|
+
if (baseInstance.isPeriodicEvent) {
|
|
47
|
+
return await processPeriodicEvent(baseInstance);
|
|
48
|
+
}
|
|
46
49
|
eventConfig.startTime = startTime;
|
|
47
50
|
while (shouldContinue) {
|
|
48
51
|
iterationCounter++;
|
|
@@ -131,6 +134,75 @@ const reevaluateShouldContinue = (eventTypeInstance, iterationCounter, startTime
|
|
|
131
134
|
return false;
|
|
132
135
|
};
|
|
133
136
|
|
|
137
|
+
// TODO: don't forget to release lock
|
|
138
|
+
const processPeriodicEvent = async (eventTypeInstance) => {
|
|
139
|
+
let queueEntry;
|
|
140
|
+
let processNext = true;
|
|
141
|
+
|
|
142
|
+
try {
|
|
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);
|
|
159
|
+
}
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
if (!queueEntry) {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
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
|
+
}
|
|
184
|
+
}
|
|
185
|
+
);
|
|
186
|
+
|
|
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
|
+
});
|
|
201
|
+
} finally {
|
|
202
|
+
await eventTypeInstance?.handleReleaseLock();
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
|
|
134
206
|
const processEventMap = async (eventTypeInstance) => {
|
|
135
207
|
eventTypeInstance.startPerformanceTracerEvents();
|
|
136
208
|
await eventTypeInstance.beforeProcessingEvents();
|
package/src/publishEvent.js
CHANGED
|
@@ -21,27 +21,34 @@ const EventQueueError = require("./EventQueueError");
|
|
|
21
21
|
* createdAt: Timestamp, // Timestamp of event creation. This field is automatically set on insert.
|
|
22
22
|
* startAfter: Timestamp, // Timestamp indicating when the event should start after.
|
|
23
23
|
* }
|
|
24
|
+
* @param {Boolean} skipBroadcast - (Optional) If set to true, event broadcasting will be skipped. Defaults to false.
|
|
24
25
|
* @throws {EventQueueError} Throws an error if the configuration is not initialized.
|
|
25
26
|
* @throws {EventQueueError} Throws an error if the event type is unknown.
|
|
26
27
|
* @throws {EventQueueError} Throws an error if the startAfter field is not a valid date.
|
|
27
28
|
* @returns {Promise} Returns a promise which resolves to the result of the database insert operation.
|
|
28
29
|
*/
|
|
29
|
-
const publishEvent = async (tx, events) => {
|
|
30
|
-
|
|
31
|
-
if (!configInstance.initialized) {
|
|
30
|
+
const publishEvent = async (tx, events, skipBroadcast = false) => {
|
|
31
|
+
if (!config.initialized) {
|
|
32
32
|
throw EventQueueError.notInitialized();
|
|
33
33
|
}
|
|
34
34
|
const eventsForProcessing = Array.isArray(events) ? events : [events];
|
|
35
35
|
for (const { type, subType, startAfter } of eventsForProcessing) {
|
|
36
|
-
const eventConfig =
|
|
36
|
+
const eventConfig = config.getEventConfig(type, subType);
|
|
37
37
|
if (!eventConfig) {
|
|
38
38
|
throw EventQueueError.unknownEventType(type, subType);
|
|
39
39
|
}
|
|
40
40
|
if (startAfter && !common.isValidDate(startAfter)) {
|
|
41
41
|
throw EventQueueError.malformedDate(startAfter);
|
|
42
42
|
}
|
|
43
|
+
|
|
44
|
+
if (eventConfig.isPeriodic) {
|
|
45
|
+
throw EventQueueError.manuelPeriodicEventInsert(type, subType);
|
|
46
|
+
}
|
|
43
47
|
}
|
|
44
|
-
|
|
48
|
+
tx._skipEventQueueBroadcase = skipBroadcast;
|
|
49
|
+
const result = await tx.run(INSERT.into(config.tableNameEventQueue).entries(eventsForProcessing));
|
|
50
|
+
tx._skipEventQueueBroadcase = false;
|
|
51
|
+
return result;
|
|
45
52
|
};
|
|
46
53
|
|
|
47
54
|
module.exports = {
|
package/src/redisPubSub.js
CHANGED
|
@@ -11,7 +11,7 @@ const COMPONENT_NAME = "eventQueue/redisPubSub";
|
|
|
11
11
|
let subscriberClientPromise;
|
|
12
12
|
|
|
13
13
|
const initEventQueueRedisSubscribe = () => {
|
|
14
|
-
if (subscriberClientPromise || !config.
|
|
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
|
-
|
|
40
|
-
|
|
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
|
@@ -9,22 +9,27 @@ const cdsHelper = require("./shared/cdsHelper");
|
|
|
9
9
|
const distributedLock = require("./shared/distributedLock");
|
|
10
10
|
const SetIntervalDriftSafe = require("./shared/SetIntervalDriftSafe");
|
|
11
11
|
const { getSubdomainForTenantId } = require("./shared/cdsHelper");
|
|
12
|
+
const periodicEvents = require("./periodicEvents");
|
|
13
|
+
const { hashStringTo32Bit } = require("./shared/common");
|
|
12
14
|
|
|
13
15
|
const COMPONENT_NAME = "eventQueue/runner";
|
|
14
16
|
const EVENT_QUEUE_RUN_ID = "EVENT_QUEUE_RUN_ID";
|
|
15
17
|
const EVENT_QUEUE_RUN_TS = "EVENT_QUEUE_RUN_TS";
|
|
18
|
+
const EVENT_QUEUE_RUN_PERIODIC_EVENT = "EVENT_QUEUE_RUN_PERIODIC_EVENT";
|
|
16
19
|
const OFFSET_FIRST_RUN = 10 * 1000;
|
|
17
20
|
|
|
18
|
-
|
|
21
|
+
let tenantIdHash;
|
|
22
|
+
let singleRunDone;
|
|
19
23
|
|
|
20
|
-
const
|
|
24
|
+
const singleTenant = () => _scheduleFunction(_checkPeriodicEventsSingleTenant, _executeRunForTenant);
|
|
21
25
|
|
|
22
|
-
const
|
|
26
|
+
const multiTenancyDb = () => _scheduleFunction(_multiTenancyPeriodicEvents, _multiTenancyDb);
|
|
23
27
|
|
|
24
|
-
const
|
|
28
|
+
const multiTenancyRedis = () => _scheduleFunction(_multiTenancyPeriodicEvents, _multiTenancyRedis);
|
|
29
|
+
|
|
30
|
+
const _scheduleFunction = async (singleRunFn, periodicFn) => {
|
|
25
31
|
const logger = cds.log(COMPONENT_NAME);
|
|
26
|
-
const
|
|
27
|
-
const eventsForAutomaticRun = configInstance.events;
|
|
32
|
+
const eventsForAutomaticRun = eventQueueConfig.allEvents;
|
|
28
33
|
if (!eventsForAutomaticRun.length) {
|
|
29
34
|
logger.warn("no events for automatic run are configured - skipping runner registration");
|
|
30
35
|
return;
|
|
@@ -32,11 +37,15 @@ const _scheduleFunction = async (fn) => {
|
|
|
32
37
|
|
|
33
38
|
const fnWithRunningCheck = () => {
|
|
34
39
|
const logger = cds.log(COMPONENT_NAME);
|
|
35
|
-
if (
|
|
40
|
+
if (eventQueueConfig.isRunnerDeactivated) {
|
|
36
41
|
logger.info("runner is deactivated via config variable. Skipping this run.");
|
|
37
42
|
return;
|
|
38
43
|
}
|
|
39
|
-
|
|
44
|
+
if (!singleRunDone) {
|
|
45
|
+
singleRunDone = true;
|
|
46
|
+
singleRunFn().catch(() => (singleRunDone = false));
|
|
47
|
+
}
|
|
48
|
+
return periodicFn();
|
|
40
49
|
};
|
|
41
50
|
|
|
42
51
|
const offsetDependingOnLastRun = await _calculateOffsetForFirstRun();
|
|
@@ -47,7 +56,7 @@ const _scheduleFunction = async (fn) => {
|
|
|
47
56
|
|
|
48
57
|
setTimeout(() => {
|
|
49
58
|
fnWithRunningCheck();
|
|
50
|
-
const intervalRunner = new SetIntervalDriftSafe(
|
|
59
|
+
const intervalRunner = new SetIntervalDriftSafe(eventQueueConfig.runInterval);
|
|
51
60
|
intervalRunner.run(fnWithRunningCheck);
|
|
52
61
|
}, offsetDependingOnLastRun).unref();
|
|
53
62
|
};
|
|
@@ -57,6 +66,8 @@ const _multiTenancyRedis = async () => {
|
|
|
57
66
|
const emptyContext = new cds.EventContext({});
|
|
58
67
|
logger.info("executing event queue run for multi instance and tenant");
|
|
59
68
|
const tenantIds = await cdsHelper.getAllTenantIds();
|
|
69
|
+
_checkAndTriggerPeriodicEventUpdate(tenantIds);
|
|
70
|
+
|
|
60
71
|
const runId = await _acquireRunId(emptyContext);
|
|
61
72
|
|
|
62
73
|
if (!runId) {
|
|
@@ -67,33 +78,33 @@ const _multiTenancyRedis = async () => {
|
|
|
67
78
|
_executeAllTenants(tenantIds, runId);
|
|
68
79
|
};
|
|
69
80
|
|
|
70
|
-
const
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
81
|
+
const _checkAndTriggerPeriodicEventUpdate = (tenantIds) => {
|
|
82
|
+
const hash = hashStringTo32Bit(JSON.stringify(tenantIds));
|
|
83
|
+
if (!tenantIdHash) {
|
|
84
|
+
tenantIdHash = hash;
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (tenantIdHash && tenantIdHash !== hash) {
|
|
88
|
+
cds.log(COMPONENT_NAME).info("tenant id hash changed, triggering updating periodic events!");
|
|
89
|
+
_multiTenancyPeriodicEvents().catch((err) => {
|
|
90
|
+
cds.log(COMPONENT_NAME).error("Error during triggering updating periodic events! Error:", err);
|
|
91
|
+
});
|
|
80
92
|
}
|
|
81
93
|
};
|
|
82
94
|
|
|
83
|
-
const
|
|
84
|
-
const configInstance = eventQueueConfig.getConfigInstance();
|
|
95
|
+
const _executeAllTenantsGeneric = (tenantIds, runId, fn) => {
|
|
85
96
|
const workerQueueInstance = getWorkerPoolInstance();
|
|
86
97
|
tenantIds.forEach((tenantId) => {
|
|
87
98
|
workerQueueInstance.addToQueue(async () => {
|
|
88
99
|
try {
|
|
89
100
|
const tenantContext = new cds.EventContext({ tenant: tenantId });
|
|
90
101
|
const couldAcquireLock = await distributedLock.acquireLock(tenantContext, runId, {
|
|
91
|
-
expiryTime:
|
|
102
|
+
expiryTime: eventQueueConfig.runInterval * 0.95,
|
|
92
103
|
});
|
|
93
104
|
if (!couldAcquireLock) {
|
|
94
105
|
return;
|
|
95
106
|
}
|
|
96
|
-
await
|
|
107
|
+
await fn(tenantId, runId);
|
|
97
108
|
} catch (err) {
|
|
98
109
|
cds.log(COMPONENT_NAME).error("executing event-queue run for tenant failed", {
|
|
99
110
|
tenantId,
|
|
@@ -103,11 +114,15 @@ const _executeAllTenants = (tenantIds, runId) => {
|
|
|
103
114
|
});
|
|
104
115
|
};
|
|
105
116
|
|
|
117
|
+
const _executeAllTenants = (tenantIds, runId) => _executeAllTenantsGeneric(tenantIds, runId, _executeRunForTenant);
|
|
118
|
+
|
|
119
|
+
const _executePeriodicEventsAllTenants = (tenantIds, runId) =>
|
|
120
|
+
_executeAllTenantsGeneric(tenantIds, runId, _checkPeriodicEventsSingleTenant);
|
|
121
|
+
|
|
106
122
|
const _executeRunForTenant = async (tenantId, runId) => {
|
|
107
123
|
const logger = cds.log(COMPONENT_NAME);
|
|
108
|
-
const configInstance = eventQueueConfig.getConfigInstance();
|
|
109
124
|
try {
|
|
110
|
-
const eventsForAutomaticRun =
|
|
125
|
+
const eventsForAutomaticRun = eventQueueConfig.allEvents;
|
|
111
126
|
const subdomain = await cdsHelper.getSubdomainForTenantId(tenantId);
|
|
112
127
|
const context = new cds.EventContext({
|
|
113
128
|
tenant: tenantId,
|
|
@@ -124,23 +139,22 @@ const _executeRunForTenant = async (tenantId, runId) => {
|
|
|
124
139
|
} catch (err) {
|
|
125
140
|
logger.error(`Couldn't process eventQueue for tenant! Next try after defined interval. Error: ${err}`, {
|
|
126
141
|
tenantId,
|
|
127
|
-
redisEnabled:
|
|
142
|
+
redisEnabled: eventQueueConfig.redisEnabled,
|
|
128
143
|
});
|
|
129
144
|
}
|
|
130
145
|
};
|
|
131
146
|
|
|
132
147
|
const _acquireRunId = async (context) => {
|
|
133
|
-
const configInstance = eventQueueConfig.getConfigInstance();
|
|
134
148
|
let runId = randomUUID();
|
|
135
149
|
const couldSetValue = await distributedLock.setValueWithExpire(context, EVENT_QUEUE_RUN_ID, runId, {
|
|
136
150
|
tenantScoped: false,
|
|
137
|
-
expiryTime:
|
|
151
|
+
expiryTime: eventQueueConfig.runInterval * 0.95,
|
|
138
152
|
});
|
|
139
153
|
|
|
140
154
|
if (couldSetValue) {
|
|
141
155
|
await distributedLock.setValueWithExpire(context, EVENT_QUEUE_RUN_TS, new Date().toISOString(), {
|
|
142
156
|
tenantScoped: false,
|
|
143
|
-
expiryTime:
|
|
157
|
+
expiryTime: eventQueueConfig.runInterval,
|
|
144
158
|
overrideValue: true,
|
|
145
159
|
});
|
|
146
160
|
} else {
|
|
@@ -153,13 +167,12 @@ const _acquireRunId = async (context) => {
|
|
|
153
167
|
};
|
|
154
168
|
|
|
155
169
|
const _calculateOffsetForFirstRun = async () => {
|
|
156
|
-
const configInstance = eventQueueConfig.getConfigInstance();
|
|
157
170
|
let offsetDependingOnLastRun = OFFSET_FIRST_RUN;
|
|
158
171
|
const now = Date.now();
|
|
159
172
|
// NOTE: this is only supported with Redis, because this is a tenant agnostic information
|
|
160
173
|
// currently there is no proper place to store this information beside t0 schema
|
|
161
174
|
try {
|
|
162
|
-
if (
|
|
175
|
+
if (eventQueueConfig.redisEnabled) {
|
|
163
176
|
const dummyContext = new cds.EventContext({});
|
|
164
177
|
let lastRunTs = await distributedLock.checkLockExistsAndReturnValue(dummyContext, EVENT_QUEUE_RUN_TS, {
|
|
165
178
|
tenantScoped: false,
|
|
@@ -168,7 +181,7 @@ const _calculateOffsetForFirstRun = async () => {
|
|
|
168
181
|
const ts = new Date(now).toISOString();
|
|
169
182
|
const couldSetValue = await distributedLock.setValueWithExpire(dummyContext, EVENT_QUEUE_RUN_TS, ts, {
|
|
170
183
|
tenantScoped: false,
|
|
171
|
-
expiryTime:
|
|
184
|
+
expiryTime: eventQueueConfig.runInterval,
|
|
172
185
|
});
|
|
173
186
|
if (couldSetValue) {
|
|
174
187
|
lastRunTs = ts;
|
|
@@ -178,7 +191,7 @@ const _calculateOffsetForFirstRun = async () => {
|
|
|
178
191
|
});
|
|
179
192
|
}
|
|
180
193
|
}
|
|
181
|
-
offsetDependingOnLastRun = new Date(lastRunTs).getTime() +
|
|
194
|
+
offsetDependingOnLastRun = new Date(lastRunTs).getTime() + eventQueueConfig.runInterval - now;
|
|
182
195
|
}
|
|
183
196
|
} catch (err) {
|
|
184
197
|
cds
|
|
@@ -211,6 +224,59 @@ const runEventCombinationForTenant = async (tenantId, type, subType) => {
|
|
|
211
224
|
}
|
|
212
225
|
};
|
|
213
226
|
|
|
227
|
+
const _multiTenancyDb = async () => {
|
|
228
|
+
const logger = cds.log(COMPONENT_NAME);
|
|
229
|
+
try {
|
|
230
|
+
logger.info("executing event queue run for single instance and multi tenant");
|
|
231
|
+
const tenantIds = await cdsHelper.getAllTenantIds();
|
|
232
|
+
_checkAndTriggerPeriodicEventUpdate(tenantIds);
|
|
233
|
+
_executeAllTenants(tenantIds, EVENT_QUEUE_RUN_ID);
|
|
234
|
+
} catch (err) {
|
|
235
|
+
logger.error(
|
|
236
|
+
`Couldn't fetch tenant ids for event queue processing! Next try after defined interval. Error: ${err}`
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
const _multiTenancyPeriodicEvents = async () => {
|
|
242
|
+
const logger = cds.log(COMPONENT_NAME);
|
|
243
|
+
try {
|
|
244
|
+
logger.info("executing event queue update periodic events");
|
|
245
|
+
const tenantIds = await cdsHelper.getAllTenantIds();
|
|
246
|
+
_executePeriodicEventsAllTenants(tenantIds, EVENT_QUEUE_RUN_PERIODIC_EVENT);
|
|
247
|
+
} catch (err) {
|
|
248
|
+
logger.error(`Couldn't fetch tenant ids for updating periodic event processing! Error: ${err}`);
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
const _checkPeriodicEventsSingleTenant = async (tenantId) => {
|
|
253
|
+
const logger = cds.log(COMPONENT_NAME);
|
|
254
|
+
if (!eventQueueConfig.updatePeriodicEvents) {
|
|
255
|
+
logger.info("updating of periodic events is disabled");
|
|
256
|
+
}
|
|
257
|
+
try {
|
|
258
|
+
const subdomain = await cdsHelper.getSubdomainForTenantId(tenantId);
|
|
259
|
+
const context = new cds.EventContext({
|
|
260
|
+
tenant: tenantId,
|
|
261
|
+
// NOTE: we need this because of logging otherwise logs would not contain the subdomain
|
|
262
|
+
http: { req: { authInfo: { getSubdomain: () => subdomain } } },
|
|
263
|
+
});
|
|
264
|
+
cds.context = context;
|
|
265
|
+
logger.info("executing updating periotic events", {
|
|
266
|
+
tenantId,
|
|
267
|
+
subdomain,
|
|
268
|
+
});
|
|
269
|
+
await cdsHelper.executeInNewTransaction(context, "update-periodic-events", async (tx) => {
|
|
270
|
+
await periodicEvents.checkAndInsertPeriodicEvents(tx.context);
|
|
271
|
+
});
|
|
272
|
+
} catch (err) {
|
|
273
|
+
logger.error(`Couldn't process eventQueue for tenant! Next try after defined interval. Error: ${err}`, {
|
|
274
|
+
tenantId,
|
|
275
|
+
redisEnabled: eventQueueConfig.redisEnabled,
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
|
|
214
280
|
module.exports = {
|
|
215
281
|
singleTenant,
|
|
216
282
|
multiTenancyDb,
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const cds = require("@sap/cds");
|
|
4
4
|
|
|
5
|
-
const
|
|
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
|
-
|
|
60
|
-
instance = new WorkerQueue(configInstance.parallelTenantProcessing);
|
|
59
|
+
instance = new WorkerQueue(config.parallelTenantProcessing);
|
|
61
60
|
}
|
|
62
61
|
return instance;
|
|
63
62
|
},
|
package/src/shared/cdsHelper.js
CHANGED
|
@@ -94,7 +94,7 @@ class TriggerRollback extends VError {
|
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
const getSubdomainForTenantId = async (tenantId) => {
|
|
97
|
-
if (!config.
|
|
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.
|
|
114
|
+
if (!config.isMultiTenancy) {
|
|
115
115
|
return null;
|
|
116
116
|
}
|
|
117
117
|
const ssp = await cds.connect.to("cds.xt.SaasProvisioningService");
|
package/src/shared/common.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
+
const crypto = require("crypto");
|
|
4
|
+
|
|
3
5
|
const { floor, abs, min } = Math;
|
|
4
6
|
|
|
5
7
|
const arrayToFlatMap = (array, key = "ID") => {
|
|
@@ -118,4 +120,16 @@ const isValidDate = (value) => {
|
|
|
118
120
|
}
|
|
119
121
|
};
|
|
120
122
|
|
|
121
|
-
|
|
123
|
+
const processChunkedSync = (inputs, chunkSize, chunkHandler) => {
|
|
124
|
+
let start = 0;
|
|
125
|
+
while (start < inputs.length) {
|
|
126
|
+
let end = start + chunkSize > inputs.length ? inputs.length : start + chunkSize;
|
|
127
|
+
const chunk = inputs.slice(start, end);
|
|
128
|
+
chunkHandler(chunk);
|
|
129
|
+
start = end;
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const hashStringTo32Bit = (value) => crypto.createHash("sha256").update(String(value)).digest("base64").slice(0, 32);
|
|
134
|
+
|
|
135
|
+
module.exports = { arrayToFlatMap, Funnel, limiter, isValidDate, processChunkedSync, hashStringTo32Bit };
|