Skip to content

Commit bfa6d10

Browse files
soccermaxMax Gruenfelder
and
Max Gruenfelder
authored
rework redis error handling (#250)
* rework redis error handling * wip * wip --------- Co-authored-by: Max Gruenfelder <[email protected]>
1 parent 394c593 commit bfa6d10

File tree

8 files changed

+57
-22
lines changed

8 files changed

+57
-22
lines changed

CHANGELOG.md

+3-1
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
66
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
77

8-
## v1.7.3 - 2024-10-XX
8+
## v1.7.3 - 2024-11-19
99

1010
### Added
1111

1212
- allow redis mode for single tenant applications
13+
- error message if redis is not available during connection check
14+
- add option `crashOnRedisUnavailable` to crash the app if redis is not available during the connection check
1315

1416
## v1.7.2 - 2024-10-22
1517

cds-plugin.js

+7-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ const cds = require("@sap/cds");
44
const cdsPackage = require("@sap/cds/package.json");
55

66
const eventQueue = require("./src");
7+
const EventQueueError = require("./src/EventQueueError");
78
const COMPONENT_NAME = "/eventQueue/plugin";
89
const SERVE_COMMAND = "serve";
910

@@ -18,5 +19,10 @@ if ((doLegacyBuildDetection && isBuild) || (!doLegacyBuildDetection && !isServe)
1819
}
1920

2021
if (Object.keys(cds.env.eventQueue ?? {}).length) {
21-
module.exports = eventQueue.initialize().catch((err) => cds.log(COMPONENT_NAME).error(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+
});
2228
}

docs/setup/index.md

+1
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ The table includes the parameter name, a description of its purpose, and the def
8080
| enableCAPTelemetry | If enabled in combination with `cap-js/telemetry`, OpenTelemetry traces about all event-queue activities are written using the `cap-js/telemetry` tracer. | false | yes |
8181
| cronTimezone | Determines whether to apply the central `cronTimezone` setting for scheduling events. If set to `true`, the event will use the defined `cronTimezone`. If set to `false`, the event will use UTC or the server's local time, based on the `utc` setting. | null | yes |
8282
| publishEventBlockList | Determines whether the publication of events to all app instances is enabled when Redis is active. If set to true, events can be published; if set to false, the publication is disabled. | true | yes |
83+
| crashOnRedisUnavailable | If enabled, the application will crash if Redis is unavailable during the connection check. | false | false |
8384

8485
# Configure Redis
8586

src/EventQueueError.js

+6-2
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ const ERROR_CODES_META = {
4141
message: "error during create client with redis-cache service",
4242
},
4343
[ERROR_CODES.REDIS_LOCAL_NO_RECONNECT]: {
44-
message: "disabled reconnect, because we are not running on cloud foundry",
44+
message: "disabled reconnect, because not running on cloud foundry",
4545
},
4646
[ERROR_CODES.MISSING_TABLE_DEFINITION]: {
4747
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 {
135135
return new EventQueueError(
136136
{
137137
name: ERROR_CODES.REDIS_CREATE_CLIENT,
138-
cause: err,
138+
...(err && { cause: err }),
139139
},
140140
message
141141
);
@@ -325,6 +325,10 @@ class EventQueueError extends VError {
325325
message
326326
);
327327
}
328+
329+
static isRedisConnectionFailure(err) {
330+
return err instanceof VError && err.name === ERROR_CODES.REDIS_CREATE_CLIENT;
331+
}
328332
}
329333

330334
module.exports = EventQueueError;

src/config.js

+9
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ class Config {
7777
#unsubscribedTenants = {};
7878
#cronTimezone;
7979
#publishEventBlockList;
80+
#crashOnRedisUnavailable;
8081
static #instance;
8182
constructor() {
8283
this.#logger = cds.log(COMPONENT_NAME);
@@ -510,6 +511,14 @@ class Config {
510511
this.#publishEventBlockList = value;
511512
}
512513

514+
get crashOnRedisUnavailable() {
515+
return this.#crashOnRedisUnavailable;
516+
}
517+
518+
set crashOnRedisUnavailable(value) {
519+
this.#crashOnRedisUnavailable = value;
520+
}
521+
513522
set globalTxTimeout(value) {
514523
this.#globalTxTimeout = value;
515524
}

src/index.d.ts

+2
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,8 @@ declare class Config {
182182
hasEventAfterCommitFlag(type: string, subType: string): boolean;
183183
_checkRedisIsBound(): boolean;
184184
checkRedisEnabled(): boolean;
185+
publishEventBlockList(): boolean;
186+
crashOnRedisUnavailable(): boolean;
185187
attachConfigChangeHandler(): void;
186188
attachRedisUnsubscribeHandler(): void;
187189
executeUnsubscribeHandlers(tenantId: string): void;

src/initialize.js

+6
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ const eventQueueAsOutbox = require("./outbox/eventQueueAsOutbox");
1616
const { getAllTenantIds } = require("./shared/cdsHelper");
1717
const { EventProcessingStatus } = require("./constants");
1818
const distributedLock = require("./shared/distributedLock");
19+
const EventQueueError = require("./EventQueueError");
1920

2021
const readFileAsync = promisify(fs.readFile);
2122

@@ -39,6 +40,7 @@ const CONFIG_VARS = [
3940
["enableCAPTelemetry", false],
4041
["cronTimezone", null],
4142
["publishEventBlockList", true],
43+
["crashOnRedisUnavailable", false],
4244
];
4345

4446
/**
@@ -61,6 +63,7 @@ const CONFIG_VARS = [
6163
* @param {boolean} [options.enableCAPTelemetry=false] - Enable telemetry for CAP.
6264
* @param {string} [options.cronTimezone=null] - Default timezone for cron jobs.
6365
* @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
6467
*/
6568
const initialize = async (options = {}) => {
6669
if (config.initialized) {
@@ -89,6 +92,9 @@ const initialize = async (options = {}) => {
8992
});
9093
if (redisEnabled) {
9194
config.redisEnabled = await redis.connectionCheck(config.redisOptions);
95+
if (!config.redisEnabled && config.crashOnRedisUnavailable) {
96+
throw EventQueueError.redisConnectionFailure();
97+
}
9298
}
9399
config.fileContent = await readConfigFromFile(config.configFilePath);
94100

src/shared/redis.js

+23-18
Original file line numberDiff line numberDiff line change
@@ -50,25 +50,27 @@ const _createClientBase = (redisOptions) => {
5050
}
5151
};
5252

53-
const createClientAndConnect = async (options, errorHandlerCreateClient) => {
53+
const createClientAndConnect = async (options, errorHandlerCreateClient, isConnectionCheck) => {
5454
try {
5555
const client = _createClientBase(options);
56-
await client.connect();
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-
});
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+
});
6464

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-
});
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();
7274
return client;
7375
} catch (err) {
7476
errorHandlerCreateClient(err);
@@ -119,7 +121,7 @@ const _resilientClientClose = async (client) => {
119121

120122
const connectionCheck = async (options) => {
121123
return new Promise((resolve, reject) => {
122-
createClientAndConnect(options, reject)
124+
createClientAndConnect(options, reject, true)
123125
.then((client) => {
124126
if (client) {
125127
_resilientClientClose(client);
@@ -131,7 +133,10 @@ const connectionCheck = async (options) => {
131133
.catch(reject);
132134
})
133135
.then(() => true)
134-
.catch(() => false);
136+
.catch((err) => {
137+
cds.log(COMPONENT_NAME).error("Redis connection check failed! Falling back to NO_REDIS mode", err);
138+
return false;
139+
});
135140
};
136141

137142
module.exports = {

0 commit comments

Comments
 (0)