@cap-js-community/event-queue 1.7.2 → 1.7.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/cds-plugin.js +7 -1
- package/package.json +1 -1
- package/src/EventQueueError.js +6 -2
- package/src/config.js +9 -0
- package/src/index.d.ts +2 -0
- package/src/initialize.js +17 -7
- package/src/runner/runner.js +81 -5
- package/src/shared/common.js +4 -0
- package/src/shared/redis.js +23 -18
package/cds-plugin.js
CHANGED
|
@@ -4,6 +4,7 @@ const cds = require("@sap/cds");
|
|
|
4
4
|
const cdsPackage = require("@sap/cds/package.json");
|
|
5
5
|
|
|
6
6
|
const eventQueue = require("./src");
|
|
7
|
+
const EventQueueError = require("./src/EventQueueError");
|
|
7
8
|
const COMPONENT_NAME = "/eventQueue/plugin";
|
|
8
9
|
const SERVE_COMMAND = "serve";
|
|
9
10
|
|
|
@@ -18,5 +19,10 @@ if ((doLegacyBuildDetection && isBuild) || (!doLegacyBuildDetection && !isServe)
|
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
if (Object.keys(cds.env.eventQueue ?? {}).length) {
|
|
21
|
-
module.exports = eventQueue.initialize().catch((err) =>
|
|
22
|
+
module.exports = eventQueue.initialize().catch((err) => {
|
|
23
|
+
if (EventQueueError.isRedisConnectionFailure(err) && eventQueue.config.crashOnRedisUnavailable) {
|
|
24
|
+
throw err;
|
|
25
|
+
}
|
|
26
|
+
cds.log(COMPONENT_NAME).error(err);
|
|
27
|
+
});
|
|
22
28
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cap-js-community/event-queue",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.3",
|
|
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",
|
package/src/EventQueueError.js
CHANGED
|
@@ -41,7 +41,7 @@ const ERROR_CODES_META = {
|
|
|
41
41
|
message: "error during create client with redis-cache service",
|
|
42
42
|
},
|
|
43
43
|
[ERROR_CODES.REDIS_LOCAL_NO_RECONNECT]: {
|
|
44
|
-
message: "disabled reconnect, because
|
|
44
|
+
message: "disabled reconnect, because not running on cloud foundry",
|
|
45
45
|
},
|
|
46
46
|
[ERROR_CODES.MISSING_TABLE_DEFINITION]: {
|
|
47
47
|
message: "Could not find table in csn. Make sure the provided table name is correct and the table is known by CDS.",
|
|
@@ -135,7 +135,7 @@ class EventQueueError extends VError {
|
|
|
135
135
|
return new EventQueueError(
|
|
136
136
|
{
|
|
137
137
|
name: ERROR_CODES.REDIS_CREATE_CLIENT,
|
|
138
|
-
cause: err,
|
|
138
|
+
...(err && { cause: err }),
|
|
139
139
|
},
|
|
140
140
|
message
|
|
141
141
|
);
|
|
@@ -325,6 +325,10 @@ class EventQueueError extends VError {
|
|
|
325
325
|
message
|
|
326
326
|
);
|
|
327
327
|
}
|
|
328
|
+
|
|
329
|
+
static isRedisConnectionFailure(err) {
|
|
330
|
+
return err instanceof VError && err.name === ERROR_CODES.REDIS_CREATE_CLIENT;
|
|
331
|
+
}
|
|
328
332
|
}
|
|
329
333
|
|
|
330
334
|
module.exports = EventQueueError;
|
package/src/config.js
CHANGED
|
@@ -77,6 +77,7 @@ class Config {
|
|
|
77
77
|
#unsubscribedTenants = {};
|
|
78
78
|
#cronTimezone;
|
|
79
79
|
#publishEventBlockList;
|
|
80
|
+
#crashOnRedisUnavailable;
|
|
80
81
|
static #instance;
|
|
81
82
|
constructor() {
|
|
82
83
|
this.#logger = cds.log(COMPONENT_NAME);
|
|
@@ -510,6 +511,14 @@ class Config {
|
|
|
510
511
|
this.#publishEventBlockList = value;
|
|
511
512
|
}
|
|
512
513
|
|
|
514
|
+
get crashOnRedisUnavailable() {
|
|
515
|
+
return this.#crashOnRedisUnavailable;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
set crashOnRedisUnavailable(value) {
|
|
519
|
+
this.#crashOnRedisUnavailable = value;
|
|
520
|
+
}
|
|
521
|
+
|
|
513
522
|
set globalTxTimeout(value) {
|
|
514
523
|
this.#globalTxTimeout = value;
|
|
515
524
|
}
|
package/src/index.d.ts
CHANGED
|
@@ -182,6 +182,8 @@ declare class Config {
|
|
|
182
182
|
hasEventAfterCommitFlag(type: string, subType: string): boolean;
|
|
183
183
|
_checkRedisIsBound(): boolean;
|
|
184
184
|
checkRedisEnabled(): boolean;
|
|
185
|
+
publishEventBlockList(): boolean;
|
|
186
|
+
crashOnRedisUnavailable(): boolean;
|
|
185
187
|
attachConfigChangeHandler(): void;
|
|
186
188
|
attachRedisUnsubscribeHandler(): void;
|
|
187
189
|
executeUnsubscribeHandlers(tenantId: string): void;
|
package/src/initialize.js
CHANGED
|
@@ -16,6 +16,7 @@ const eventQueueAsOutbox = require("./outbox/eventQueueAsOutbox");
|
|
|
16
16
|
const { getAllTenantIds } = require("./shared/cdsHelper");
|
|
17
17
|
const { EventProcessingStatus } = require("./constants");
|
|
18
18
|
const distributedLock = require("./shared/distributedLock");
|
|
19
|
+
const EventQueueError = require("./EventQueueError");
|
|
19
20
|
|
|
20
21
|
const readFileAsync = promisify(fs.readFile);
|
|
21
22
|
|
|
@@ -39,6 +40,7 @@ const CONFIG_VARS = [
|
|
|
39
40
|
["enableCAPTelemetry", false],
|
|
40
41
|
["cronTimezone", null],
|
|
41
42
|
["publishEventBlockList", true],
|
|
43
|
+
["crashOnRedisUnavailable", false],
|
|
42
44
|
];
|
|
43
45
|
|
|
44
46
|
/**
|
|
@@ -61,6 +63,7 @@ const CONFIG_VARS = [
|
|
|
61
63
|
* @param {boolean} [options.enableCAPTelemetry=false] - Enable telemetry for CAP.
|
|
62
64
|
* @param {string} [options.cronTimezone=null] - Default timezone for cron jobs.
|
|
63
65
|
* @param {string} [options.publishEventBlockList=true] - If redis is available event blocklist is distributed to all application instances
|
|
66
|
+
* @param {string} [options.crashOnRedisUnavailable=true] - If enabled an error is thrown if the redis connection check is not successful
|
|
64
67
|
*/
|
|
65
68
|
const initialize = async (options = {}) => {
|
|
66
69
|
if (config.initialized) {
|
|
@@ -89,6 +92,9 @@ const initialize = async (options = {}) => {
|
|
|
89
92
|
});
|
|
90
93
|
if (redisEnabled) {
|
|
91
94
|
config.redisEnabled = await redis.connectionCheck(config.redisOptions);
|
|
95
|
+
if (!config.redisEnabled && config.crashOnRedisUnavailable) {
|
|
96
|
+
throw EventQueueError.redisConnectionFailure();
|
|
97
|
+
}
|
|
92
98
|
}
|
|
93
99
|
config.fileContent = await readConfigFromFile(config.configFilePath);
|
|
94
100
|
|
|
@@ -139,17 +145,21 @@ const registerEventProcessors = () => {
|
|
|
139
145
|
|
|
140
146
|
const errorHandler = (err) => cds.log(COMPONENT).error("error during init runner", err);
|
|
141
147
|
|
|
142
|
-
if (!config.isMultiTenancy) {
|
|
143
|
-
runner.singleTenant().catch(errorHandler);
|
|
144
|
-
return;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
148
|
if (config.redisEnabled) {
|
|
148
149
|
initEventQueueRedisSubscribe();
|
|
149
150
|
config.attachConfigChangeHandler();
|
|
150
|
-
|
|
151
|
-
|
|
151
|
+
if (config.isMultiTenancy) {
|
|
152
|
+
runner.multiTenancyRedis().catch(errorHandler);
|
|
153
|
+
} else {
|
|
154
|
+
runner.singleTenantRedis().catch(errorHandler);
|
|
155
|
+
}
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (config.isMultiTenancy) {
|
|
152
160
|
runner.multiTenancyDb().catch(errorHandler);
|
|
161
|
+
} else {
|
|
162
|
+
runner.singleTenantDb().catch(errorHandler);
|
|
153
163
|
}
|
|
154
164
|
};
|
|
155
165
|
|
package/src/runner/runner.js
CHANGED
|
@@ -27,12 +27,14 @@ let OFFSET_FIRST_RUN = 10 * 1000;
|
|
|
27
27
|
let tenantIdHash;
|
|
28
28
|
let singleRunDone;
|
|
29
29
|
|
|
30
|
-
const
|
|
30
|
+
const singleTenantDb = () => _scheduleFunction(_checkPeriodicEventsSingleTenantOneTime, _singleTenantDb);
|
|
31
31
|
|
|
32
32
|
const multiTenancyDb = () => _scheduleFunction(async () => {}, _multiTenancyDb);
|
|
33
33
|
|
|
34
34
|
const multiTenancyRedis = () => _scheduleFunction(async () => {}, _multiTenancyRedis);
|
|
35
35
|
|
|
36
|
+
const singleTenantRedis = () => _scheduleFunction(_checkPeriodicEventsSingleTenantOneTime, _singleTenantRedis);
|
|
37
|
+
|
|
36
38
|
const _scheduleFunction = async (singleRunFn, periodicFn) => {
|
|
37
39
|
const logger = cds.log(COMPONENT_NAME);
|
|
38
40
|
const eventsForAutomaticRun = eventQueueConfig.allEvents;
|
|
@@ -300,6 +302,54 @@ const _singleTenantDb = async () => {
|
|
|
300
302
|
);
|
|
301
303
|
};
|
|
302
304
|
|
|
305
|
+
const _singleTenantRedis = async () => {
|
|
306
|
+
const id = cds.utils.uuid();
|
|
307
|
+
const logger = cds.log(COMPONENT_NAME);
|
|
308
|
+
try {
|
|
309
|
+
// NOTE: do checks for open events on one app instance distribute from this instance to all others
|
|
310
|
+
const dummyContext = new cds.EventContext({});
|
|
311
|
+
const couldAcquireLock = await trace(
|
|
312
|
+
dummyContext,
|
|
313
|
+
"acquire-lock-master-runner",
|
|
314
|
+
async () => {
|
|
315
|
+
return await distributedLock.acquireLock(dummyContext, EVENT_QUEUE_RUN_REDIS_CHECK, {
|
|
316
|
+
expiryTime: eventQueueConfig.runInterval * 0.95,
|
|
317
|
+
tenantScoped: false,
|
|
318
|
+
});
|
|
319
|
+
},
|
|
320
|
+
{ newRootSpan: true }
|
|
321
|
+
);
|
|
322
|
+
if (!couldAcquireLock) {
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
await trace(
|
|
327
|
+
{ id },
|
|
328
|
+
"get-openEvents-and-publish",
|
|
329
|
+
async () => {
|
|
330
|
+
return await cds.tx({}, async (tx) => {
|
|
331
|
+
const entries = await openEvents.getOpenQueueEntries(tx);
|
|
332
|
+
logger.info("broadcasting events for run", {
|
|
333
|
+
entries: entries.length,
|
|
334
|
+
});
|
|
335
|
+
if (!entries.length) {
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
// Do not wait until this is finished - as broadcastEvent has a retry mechanism and can delay this loop
|
|
339
|
+
redisPub.broadcastEvent(null, entries).catch((err) => {
|
|
340
|
+
logger.error("broadcasting event failed", err, {
|
|
341
|
+
entries: entries.length,
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
});
|
|
345
|
+
},
|
|
346
|
+
{ newRootSpan: true }
|
|
347
|
+
);
|
|
348
|
+
} catch (err) {
|
|
349
|
+
logger.info("executing event queue run for single tenant via redis", err);
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
|
|
303
353
|
const _acquireRunId = async (context) => {
|
|
304
354
|
let runId = randomUUID();
|
|
305
355
|
const couldSetValue = await distributedLock.setValueWithExpire(context, EVENT_QUEUE_RUN_ID, runId, {
|
|
@@ -401,9 +451,33 @@ const _multiTenancyPeriodicEvents = async (tenantIds) => {
|
|
|
401
451
|
}
|
|
402
452
|
};
|
|
403
453
|
|
|
404
|
-
const _checkPeriodicEventsSingleTenantOneTime = async () =>
|
|
405
|
-
|
|
406
|
-
|
|
454
|
+
const _checkPeriodicEventsSingleTenantOneTime = async () => {
|
|
455
|
+
const logger = cds.log(COMPONENT_NAME);
|
|
456
|
+
if (!eventQueueConfig.updatePeriodicEvents || !eventQueueConfig.periodicEvents.length) {
|
|
457
|
+
logger.info("updating of periodic events is disabled or no periodic events configured", {
|
|
458
|
+
updateEnabled: eventQueueConfig.updatePeriodicEvents,
|
|
459
|
+
events: eventQueueConfig.periodicEvents.length,
|
|
460
|
+
});
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const dummyContext = new cds.EventContext({});
|
|
465
|
+
return await trace(
|
|
466
|
+
dummyContext,
|
|
467
|
+
"update-periodic-events",
|
|
468
|
+
async () => {
|
|
469
|
+
const couldAcquireLock = await distributedLock.acquireLock(dummyContext, EVENT_QUEUE_UPDATE_PERIODIC_EVENTS, {
|
|
470
|
+
expiryTime: 60 * 1000,
|
|
471
|
+
tenantScoped: false,
|
|
472
|
+
});
|
|
473
|
+
if (!couldAcquireLock) {
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
return await cds.tx({}, async (tx) => await periodicEvents.checkAndInsertPeriodicEvents(tx.context));
|
|
477
|
+
},
|
|
478
|
+
{ newRootSpan: true }
|
|
479
|
+
);
|
|
480
|
+
};
|
|
407
481
|
|
|
408
482
|
const _checkPeriodicEventsSingleTenant = async (context) => {
|
|
409
483
|
const logger = cds.log(COMPONENT_NAME);
|
|
@@ -429,12 +503,14 @@ const _checkPeriodicEventsSingleTenant = async (context) => {
|
|
|
429
503
|
};
|
|
430
504
|
|
|
431
505
|
module.exports = {
|
|
432
|
-
|
|
506
|
+
singleTenantDb,
|
|
433
507
|
multiTenancyDb,
|
|
434
508
|
multiTenancyRedis,
|
|
509
|
+
singleTenantRedis,
|
|
435
510
|
__: {
|
|
436
511
|
_singleTenantDb,
|
|
437
512
|
_multiTenancyRedis,
|
|
513
|
+
_singleTenantRedis,
|
|
438
514
|
_multiTenancyDb,
|
|
439
515
|
_calculateOffsetForFirstRun,
|
|
440
516
|
_acquireRunId,
|
package/src/shared/common.js
CHANGED
package/src/shared/redis.js
CHANGED
|
@@ -50,25 +50,27 @@ const _createClientBase = (redisOptions) => {
|
|
|
50
50
|
}
|
|
51
51
|
};
|
|
52
52
|
|
|
53
|
-
const createClientAndConnect = async (options, errorHandlerCreateClient) => {
|
|
53
|
+
const createClientAndConnect = async (options, errorHandlerCreateClient, isConnectionCheck) => {
|
|
54
54
|
try {
|
|
55
55
|
const client = _createClientBase(options);
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
56
|
+
if (!isConnectionCheck) {
|
|
57
|
+
client.on("error", (err) => {
|
|
58
|
+
const dateNow = Date.now();
|
|
59
|
+
if (dateNow - lastErrorLog > LOG_AFTER_SEC * 1000) {
|
|
60
|
+
cds.log(COMPONENT_NAME).error("error from redis client for pub/sub failed", err);
|
|
61
|
+
lastErrorLog = dateNow;
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
64
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
65
|
+
client.on("reconnecting", () => {
|
|
66
|
+
const dateNow = Date.now();
|
|
67
|
+
if (dateNow - lastErrorLog > LOG_AFTER_SEC * 1000) {
|
|
68
|
+
cds.log(COMPONENT_NAME).info("redis client trying reconnect...");
|
|
69
|
+
lastErrorLog = dateNow;
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
await client.connect();
|
|
72
74
|
return client;
|
|
73
75
|
} catch (err) {
|
|
74
76
|
errorHandlerCreateClient(err);
|
|
@@ -119,7 +121,7 @@ const _resilientClientClose = async (client) => {
|
|
|
119
121
|
|
|
120
122
|
const connectionCheck = async (options) => {
|
|
121
123
|
return new Promise((resolve, reject) => {
|
|
122
|
-
createClientAndConnect(options, reject)
|
|
124
|
+
createClientAndConnect(options, reject, true)
|
|
123
125
|
.then((client) => {
|
|
124
126
|
if (client) {
|
|
125
127
|
_resilientClientClose(client);
|
|
@@ -131,7 +133,10 @@ const connectionCheck = async (options) => {
|
|
|
131
133
|
.catch(reject);
|
|
132
134
|
})
|
|
133
135
|
.then(() => true)
|
|
134
|
-
.catch(() =>
|
|
136
|
+
.catch((err) => {
|
|
137
|
+
cds.log(COMPONENT_NAME).error("Redis connection check failed! Falling back to NO_REDIS mode", err);
|
|
138
|
+
return false;
|
|
139
|
+
});
|
|
135
140
|
};
|
|
136
141
|
|
|
137
142
|
module.exports = {
|