@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
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.3",
|
|
4
4
|
"description": "An event queue that enables secure transactional processing of asynchronous events, featuring instant event processing with Redis Pub/Sub and load distribution across all application instances.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"files": [
|
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,8 +7,8 @@ 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/
|
|
11
|
-
const
|
|
10
|
+
const eventScheduler = require("./shared/eventScheduler");
|
|
11
|
+
const eventConfig = require("./config");
|
|
12
12
|
const PerformanceTracer = require("./shared/PerformanceTracer");
|
|
13
13
|
|
|
14
14
|
const IMPLEMENT_ERROR_MESSAGE = "needs to be reimplemented";
|
|
@@ -31,37 +31,43 @@ class EventQueueProcessorBase {
|
|
|
31
31
|
#eventType = null;
|
|
32
32
|
#eventSubType = null;
|
|
33
33
|
#config = null;
|
|
34
|
+
#eventSchedulerInstance = null;
|
|
35
|
+
#eventConfig;
|
|
36
|
+
#isPeriodic;
|
|
34
37
|
|
|
35
38
|
constructor(context, eventType, eventSubType, config) {
|
|
36
39
|
this.__context = context;
|
|
37
40
|
this.__baseContext = context;
|
|
38
41
|
this.__tx = cds.tx(context);
|
|
39
42
|
this.__baseLogger = cds.log(COMPONENT_NAME);
|
|
43
|
+
this.#eventSchedulerInstance = eventScheduler.getInstance();
|
|
44
|
+
this.#config = eventConfig;
|
|
45
|
+
this.#isPeriodic = this.#config.isPeriodicEvent(eventType, eventSubType);
|
|
40
46
|
this.__logger = null;
|
|
41
47
|
this.__eventProcessingMap = {};
|
|
42
48
|
this.__statusMap = {};
|
|
43
49
|
this.__commitedStatusMap = {};
|
|
44
50
|
this.#eventType = eventType;
|
|
45
51
|
this.#eventSubType = eventSubType;
|
|
46
|
-
this
|
|
47
|
-
this.__parallelEventProcessing = this.
|
|
52
|
+
this.#eventConfig = config ?? {};
|
|
53
|
+
this.__parallelEventProcessing = this.#eventConfig.parallelEventProcessing ?? DEFAULT_PARALLEL_EVENT_PROCESSING;
|
|
48
54
|
if (this.__parallelEventProcessing > LIMIT_PARALLEL_EVENT_PROCESSING) {
|
|
49
55
|
this.__parallelEventProcessing = LIMIT_PARALLEL_EVENT_PROCESSING;
|
|
50
56
|
}
|
|
51
57
|
// NOTE: keep the feature, this might be needed again
|
|
52
58
|
this.__concurrentEventProcessing = false;
|
|
53
|
-
this.__startTime = this.
|
|
54
|
-
this.__retryAttempts = this.
|
|
55
|
-
this.__selectMaxChunkSize = this.
|
|
56
|
-
this.__selectNextChunk = !!this.
|
|
59
|
+
this.__startTime = this.#eventConfig.startTime ?? new Date();
|
|
60
|
+
this.__retryAttempts = this.#eventConfig.retryAttempts ?? DEFAULT_RETRY_ATTEMPTS;
|
|
61
|
+
this.__selectMaxChunkSize = this.#eventConfig.selectMaxChunkSize ?? SELECT_LIMIT_EVENTS_PER_TICK;
|
|
62
|
+
this.__selectNextChunk = !!this.#eventConfig.checkForNextChunk;
|
|
57
63
|
this.__keepalivePromises = {};
|
|
58
|
-
this.__outdatedCheckEnabled = this.
|
|
59
|
-
this.__transactionMode = this.
|
|
60
|
-
if (this.
|
|
64
|
+
this.__outdatedCheckEnabled = this.#eventConfig.eventOutdatedCheck ?? true;
|
|
65
|
+
this.__transactionMode = this.#eventConfig.transactionMode ?? TransactionMode.isolated;
|
|
66
|
+
if (this.#eventConfig.deleteFinishedEventsAfterDays) {
|
|
61
67
|
this.__deleteFinishedEventsAfter =
|
|
62
|
-
Number.isInteger(this.
|
|
63
|
-
this.
|
|
64
|
-
? this.
|
|
68
|
+
Number.isInteger(this.#eventConfig.deleteFinishedEventsAfterDays) &&
|
|
69
|
+
this.#eventConfig.deleteFinishedEventsAfterDays > 0
|
|
70
|
+
? this.#eventConfig.deleteFinishedEventsAfterDays
|
|
65
71
|
: DEFAULT_DELETE_FINISHED_EVENTS_AFTER;
|
|
66
72
|
} else {
|
|
67
73
|
this.__deleteFinishedEventsAfter = DEFAULT_DELETE_FINISHED_EVENTS_AFTER;
|
|
@@ -71,7 +77,6 @@ class EventQueueProcessorBase {
|
|
|
71
77
|
this.__txUsageAllowed = true;
|
|
72
78
|
this.__txMap = {};
|
|
73
79
|
this.__txRollback = {};
|
|
74
|
-
this.#config = eventQueueConfig.getConfigInstance();
|
|
75
80
|
this.__queueEntries = [];
|
|
76
81
|
}
|
|
77
82
|
|
|
@@ -303,6 +308,29 @@ class EventQueueProcessorBase {
|
|
|
303
308
|
return Object.fromEntries(queueEntries.map((queueEntry) => [queueEntry.ID, EventProcessingStatus.Error]));
|
|
304
309
|
}
|
|
305
310
|
|
|
311
|
+
handleErrorDuringPeriodicEventProcessing(error, queueEntry) {
|
|
312
|
+
this.logger.error(
|
|
313
|
+
`Caught error during event periodic processing. Please catch your promises/exceptions. Error: ${error}`,
|
|
314
|
+
{
|
|
315
|
+
eventType: this.#eventType,
|
|
316
|
+
eventSubType: this.#eventSubType,
|
|
317
|
+
queueEntryId: queueEntry.ID,
|
|
318
|
+
}
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
async setPeriodicEventStatus(queueEntryIds) {
|
|
323
|
+
await this.tx.run(
|
|
324
|
+
UPDATE.entity(this.#config.tableNameEventQueue)
|
|
325
|
+
.set({
|
|
326
|
+
status: EventProcessingStatus.Done,
|
|
327
|
+
})
|
|
328
|
+
.where({
|
|
329
|
+
ID: queueEntryIds,
|
|
330
|
+
})
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
|
|
306
334
|
/**
|
|
307
335
|
* This function validates for all selected events one status has been submitted. It's also validated that only for
|
|
308
336
|
* selected events a status has been submitted. Persisting the status of events is done in a dedicated database tx.
|
|
@@ -587,9 +615,8 @@ class EventQueueProcessorBase {
|
|
|
587
615
|
}
|
|
588
616
|
|
|
589
617
|
#handleDelayedEvents(delayedEvents) {
|
|
590
|
-
const eventSchedulerInstance = eventScheduler.getInstance();
|
|
591
618
|
for (const delayedEvent of delayedEvents) {
|
|
592
|
-
eventSchedulerInstance.scheduleEvent(
|
|
619
|
+
this.#eventSchedulerInstance.scheduleEvent(
|
|
593
620
|
this.__context.tenant,
|
|
594
621
|
this.#eventType,
|
|
595
622
|
this.#eventSubType,
|
|
@@ -812,6 +839,77 @@ class EventQueueProcessorBase {
|
|
|
812
839
|
}
|
|
813
840
|
}
|
|
814
841
|
|
|
842
|
+
async scheduleNextPeriodEvent(queueEntry) {
|
|
843
|
+
const interval = this.#eventConfig.interval;
|
|
844
|
+
const newEvent = {
|
|
845
|
+
type: this.#eventType,
|
|
846
|
+
subType: this.#eventSubType,
|
|
847
|
+
startAfter: new Date(new Date(queueEntry.startAfter).getTime() + interval * 1000),
|
|
848
|
+
};
|
|
849
|
+
const { relative } = this.#eventSchedulerInstance.calculateOffset(
|
|
850
|
+
this.#eventType,
|
|
851
|
+
this.#eventSubType,
|
|
852
|
+
newEvent.startAfter
|
|
853
|
+
);
|
|
854
|
+
|
|
855
|
+
// more than one interval behind - shift tick to keep up
|
|
856
|
+
if (relative < 0 && Math.abs(relative) >= this.#eventConfig.interval * 1000) {
|
|
857
|
+
newEvent.startAfter = new Date(Date.now() + 5 * 1000);
|
|
858
|
+
this.logger.info("interval adjusted because shifted more than one interval", {
|
|
859
|
+
eventType: this.#eventType,
|
|
860
|
+
eventSubType: this.#eventSubType,
|
|
861
|
+
newStartAfter: newEvent.startAfter,
|
|
862
|
+
});
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
this.tx._skipEventQueueBroadcase = true;
|
|
866
|
+
await this.tx.run(INSERT.into(this.#config.tableNameEventQueue).entries({ ...newEvent }));
|
|
867
|
+
this.tx._skipEventQueueBroadcase = false;
|
|
868
|
+
if (interval < this.#config.runInterval) {
|
|
869
|
+
this.#handleDelayedEvents([newEvent]);
|
|
870
|
+
const { relative: relativeAfterSchedule } = this.#eventSchedulerInstance.calculateOffset(
|
|
871
|
+
this.#eventType,
|
|
872
|
+
this.#eventSubType,
|
|
873
|
+
newEvent.startAfter
|
|
874
|
+
);
|
|
875
|
+
// next tick is already behind schedule --> execute direct
|
|
876
|
+
if (relativeAfterSchedule <= 0) {
|
|
877
|
+
this.logger.info("running behind schedule - executing next tick immediately", {
|
|
878
|
+
eventType: this.#eventType,
|
|
879
|
+
eventSubType: this.#eventSubType,
|
|
880
|
+
newStartAfter: newEvent.startAfter,
|
|
881
|
+
});
|
|
882
|
+
return true;
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
async handleDuplicatedPeriodicEventEntry(queueEntries) {
|
|
888
|
+
this.logger.error("More than one open events for the same configuration which is not allowed!", {
|
|
889
|
+
eventType: this.#eventType,
|
|
890
|
+
eventSubType: this.#eventSubType,
|
|
891
|
+
queueEntriesIds: queueEntries.map(({ ID }) => ID),
|
|
892
|
+
});
|
|
893
|
+
|
|
894
|
+
let queueEntryToUse;
|
|
895
|
+
const obsoleteEntries = [];
|
|
896
|
+
for (const queueEntry of queueEntries) {
|
|
897
|
+
if (!queueEntryToUse) {
|
|
898
|
+
queueEntryToUse = queueEntry;
|
|
899
|
+
continue;
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
if (queueEntryToUse.startAfter <= queueEntry.queueEntry) {
|
|
903
|
+
obsoleteEntries.push(queueEntryToUse);
|
|
904
|
+
queueEntryToUse = queueEntry;
|
|
905
|
+
} else {
|
|
906
|
+
obsoleteEntries.push(queueEntry);
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
await this.setPeriodicEventStatus(obsoleteEntries.map(({ ID }) => ID));
|
|
910
|
+
return queueEntryToUse;
|
|
911
|
+
}
|
|
912
|
+
|
|
815
913
|
statusMapContainsError(statusMap) {
|
|
816
914
|
return Object.values(statusMap).includes(EventProcessingStatus.Error);
|
|
817
915
|
}
|
|
@@ -920,6 +1018,10 @@ class EventQueueProcessorBase {
|
|
|
920
1018
|
setTxForEventProcessing(key, tx) {
|
|
921
1019
|
this.__txMap[key] = tx;
|
|
922
1020
|
}
|
|
1021
|
+
|
|
1022
|
+
get isPeriodicEvent() {
|
|
1023
|
+
return this.#eventConfig.isPeriodic;
|
|
1024
|
+
}
|
|
923
1025
|
}
|
|
924
1026
|
|
|
925
1027
|
module.exports = EventQueueProcessorBase;
|
package/src/config.js
CHANGED
|
@@ -4,13 +4,13 @@ const cds = require("@sap/cds");
|
|
|
4
4
|
|
|
5
5
|
const { getEnvInstance } = require("./shared/env");
|
|
6
6
|
const redis = require("./shared/redis");
|
|
7
|
-
|
|
8
|
-
let instance;
|
|
7
|
+
const EventQueueError = require("./EventQueueError");
|
|
9
8
|
|
|
10
9
|
const FOR_UPDATE_TIMEOUT = 10;
|
|
11
10
|
const GLOBAL_TX_TIMEOUT = 30 * 60 * 1000;
|
|
12
11
|
const REDIS_CONFIG_CHANNEL = "EVENT_QUEUE_CONFIG_CHANNEL";
|
|
13
12
|
const COMPONENT_NAME = "eventQueue/config";
|
|
13
|
+
const MIN_INTERVAL_SEC = 10;
|
|
14
14
|
|
|
15
15
|
class Config {
|
|
16
16
|
#logger;
|
|
@@ -30,6 +30,8 @@ class Config {
|
|
|
30
30
|
#disableRedis;
|
|
31
31
|
#env;
|
|
32
32
|
#eventMap;
|
|
33
|
+
#updatePeriodicEvents;
|
|
34
|
+
static #instance;
|
|
33
35
|
constructor() {
|
|
34
36
|
this.#logger = cds.log(COMPONENT_NAME);
|
|
35
37
|
this.#config = null;
|
|
@@ -50,11 +52,11 @@ class Config {
|
|
|
50
52
|
}
|
|
51
53
|
|
|
52
54
|
getEventConfig(type, subType) {
|
|
53
|
-
return this.#eventMap[
|
|
55
|
+
return this.#eventMap[this.generateKey(type, subType)];
|
|
54
56
|
}
|
|
55
57
|
|
|
56
58
|
hasEventAfterCommitFlag(type, subType) {
|
|
57
|
-
return this.#eventMap[
|
|
59
|
+
return this.#eventMap[this.generateKey(type, subType)]?.processAfterCommit ?? true;
|
|
58
60
|
}
|
|
59
61
|
|
|
60
62
|
_checkRedisIsBound() {
|
|
@@ -101,10 +103,51 @@ class Config {
|
|
|
101
103
|
|
|
102
104
|
set fileContent(config) {
|
|
103
105
|
this.#config = config;
|
|
106
|
+
config.events = config.events ?? [];
|
|
107
|
+
config.periodicEvents = config.periodicEvents ?? [];
|
|
104
108
|
this.#eventMap = config.events.reduce((result, event) => {
|
|
109
|
+
this.validateAdHocEvents(result, event);
|
|
105
110
|
result[[event.type, event.subType].join("##")] = event;
|
|
106
111
|
return result;
|
|
107
112
|
}, {});
|
|
113
|
+
this.#eventMap = config.periodicEvents.reduce((result, event) => {
|
|
114
|
+
const SUFFIX_PERIODIC = "_PERIODIC";
|
|
115
|
+
event.type = `${event.type}${SUFFIX_PERIODIC}`;
|
|
116
|
+
event.isPeriodic = true;
|
|
117
|
+
this.validatePeriodicConfig(result, event);
|
|
118
|
+
result[[event.type, event.subType].join("##")] = event;
|
|
119
|
+
return result;
|
|
120
|
+
}, this.#eventMap);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
validatePeriodicConfig(eventMap, config) {
|
|
124
|
+
const key = this.generateKey(config.type, config.subType);
|
|
125
|
+
if (eventMap[key] && eventMap[key].isPeriodic) {
|
|
126
|
+
throw EventQueueError.duplicateEventRegistration(config.type, config.subType);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (!config.interval || config.interval <= MIN_INTERVAL_SEC) {
|
|
130
|
+
throw EventQueueError.invalidInterval(config.type, config.subType, config.interval);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (!config.impl) {
|
|
134
|
+
throw EventQueueError.missingImpl(config.type, config.subType);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
validateAdHocEvents(eventMap, config) {
|
|
139
|
+
const key = this.generateKey(config.type, config.subType);
|
|
140
|
+
if (eventMap[key] && !eventMap[key].isPeriodic) {
|
|
141
|
+
throw EventQueueError.duplicateEventRegistration(config.type, config.subType);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (!config.impl) {
|
|
145
|
+
throw EventQueueError.missingImpl(config.type, config.subType);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
generateKey(type, subType) {
|
|
150
|
+
return [type, subType].join("##");
|
|
108
151
|
}
|
|
109
152
|
|
|
110
153
|
get fileContent() {
|
|
@@ -115,6 +158,18 @@ class Config {
|
|
|
115
158
|
return this.#config.events;
|
|
116
159
|
}
|
|
117
160
|
|
|
161
|
+
get periodicEvents() {
|
|
162
|
+
return this.#config.periodicEvents;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
isPeriodicEvent(type, subType) {
|
|
166
|
+
return this.#eventMap[this.generateKey(type, subType)]?.isPeriodic;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
get allEvents() {
|
|
170
|
+
return this.#config.events.concat(this.#config.periodicEvents);
|
|
171
|
+
}
|
|
172
|
+
|
|
118
173
|
get forUpdateTimeout() {
|
|
119
174
|
return this.#forUpdateTimeout;
|
|
120
175
|
}
|
|
@@ -211,18 +266,29 @@ class Config {
|
|
|
211
266
|
return this.#disableRedis;
|
|
212
267
|
}
|
|
213
268
|
|
|
269
|
+
set updatePeriodicEvents(value) {
|
|
270
|
+
this.#updatePeriodicEvents = value;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
get updatePeriodicEvents() {
|
|
274
|
+
return this.#updatePeriodicEvents;
|
|
275
|
+
}
|
|
276
|
+
|
|
214
277
|
get isMultiTenancy() {
|
|
215
278
|
return !!cds.requires.multitenancy;
|
|
216
279
|
}
|
|
217
|
-
}
|
|
218
280
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
281
|
+
/**
|
|
282
|
+
@return { Config }
|
|
283
|
+
**/
|
|
284
|
+
static get instance() {
|
|
285
|
+
if (!Config.#instance) {
|
|
286
|
+
Config.#instance = new Config();
|
|
287
|
+
}
|
|
288
|
+
return Config.#instance;
|
|
222
289
|
}
|
|
223
|
-
|
|
224
|
-
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const instance = Config.instance;
|
|
225
293
|
|
|
226
|
-
module.exports =
|
|
227
|
-
getConfigInstance,
|
|
228
|
-
};
|
|
294
|
+
module.exports = instance;
|
package/src/dbHandler.js
CHANGED
|
@@ -8,9 +8,11 @@ const config = require("./config");
|
|
|
8
8
|
const COMPONENT_NAME = "eventQueue/dbHandler";
|
|
9
9
|
|
|
10
10
|
const registerEventQueueDbHandler = (dbService) => {
|
|
11
|
-
const
|
|
12
|
-
const def = dbService.model.definitions[configInstance.tableNameEventQueue];
|
|
11
|
+
const def = dbService.model.definitions[config.tableNameEventQueue];
|
|
13
12
|
dbService.after("CREATE", def, (_, req) => {
|
|
13
|
+
if (req.tx._skipEventQueueBroadcase) {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
14
16
|
req.tx._ = req.tx._ ?? {};
|
|
15
17
|
req.tx._.eventQueuePublishEvents = req.tx._.eventQueuePublishEvents ?? {};
|
|
16
18
|
const eventQueuePublishEvents = req.tx._.eventQueuePublishEvents;
|
|
@@ -18,7 +20,7 @@ const registerEventQueueDbHandler = (dbService) => {
|
|
|
18
20
|
const eventCombinations = Object.keys(
|
|
19
21
|
data.reduce((result, event) => {
|
|
20
22
|
const key = [event.type, event.subType].join("##");
|
|
21
|
-
if (!
|
|
23
|
+
if (!config.hasEventAfterCommitFlag(event.type, event.subType) || eventQueuePublishEvents[key]) {
|
|
22
24
|
return result;
|
|
23
25
|
}
|
|
24
26
|
eventQueuePublishEvents[key] = true;
|
package/src/index.js
CHANGED
package/src/initialize.js
CHANGED
|
@@ -11,7 +11,7 @@ const VError = require("verror");
|
|
|
11
11
|
const EventQueueError = require("./EventQueueError");
|
|
12
12
|
const runner = require("./runner");
|
|
13
13
|
const dbHandler = require("./dbHandler");
|
|
14
|
-
const
|
|
14
|
+
const config = require("./config");
|
|
15
15
|
const { initEventQueueRedisSubscribe, closeSubscribeClient } = require("./redisPubSub");
|
|
16
16
|
const { closeMainClient } = require("./shared/redis");
|
|
17
17
|
|
|
@@ -34,6 +34,7 @@ const CONFIG_VARS = [
|
|
|
34
34
|
["tableNameEventLock", BASE_TABLES.LOCK],
|
|
35
35
|
["disableRedis", false],
|
|
36
36
|
["skipCsnCheck", false],
|
|
37
|
+
["updatePeriodicEvents", true],
|
|
37
38
|
];
|
|
38
39
|
|
|
39
40
|
const initialize = async ({
|
|
@@ -47,16 +48,16 @@ const initialize = async ({
|
|
|
47
48
|
tableNameEventLock,
|
|
48
49
|
disableRedis,
|
|
49
50
|
skipCsnCheck,
|
|
51
|
+
updatePeriodicEvents,
|
|
50
52
|
} = {}) => {
|
|
51
53
|
// TODO: initialize check:
|
|
52
54
|
// - content of yaml check
|
|
53
55
|
// - betweenRuns and parallelTenantProcessing
|
|
54
56
|
|
|
55
|
-
|
|
56
|
-
if (configInstance.initialized) {
|
|
57
|
+
if (config.initialized) {
|
|
57
58
|
return;
|
|
58
59
|
}
|
|
59
|
-
|
|
60
|
+
config.initialized = true;
|
|
60
61
|
|
|
61
62
|
mixConfigVarsWithEnv(
|
|
62
63
|
configFilePath,
|
|
@@ -68,28 +69,29 @@ const initialize = async ({
|
|
|
68
69
|
tableNameEventQueue,
|
|
69
70
|
tableNameEventLock,
|
|
70
71
|
disableRedis,
|
|
71
|
-
skipCsnCheck
|
|
72
|
+
skipCsnCheck,
|
|
73
|
+
updatePeriodicEvents
|
|
72
74
|
);
|
|
73
75
|
|
|
74
76
|
const logger = cds.log(COMPONENT);
|
|
75
|
-
|
|
76
|
-
|
|
77
|
+
config.fileContent = await readConfigFromFile(config.configFilePath);
|
|
78
|
+
config.checkRedisEnabled();
|
|
77
79
|
|
|
78
80
|
const dbService = await cds.connect.to("db");
|
|
79
81
|
await (cds.model ? Promise.resolve() : new Promise((resolve) => cds.on("serving", resolve)));
|
|
80
|
-
!
|
|
81
|
-
if (
|
|
82
|
+
!config.skipCsnCheck && (await csnCheck());
|
|
83
|
+
if (config.processEventsAfterPublish) {
|
|
82
84
|
dbHandler.registerEventQueueDbHandler(dbService);
|
|
83
85
|
}
|
|
84
86
|
|
|
85
87
|
registerEventProcessors();
|
|
86
88
|
registerCdsShutdown();
|
|
87
89
|
logger.info("event queue initialized", {
|
|
88
|
-
registerAsEventProcessor:
|
|
89
|
-
multiTenancyEnabled:
|
|
90
|
-
redisEnabled:
|
|
91
|
-
runInterval:
|
|
92
|
-
|
|
90
|
+
registerAsEventProcessor: config.registerAsEventProcessor,
|
|
91
|
+
multiTenancyEnabled: config.isMultiTenancy,
|
|
92
|
+
redisEnabled: config.redisEnabled,
|
|
93
|
+
runInterval: config.runInterval,
|
|
94
|
+
config: config.parallelTenantProcessing,
|
|
93
95
|
});
|
|
94
96
|
};
|
|
95
97
|
|
|
@@ -111,42 +113,38 @@ const readConfigFromFile = async (configFilepath) => {
|
|
|
111
113
|
};
|
|
112
114
|
|
|
113
115
|
const registerEventProcessors = () => {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
if (!configInstance.registerAsEventProcessor) {
|
|
116
|
+
if (!config.registerAsEventProcessor) {
|
|
117
117
|
return;
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
-
|
|
121
|
-
|
|
120
|
+
const errorHandler = (err) => cds.log(COMPONENT).error("error during init runner", err);
|
|
121
|
+
|
|
122
|
+
if (!config.isMultiTenancy) {
|
|
123
|
+
runner.singleTenant().catch(errorHandler);
|
|
122
124
|
return;
|
|
123
125
|
}
|
|
124
126
|
|
|
125
|
-
if (
|
|
127
|
+
if (config.redisEnabled) {
|
|
126
128
|
initEventQueueRedisSubscribe();
|
|
127
|
-
|
|
128
|
-
runner.multiTenancyRedis();
|
|
129
|
+
config.attachConfigChangeHandler();
|
|
130
|
+
runner.multiTenancyRedis().catch(errorHandler);
|
|
129
131
|
} else {
|
|
130
|
-
runner.multiTenancyDb();
|
|
132
|
+
runner.multiTenancyDb().catch(errorHandler);
|
|
131
133
|
}
|
|
132
134
|
};
|
|
133
135
|
|
|
134
136
|
const csnCheck = async () => {
|
|
135
|
-
const
|
|
136
|
-
const eventCsn = cds.model.definitions[configInstance.tableNameEventQueue];
|
|
137
|
+
const eventCsn = cds.model.definitions[config.tableNameEventQueue];
|
|
137
138
|
if (!eventCsn) {
|
|
138
|
-
throw EventQueueError.missingTableInCsn(
|
|
139
|
+
throw EventQueueError.missingTableInCsn(config.tableNameEventQueue);
|
|
139
140
|
}
|
|
140
141
|
|
|
141
|
-
const lockCsn = cds.model.definitions[
|
|
142
|
+
const lockCsn = cds.model.definitions[config.tableNameEventLock];
|
|
142
143
|
if (!lockCsn) {
|
|
143
|
-
throw EventQueueError.missingTableInCsn(
|
|
144
|
+
throw EventQueueError.missingTableInCsn(config.tableNameEventLock);
|
|
144
145
|
}
|
|
145
146
|
|
|
146
|
-
if (
|
|
147
|
-
configInstance.tableNameEventQueue === BASE_TABLES.EVENT &&
|
|
148
|
-
configInstance.tableNameEventLock === BASE_TABLES.LOCK
|
|
149
|
-
) {
|
|
147
|
+
if (config.tableNameEventQueue === BASE_TABLES.EVENT && config.tableNameEventLock === BASE_TABLES.LOCK) {
|
|
150
148
|
return; // no need to check base tables
|
|
151
149
|
}
|
|
152
150
|
|
|
@@ -176,10 +174,9 @@ const checkCustomTable = (baseCsn, customCsn) => {
|
|
|
176
174
|
};
|
|
177
175
|
|
|
178
176
|
const mixConfigVarsWithEnv = (...args) => {
|
|
179
|
-
const configInstance = getConfigInstance();
|
|
180
177
|
CONFIG_VARS.forEach(([configName, defaultValue], index) => {
|
|
181
178
|
const configValue = args[index];
|
|
182
|
-
|
|
179
|
+
config[configName] = configValue ?? cds.env.eventQueue?.[configName] ?? defaultValue;
|
|
183
180
|
});
|
|
184
181
|
};
|
|
185
182
|
|