Skip to content

Commit 4ae84a5

Browse files
committed
comply with default values from hld
1 parent f64edfc commit 4ae84a5

File tree

2 files changed

+112
-84
lines changed

2 files changed

+112
-84
lines changed

packages/client/lib/client/enterprise-maintenance-manager.ts

Lines changed: 62 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -26,51 +26,81 @@ export interface SocketTimeoutUpdate {
2626
timeout?: number;
2727
}
2828

29+
const DEFAULT_OPTIONS = {
30+
maintPushNotifications: "auto",
31+
maintMovingEndpointType: "auto",
32+
maintRelaxedCommandTimeout: 1000,
33+
maintRelaxedSocketTimeout: 1000,
34+
} as const;
35+
2936
export const dbgMaintenance = (...args: any[]) => {
3037
if (!process.env.DEBUG_MAINTENANCE) return;
31-
return console.log('[MNT]', ...args);
32-
}
38+
return console.log("[MNT]", ...args);
39+
};
3340

3441
export default class EnterpriseMaintenanceManager extends EventEmitter {
3542
#commandsQueue: RedisCommandsQueue;
3643
#options: RedisClientOptions;
3744
#isMaintenance = 0;
3845

39-
static async getHandshakeCommand(tls: boolean, host: string): Promise<Array<RedisArgument>> {
40-
const movingEndpointType = await determineEndpoint(tls, host);
41-
return ["CLIENT", "MAINT_NOTIFICATIONS", "ON", "moving-endpoint-type", movingEndpointType];
46+
static async getHandshakeCommand(
47+
tls: boolean,
48+
host: string,
49+
options: RedisClientOptions,
50+
): Promise<
51+
| { cmd: Array<RedisArgument>; errorHandler: (error: Error) => void }
52+
| undefined
53+
> {
54+
if (options.maintPushNotifications === "disabled") return;
55+
56+
const movingEndpointType = await determineEndpoint(tls, host, options);
57+
return {
58+
cmd: [
59+
"CLIENT",
60+
"MAINT_NOTIFICATIONS",
61+
"ON",
62+
"moving-endpoint-type",
63+
movingEndpointType,
64+
],
65+
errorHandler: (error: Error) => {
66+
dbgMaintenance('handshake failed:', error);
67+
if(options.maintPushNotifications === 'enabled') {
68+
throw error;
69+
}
70+
},
71+
};
4272
}
4373

4474
constructor(commandsQueue: RedisCommandsQueue, options: RedisClientOptions) {
4575
super();
4676
this.#commandsQueue = commandsQueue;
47-
this.#options = options;
77+
this.#options = { ...DEFAULT_OPTIONS, ...options };
4878

4979
this.#commandsQueue.addPushHandler(this.#onPush);
5080
}
5181

5282
#onPush = (push: Array<any>): boolean => {
53-
dbgMaintenance(push.map(item => item.toString()))
83+
dbgMaintenance(push.map((item) => item.toString()));
5484
switch (push[0].toString()) {
5585
case PN.MOVING: {
5686
// [ 'MOVING', '17', '15', '54.78.247.156:12075' ]
5787
// ^seq ^after ^new ip
5888
const afterMs = push[2];
5989
const url = push[3];
6090
const [host, port] = url.toString().split(":");
61-
dbgMaintenance('Received MOVING:', afterMs, host, Number(port));
91+
dbgMaintenance("Received MOVING:", afterMs, host, Number(port));
6292
this.#onMoving(afterMs, host, Number(port));
6393
return true;
6494
}
6595
case PN.MIGRATING:
6696
case PN.FAILING_OVER: {
67-
dbgMaintenance('Received MIGRATING|FAILING_OVER');
97+
dbgMaintenance("Received MIGRATING|FAILING_OVER");
6898
this.#onMigrating();
6999
return true;
70100
}
71101
case PN.MIGRATED:
72102
case PN.FAILED_OVER: {
73-
dbgMaintenance('Received MIGRATED|FAILED_OVER');
103+
dbgMaintenance("Received MIGRATED|FAILED_OVER");
74104
this.#onMigrated();
75105
return true;
76106
}
@@ -99,16 +129,18 @@ export default class EnterpriseMaintenanceManager extends EventEmitter {
99129
this.#onMigrating();
100130

101131
// 2 [ACTION] Pause writing
102-
dbgMaintenance('Pausing writing of new commands to old socket');
132+
dbgMaintenance("Pausing writing of new commands to old socket");
103133
this.emit(MAINTENANCE_EVENTS.PAUSE_WRITING);
104134

105135
const newSocket = new RedisSocket({
106136
...this.#options.socket,
107137
host,
108138
port,
109139
});
110-
dbgMaintenance(`Set timeout for new socket to ${this.#options.gracefulMaintenance?.relaxedSocketTimeout}`);
111-
newSocket.setMaintenanceTimeout(this.#options.gracefulMaintenance?.relaxedSocketTimeout);
140+
dbgMaintenance(
141+
`Set timeout for new socket to ${this.#options.maintRelaxedSocketTimeout}`,
142+
);
143+
newSocket.setMaintenanceTimeout(this.#options.maintRelaxedSocketTimeout);
112144
dbgMaintenance(`Connecting to new socket: ${host}:${port}`);
113145
await newSocket.connect();
114146
dbgMaintenance(`Connected to new socket`);
@@ -120,7 +152,7 @@ export default class EnterpriseMaintenanceManager extends EventEmitter {
120152
// 4 [EVENT] In-flight commands completed
121153

122154
// 5 + 6
123-
dbgMaintenance('Resume writing')
155+
dbgMaintenance("Resume writing");
124156
this.emit(MAINTENANCE_EVENTS.RESUME_WRITING, newSocket);
125157
this.#onMigrated();
126158
};
@@ -130,23 +162,23 @@ export default class EnterpriseMaintenanceManager extends EventEmitter {
130162
if (this.#isMaintenance > 1) {
131163
dbgMaintenance(`Timeout relaxation already done`);
132164
return;
133-
};
165+
}
134166

135167
this.#commandsQueue.inMaintenance = true;
136168
this.#commandsQueue.setMaintenanceCommandTimeout(
137-
this.#options.gracefulMaintenance?.relaxedCommandTimeout,
169+
this.#options.maintRelaxedCommandTimeout,
138170
);
139171

140172
this.emit(MAINTENANCE_EVENTS.TIMEOUTS_UPDATE, {
141173
inMaintenance: true,
142-
timeout: this.#options.gracefulMaintenance?.relaxedSocketTimeout,
174+
timeout: this.#options.maintRelaxedSocketTimeout,
143175
} satisfies SocketTimeoutUpdate);
144176
};
145177

146178
#onMigrated = async () => {
147179
this.#isMaintenance--;
148180
assert(this.#isMaintenance >= 0);
149-
if(this.#isMaintenance > 0) {
181+
if (this.#isMaintenance > 0) {
150182
dbgMaintenance(`Not ready to unrelax timeouts yet`);
151183
return;
152184
}
@@ -161,11 +193,13 @@ export default class EnterpriseMaintenanceManager extends EventEmitter {
161193
};
162194
}
163195

164-
type MovingEndpointType =
196+
export type MovingEndpointType =
197+
| "auto"
165198
| "internal-ip"
166199
| "internal-fqdn"
167200
| "external-ip"
168-
| "external-fqdn";
201+
| "external-fqdn"
202+
| "none";
169203

170204
function isPrivateIP(ip: string): boolean {
171205
const version = isIP(ip);
@@ -191,20 +225,23 @@ function isPrivateIP(ip: string): boolean {
191225
async function determineEndpoint(
192226
tlsEnabled: boolean,
193227
host: string,
228+
options: RedisClientOptions
194229
): Promise<MovingEndpointType> {
195-
const ip = isIP(host)
196-
? host
197-
: (await lookup(host, {family: 0})).address
230+
231+
assert(options.maintMovingEndpointType !== undefined);
232+
if (options.maintMovingEndpointType !== 'auto') return options.maintMovingEndpointType;
233+
234+
const ip = isIP(host) ? host : (await lookup(host, { family: 0 })).address;
198235

199236
const isPrivate = isPrivateIP(ip);
200237

201-
let result: MovingEndpointType
238+
let result: MovingEndpointType;
202239
if (tlsEnabled) {
203240
result = isPrivate ? "internal-fqdn" : "external-fqdn";
204241
} else {
205242
result = isPrivate ? "internal-ip" : "external-ip";
206243
}
207244

208-
dbgMaintenance(`Determine endpoint format: ${result}`)
245+
dbgMaintenance(`Determine endpoint format: ${result}`);
209246
return result;
210247
}

packages/client/lib/client/index.ts

Lines changed: 50 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import { BasicClientSideCache, ClientSideCacheConfig, ClientSideCacheProvider }
2020
import { BasicCommandParser, CommandParser } from './parser';
2121
import SingleEntryCache from '../single-entry-cache';
2222
import { version } from '../../package.json'
23-
import EnterpriseMaintenanceManager, { MAINTENANCE_EVENTS, SocketTimeoutUpdate, dbgMaintenance } from './enterprise-maintenance-manager';
23+
import EnterpriseMaintenanceManager, { MAINTENANCE_EVENTS, MovingEndpointType, SocketTimeoutUpdate, dbgMaintenance } from './enterprise-maintenance-manager';
2424
import assert from 'node:assert';
2525

2626
export interface RedisClientOptions<
@@ -146,53 +146,46 @@ export interface RedisClientOptions<
146146
* Tag to append to library name that is sent to the Redis server
147147
*/
148148
clientInfoTag?: string;
149-
150149
/**
151-
* Configuration for handling Redis Enterprise graceful maintenance scenarios.
150+
* Controls how the client handles Redis Enterprise maintenance push notifications.
152151
*
153-
* When Redis Enterprise performs maintenance operations, nodes will be replaced, resulting in disconnects.
154-
* This configuration allows the client to handle these scenarios gracefully by automatically
155-
* reconnecting and managing command execution during maintenance windows.
152+
* - `disabled`: The feature is not used by the client.
153+
* - `enabled`: The client attempts to enable the feature on the server. If the server responds with an error, the connection is interrupted.
154+
* - `auto`: The client attempts to enable the feature on the server. If the server returns an error, the client disables the feature and continues.
156155
*
157-
* @example Basic graceful maintenance configuration
158-
* ```
159-
* const client = createClient({
160-
* gracefulMaintenance: {
161-
* handleFailedCommands: 'retry',
162-
* handleTimeouts: 'exception',
163-
* }
164-
* });
165-
* ```
156+
* The default is `auto`.
157+
*/
158+
maintPushNotifications?: 'disabled' | 'enabled' | 'auto';
159+
/**
160+
* Controls how the client requests the endpoint to reconnect to during a MOVING notification in Redis Enterprise maintenance.
166161
*
167-
* @example Graceful maintenance with timeout smoothing
168-
* ```
169-
* const client = createClient({
170-
* gracefulMaintenance: {
171-
* handleFailedCommands: 'retry',
172-
* handleTimeouts: 5000, // Extend timeouts to 5 seconds during maintenance
173-
* }
174-
* });
175-
* ```
162+
* - `auto`: If the connection is opened to a name or IP address that is from/resolves to a reserved private IP range, request an internal endpoint (e.g., internal-ip), otherwise an external one. If TLS is enabled, then request a FQDN.
163+
* - `internal-ip`: Enforce requesting the internal IP.
164+
* - `internal-fqdn`: Enforce requesting the internal FQDN.
165+
* - `external-ip`: Enforce requesting the external IP address.
166+
* - `external-fqdn`: Enforce requesting the external FQDN.
167+
* - `none`: Used to request a null endpoint, which tells the client to reconnect based on its current config
168+
169+
* The default is `auto`.
176170
*/
177-
gracefulMaintenance?: {
178-
/**
179-
* Designates how failed commands should be handled. A failed command is when the time isn’t sufficient to deal with the responses on the old connection before the server shuts it down
180-
*/
181-
handleFailedCommands?: 'exception' | 'retry',
182-
/**
183-
* Specifies a more relaxed timeout (in milliseconds) for commands during a maintenance window.
184-
* This helps minimize command timeouts during maintenance. If not provided, the `commandOptions.timeout`
185-
* will be used instead. Timeouts during maintenance period result in a `CommandTimeoutDuringMaintanance` error.
186-
*/
187-
relaxedCommandTimeout?: number,
188-
/**
189-
* Specifies a more relaxed timeout (in milliseconds) for the socket during a maintenance window.
190-
* This helps minimize socket timeouts during maintenance. If not provided, the `socket.timeout`
191-
* will be used instead. Timeouts during maintenance period result in a `SocketTimeoutDuringMaintanance` error.
192-
*/
193-
relaxedSocketTimeout?: number
194-
}
195-
}
171+
maintMovingEndpointType?: MovingEndpointType;
172+
/**
173+
* Specifies a more relaxed timeout (in milliseconds) for commands during a maintenance window.
174+
* This helps minimize command timeouts during maintenance. If not provided, the `commandOptions.timeout`
175+
* will be used instead. Timeouts during maintenance period result in a `CommandTimeoutDuringMaintanance` error.
176+
*
177+
* The default is 10000
178+
*/
179+
maintRelaxedCommandTimeout?: number;
180+
/**
181+
* Specifies a more relaxed timeout (in milliseconds) for the socket during a maintenance window.
182+
* This helps minimize socket timeouts during maintenance. If not provided, the `socket.timeout`
183+
* will be used instead. Timeouts during maintenance period result in a `SocketTimeoutDuringMaintanance` error.
184+
*
185+
* The default is 10000
186+
*/
187+
maintRelaxedSocketTimeout?: number;
188+
};
196189

197190
type WithCommands<
198191
RESP extends RespVersions,
@@ -519,15 +512,15 @@ export default class RedisClient<
519512
this.#queue = this.#initiateQueue();
520513
this.#socket = this.#createSocket(this.#options);
521514

522-
if(options?.gracefulMaintenance) {
515+
if(options?.maintPushNotifications !== 'disabled') {
523516
new EnterpriseMaintenanceManager(this.#queue, this.#options!)
524-
.on(MAINTENANCE_EVENTS.PAUSE_WRITING, () => this._self.#pausedForMaintenance = true )
517+
.on(MAINTENANCE_EVENTS.PAUSE_WRITING, () => this._self.#pausedForMaintenance = true)
525518
.on(MAINTENANCE_EVENTS.RESUME_WRITING, this.#resumeFromMaintenance.bind(this))
526519
.on(MAINTENANCE_EVENTS.TIMEOUTS_UPDATE, (value: SocketTimeoutUpdate) => {
527520
this._self.#socket.inMaintenance = value.inMaintenance;
528521
this._self.#socket.setMaintenanceTimeout(value.timeout);
529-
})
530-
}
522+
});
523+
};
531524

532525
if (options?.clientSideCache) {
533526
if (options.clientSideCache instanceof ClientSideCacheProvider) {
@@ -586,9 +579,13 @@ export default class RedisClient<
586579
}
587580

588581
if (options) {
582+
583+
if (!options.maintPushNotifications) options.maintPushNotifications = 'auto';
584+
589585
return RedisClient.parseOptions(options);
590586
}
591587

588+
592589
return options;
593590
}
594591

@@ -762,18 +759,12 @@ export default class RedisClient<
762759
commands.push({cmd: this.#clientSideCache.trackingOn()});
763760
}
764761

765-
if(this.#options?.gracefulMaintenance) {
766-
const socket = this.#options.socket;
767-
assert(socket !== undefined);
768-
const { tls, host } = socket as RedisTcpSocketOptions;
769-
assert(host !== undefined);
770-
commands.push({
771-
cmd: await EnterpriseMaintenanceManager.getHandshakeCommand(!!tls, host),
772-
errorHandler: (err: Error) => {
773-
dbgMaintenance("Maintenance handshake failed: ", err);
774-
}
775-
});
776-
}
762+
assert(this.#options?.socket !== undefined);
763+
const { tls, host } = this.#options?.socket as RedisTcpSocketOptions;
764+
const maintenanceHandshakeCmd = await EnterpriseMaintenanceManager.getHandshakeCommand(!!tls, host!, this.#options);
765+
if(maintenanceHandshakeCmd) {
766+
commands.push(maintenanceHandshakeCmd);
767+
};
777768

778769
return commands;
779770
}

0 commit comments

Comments
 (0)