@cap-js-community/event-queue 0.1.58 → 0.2.1
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/db/Event.cds +1 -0
- package/package.json +5 -4
- package/src/EventQueueError.js +15 -0
- package/src/EventQueueProcessorBase.js +136 -105
- package/src/config.js +77 -51
- package/src/dbHandler.js +11 -2
- package/src/initialize.js +27 -26
- package/src/processEventQueue.js +4 -2
- package/src/publishEvent.js +27 -1
- package/src/redisPubSub.js +15 -27
- package/src/runner.js +23 -1
- package/src/shared/EventScheduler.js +53 -0
- package/src/shared/common.js +12 -1
- package/src/shared/redis.js +23 -2
package/src/config.js
CHANGED
|
@@ -13,38 +13,56 @@ const REDIS_CONFIG_CHANNEL = "EVENT_QUEUE_CONFIG_CHANNEL";
|
|
|
13
13
|
const COMPONENT_NAME = "eventQueue/config";
|
|
14
14
|
|
|
15
15
|
class Config {
|
|
16
|
+
#logger;
|
|
17
|
+
#config;
|
|
18
|
+
#forUpdateTimeout;
|
|
19
|
+
#globalTxTimeout;
|
|
20
|
+
#runInterval;
|
|
21
|
+
#redisEnabled;
|
|
22
|
+
#initialized;
|
|
23
|
+
#parallelTenantProcessing;
|
|
24
|
+
#tableNameEventQueue;
|
|
25
|
+
#tableNameEventLock;
|
|
26
|
+
#isRunnerDeactivated;
|
|
27
|
+
#configFilePath;
|
|
28
|
+
#processEventsAfterPublish;
|
|
29
|
+
#skipCsnCheck;
|
|
30
|
+
#disableRedis;
|
|
31
|
+
#env;
|
|
32
|
+
#eventMap;
|
|
16
33
|
constructor() {
|
|
17
|
-
this
|
|
18
|
-
this
|
|
19
|
-
this
|
|
20
|
-
this
|
|
21
|
-
this
|
|
22
|
-
this
|
|
23
|
-
this
|
|
24
|
-
this
|
|
25
|
-
this
|
|
26
|
-
this
|
|
27
|
-
this
|
|
28
|
-
this
|
|
29
|
-
this
|
|
30
|
-
this
|
|
31
|
-
this
|
|
34
|
+
this.#logger = cds.log(COMPONENT_NAME);
|
|
35
|
+
this.#config = null;
|
|
36
|
+
this.#forUpdateTimeout = FOR_UPDATE_TIMEOUT;
|
|
37
|
+
this.#globalTxTimeout = GLOBAL_TX_TIMEOUT;
|
|
38
|
+
this.#runInterval = null;
|
|
39
|
+
this.#redisEnabled = null;
|
|
40
|
+
this.#initialized = false;
|
|
41
|
+
this.#parallelTenantProcessing = null;
|
|
42
|
+
this.#tableNameEventQueue = null;
|
|
43
|
+
this.#tableNameEventLock = null;
|
|
44
|
+
this.#isRunnerDeactivated = false;
|
|
45
|
+
this.#configFilePath = null;
|
|
46
|
+
this.#processEventsAfterPublish = null;
|
|
47
|
+
this.#skipCsnCheck = null;
|
|
48
|
+
this.#disableRedis = null;
|
|
49
|
+
this.#env = getEnvInstance();
|
|
32
50
|
}
|
|
33
51
|
|
|
34
52
|
getEventConfig(type, subType) {
|
|
35
|
-
return this
|
|
53
|
+
return this.#eventMap[[type, subType].join("##")];
|
|
36
54
|
}
|
|
37
55
|
|
|
38
56
|
hasEventAfterCommitFlag(type, subType) {
|
|
39
|
-
return this
|
|
57
|
+
return this.#eventMap[[type, subType].join("##")]?.processAfterCommit ?? true;
|
|
40
58
|
}
|
|
41
59
|
|
|
42
60
|
_checkRedisIsBound() {
|
|
43
|
-
return !!this.
|
|
61
|
+
return !!this.#env.getRedisCredentialsFromEnv();
|
|
44
62
|
}
|
|
45
63
|
|
|
46
64
|
checkRedisEnabled() {
|
|
47
|
-
this
|
|
65
|
+
this.#redisEnabled = !this.#disableRedis && this._checkRedisIsBound() && this.#env.isOnCF;
|
|
48
66
|
}
|
|
49
67
|
|
|
50
68
|
attachConfigChangeHandler() {
|
|
@@ -52,11 +70,11 @@ class Config {
|
|
|
52
70
|
try {
|
|
53
71
|
const { key, value } = JSON.parse(messageData);
|
|
54
72
|
if (this[key] !== value) {
|
|
55
|
-
this.
|
|
73
|
+
this.#logger.info("received config change", { key, value });
|
|
56
74
|
this[key] = value;
|
|
57
75
|
}
|
|
58
76
|
} catch (err) {
|
|
59
|
-
this.
|
|
77
|
+
this.#logger.error("could not parse event config change", {
|
|
60
78
|
messageData,
|
|
61
79
|
});
|
|
62
80
|
}
|
|
@@ -65,124 +83,132 @@ class Config {
|
|
|
65
83
|
|
|
66
84
|
publishConfigChange(key, value) {
|
|
67
85
|
if (!this.redisEnabled) {
|
|
68
|
-
this.
|
|
86
|
+
this.#logger.info("redis not connected, config change won't be published", { key, value });
|
|
69
87
|
return;
|
|
70
88
|
}
|
|
71
89
|
redis.publishMessage(REDIS_CONFIG_CHANNEL, JSON.stringify({ key, value })).catch((error) => {
|
|
72
|
-
this.
|
|
90
|
+
this.#logger.error(`publishing config change failed key: ${key}, value: ${value}`, error);
|
|
73
91
|
});
|
|
74
92
|
}
|
|
75
93
|
|
|
76
94
|
get isRunnerDeactivated() {
|
|
77
|
-
return this
|
|
95
|
+
return this.#isRunnerDeactivated;
|
|
78
96
|
}
|
|
79
97
|
|
|
80
98
|
set isRunnerDeactivated(value) {
|
|
81
|
-
this
|
|
99
|
+
this.#isRunnerDeactivated = value;
|
|
82
100
|
}
|
|
83
101
|
|
|
84
102
|
set fileContent(config) {
|
|
85
|
-
this
|
|
86
|
-
this
|
|
103
|
+
this.#config = config;
|
|
104
|
+
this.#eventMap = config.events.reduce((result, event) => {
|
|
87
105
|
result[[event.type, event.subType].join("##")] = event;
|
|
88
106
|
return result;
|
|
89
107
|
}, {});
|
|
90
108
|
}
|
|
91
109
|
|
|
92
110
|
get fileContent() {
|
|
93
|
-
return this
|
|
111
|
+
return this.#config;
|
|
94
112
|
}
|
|
95
113
|
|
|
96
114
|
get events() {
|
|
97
|
-
return this.
|
|
115
|
+
return this.#config.events;
|
|
98
116
|
}
|
|
99
117
|
|
|
100
118
|
get forUpdateTimeout() {
|
|
101
|
-
return this
|
|
119
|
+
return this.#forUpdateTimeout;
|
|
102
120
|
}
|
|
103
121
|
|
|
104
122
|
get globalTxTimeout() {
|
|
105
|
-
return this
|
|
123
|
+
return this.#globalTxTimeout;
|
|
106
124
|
}
|
|
107
125
|
|
|
108
126
|
set forUpdateTimeout(value) {
|
|
109
|
-
this
|
|
127
|
+
this.#forUpdateTimeout = value;
|
|
110
128
|
}
|
|
111
129
|
|
|
112
130
|
set globalTxTimeout(value) {
|
|
113
|
-
this
|
|
131
|
+
this.#globalTxTimeout = value;
|
|
114
132
|
}
|
|
115
133
|
|
|
116
134
|
get runInterval() {
|
|
117
|
-
return this
|
|
135
|
+
return this.#runInterval;
|
|
118
136
|
}
|
|
119
137
|
|
|
120
138
|
set runInterval(value) {
|
|
121
|
-
this
|
|
139
|
+
this.#runInterval = value;
|
|
122
140
|
}
|
|
123
141
|
|
|
124
142
|
get redisEnabled() {
|
|
125
|
-
return this
|
|
143
|
+
return this.#redisEnabled;
|
|
126
144
|
}
|
|
127
145
|
|
|
128
146
|
set redisEnabled(value) {
|
|
129
|
-
this
|
|
147
|
+
this.#redisEnabled = value;
|
|
130
148
|
}
|
|
131
149
|
|
|
132
150
|
get initialized() {
|
|
133
|
-
return this
|
|
151
|
+
return this.#initialized;
|
|
134
152
|
}
|
|
135
153
|
|
|
136
154
|
set initialized(value) {
|
|
137
|
-
this
|
|
155
|
+
this.#initialized = value;
|
|
138
156
|
}
|
|
139
157
|
|
|
140
158
|
get parallelTenantProcessing() {
|
|
141
|
-
return this
|
|
159
|
+
return this.#parallelTenantProcessing;
|
|
142
160
|
}
|
|
143
161
|
|
|
144
162
|
set parallelTenantProcessing(value) {
|
|
145
|
-
this
|
|
163
|
+
this.#parallelTenantProcessing = value;
|
|
146
164
|
}
|
|
147
165
|
|
|
148
166
|
get tableNameEventQueue() {
|
|
149
|
-
return this
|
|
167
|
+
return this.#tableNameEventQueue;
|
|
150
168
|
}
|
|
151
169
|
|
|
152
170
|
set tableNameEventQueue(value) {
|
|
153
|
-
this
|
|
171
|
+
this.#tableNameEventQueue = value;
|
|
154
172
|
}
|
|
155
173
|
|
|
156
174
|
get tableNameEventLock() {
|
|
157
|
-
return this
|
|
175
|
+
return this.#tableNameEventLock;
|
|
158
176
|
}
|
|
159
177
|
|
|
160
178
|
set tableNameEventLock(value) {
|
|
161
|
-
this
|
|
179
|
+
this.#tableNameEventLock = value;
|
|
162
180
|
}
|
|
163
181
|
|
|
164
182
|
set configFilePath(value) {
|
|
165
|
-
this
|
|
183
|
+
this.#configFilePath = value;
|
|
166
184
|
}
|
|
167
185
|
|
|
168
186
|
get configFilePath() {
|
|
169
|
-
return this
|
|
187
|
+
return this.#configFilePath;
|
|
170
188
|
}
|
|
171
189
|
|
|
172
190
|
set processEventsAfterPublish(value) {
|
|
173
|
-
this
|
|
191
|
+
this.#processEventsAfterPublish = value;
|
|
174
192
|
}
|
|
175
193
|
|
|
176
194
|
get processEventsAfterPublish() {
|
|
177
|
-
return this
|
|
195
|
+
return this.#processEventsAfterPublish;
|
|
178
196
|
}
|
|
179
197
|
|
|
180
198
|
set skipCsnCheck(value) {
|
|
181
|
-
this
|
|
199
|
+
this.#skipCsnCheck = value;
|
|
182
200
|
}
|
|
183
201
|
|
|
184
202
|
get skipCsnCheck() {
|
|
185
|
-
return this
|
|
203
|
+
return this.#skipCsnCheck;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
set disableRedis(value) {
|
|
207
|
+
this.#disableRedis = value;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
get disableRedis() {
|
|
211
|
+
return this.#disableRedis;
|
|
186
212
|
}
|
|
187
213
|
|
|
188
214
|
get isMultiTenancy() {
|
package/src/dbHandler.js
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
-
const
|
|
3
|
+
const cds = require("@sap/cds");
|
|
4
|
+
|
|
5
|
+
const { broadcastEvent } = require("./redisPubSub");
|
|
4
6
|
const config = require("./config");
|
|
5
7
|
|
|
8
|
+
const COMPONENT_NAME = "eventQueue/dbHandler";
|
|
9
|
+
|
|
6
10
|
const registerEventQueueDbHandler = (dbService) => {
|
|
7
11
|
const configInstance = config.getConfigInstance();
|
|
8
12
|
const def = dbService.model.definitions[configInstance.tableNameEventQueue];
|
|
@@ -26,7 +30,12 @@ const registerEventQueueDbHandler = (dbService) => {
|
|
|
26
30
|
eventCombinations.length &&
|
|
27
31
|
req.on("succeeded", () => {
|
|
28
32
|
for (const eventCombination of eventCombinations) {
|
|
29
|
-
|
|
33
|
+
broadcastEvent(req.tenant, ...eventCombination.split("##")).catch((err) => {
|
|
34
|
+
cds.log(COMPONENT_NAME).error("db handler failure during broadcasting event", err, {
|
|
35
|
+
tenant: req.tenant,
|
|
36
|
+
eventCombination,
|
|
37
|
+
});
|
|
38
|
+
});
|
|
30
39
|
}
|
|
31
40
|
});
|
|
32
41
|
});
|
package/src/initialize.js
CHANGED
|
@@ -5,7 +5,6 @@ const fs = require("fs");
|
|
|
5
5
|
const path = require("path");
|
|
6
6
|
|
|
7
7
|
const cds = require("@sap/cds");
|
|
8
|
-
|
|
9
8
|
const yaml = require("yaml");
|
|
10
9
|
const VError = require("verror");
|
|
11
10
|
|
|
@@ -13,7 +12,8 @@ const EventQueueError = require("./EventQueueError");
|
|
|
13
12
|
const runner = require("./runner");
|
|
14
13
|
const dbHandler = require("./dbHandler");
|
|
15
14
|
const { getConfigInstance } = require("./config");
|
|
16
|
-
const { initEventQueueRedisSubscribe } = require("./redisPubSub");
|
|
15
|
+
const { initEventQueueRedisSubscribe, closeSubscribeClient } = require("./redisPubSub");
|
|
16
|
+
const { closeMainClient } = require("./shared/redis");
|
|
17
17
|
|
|
18
18
|
const readFileAsync = promisify(fs.readFile);
|
|
19
19
|
|
|
@@ -23,6 +23,18 @@ const BASE_TABLES = {
|
|
|
23
23
|
EVENT: "sap.eventqueue.Event",
|
|
24
24
|
LOCK: "sap.eventqueue.Lock",
|
|
25
25
|
};
|
|
26
|
+
const CONFIG_VARS = [
|
|
27
|
+
["configFilePath", null],
|
|
28
|
+
["registerAsEventProcessor", true],
|
|
29
|
+
["processEventsAfterPublish", true],
|
|
30
|
+
["isRunnerDeactivated", false],
|
|
31
|
+
["runInterval", 5 * 60 * 1000],
|
|
32
|
+
["parallelTenantProcessing", 5],
|
|
33
|
+
["tableNameEventQueue", BASE_TABLES.EVENT],
|
|
34
|
+
["tableNameEventLock", BASE_TABLES.LOCK],
|
|
35
|
+
["disableRedis", false],
|
|
36
|
+
["skipCsnCheck", false],
|
|
37
|
+
];
|
|
26
38
|
|
|
27
39
|
const initialize = async ({
|
|
28
40
|
configFilePath,
|
|
@@ -33,6 +45,7 @@ const initialize = async ({
|
|
|
33
45
|
parallelTenantProcessing,
|
|
34
46
|
tableNameEventQueue,
|
|
35
47
|
tableNameEventLock,
|
|
48
|
+
disableRedis,
|
|
36
49
|
skipCsnCheck,
|
|
37
50
|
} = {}) => {
|
|
38
51
|
// TODO: initialize check:
|
|
@@ -54,6 +67,7 @@ const initialize = async ({
|
|
|
54
67
|
parallelTenantProcessing,
|
|
55
68
|
tableNameEventQueue,
|
|
56
69
|
tableNameEventLock,
|
|
70
|
+
disableRedis,
|
|
57
71
|
skipCsnCheck
|
|
58
72
|
);
|
|
59
73
|
|
|
@@ -69,6 +83,7 @@ const initialize = async ({
|
|
|
69
83
|
}
|
|
70
84
|
|
|
71
85
|
registerEventProcessors();
|
|
86
|
+
registerCdsShutdown();
|
|
72
87
|
logger.info("event queue initialized", {
|
|
73
88
|
registerAsEventProcessor: configInstance.registerAsEventProcessor,
|
|
74
89
|
multiTenancyEnabled: configInstance.isMultiTenancy,
|
|
@@ -160,32 +175,18 @@ const checkCustomTable = (baseCsn, customCsn) => {
|
|
|
160
175
|
}
|
|
161
176
|
};
|
|
162
177
|
|
|
163
|
-
const mixConfigVarsWithEnv = (
|
|
164
|
-
configFilePath,
|
|
165
|
-
registerAsEventProcessor,
|
|
166
|
-
processEventsAfterPublish,
|
|
167
|
-
isRunnerDeactivated,
|
|
168
|
-
runInterval,
|
|
169
|
-
parallelTenantProcessing,
|
|
170
|
-
tableNameEventQueue,
|
|
171
|
-
tableNameEventLock,
|
|
172
|
-
skipCsnCheck
|
|
173
|
-
) => {
|
|
178
|
+
const mixConfigVarsWithEnv = (...args) => {
|
|
174
179
|
const configInstance = getConfigInstance();
|
|
180
|
+
CONFIG_VARS.forEach(([configName, defaultValue], index) => {
|
|
181
|
+
const configValue = args[index];
|
|
182
|
+
configInstance[configName] = configValue ?? cds.env.eventQueue?.[configName] ?? defaultValue;
|
|
183
|
+
});
|
|
184
|
+
};
|
|
175
185
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
configInstance.processEventsAfterPublish =
|
|
181
|
-
processEventsAfterPublish ?? cds.env.eventQueue?.processEventsAfterPublish ?? true;
|
|
182
|
-
configInstance.runInterval = runInterval ?? cds.env.eventQueue?.runInterval ?? 5 * 60 * 1000;
|
|
183
|
-
configInstance.parallelTenantProcessing =
|
|
184
|
-
parallelTenantProcessing ?? cds.env.eventQueue?.parallelTenantProcessing ?? 5;
|
|
185
|
-
configInstance.tableNameEventQueue =
|
|
186
|
-
tableNameEventQueue ?? cds.env.eventQueue?.tableNameEventQueue ?? BASE_TABLES.EVENT;
|
|
187
|
-
configInstance.tableNameEventLock = tableNameEventLock ?? cds.env.eventQueue?.tableNameEventLock ?? BASE_TABLES.LOCK;
|
|
188
|
-
configInstance.skipCsnCheck = skipCsnCheck ?? cds.env.eventQueue?.skipCsnCheck ?? false;
|
|
186
|
+
const registerCdsShutdown = () => {
|
|
187
|
+
cds.on("shutdown", async () => {
|
|
188
|
+
await Promise.allSettled([closeMainClient(), closeSubscribeClient()]);
|
|
189
|
+
});
|
|
189
190
|
};
|
|
190
191
|
|
|
191
192
|
module.exports = {
|
package/src/processEventQueue.js
CHANGED
|
@@ -8,7 +8,6 @@ const { getConfigInstance } = require("./config");
|
|
|
8
8
|
const { TransactionMode } = require("./constants");
|
|
9
9
|
const { limiter, Funnel } = require("./shared/common");
|
|
10
10
|
|
|
11
|
-
const EventQueueBase = require("./EventQueueProcessorBase");
|
|
12
11
|
const { executeInNewTransaction, TriggerRollback } = require("./shared/cdsHelper");
|
|
13
12
|
|
|
14
13
|
const COMPONENT_NAME = "eventQueue/processEventQueue";
|
|
@@ -33,7 +32,10 @@ const processEventQueue = async (context, eventType, eventSubType, startTime = n
|
|
|
33
32
|
const eventConfig = getConfigInstance().getEventConfig(eventType, eventSubType);
|
|
34
33
|
const [err, EventTypeClass] = resilientRequire(eventConfig?.impl);
|
|
35
34
|
if (!eventConfig || err || !(typeof EventTypeClass.constructor === "function")) {
|
|
36
|
-
|
|
35
|
+
cds.log(COMPONENT_NAME).error("No Implementation found in the provided configuration file.", {
|
|
36
|
+
eventType,
|
|
37
|
+
eventSubType,
|
|
38
|
+
});
|
|
37
39
|
return;
|
|
38
40
|
}
|
|
39
41
|
baseInstance = new EventTypeClass(context, eventType, eventSubType, eventConfig);
|
package/src/publishEvent.js
CHANGED
|
@@ -1,19 +1,45 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
const config = require("./config");
|
|
4
|
+
const common = require("./shared/common");
|
|
4
5
|
const EventQueueError = require("./EventQueueError");
|
|
5
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Asynchronously publishes a series of events to the event queue.
|
|
9
|
+
*
|
|
10
|
+
* @param {Transaction} tx - The transaction object to be used for database operations.
|
|
11
|
+
* @param {Array|Object} events - An array of event objects or a single event object. Each event object should match the Event table structure:
|
|
12
|
+
* {
|
|
13
|
+
* type: String, // Event type. This is a required field.
|
|
14
|
+
* subType: String, // Event subtype. This is a required field.
|
|
15
|
+
* referenceEntity: String, // Reference entity associated with the event.
|
|
16
|
+
* referenceEntityKey: UUID, // UUID key of the reference entity.
|
|
17
|
+
* status: Status, // Status of the event, defaults to 0.
|
|
18
|
+
* payload: LargeString, // Payload of the event.
|
|
19
|
+
* attempts: Integer, // The number of attempts made, defaults to 0.
|
|
20
|
+
* lastAttemptTimestamp: Timestamp, // Timestamp of the last attempt.
|
|
21
|
+
* createdAt: Timestamp, // Timestamp of event creation. This field is automatically set on insert.
|
|
22
|
+
* startAfter: Timestamp, // Timestamp indicating when the event should start after.
|
|
23
|
+
* }
|
|
24
|
+
* @throws {EventQueueError} Throws an error if the configuration is not initialized.
|
|
25
|
+
* @throws {EventQueueError} Throws an error if the event type is unknown.
|
|
26
|
+
* @throws {EventQueueError} Throws an error if the startAfter field is not a valid date.
|
|
27
|
+
* @returns {Promise} Returns a promise which resolves to the result of the database insert operation.
|
|
28
|
+
*/
|
|
6
29
|
const publishEvent = async (tx, events) => {
|
|
7
30
|
const configInstance = config.getConfigInstance();
|
|
8
31
|
if (!configInstance.initialized) {
|
|
9
32
|
throw EventQueueError.notInitialized();
|
|
10
33
|
}
|
|
11
34
|
const eventsForProcessing = Array.isArray(events) ? events : [events];
|
|
12
|
-
for (const { type, subType } of eventsForProcessing) {
|
|
35
|
+
for (const { type, subType, startAfter } of eventsForProcessing) {
|
|
13
36
|
const eventConfig = configInstance.getEventConfig(type, subType);
|
|
14
37
|
if (!eventConfig) {
|
|
15
38
|
throw EventQueueError.unknownEventType(type, subType);
|
|
16
39
|
}
|
|
40
|
+
if (startAfter && !common.isValidDate(startAfter)) {
|
|
41
|
+
throw EventQueueError.malformedDate(startAfter);
|
|
42
|
+
}
|
|
17
43
|
}
|
|
18
44
|
return await tx.run(INSERT.into(configInstance.tableNameEventQueue).entries(eventsForProcessing));
|
|
19
45
|
};
|
package/src/redisPubSub.js
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
const redis = require("./shared/redis");
|
|
4
|
-
const { processEventQueue } = require("./processEventQueue");
|
|
5
|
-
const { getSubdomainForTenantId } = require("./shared/cdsHelper");
|
|
6
4
|
const { checkLockExistsAndReturnValue } = require("./shared/distributedLock");
|
|
7
5
|
const config = require("./config");
|
|
8
|
-
const {
|
|
6
|
+
const { runEventCombinationForTenant } = require("./runner");
|
|
9
7
|
|
|
10
8
|
const EVENT_MESSAGE_CHANNEL = "EVENT_QUEUE_MESSAGE_CHANNEL";
|
|
11
9
|
const COMPONENT_NAME = "eventQueue/redisPubSub";
|
|
@@ -23,19 +21,12 @@ const messageHandlerProcessEvents = async (messageData) => {
|
|
|
23
21
|
const logger = cds.log(COMPONENT_NAME);
|
|
24
22
|
try {
|
|
25
23
|
const { tenantId, type, subType } = JSON.parse(messageData);
|
|
26
|
-
const subdomain = await getSubdomainForTenantId(tenantId);
|
|
27
|
-
const context = new cds.EventContext({
|
|
28
|
-
tenant: tenantId,
|
|
29
|
-
// NOTE: we need this because of logging otherwise logs would not contain the subdomain
|
|
30
|
-
http: { req: { authInfo: { getSubdomain: () => subdomain } } },
|
|
31
|
-
});
|
|
32
|
-
cds.context = context;
|
|
33
24
|
logger.debug("received redis event", {
|
|
34
25
|
tenantId,
|
|
35
26
|
type,
|
|
36
27
|
subType,
|
|
37
28
|
});
|
|
38
|
-
|
|
29
|
+
await runEventCombinationForTenant(tenantId, type, subType);
|
|
39
30
|
} catch (err) {
|
|
40
31
|
logger.error("could not parse event information", {
|
|
41
32
|
messageData,
|
|
@@ -43,12 +34,12 @@ const messageHandlerProcessEvents = async (messageData) => {
|
|
|
43
34
|
}
|
|
44
35
|
};
|
|
45
36
|
|
|
46
|
-
const
|
|
37
|
+
const broadcastEvent = async (tenantId, type, subType) => {
|
|
47
38
|
const logger = cds.log(COMPONENT_NAME);
|
|
48
39
|
const configInstance = config.getConfigInstance();
|
|
49
40
|
if (!configInstance.redisEnabled) {
|
|
50
41
|
if (configInstance.registerAsEventProcessor) {
|
|
51
|
-
await
|
|
42
|
+
await runEventCombinationForTenant(tenantId, type, subType);
|
|
52
43
|
}
|
|
53
44
|
return;
|
|
54
45
|
}
|
|
@@ -76,22 +67,19 @@ const publishEvent = async (tenantId, type, subType) => {
|
|
|
76
67
|
}
|
|
77
68
|
};
|
|
78
69
|
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
// NOTE: we need this because of logging otherwise logs would not contain the subdomain
|
|
89
|
-
http: { req: { authInfo: { getSubdomain: () => subdomain } } },
|
|
90
|
-
});
|
|
91
|
-
getWorkerPoolInstance().addToQueue(async () => processEventQueue(context, type, subType));
|
|
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
|
+
}
|
|
92
79
|
};
|
|
93
80
|
|
|
94
81
|
module.exports = {
|
|
95
82
|
initEventQueueRedisSubscribe,
|
|
96
|
-
|
|
83
|
+
broadcastEvent,
|
|
84
|
+
closeSubscribeClient,
|
|
97
85
|
};
|
package/src/runner.js
CHANGED
|
@@ -3,11 +3,12 @@
|
|
|
3
3
|
const { randomUUID } = require("crypto");
|
|
4
4
|
|
|
5
5
|
const eventQueueConfig = require("./config");
|
|
6
|
-
const { eventQueueRunner } = require("./processEventQueue");
|
|
6
|
+
const { eventQueueRunner, processEventQueue } = require("./processEventQueue");
|
|
7
7
|
const { getWorkerPoolInstance } = require("./shared/WorkerQueue");
|
|
8
8
|
const cdsHelper = require("./shared/cdsHelper");
|
|
9
9
|
const distributedLock = require("./shared/distributedLock");
|
|
10
10
|
const SetIntervalDriftSafe = require("./shared/SetIntervalDriftSafe");
|
|
11
|
+
const { getSubdomainForTenantId } = require("./shared/cdsHelper");
|
|
11
12
|
|
|
12
13
|
const COMPONENT_NAME = "eventQueue/runner";
|
|
13
14
|
const EVENT_QUEUE_RUN_ID = "EVENT_QUEUE_RUN_ID";
|
|
@@ -190,10 +191,31 @@ const _calculateOffsetForFirstRun = async () => {
|
|
|
190
191
|
return offsetDependingOnLastRun;
|
|
191
192
|
};
|
|
192
193
|
|
|
194
|
+
const runEventCombinationForTenant = async (tenantId, type, subType) => {
|
|
195
|
+
try {
|
|
196
|
+
const subdomain = await getSubdomainForTenantId(tenantId);
|
|
197
|
+
const context = new cds.EventContext({
|
|
198
|
+
tenant: tenantId,
|
|
199
|
+
// NOTE: we need this because of logging otherwise logs would not contain the subdomain
|
|
200
|
+
http: { req: { authInfo: { getSubdomain: () => subdomain } } },
|
|
201
|
+
});
|
|
202
|
+
cds.context = context;
|
|
203
|
+
getWorkerPoolInstance().addToQueue(async () => await processEventQueue(context, type, subType));
|
|
204
|
+
} catch (err) {
|
|
205
|
+
const logger = cds.log(COMPONENT_NAME);
|
|
206
|
+
logger.error("error executing event combination for tenant", err, {
|
|
207
|
+
tenantId,
|
|
208
|
+
type,
|
|
209
|
+
subType,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
|
|
193
214
|
module.exports = {
|
|
194
215
|
singleTenant,
|
|
195
216
|
multiTenancyDb,
|
|
196
217
|
multiTenancyRedis,
|
|
218
|
+
runEventCombinationForTenant,
|
|
197
219
|
_: {
|
|
198
220
|
_multiTenancyRedis,
|
|
199
221
|
_multiTenancyDb,
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const cds = require("@sap/cds");
|
|
4
|
+
|
|
5
|
+
const { broadcastEvent } = require("../redisPubSub");
|
|
6
|
+
|
|
7
|
+
const COMPONENT_NAME = "eventQueue/shared/EventScheduler";
|
|
8
|
+
|
|
9
|
+
let instance;
|
|
10
|
+
class EventScheduler {
|
|
11
|
+
#scheduledEvents = {};
|
|
12
|
+
constructor() {}
|
|
13
|
+
|
|
14
|
+
scheduleEvent(tenantId, type, subType, startAfter) {
|
|
15
|
+
const startAfterSeconds = startAfter.getSeconds();
|
|
16
|
+
const secondsUntilNextTen = 10 - (startAfterSeconds % 10);
|
|
17
|
+
const roundUpDate = new Date(startAfter.getTime() + secondsUntilNextTen * 1000);
|
|
18
|
+
const key = [tenantId, type, subType, roundUpDate.toISOString()].join("##");
|
|
19
|
+
if (this.#scheduledEvents[key]) {
|
|
20
|
+
return; // event combination already scheduled
|
|
21
|
+
}
|
|
22
|
+
this.#scheduledEvents[key] = true;
|
|
23
|
+
cds.log(COMPONENT_NAME).info("scheduling event queue run for delayed event", {
|
|
24
|
+
type,
|
|
25
|
+
subType,
|
|
26
|
+
delaySeconds: (roundUpDate.getTime() - Date.now()) / 1000,
|
|
27
|
+
});
|
|
28
|
+
setTimeout(() => {
|
|
29
|
+
delete this.#scheduledEvents[key];
|
|
30
|
+
broadcastEvent(tenantId, type, subType).catch((err) => {
|
|
31
|
+
cds.log(COMPONENT_NAME).error("could not execute scheduled event", err, {
|
|
32
|
+
tenantId,
|
|
33
|
+
type,
|
|
34
|
+
subType,
|
|
35
|
+
scheduledFor: roundUpDate.toISOString(),
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
}, roundUpDate.getTime() - Date.now()).unref();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
clearScheduledEvents() {
|
|
42
|
+
this.#scheduledEvents = {};
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
module.exports = {
|
|
47
|
+
getInstance: () => {
|
|
48
|
+
if (!instance) {
|
|
49
|
+
instance = new EventScheduler();
|
|
50
|
+
}
|
|
51
|
+
return instance;
|
|
52
|
+
},
|
|
53
|
+
};
|
package/src/shared/common.js
CHANGED
|
@@ -107,4 +107,15 @@ const limiter = async (limit, payloads, iterator) => {
|
|
|
107
107
|
return Promise.allSettled(returnPromises);
|
|
108
108
|
};
|
|
109
109
|
|
|
110
|
-
|
|
110
|
+
const isValidDate = (value) => {
|
|
111
|
+
if (typeof value === "string") {
|
|
112
|
+
const date = Date.parse(value);
|
|
113
|
+
return !isNaN(date);
|
|
114
|
+
} else if (value instanceof Date) {
|
|
115
|
+
return !isNaN(value.getTime());
|
|
116
|
+
} else {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
module.exports = { arrayToFlatMap, Funnel, limiter, isValidDate };
|