@cap-js-community/event-queue 0.2.0 → 0.2.2
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/README.md +1 -1
- package/package.json +1 -1
- package/src/EventQueueError.js +60 -0
- package/src/EventQueueProcessorBase.js +69 -1
- package/src/config.js +62 -2
- package/src/dbHandler.js +3 -0
- package/src/initialize.js +18 -5
- package/src/periodicEvents.js +109 -0
- package/src/processEventQueue.js +63 -0
- package/src/publishEvent.js +10 -2
- package/src/redisPubSub.js +12 -0
- package/src/runner.js +87 -19
- package/src/shared/common.js +15 -1
- package/src/shared/{EventScheduler.js → eventScheduler.js} +12 -4
- package/src/shared/redis.js +23 -2
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/@cap-js-community/event-queue)
|
|
4
4
|
[](https://www.npmjs.com/package/@cap-js-community/event-queue)
|
|
5
5
|
[](https://api.reuse.software/info/github.com/cap-js-community/event-queue)
|
|
6
|
-
[](https://github.com/cap-js-community/event-queue/commits/main)
|
|
7
7
|
|
|
8
8
|
The Event-Queue is a framework built on top of CAP Node.js, specifically designed to enable efficient and streamlined
|
|
9
9
|
asynchronous event processing in a multi-tenancy environment. With a strong emphasis on load balancing, this package
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cap-js-community/event-queue",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
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": [
|
package/src/EventQueueError.js
CHANGED
|
@@ -12,6 +12,10 @@ const ERROR_CODES = {
|
|
|
12
12
|
MISSING_ELEMENT_IN_TABLE: "MISSING_ELEMENT_IN_TABLE",
|
|
13
13
|
TYPE_MISMATCH_TABLE: "TYPE_MISMATCH_TABLE",
|
|
14
14
|
NO_VALID_DATE: "NO_VALID_DATE",
|
|
15
|
+
INVALID_INTERVAL: "INVALID_INTERVAL",
|
|
16
|
+
MISSING_IMPL: "MISSING_IMPL",
|
|
17
|
+
DUPLICATE_EVENT_REGISTRATION: "DUPLICATE_EVENT_REGISTRATION",
|
|
18
|
+
NO_MANUEL_INSERT_OF_PERIODIC: "NO_MANUEL_INSERT_OF_PERIODIC",
|
|
15
19
|
};
|
|
16
20
|
|
|
17
21
|
const ERROR_CODES_META = {
|
|
@@ -43,6 +47,18 @@ const ERROR_CODES_META = {
|
|
|
43
47
|
[ERROR_CODES.NO_VALID_DATE]: {
|
|
44
48
|
message: "One or more events contain a date in a malformed format.",
|
|
45
49
|
},
|
|
50
|
+
[ERROR_CODES.INVALID_INTERVAL]: {
|
|
51
|
+
message: "Invalid interval, the value needs to greater than 10 seconds.",
|
|
52
|
+
},
|
|
53
|
+
[ERROR_CODES.MISSING_IMPL]: {
|
|
54
|
+
message: "Missing path to event class implementation.",
|
|
55
|
+
},
|
|
56
|
+
[ERROR_CODES.DUPLICATE_EVENT_REGISTRATION]: {
|
|
57
|
+
message: "Duplicate event registration, check the uniqueness of type and subType.",
|
|
58
|
+
},
|
|
59
|
+
[ERROR_CODES.NO_MANUEL_INSERT_OF_PERIODIC]: {
|
|
60
|
+
message: "Periodic events are managed by the framework and are not allowed to insert manually.",
|
|
61
|
+
},
|
|
46
62
|
};
|
|
47
63
|
|
|
48
64
|
class EventQueueError extends VError {
|
|
@@ -146,6 +162,50 @@ class EventQueueError extends VError {
|
|
|
146
162
|
message
|
|
147
163
|
);
|
|
148
164
|
}
|
|
165
|
+
|
|
166
|
+
static invalidInterval(type, subType, interval) {
|
|
167
|
+
const { message } = ERROR_CODES_META[ERROR_CODES.INVALID_INTERVAL];
|
|
168
|
+
return new EventQueueError(
|
|
169
|
+
{
|
|
170
|
+
name: ERROR_CODES.INVALID_INTERVAL,
|
|
171
|
+
info: { type, subType, interval },
|
|
172
|
+
},
|
|
173
|
+
message
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
static missingImpl(type, subType) {
|
|
178
|
+
const { message } = ERROR_CODES_META[ERROR_CODES.MISSING_IMPL];
|
|
179
|
+
return new EventQueueError(
|
|
180
|
+
{
|
|
181
|
+
name: ERROR_CODES.MISSING_IMPL,
|
|
182
|
+
info: { type, subType },
|
|
183
|
+
},
|
|
184
|
+
message
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
static duplicateEventRegistration(type, subType) {
|
|
189
|
+
const { message } = ERROR_CODES_META[ERROR_CODES.DUPLICATE_EVENT_REGISTRATION];
|
|
190
|
+
return new EventQueueError(
|
|
191
|
+
{
|
|
192
|
+
name: ERROR_CODES.DUPLICATE_EVENT_REGISTRATION,
|
|
193
|
+
info: { type, subType },
|
|
194
|
+
},
|
|
195
|
+
message
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
static manuelPeriodicEventInsert(type, subType) {
|
|
200
|
+
const { message } = ERROR_CODES_META[ERROR_CODES.NO_MANUEL_INSERT_OF_PERIODIC];
|
|
201
|
+
return new EventQueueError(
|
|
202
|
+
{
|
|
203
|
+
name: ERROR_CODES.NO_MANUEL_INSERT_OF_PERIODIC,
|
|
204
|
+
info: { type, subType },
|
|
205
|
+
},
|
|
206
|
+
message
|
|
207
|
+
);
|
|
208
|
+
}
|
|
149
209
|
}
|
|
150
210
|
|
|
151
211
|
module.exports = EventQueueError;
|
|
@@ -7,7 +7,7 @@ const { EventProcessingStatus, TransactionMode } = require("./constants");
|
|
|
7
7
|
const distributedLock = require("./shared/distributedLock");
|
|
8
8
|
const EventQueueError = require("./EventQueueError");
|
|
9
9
|
const { arrayToFlatMap } = require("./shared/common");
|
|
10
|
-
const eventScheduler = require("./shared/
|
|
10
|
+
const eventScheduler = require("./shared/eventScheduler");
|
|
11
11
|
const eventQueueConfig = require("./config");
|
|
12
12
|
const PerformanceTracer = require("./shared/PerformanceTracer");
|
|
13
13
|
|
|
@@ -303,6 +303,29 @@ class EventQueueProcessorBase {
|
|
|
303
303
|
return Object.fromEntries(queueEntries.map((queueEntry) => [queueEntry.ID, EventProcessingStatus.Error]));
|
|
304
304
|
}
|
|
305
305
|
|
|
306
|
+
handleErrorDuringPeriodicEventProcessing(error, queueEntry) {
|
|
307
|
+
this.logger.error(
|
|
308
|
+
`Caught error during event periodic processing. Please catch your promises/exceptions. Error: ${error}`,
|
|
309
|
+
{
|
|
310
|
+
eventType: this.#eventType,
|
|
311
|
+
eventSubType: this.#eventSubType,
|
|
312
|
+
queueEntryId: queueEntry.ID,
|
|
313
|
+
}
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
async setPeriodicEventStatus(queueEntryIds) {
|
|
318
|
+
await this.tx.run(
|
|
319
|
+
UPDATE.entity(this.#config.tableNameEventQueue)
|
|
320
|
+
.set({
|
|
321
|
+
status: EventProcessingStatus.Done,
|
|
322
|
+
})
|
|
323
|
+
.where({
|
|
324
|
+
ID: queueEntryIds,
|
|
325
|
+
})
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
|
|
306
329
|
/**
|
|
307
330
|
* This function validates for all selected events one status has been submitted. It's also validated that only for
|
|
308
331
|
* selected events a status has been submitted. Persisting the status of events is done in a dedicated database tx.
|
|
@@ -812,6 +835,47 @@ class EventQueueProcessorBase {
|
|
|
812
835
|
}
|
|
813
836
|
}
|
|
814
837
|
|
|
838
|
+
async scheduleNextPeriodEvent(queueEntry) {
|
|
839
|
+
const interval = this.__eventConfig.interval;
|
|
840
|
+
const newEvent = {
|
|
841
|
+
type: this.#eventType,
|
|
842
|
+
subType: this.#eventSubType,
|
|
843
|
+
startAfter: new Date(new Date(queueEntry.startAfter).getTime() + interval * 1000),
|
|
844
|
+
};
|
|
845
|
+
this.tx._skipEventQueueBroadcase = true;
|
|
846
|
+
await this.tx.run(INSERT.into(this.#config.tableNameEventQueue).entries({ ...newEvent }));
|
|
847
|
+
this.tx._skipEventQueueBroadcase = false;
|
|
848
|
+
if (interval < this.#config.runInterval) {
|
|
849
|
+
this.#handleDelayedEvents([newEvent]);
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
async handleDuplicatedPeriodicEventEntry(queueEntries) {
|
|
854
|
+
this.logger.error("More than one open events for the same configuration which is not allowed!", {
|
|
855
|
+
eventType: this.#eventType,
|
|
856
|
+
eventSubType: this.#eventSubType,
|
|
857
|
+
queueEntriesIds: queueEntries.map(({ ID }) => ID),
|
|
858
|
+
});
|
|
859
|
+
|
|
860
|
+
let queueEntryToUse;
|
|
861
|
+
const obsoleteEntries = [];
|
|
862
|
+
for (const queueEntry of queueEntries) {
|
|
863
|
+
if (!queueEntryToUse) {
|
|
864
|
+
queueEntryToUse = queueEntry;
|
|
865
|
+
continue;
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
if (queueEntryToUse.startAfter <= queueEntry.queueEntry) {
|
|
869
|
+
obsoleteEntries.push(queueEntryToUse);
|
|
870
|
+
queueEntryToUse = queueEntry;
|
|
871
|
+
} else {
|
|
872
|
+
obsoleteEntries.push(queueEntry);
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
await this.setPeriodicEventStatus(obsoleteEntries.map(({ ID }) => ID));
|
|
876
|
+
return queueEntryToUse;
|
|
877
|
+
}
|
|
878
|
+
|
|
815
879
|
statusMapContainsError(statusMap) {
|
|
816
880
|
return Object.values(statusMap).includes(EventProcessingStatus.Error);
|
|
817
881
|
}
|
|
@@ -920,6 +984,10 @@ class EventQueueProcessorBase {
|
|
|
920
984
|
setTxForEventProcessing(key, tx) {
|
|
921
985
|
this.__txMap[key] = tx;
|
|
922
986
|
}
|
|
987
|
+
|
|
988
|
+
get isPeriodicEvent() {
|
|
989
|
+
return this.__eventConfig.isPeriodic;
|
|
990
|
+
}
|
|
923
991
|
}
|
|
924
992
|
|
|
925
993
|
module.exports = EventQueueProcessorBase;
|
package/src/config.js
CHANGED
|
@@ -4,6 +4,7 @@ const cds = require("@sap/cds");
|
|
|
4
4
|
|
|
5
5
|
const { getEnvInstance } = require("./shared/env");
|
|
6
6
|
const redis = require("./shared/redis");
|
|
7
|
+
const EventQueueError = require("./EventQueueError");
|
|
7
8
|
|
|
8
9
|
let instance;
|
|
9
10
|
|
|
@@ -11,6 +12,7 @@ const FOR_UPDATE_TIMEOUT = 10;
|
|
|
11
12
|
const GLOBAL_TX_TIMEOUT = 30 * 60 * 1000;
|
|
12
13
|
const REDIS_CONFIG_CHANNEL = "EVENT_QUEUE_CONFIG_CHANNEL";
|
|
13
14
|
const COMPONENT_NAME = "eventQueue/config";
|
|
15
|
+
const MIN_INTERVAL_SEC = 10;
|
|
14
16
|
|
|
15
17
|
class Config {
|
|
16
18
|
#logger;
|
|
@@ -30,6 +32,7 @@ class Config {
|
|
|
30
32
|
#disableRedis;
|
|
31
33
|
#env;
|
|
32
34
|
#eventMap;
|
|
35
|
+
#updatePeriodicEvents;
|
|
33
36
|
constructor() {
|
|
34
37
|
this.#logger = cds.log(COMPONENT_NAME);
|
|
35
38
|
this.#config = null;
|
|
@@ -50,11 +53,11 @@ class Config {
|
|
|
50
53
|
}
|
|
51
54
|
|
|
52
55
|
getEventConfig(type, subType) {
|
|
53
|
-
return this.#eventMap[
|
|
56
|
+
return this.#eventMap[this.generateKey(type, subType)];
|
|
54
57
|
}
|
|
55
58
|
|
|
56
59
|
hasEventAfterCommitFlag(type, subType) {
|
|
57
|
-
return this.#eventMap[
|
|
60
|
+
return this.#eventMap[this.generateKey(type, subType)]?.processAfterCommit ?? true;
|
|
58
61
|
}
|
|
59
62
|
|
|
60
63
|
_checkRedisIsBound() {
|
|
@@ -101,10 +104,47 @@ class Config {
|
|
|
101
104
|
|
|
102
105
|
set fileContent(config) {
|
|
103
106
|
this.#config = config;
|
|
107
|
+
config.events = config.events ?? [];
|
|
108
|
+
config.periodicEvents = config.periodicEvents ?? [];
|
|
104
109
|
this.#eventMap = config.events.reduce((result, event) => {
|
|
110
|
+
this.validateAdHocEvents(result, event);
|
|
105
111
|
result[[event.type, event.subType].join("##")] = event;
|
|
106
112
|
return result;
|
|
107
113
|
}, {});
|
|
114
|
+
this.#eventMap = config.periodicEvents.reduce((result, event) => {
|
|
115
|
+
this.validatePeriodicConfig(result, event);
|
|
116
|
+
event.isPeriodic = true;
|
|
117
|
+
result[[event.type, event.subType].join("##")] = event;
|
|
118
|
+
return result;
|
|
119
|
+
}, this.#eventMap);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
validatePeriodicConfig(eventMap, config) {
|
|
123
|
+
if (eventMap[this.generateKey(config.type, config.subType)]) {
|
|
124
|
+
throw EventQueueError.duplicateEventRegistration(config.type, config.subType);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (!config.interval || config.interval <= MIN_INTERVAL_SEC) {
|
|
128
|
+
throw EventQueueError.invalidInterval(config.type, config.subType, config.interval);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (!config.impl) {
|
|
132
|
+
throw EventQueueError.missingImpl(config.type, config.subType);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
validateAdHocEvents(eventMap, config) {
|
|
137
|
+
if (eventMap[this.generateKey(config.type, config.subType)]) {
|
|
138
|
+
throw EventQueueError.duplicateEventRegistration(config.type, config.subType);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (!config.impl) {
|
|
142
|
+
throw EventQueueError.missingImpl(config.type, config.subType);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
generateKey(type, subType) {
|
|
147
|
+
return [type, subType].join("##");
|
|
108
148
|
}
|
|
109
149
|
|
|
110
150
|
get fileContent() {
|
|
@@ -115,6 +155,18 @@ class Config {
|
|
|
115
155
|
return this.#config.events;
|
|
116
156
|
}
|
|
117
157
|
|
|
158
|
+
get periodicEvents() {
|
|
159
|
+
return this.#config.periodicEvents;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
isPeriodicEvent(type, subType) {
|
|
163
|
+
return this.#eventMap[this.generateKey(type, subType)]?.isPeriodic;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
get allEvents() {
|
|
167
|
+
return this.#config.events.concat(this.#config.periodicEvents);
|
|
168
|
+
}
|
|
169
|
+
|
|
118
170
|
get forUpdateTimeout() {
|
|
119
171
|
return this.#forUpdateTimeout;
|
|
120
172
|
}
|
|
@@ -211,6 +263,14 @@ class Config {
|
|
|
211
263
|
return this.#disableRedis;
|
|
212
264
|
}
|
|
213
265
|
|
|
266
|
+
set updatePeriodicEvents(value) {
|
|
267
|
+
this.#updatePeriodicEvents = value;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
get updatePeriodicEvents() {
|
|
271
|
+
return this.#updatePeriodicEvents;
|
|
272
|
+
}
|
|
273
|
+
|
|
214
274
|
get isMultiTenancy() {
|
|
215
275
|
return !!cds.requires.multitenancy;
|
|
216
276
|
}
|
package/src/dbHandler.js
CHANGED
|
@@ -11,6 +11,9 @@ const registerEventQueueDbHandler = (dbService) => {
|
|
|
11
11
|
const configInstance = config.getConfigInstance();
|
|
12
12
|
const def = dbService.model.definitions[configInstance.tableNameEventQueue];
|
|
13
13
|
dbService.after("CREATE", def, (_, req) => {
|
|
14
|
+
if (req.tx._skipEventQueueBroadcase) {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
14
17
|
req.tx._ = req.tx._ ?? {};
|
|
15
18
|
req.tx._.eventQueuePublishEvents = req.tx._.eventQueuePublishEvents ?? {};
|
|
16
19
|
const eventQueuePublishEvents = req.tx._.eventQueuePublishEvents;
|
package/src/initialize.js
CHANGED
|
@@ -12,7 +12,8 @@ const EventQueueError = require("./EventQueueError");
|
|
|
12
12
|
const runner = require("./runner");
|
|
13
13
|
const dbHandler = require("./dbHandler");
|
|
14
14
|
const { getConfigInstance } = require("./config");
|
|
15
|
-
const { initEventQueueRedisSubscribe } = require("./redisPubSub");
|
|
15
|
+
const { initEventQueueRedisSubscribe, closeSubscribeClient } = require("./redisPubSub");
|
|
16
|
+
const { closeMainClient } = require("./shared/redis");
|
|
16
17
|
|
|
17
18
|
const readFileAsync = promisify(fs.readFile);
|
|
18
19
|
|
|
@@ -33,6 +34,7 @@ const CONFIG_VARS = [
|
|
|
33
34
|
["tableNameEventLock", BASE_TABLES.LOCK],
|
|
34
35
|
["disableRedis", false],
|
|
35
36
|
["skipCsnCheck", false],
|
|
37
|
+
["updatePeriodicEvents", true],
|
|
36
38
|
];
|
|
37
39
|
|
|
38
40
|
const initialize = async ({
|
|
@@ -46,6 +48,7 @@ const initialize = async ({
|
|
|
46
48
|
tableNameEventLock,
|
|
47
49
|
disableRedis,
|
|
48
50
|
skipCsnCheck,
|
|
51
|
+
updatePeriodicEvents,
|
|
49
52
|
} = {}) => {
|
|
50
53
|
// TODO: initialize check:
|
|
51
54
|
// - content of yaml check
|
|
@@ -67,7 +70,8 @@ const initialize = async ({
|
|
|
67
70
|
tableNameEventQueue,
|
|
68
71
|
tableNameEventLock,
|
|
69
72
|
disableRedis,
|
|
70
|
-
skipCsnCheck
|
|
73
|
+
skipCsnCheck,
|
|
74
|
+
updatePeriodicEvents
|
|
71
75
|
);
|
|
72
76
|
|
|
73
77
|
const logger = cds.log(COMPONENT);
|
|
@@ -82,6 +86,7 @@ const initialize = async ({
|
|
|
82
86
|
}
|
|
83
87
|
|
|
84
88
|
registerEventProcessors();
|
|
89
|
+
registerCdsShutdown();
|
|
85
90
|
logger.info("event queue initialized", {
|
|
86
91
|
registerAsEventProcessor: configInstance.registerAsEventProcessor,
|
|
87
92
|
multiTenancyEnabled: configInstance.isMultiTenancy,
|
|
@@ -115,17 +120,19 @@ const registerEventProcessors = () => {
|
|
|
115
120
|
return;
|
|
116
121
|
}
|
|
117
122
|
|
|
123
|
+
const errorHandler = (err) => cds.log(COMPONENT).error("error during init runner", err);
|
|
124
|
+
|
|
118
125
|
if (!configInstance.isMultiTenancy) {
|
|
119
|
-
runner.singleTenant();
|
|
126
|
+
runner.singleTenant().catch(errorHandler);
|
|
120
127
|
return;
|
|
121
128
|
}
|
|
122
129
|
|
|
123
130
|
if (configInstance.redisEnabled) {
|
|
124
131
|
initEventQueueRedisSubscribe();
|
|
125
132
|
configInstance.attachConfigChangeHandler();
|
|
126
|
-
runner.multiTenancyRedis();
|
|
133
|
+
runner.multiTenancyRedis().catch(errorHandler);
|
|
127
134
|
} else {
|
|
128
|
-
runner.multiTenancyDb();
|
|
135
|
+
runner.multiTenancyDb().catch(errorHandler);
|
|
129
136
|
}
|
|
130
137
|
};
|
|
131
138
|
|
|
@@ -181,6 +188,12 @@ const mixConfigVarsWithEnv = (...args) => {
|
|
|
181
188
|
});
|
|
182
189
|
};
|
|
183
190
|
|
|
191
|
+
const registerCdsShutdown = () => {
|
|
192
|
+
cds.on("shutdown", async () => {
|
|
193
|
+
await Promise.allSettled([closeMainClient(), closeSubscribeClient()]);
|
|
194
|
+
});
|
|
195
|
+
};
|
|
196
|
+
|
|
184
197
|
module.exports = {
|
|
185
198
|
initialize,
|
|
186
199
|
};
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const cds = require("@sap/cds");
|
|
4
|
+
|
|
5
|
+
const { EventProcessingStatus } = require("./constants");
|
|
6
|
+
const { processChunkedSync } = require("./shared/common");
|
|
7
|
+
const { getConfigInstance } = require("./config");
|
|
8
|
+
|
|
9
|
+
const COMPONENT_NAME = "eventQueue/periodicEvents";
|
|
10
|
+
|
|
11
|
+
const checkAndInsertPeriodicEvents = async (context) => {
|
|
12
|
+
const tx = cds.tx(context);
|
|
13
|
+
const configInstance = getConfigInstance();
|
|
14
|
+
const baseCqn = SELECT.from(configInstance.tableNameEventQueue)
|
|
15
|
+
.where([
|
|
16
|
+
{ list: [{ ref: ["type"] }, { ref: ["subType"] }] },
|
|
17
|
+
"IN",
|
|
18
|
+
{
|
|
19
|
+
list: configInstance.periodicEvents.map((periodicEvent) => ({
|
|
20
|
+
list: [{ val: periodicEvent.type }, { val: periodicEvent.subType }],
|
|
21
|
+
})),
|
|
22
|
+
},
|
|
23
|
+
"AND",
|
|
24
|
+
{ ref: ["status"] },
|
|
25
|
+
"=",
|
|
26
|
+
{ val: EventProcessingStatus.Open },
|
|
27
|
+
])
|
|
28
|
+
.columns(["ID", "type", "subType", "startAfter"]);
|
|
29
|
+
const currentPeriodEvents = await tx.run(baseCqn);
|
|
30
|
+
|
|
31
|
+
if (!currentPeriodEvents.length) {
|
|
32
|
+
// fresh insert all
|
|
33
|
+
return await insertPeriodEvents(tx, configInstance.periodicEvents);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const exitingEventMap = currentPeriodEvents.reduce((result, current) => {
|
|
37
|
+
const key = _generateKey(current);
|
|
38
|
+
result[key] = current;
|
|
39
|
+
return result;
|
|
40
|
+
}, {});
|
|
41
|
+
|
|
42
|
+
const { newEvents, existingEvents } = configInstance.periodicEvents.reduce(
|
|
43
|
+
(result, event) => {
|
|
44
|
+
if (exitingEventMap[_generateKey(event)]) {
|
|
45
|
+
result.existingEvents.push(exitingEventMap[_generateKey(event)]);
|
|
46
|
+
} else {
|
|
47
|
+
result.newEvents.push(event);
|
|
48
|
+
}
|
|
49
|
+
return result;
|
|
50
|
+
},
|
|
51
|
+
{ newEvents: [], existingEvents: [] }
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
const currentDate = new Date();
|
|
55
|
+
const exitingWithNotMatchingInterval = existingEvents.filter((existingEvent) => {
|
|
56
|
+
const config = configInstance.getEventConfig(existingEvent.type, existingEvent.subType);
|
|
57
|
+
const eventStartAfter = new Date(existingEvent.startAfter);
|
|
58
|
+
// check if to far in future
|
|
59
|
+
const dueInWithNewInterval = new Date(currentDate.getTime() + config.interval * 1000);
|
|
60
|
+
return eventStartAfter >= dueInWithNewInterval;
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
exitingWithNotMatchingInterval.length &&
|
|
64
|
+
cds.log(COMPONENT_NAME).info("deleting periodic events because they have changed", {
|
|
65
|
+
changedEvents: exitingWithNotMatchingInterval.map(({ type, subType }) => ({ type, subType })),
|
|
66
|
+
});
|
|
67
|
+
await tx.run(
|
|
68
|
+
DELETE.from(configInstance.tableNameEventQueue).where(
|
|
69
|
+
"ID IN",
|
|
70
|
+
exitingWithNotMatchingInterval.map(({ ID }) => ID)
|
|
71
|
+
)
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
const newOrChangedEvents = newEvents.concat(exitingWithNotMatchingInterval);
|
|
75
|
+
|
|
76
|
+
if (!newOrChangedEvents.length) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return await insertPeriodEvents(tx, newOrChangedEvents);
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const insertPeriodEvents = async (tx, events) => {
|
|
84
|
+
const startAfter = new Date();
|
|
85
|
+
const configInstance = getConfigInstance();
|
|
86
|
+
processChunkedSync(events, 4, (chunk) => {
|
|
87
|
+
cds.log(COMPONENT_NAME).info("inserting changed or new periodic events", {
|
|
88
|
+
events: chunk.map(({ type, subType }) => {
|
|
89
|
+
const { interval } = configInstance.getEventConfig(type, subType);
|
|
90
|
+
return { type, subType, interval };
|
|
91
|
+
}),
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
const periodEventsInsert = events.map((periodicEvent) => ({
|
|
95
|
+
type: periodicEvent.type,
|
|
96
|
+
subType: periodicEvent.subType,
|
|
97
|
+
startAfter: startAfter,
|
|
98
|
+
}));
|
|
99
|
+
|
|
100
|
+
tx._skipEventQueueBroadcase = true;
|
|
101
|
+
await tx.run(INSERT.into(configInstance.tableNameEventQueue).entries(periodEventsInsert));
|
|
102
|
+
tx._skipEventQueueBroadcase = false;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const _generateKey = ({ type, subType }) => [type, subType].join("##");
|
|
106
|
+
|
|
107
|
+
module.exports = {
|
|
108
|
+
checkAndInsertPeriodicEvents,
|
|
109
|
+
};
|
package/src/processEventQueue.js
CHANGED
|
@@ -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,66 @@ 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
|
+
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];
|
|
154
|
+
}
|
|
155
|
+
await eventTypeInstance.scheduleNextPeriodEvent(queueEntry);
|
|
156
|
+
}
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
if (!queueEntry) {
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
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();
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
);
|
|
183
|
+
|
|
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
|
+
);
|
|
192
|
+
} finally {
|
|
193
|
+
await eventTypeInstance?.handleReleaseLock();
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
|
|
134
197
|
const processEventMap = async (eventTypeInstance) => {
|
|
135
198
|
eventTypeInstance.startPerformanceTracerEvents();
|
|
136
199
|
await eventTypeInstance.beforeProcessingEvents();
|
package/src/publishEvent.js
CHANGED
|
@@ -21,12 +21,13 @@ 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
|
+
const publishEvent = async (tx, events, skipBroadcast = false) => {
|
|
30
31
|
const configInstance = config.getConfigInstance();
|
|
31
32
|
if (!configInstance.initialized) {
|
|
32
33
|
throw EventQueueError.notInitialized();
|
|
@@ -40,8 +41,15 @@ const publishEvent = async (tx, events) => {
|
|
|
40
41
|
if (startAfter && !common.isValidDate(startAfter)) {
|
|
41
42
|
throw EventQueueError.malformedDate(startAfter);
|
|
42
43
|
}
|
|
44
|
+
|
|
45
|
+
if (eventConfig.isPeriodic) {
|
|
46
|
+
throw EventQueueError.manuelPeriodicEventInsert(type, subType);
|
|
47
|
+
}
|
|
43
48
|
}
|
|
44
|
-
|
|
49
|
+
tx._skipEventQueueBroadcase = skipBroadcast;
|
|
50
|
+
const result = await tx.run(INSERT.into(configInstance.tableNameEventQueue).entries(eventsForProcessing));
|
|
51
|
+
tx._skipEventQueueBroadcase = false;
|
|
52
|
+
return result;
|
|
45
53
|
};
|
|
46
54
|
|
|
47
55
|
module.exports = {
|
package/src/redisPubSub.js
CHANGED
|
@@ -67,7 +67,19 @@ const broadcastEvent = async (tenantId, type, subType) => {
|
|
|
67
67
|
}
|
|
68
68
|
};
|
|
69
69
|
|
|
70
|
+
const closeSubscribeClient = async () => {
|
|
71
|
+
try {
|
|
72
|
+
const client = await subscriberClientPromise;
|
|
73
|
+
if (client?.quit) {
|
|
74
|
+
await client.quit();
|
|
75
|
+
}
|
|
76
|
+
} catch (err) {
|
|
77
|
+
// ignore errors during shutdown
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
70
81
|
module.exports = {
|
|
71
82
|
initEventQueueRedisSubscribe,
|
|
72
83
|
broadcastEvent,
|
|
84
|
+
closeSubscribeClient,
|
|
73
85
|
};
|
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;
|
|
19
22
|
|
|
20
|
-
const
|
|
23
|
+
const singleTenant = () => _scheduleFunction(_checkPeriodicEventsSingleTenant, _executeRunForTenant);
|
|
21
24
|
|
|
22
|
-
const
|
|
25
|
+
const multiTenancyDb = () => _scheduleFunction(_multiTenancyPeriodicEvents, _multiTenancyDb);
|
|
23
26
|
|
|
24
|
-
const
|
|
27
|
+
const multiTenancyRedis = () => _scheduleFunction(_multiTenancyPeriodicEvents, _multiTenancyRedis);
|
|
28
|
+
|
|
29
|
+
const _scheduleFunction = async (singleRunFn, periodicFn) => {
|
|
25
30
|
const logger = cds.log(COMPONENT_NAME);
|
|
26
31
|
const configInstance = eventQueueConfig.getConfigInstance();
|
|
27
|
-
const eventsForAutomaticRun = configInstance.
|
|
32
|
+
const eventsForAutomaticRun = configInstance.allEvents;
|
|
28
33
|
if (!eventsForAutomaticRun.length) {
|
|
29
34
|
logger.warn("no events for automatic run are configured - skipping runner registration");
|
|
30
35
|
return;
|
|
@@ -36,7 +41,7 @@ const _scheduleFunction = async (fn) => {
|
|
|
36
41
|
logger.info("runner is deactivated via config variable. Skipping this run.");
|
|
37
42
|
return;
|
|
38
43
|
}
|
|
39
|
-
return
|
|
44
|
+
return periodicFn();
|
|
40
45
|
};
|
|
41
46
|
|
|
42
47
|
const offsetDependingOnLastRun = await _calculateOffsetForFirstRun();
|
|
@@ -46,6 +51,7 @@ const _scheduleFunction = async (fn) => {
|
|
|
46
51
|
});
|
|
47
52
|
|
|
48
53
|
setTimeout(() => {
|
|
54
|
+
singleRunFn();
|
|
49
55
|
fnWithRunningCheck();
|
|
50
56
|
const intervalRunner = new SetIntervalDriftSafe(configInstance.runInterval);
|
|
51
57
|
intervalRunner.run(fnWithRunningCheck);
|
|
@@ -57,6 +63,8 @@ const _multiTenancyRedis = async () => {
|
|
|
57
63
|
const emptyContext = new cds.EventContext({});
|
|
58
64
|
logger.info("executing event queue run for multi instance and tenant");
|
|
59
65
|
const tenantIds = await cdsHelper.getAllTenantIds();
|
|
66
|
+
_checkAndTriggerPriodicEventUpdate(tenantIds);
|
|
67
|
+
|
|
60
68
|
const runId = await _acquireRunId(emptyContext);
|
|
61
69
|
|
|
62
70
|
if (!runId) {
|
|
@@ -67,20 +75,21 @@ const _multiTenancyRedis = async () => {
|
|
|
67
75
|
_executeAllTenants(tenantIds, runId);
|
|
68
76
|
};
|
|
69
77
|
|
|
70
|
-
const
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
78
|
+
const _checkAndTriggerPriodicEventUpdate = (tenantIds) => {
|
|
79
|
+
const hash = hashStringTo32Bit(JSON.stringify(tenantIds));
|
|
80
|
+
if (!tenantIdHash) {
|
|
81
|
+
tenantIdHash = hash;
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (tenantIdHash && tenantIdHash !== hash) {
|
|
85
|
+
cds.log(COMPONENT_NAME).info("tenant id hash changed, triggering updating periodic events!");
|
|
86
|
+
_multiTenancyPeriodicEvents().catch((err) => {
|
|
87
|
+
cds.log(COMPONENT_NAME).error("Error during triggering updating periodic events! Error:", err);
|
|
88
|
+
});
|
|
80
89
|
}
|
|
81
90
|
};
|
|
82
91
|
|
|
83
|
-
const
|
|
92
|
+
const _executeAllTenantsGeneric = (tenantIds, runId, fn) => {
|
|
84
93
|
const configInstance = eventQueueConfig.getConfigInstance();
|
|
85
94
|
const workerQueueInstance = getWorkerPoolInstance();
|
|
86
95
|
tenantIds.forEach((tenantId) => {
|
|
@@ -93,7 +102,7 @@ const _executeAllTenants = (tenantIds, runId) => {
|
|
|
93
102
|
if (!couldAcquireLock) {
|
|
94
103
|
return;
|
|
95
104
|
}
|
|
96
|
-
await
|
|
105
|
+
await fn(tenantId, runId);
|
|
97
106
|
} catch (err) {
|
|
98
107
|
cds.log(COMPONENT_NAME).error("executing event-queue run for tenant failed", {
|
|
99
108
|
tenantId,
|
|
@@ -103,11 +112,16 @@ const _executeAllTenants = (tenantIds, runId) => {
|
|
|
103
112
|
});
|
|
104
113
|
};
|
|
105
114
|
|
|
115
|
+
const _executeAllTenants = (tenantIds, runId) => _executeAllTenantsGeneric(tenantIds, runId, _executeRunForTenant);
|
|
116
|
+
|
|
117
|
+
const _executePeriodicEventsAllTenants = (tenantIds, runId) =>
|
|
118
|
+
_executeAllTenantsGeneric(tenantIds, runId, _checkPeriodicEventsSingleTenant);
|
|
119
|
+
|
|
106
120
|
const _executeRunForTenant = async (tenantId, runId) => {
|
|
107
121
|
const logger = cds.log(COMPONENT_NAME);
|
|
108
122
|
const configInstance = eventQueueConfig.getConfigInstance();
|
|
109
123
|
try {
|
|
110
|
-
const eventsForAutomaticRun = configInstance.
|
|
124
|
+
const eventsForAutomaticRun = configInstance.allEvents;
|
|
111
125
|
const subdomain = await cdsHelper.getSubdomainForTenantId(tenantId);
|
|
112
126
|
const context = new cds.EventContext({
|
|
113
127
|
tenant: tenantId,
|
|
@@ -211,6 +225,60 @@ const runEventCombinationForTenant = async (tenantId, type, subType) => {
|
|
|
211
225
|
}
|
|
212
226
|
};
|
|
213
227
|
|
|
228
|
+
const _multiTenancyDb = async () => {
|
|
229
|
+
const logger = cds.log(COMPONENT_NAME);
|
|
230
|
+
try {
|
|
231
|
+
logger.info("executing event queue run for single instance and multi tenant");
|
|
232
|
+
const tenantIds = await cdsHelper.getAllTenantIds();
|
|
233
|
+
_checkAndTriggerPriodicEventUpdate(tenantIds);
|
|
234
|
+
_executeAllTenants(tenantIds, EVENT_QUEUE_RUN_ID);
|
|
235
|
+
} catch (err) {
|
|
236
|
+
logger.error(
|
|
237
|
+
`Couldn't fetch tenant ids for event queue processing! Next try after defined interval. Error: ${err}`
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
const _multiTenancyPeriodicEvents = async () => {
|
|
243
|
+
const logger = cds.log(COMPONENT_NAME);
|
|
244
|
+
try {
|
|
245
|
+
logger.info("executing event queue update periodic events");
|
|
246
|
+
const tenantIds = await cdsHelper.getAllTenantIds();
|
|
247
|
+
_executePeriodicEventsAllTenants(tenantIds, EVENT_QUEUE_RUN_PERIODIC_EVENT);
|
|
248
|
+
} catch (err) {
|
|
249
|
+
logger.error(`Couldn't fetch tenant ids for updating periodic event processing! Error: ${err}`);
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
const _checkPeriodicEventsSingleTenant = async (tenantId) => {
|
|
254
|
+
const logger = cds.log(COMPONENT_NAME);
|
|
255
|
+
const configInstance = eventQueueConfig.getConfigInstance();
|
|
256
|
+
if (!configInstance.updatePeriodicEvents) {
|
|
257
|
+
logger.info("updating of periodic events is disabled");
|
|
258
|
+
}
|
|
259
|
+
try {
|
|
260
|
+
const subdomain = await cdsHelper.getSubdomainForTenantId(tenantId);
|
|
261
|
+
const context = new cds.EventContext({
|
|
262
|
+
tenant: tenantId,
|
|
263
|
+
// NOTE: we need this because of logging otherwise logs would not contain the subdomain
|
|
264
|
+
http: { req: { authInfo: { getSubdomain: () => subdomain } } },
|
|
265
|
+
});
|
|
266
|
+
cds.context = context;
|
|
267
|
+
logger.info("executing updating periotic events", {
|
|
268
|
+
tenantId,
|
|
269
|
+
subdomain,
|
|
270
|
+
});
|
|
271
|
+
await cdsHelper.executeInNewTransaction(context, "update-periodic-events", async (tx) => {
|
|
272
|
+
await periodicEvents.checkAndInsertPeriodicEvents(tx.context);
|
|
273
|
+
});
|
|
274
|
+
} catch (err) {
|
|
275
|
+
logger.error(`Couldn't process eventQueue for tenant! Next try after defined interval. Error: ${err}`, {
|
|
276
|
+
tenantId,
|
|
277
|
+
redisEnabled: configInstance.redisEnabled,
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
|
|
214
282
|
module.exports = {
|
|
215
283
|
singleTenant,
|
|
216
284
|
multiTenancyDb,
|
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 };
|
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
const cds = require("@sap/cds");
|
|
4
4
|
|
|
5
5
|
const { broadcastEvent } = require("../redisPubSub");
|
|
6
|
+
const config = require("./../config");
|
|
6
7
|
|
|
7
|
-
const COMPONENT_NAME = "eventQueue/shared/
|
|
8
|
+
const COMPONENT_NAME = "eventQueue/shared/eventScheduler";
|
|
8
9
|
|
|
9
10
|
let instance;
|
|
10
11
|
class EventScheduler {
|
|
@@ -12,9 +13,10 @@ class EventScheduler {
|
|
|
12
13
|
constructor() {}
|
|
13
14
|
|
|
14
15
|
scheduleEvent(tenantId, type, subType, startAfter) {
|
|
15
|
-
const
|
|
16
|
-
const
|
|
17
|
-
const
|
|
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);
|
|
18
20
|
const key = [tenantId, type, subType, roundUpDate.toISOString()].join("##");
|
|
19
21
|
if (this.#scheduledEvents[key]) {
|
|
20
22
|
return; // event combination already scheduled
|
|
@@ -38,6 +40,12 @@ class EventScheduler {
|
|
|
38
40
|
}, roundUpDate.getTime() - Date.now()).unref();
|
|
39
41
|
}
|
|
40
42
|
|
|
43
|
+
calculateFutureTime(date, seoncds) {
|
|
44
|
+
const startAfterSeconds = date.getSeconds();
|
|
45
|
+
const secondsUntil = seoncds - (startAfterSeconds % seoncds);
|
|
46
|
+
return new Date(date.getTime() + secondsUntil * 1000);
|
|
47
|
+
}
|
|
48
|
+
|
|
41
49
|
clearScheduledEvents() {
|
|
42
50
|
this.#scheduledEvents = {};
|
|
43
51
|
}
|
package/src/shared/redis.js
CHANGED
|
@@ -29,9 +29,18 @@ const _createClientBase = () => {
|
|
|
29
29
|
if (env.isOnCF) {
|
|
30
30
|
try {
|
|
31
31
|
const credentials = env.getRedisCredentialsFromEnv();
|
|
32
|
-
|
|
33
|
-
// https://github.com/go-redis/redis/issues/1343
|
|
32
|
+
const redisIsCluster = credentials.cluster_mode;
|
|
34
33
|
const url = credentials.uri.replace(/(?<=rediss:\/\/)[\w-]+?(?=:)/, "");
|
|
34
|
+
if (redisIsCluster) {
|
|
35
|
+
return redis.createCluster({
|
|
36
|
+
rootNodes: [{ url }],
|
|
37
|
+
// https://github.com/redis/node-redis/issues/1782
|
|
38
|
+
defaults: {
|
|
39
|
+
password: credentials.password,
|
|
40
|
+
socket: { tls: credentials.tls },
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
}
|
|
35
44
|
return redis.createClient({ url });
|
|
36
45
|
} catch (err) {
|
|
37
46
|
throw EventQueueError.redisConnectionFailure(err);
|
|
@@ -87,9 +96,21 @@ const publishMessage = async (channel, message) => {
|
|
|
87
96
|
|
|
88
97
|
const _localReconnectStrategy = () => EventQueueError.redisNoReconnect();
|
|
89
98
|
|
|
99
|
+
const closeMainClient = async () => {
|
|
100
|
+
try {
|
|
101
|
+
const client = await mainClientPromise;
|
|
102
|
+
if (client?.quit) {
|
|
103
|
+
await client.quit();
|
|
104
|
+
}
|
|
105
|
+
} catch (err) {
|
|
106
|
+
// ignore errors during shutdown
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
|
|
90
110
|
module.exports = {
|
|
91
111
|
createClientAndConnect,
|
|
92
112
|
createMainClientAndConnect,
|
|
93
113
|
subscribeRedisChannel,
|
|
94
114
|
publishMessage,
|
|
115
|
+
closeMainClient,
|
|
95
116
|
};
|