@cap-js-community/event-queue 1.11.0-beta.0 → 1.11.0-beta.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/package.json +1 -1
- package/src/EventQueueProcessorBase.js +6 -6
- package/src/config.js +1 -1
- package/src/initialize.js +0 -3
- package/src/shared/cdsHelper.js +1 -1
- package/src/shared/distributedLock.js +65 -6
- package/srv/service/admin-service.cds +12 -0
- package/srv/service/admin-service.js +15 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cap-js-community/event-queue",
|
|
3
|
-
"version": "1.11.0-beta.
|
|
3
|
+
"version": "1.11.0-beta.2",
|
|
4
4
|
"description": "An event queue that enables secure transactional processing of asynchronous and periodic events, featuring instant event processing with Redis Pub/Sub and load distribution across all application instances.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"types": "src/index.d.ts",
|
|
@@ -76,7 +76,7 @@ class EventQueueProcessorBase {
|
|
|
76
76
|
this.__txMap = {};
|
|
77
77
|
this.__txRollback = {};
|
|
78
78
|
this.__queueEntries = [];
|
|
79
|
-
this.#keepAliveRunner = new SetIntervalDriftSafe(this.#eventConfig.keepAliveInterval);
|
|
79
|
+
this.#keepAliveRunner = new SetIntervalDriftSafe(this.#eventConfig.keepAliveInterval * 1000);
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
/**
|
|
@@ -617,7 +617,7 @@ class EventQueueProcessorBase {
|
|
|
617
617
|
"OR lastAttemptTimestamp IS NULL ) OR ( status =",
|
|
618
618
|
EventProcessingStatus.InProgress,
|
|
619
619
|
"AND lastAttemptTimestamp <=",
|
|
620
|
-
new Date(baseDate - this.#eventConfig.keepAliveMaxInProgressTime).toISOString(),
|
|
620
|
+
new Date(baseDate - this.#eventConfig.keepAliveMaxInProgressTime * 1000).toISOString(),
|
|
621
621
|
") )",
|
|
622
622
|
]
|
|
623
623
|
: [
|
|
@@ -628,7 +628,7 @@ class EventQueueProcessorBase {
|
|
|
628
628
|
") OR ( status =",
|
|
629
629
|
EventProcessingStatus.InProgress,
|
|
630
630
|
"AND lastAttemptTimestamp <=",
|
|
631
|
-
new Date(baseDate - this.#eventConfig.keepAliveMaxInProgressTime).toISOString(),
|
|
631
|
+
new Date(baseDate - this.#eventConfig.keepAliveMaxInProgressTime * 1000).toISOString(),
|
|
632
632
|
") )",
|
|
633
633
|
])
|
|
634
634
|
)
|
|
@@ -868,7 +868,7 @@ class EventQueueProcessorBase {
|
|
|
868
868
|
}
|
|
869
869
|
|
|
870
870
|
continuesKeepAlive() {
|
|
871
|
-
if (Date.now() - this.lockAcquiredTime.getTime() >= this.#eventConfig.keepAliveInterval) {
|
|
871
|
+
if (Date.now() - this.lockAcquiredTime.getTime() >= this.#eventConfig.keepAliveInterval * 1000) {
|
|
872
872
|
trace(this.baseContext, "keepAlive-between-iterations", async () => {
|
|
873
873
|
await this.#renewDistributedLock();
|
|
874
874
|
}).catch((err) => this.logger.error("renewing lock between intervals failed!", err));
|
|
@@ -961,7 +961,7 @@ class EventQueueProcessorBase {
|
|
|
961
961
|
const lockAcquired = await distributedLock.acquireLock(
|
|
962
962
|
this.__context,
|
|
963
963
|
[this.#eventType, this.#eventSubType].join("##"),
|
|
964
|
-
{ keepTrackOfLock: true, expiryTime: this.#eventConfig.keepAliveMaxInProgressTime }
|
|
964
|
+
{ keepTrackOfLock: true, expiryTime: this.#eventConfig.keepAliveMaxInProgressTime * 1000 }
|
|
965
965
|
);
|
|
966
966
|
if (!lockAcquired) {
|
|
967
967
|
this.logger.debug("no lock available, exit processing", {
|
|
@@ -983,7 +983,7 @@ class EventQueueProcessorBase {
|
|
|
983
983
|
const lockAcquired = await distributedLock.renewLock(
|
|
984
984
|
this.__context,
|
|
985
985
|
[this.#eventType, this.#eventSubType].join("##"),
|
|
986
|
-
{ expiryTime: this.#eventConfig.keepAliveMaxInProgressTime }
|
|
986
|
+
{ expiryTime: this.#eventConfig.keepAliveMaxInProgressTime * 1000 }
|
|
987
987
|
);
|
|
988
988
|
if (!lockAcquired) {
|
|
989
989
|
this.logger.error("renewing distributed lock failed!", {
|
package/src/config.js
CHANGED
|
@@ -550,7 +550,7 @@ class Config {
|
|
|
550
550
|
event.load = event.load ?? DEFAULT_LOAD;
|
|
551
551
|
event.priority = event.priority ?? DEFAULT_PRIORITY;
|
|
552
552
|
event.increasePriorityOverTime = event.increasePriorityOverTime ?? DEFAULT_INCREASE_PRIORITY;
|
|
553
|
-
event.keepAliveInterval =
|
|
553
|
+
event.keepAliveInterval = event.keepAliveInterval ?? DEFAULT_KEEP_ALIVE_INTERVAL;
|
|
554
554
|
event.keepAliveMaxInProgressTime = event.keepAliveInterval * DEFAULT_MAX_FACTOR_STUCK_2_KEEP_ALIVE_INTERVAL;
|
|
555
555
|
event.checkForNextChunk = event.checkForNextChunk ?? DEFAULT_CHECK_FOR_NEXT_CHUNK;
|
|
556
556
|
}
|
package/src/initialize.js
CHANGED
package/src/shared/cdsHelper.js
CHANGED
|
@@ -123,7 +123,7 @@ const _getAllTenantBase = async () => {
|
|
|
123
123
|
|
|
124
124
|
// NOTE: tmp workaround until cds-mtxs fixes the connect.to service
|
|
125
125
|
for (let i = 0; i < 10; i++) {
|
|
126
|
-
if (cds.services["saas-registry"]) {
|
|
126
|
+
if (cds.services["cds.xt.SaasProvisioningService"] || cds.services["saas-registry"]) {
|
|
127
127
|
break;
|
|
128
128
|
}
|
|
129
129
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
@@ -17,7 +17,7 @@ const acquireLock = async (
|
|
|
17
17
|
if (config.redisEnabled) {
|
|
18
18
|
return await _acquireLockRedis(context, fullKey, expiryTime, { keepTrackOfLock });
|
|
19
19
|
} else {
|
|
20
|
-
return await _acquireLockDB(context, fullKey, expiryTime);
|
|
20
|
+
return await _acquireLockDB(context, fullKey, expiryTime, { keepTrackOfLock });
|
|
21
21
|
}
|
|
22
22
|
};
|
|
23
23
|
|
|
@@ -73,7 +73,7 @@ const _acquireLockRedis = async (
|
|
|
73
73
|
context,
|
|
74
74
|
fullKey,
|
|
75
75
|
expiryTime,
|
|
76
|
-
{ value =
|
|
76
|
+
{ value = Date.now(), overrideValue = false, keepTrackOfLock } = {}
|
|
77
77
|
) => {
|
|
78
78
|
const client = await redis.createMainClientAndConnect(config.redisOptions);
|
|
79
79
|
const result = await client.set(fullKey, value, {
|
|
@@ -82,7 +82,7 @@ const _acquireLockRedis = async (
|
|
|
82
82
|
});
|
|
83
83
|
const isOk = result === REDIS_COMMAND_OK;
|
|
84
84
|
if (isOk && keepTrackOfLock) {
|
|
85
|
-
existingLocks[fullKey] =
|
|
85
|
+
existingLocks[fullKey] = context.tenant;
|
|
86
86
|
}
|
|
87
87
|
return isOk;
|
|
88
88
|
};
|
|
@@ -129,9 +129,15 @@ const _releaseLockDb = async (context, fullKey) => {
|
|
|
129
129
|
await cdsHelper.executeInNewTransaction(context, "distributedLock-release", async (tx) => {
|
|
130
130
|
await tx.run(DELETE.from(config.tableNameEventLock).where("code =", fullKey));
|
|
131
131
|
});
|
|
132
|
+
delete existingLocks[fullKey];
|
|
132
133
|
};
|
|
133
134
|
|
|
134
|
-
const _acquireLockDB = async (
|
|
135
|
+
const _acquireLockDB = async (
|
|
136
|
+
context,
|
|
137
|
+
fullKey,
|
|
138
|
+
expiryTime,
|
|
139
|
+
{ value = "true", overrideValue = false, keepTrackOfLock } = {}
|
|
140
|
+
) => {
|
|
135
141
|
let result;
|
|
136
142
|
await cdsHelper.executeInNewTransaction(context, "distributedLock-acquire", async (tx) => {
|
|
137
143
|
try {
|
|
@@ -171,6 +177,9 @@ const _acquireLockDB = async (context, fullKey, expiryTime, { value = "true", ov
|
|
|
171
177
|
}
|
|
172
178
|
}
|
|
173
179
|
});
|
|
180
|
+
if (result && keepTrackOfLock) {
|
|
181
|
+
existingLocks[fullKey] = context.tenant;
|
|
182
|
+
}
|
|
174
183
|
return result;
|
|
175
184
|
};
|
|
176
185
|
|
|
@@ -181,14 +190,63 @@ const _generateKey = (context, tenantScoped, key) => {
|
|
|
181
190
|
return `${keyParts.join("##")}`;
|
|
182
191
|
};
|
|
183
192
|
|
|
193
|
+
const getAllLocksRedis = async () => {
|
|
194
|
+
const client = await redis.createMainClientAndConnect(config.redisOptions);
|
|
195
|
+
const batchSize = 500;
|
|
196
|
+
const results = [];
|
|
197
|
+
let pipeline = client.multi();
|
|
198
|
+
const output = [];
|
|
199
|
+
let count = 0;
|
|
200
|
+
|
|
201
|
+
// NOTE: use SCAN because KEYS is not supported for cluster clients
|
|
202
|
+
for await (const key of client.scanIterator({ MATCH: "EVENT*", COUNT: 1000 })) {
|
|
203
|
+
const [, tenant, guidOrType, subType] = key.split("##");
|
|
204
|
+
if (!subType) {
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
output.push({
|
|
209
|
+
tenant: tenant,
|
|
210
|
+
type: guidOrType,
|
|
211
|
+
subType: subType,
|
|
212
|
+
});
|
|
213
|
+
pipeline.ttl(key).get(key);
|
|
214
|
+
count++;
|
|
215
|
+
|
|
216
|
+
if (count >= batchSize) {
|
|
217
|
+
const replies = await pipeline.exec();
|
|
218
|
+
results.push(...replies);
|
|
219
|
+
pipeline = client.multi();
|
|
220
|
+
count = 0;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
if (count > 0) {
|
|
224
|
+
const replies = await pipeline.exec();
|
|
225
|
+
results.push(...replies);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
let counter = 0;
|
|
229
|
+
for (const row of output) {
|
|
230
|
+
const ttl = results[counter];
|
|
231
|
+
const createdAt = results[counter + 1];
|
|
232
|
+
Object.assign(row, { ttl, createdAt });
|
|
233
|
+
counter = counter + 2;
|
|
234
|
+
}
|
|
235
|
+
return output;
|
|
236
|
+
};
|
|
237
|
+
|
|
184
238
|
const shutdownHandler = async () => {
|
|
185
239
|
const logger = cds.log(COMPONENT_NAME);
|
|
186
240
|
logger.info("received shutdown event, trying to release all locks", {
|
|
187
241
|
numberOfLocks: Object.keys(existingLocks).length,
|
|
188
242
|
});
|
|
189
243
|
const result = await Promise.allSettled(
|
|
190
|
-
Object.
|
|
191
|
-
|
|
244
|
+
Object.entries(existingLocks).map(async ([key, tenant]) => {
|
|
245
|
+
if (config.redisEnabled) {
|
|
246
|
+
await _releaseLockRedis({ tenant }, key);
|
|
247
|
+
} else {
|
|
248
|
+
await _releaseLockDb({ tenant }, key);
|
|
249
|
+
}
|
|
192
250
|
logger.info("lock released", { key });
|
|
193
251
|
})
|
|
194
252
|
);
|
|
@@ -206,4 +264,5 @@ module.exports = {
|
|
|
206
264
|
setValueWithExpire,
|
|
207
265
|
shutdownHandler,
|
|
208
266
|
renewLock,
|
|
267
|
+
getAllLocksRedis,
|
|
209
268
|
};
|
|
@@ -22,6 +22,18 @@ service EventQueueAdminService {
|
|
|
22
22
|
attempts: Integer) returns Event;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
@cds.persistence.skip
|
|
26
|
+
@readonly
|
|
27
|
+
entity Lock {
|
|
28
|
+
key tenant: String;
|
|
29
|
+
key type: String;
|
|
30
|
+
key subType: String;
|
|
31
|
+
landscape: String;
|
|
32
|
+
space: String;
|
|
33
|
+
ttl: Integer;
|
|
34
|
+
createdAt: Integer;
|
|
35
|
+
}
|
|
36
|
+
|
|
25
37
|
@readonly
|
|
26
38
|
@cds.persistence.skip
|
|
27
39
|
entity Tenant {
|
|
@@ -4,14 +4,15 @@ const cds = require("@sap/cds");
|
|
|
4
4
|
const cdsHelper = require("../../src/shared/cdsHelper");
|
|
5
5
|
const { EventProcessingStatus } = require("../../src");
|
|
6
6
|
const config = require("../../src/config");
|
|
7
|
+
const distributedLock = require("../../src/shared/distributedLock");
|
|
7
8
|
|
|
8
9
|
module.exports = class AdminService extends cds.ApplicationService {
|
|
9
10
|
async init() {
|
|
10
|
-
const { Event: EventService, Tenant } = this.entities();
|
|
11
|
+
const { Event: EventService, Tenant, Lock: LockService } = this.entities();
|
|
11
12
|
const { Event: EventDb } = cds.db.entities("sap.eventqueue");
|
|
12
13
|
const { landscape, space } = this.getLandscapeAndSpace();
|
|
13
14
|
|
|
14
|
-
this.before("*",
|
|
15
|
+
this.before("*", (req) => {
|
|
15
16
|
if (!config.enableAdminService) {
|
|
16
17
|
req.reject(403, "Admin service is disabled by configuration");
|
|
17
18
|
}
|
|
@@ -47,6 +48,18 @@ module.exports = class AdminService extends cds.ApplicationService {
|
|
|
47
48
|
});
|
|
48
49
|
});
|
|
49
50
|
|
|
51
|
+
this.on("READ", LockService, async () => {
|
|
52
|
+
if (!config.redisEnabled) {
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
const locks = await distributedLock.getAllLocksRedis();
|
|
56
|
+
return locks.map((lock) => ({
|
|
57
|
+
...lock,
|
|
58
|
+
landscape: landscape,
|
|
59
|
+
space: space,
|
|
60
|
+
}));
|
|
61
|
+
});
|
|
62
|
+
|
|
50
63
|
this.on("READ", Tenant, async () => {
|
|
51
64
|
const tenants = await cdsHelper.getAllTenantWithSubdomain();
|
|
52
65
|
return tenants ?? [];
|