You've already forked node-redis
mirror of
https://github.com/redis/node-redis.git
synced 2025-12-12 21:21:15 +03:00
* feat(errors): Add specialized timeout error types for maintenance scenarios - Added `SocketTimeoutDuringMaintananceError`, a subclass of `TimeoutError`, to handle socket timeouts during maintenance. - Added `CommandTimeoutDuringMaintenanceError`, another subclass of `TimeoutError`, to address command write timeouts during maintenance. * feat(linked-list): Add EmptyAwareSinglyLinkedList and enhance DoublyLinkedList functionality - Introduced `EmptyAwareSinglyLinkedList`, a subclass of `SinglyLinkedList` that emits an `empty` event when the list becomes empty due to `reset`, `shift`, or `remove` operations. - Added `nodes()` iterator method to `DoublyLinkedList` for iterating over nodes directly. - Enhanced unit tests for `DoublyLinkedList` and `SinglyLinkedList` to cover edge cases and new functionality. - Added comprehensive tests for `EmptyAwareSinglyLinkedList` to validate `empty` event emission under various scenarios. - Improved code formatting and consistency. * refactor(commands-queue): Improve push notification handling - Replaced `setInvalidateCallback` with a more flexible `addPushHandler` method, allowing multiple handlers for push notifications. - Introduced the `PushHandler` type to standardize push notification processing. - Refactored `RedisCommandsQueue` to use a `#pushHandlers` array, enabling dynamic and modular handling of push notifications. - Updated `RedisClient` to leverage the new handler mechanism for `invalidate` push notifications, simplifying and decoupling logic. * feat(commands-queue): Add method to wait for in-flight commands to complete - Introduced `waitForInflightCommandsToComplete` method to asynchronously wait for all in-flight commands to finish processing. - Utilized the `empty` event from `#waitingForReply` to signal when all commands have been completed. * feat(commands-queue): Introduce maintenance mode support for commands-queue - Added `#maintenanceCommandTimeout` and `setMaintenanceCommandTimeout` method to dynamically adjust command timeouts during maintenance * refator(client): Extract socket event listener setup into helper method * refactor(socket): Add maintenance mode support and dynamic timeout handling - Added `#maintenanceTimeout` and `setMaintenanceTimeout` method to dynamically adjust socket timeouts during maintenance. * feat(client): Add Redis Enterprise maintenance configuration options - Added `maintPushNotifications` option to control how the client handles Redis Enterprise maintenance push notifications (`disabled`, `enabled`, `au to`). - Added `maintMovingEndpointType` option to specify the endpoint type for reconnecting during a MOVING notification (`auto`, `internal-ip`, `external-ip`, etc.). - Added `maintRelaxedCommandTimeout` option to define a relaxed timeout for commands during maintenance. - Added `maintRelaxedSocketTimeout` option to define a relaxed timeout for the socket during maintenance. - Enforced RESP3 requirement for maintenance-related features (`maintPushNotifications`). * feat(client): Add socket helpers and pause mechanism - Introduced `#paused` flag with corresponding `_pause` and `_unpause` methods to temporarily halt writing commands to the socket during maintenance windows. - Updated `#write` method to respect the `#paused` flag, preventing new commands from being written during maintenance. - Added `_ejectSocket` method to safely detach from and return the current socket - Added `_insertSocket` method to receive and start using a new socket * feat(client): Add Redis Enterprise maintenance handling capabilities - Introduced `EnterpriseMaintenanceManager` to manage Redis Enterprise maintenance events and push notifications. - Integrated `EnterpriseMaintenanceManager` into `RedisClient` to handle maintenance push notifications and manage socket transitions. - Implemented graceful handling of MOVING, MIGRATING, and FAILOVER push notifications, including socket replacement and timeout adjustments. * test: add E2E test infrastructure for Redis maintenance scenarios * test: add E2E tests for Redis Enterprise maintenance timeout handling (#3) * test: add connection handoff test --------- Co-authored-by: Pavel Pashov <pavel.pashov@redis.com> Co-authored-by: Pavel Pashov <60297174+PavelPashov@users.noreply.github.com>
198 lines
5.1 KiB
TypeScript
198 lines
5.1 KiB
TypeScript
import { readFileSync } from "fs";
|
|
import { createClient, RedisClientOptions } from "../../..";
|
|
import { stub } from "sinon";
|
|
|
|
type DatabaseEndpoint = {
|
|
addr: string[];
|
|
addr_type: string;
|
|
dns_name: string;
|
|
oss_cluster_api_preferred_endpoint_type: string;
|
|
oss_cluster_api_preferred_ip_type: string;
|
|
port: number;
|
|
proxy_policy: string;
|
|
uid: string;
|
|
};
|
|
|
|
type DatabaseConfig = {
|
|
bdb_id: number;
|
|
username: string;
|
|
password: string;
|
|
tls: boolean;
|
|
raw_endpoints: DatabaseEndpoint[];
|
|
endpoints: string[];
|
|
};
|
|
|
|
type DatabasesConfig = {
|
|
[databaseName: string]: DatabaseConfig;
|
|
};
|
|
|
|
type EnvConfig = {
|
|
redisEndpointsConfigPath: string;
|
|
faultInjectorUrl: string;
|
|
};
|
|
|
|
/**
|
|
* Reads environment variables required for the test scenario
|
|
* @returns Environment configuration object
|
|
* @throws Error if required environment variables are not set
|
|
*/
|
|
export function getEnvConfig(): EnvConfig {
|
|
if (!process.env.REDIS_ENDPOINTS_CONFIG_PATH) {
|
|
throw new Error(
|
|
"REDIS_ENDPOINTS_CONFIG_PATH environment variable must be set"
|
|
);
|
|
}
|
|
|
|
if (!process.env.FAULT_INJECTION_API_URL) {
|
|
throw new Error("FAULT_INJECTION_API_URL environment variable must be set");
|
|
}
|
|
|
|
return {
|
|
redisEndpointsConfigPath: process.env.REDIS_ENDPOINTS_CONFIG_PATH,
|
|
faultInjectorUrl: process.env.FAULT_INJECTION_API_URL,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Reads database configuration from a file
|
|
* @param filePath - The path to the database configuration file
|
|
* @returns Parsed database configuration object
|
|
* @throws Error if file doesn't exist or JSON is invalid
|
|
*/
|
|
export function getDatabaseConfigFromEnv(filePath: string): DatabasesConfig {
|
|
try {
|
|
const fileContent = readFileSync(filePath, "utf8");
|
|
return JSON.parse(fileContent) as DatabasesConfig;
|
|
} catch (error) {
|
|
throw new Error(`Failed to read or parse database config from ${filePath}`);
|
|
}
|
|
}
|
|
|
|
export interface RedisConnectionConfig {
|
|
host: string;
|
|
port: number;
|
|
username: string;
|
|
password: string;
|
|
tls: boolean;
|
|
bdbId: number;
|
|
}
|
|
|
|
/**
|
|
* Gets Redis connection parameters for a specific database
|
|
* @param databasesConfig - The parsed database configuration object
|
|
* @param databaseName - Optional name of the database to retrieve (defaults to the first one)
|
|
* @returns Redis connection configuration with host, port, username, password, and tls
|
|
* @throws Error if the specified database is not found in the configuration
|
|
*/
|
|
export function getDatabaseConfig(
|
|
databasesConfig: DatabasesConfig,
|
|
databaseName?: string
|
|
): RedisConnectionConfig {
|
|
const dbConfig = databaseName
|
|
? databasesConfig[databaseName]
|
|
: Object.values(databasesConfig)[0];
|
|
|
|
if (!dbConfig) {
|
|
throw new Error(
|
|
`Database ${databaseName ? databaseName : ""} not found in configuration`
|
|
);
|
|
}
|
|
|
|
const endpoint = dbConfig.raw_endpoints[0]; // Use the first endpoint
|
|
|
|
return {
|
|
host: endpoint.dns_name,
|
|
port: endpoint.port,
|
|
username: dbConfig.username,
|
|
password: dbConfig.password,
|
|
tls: dbConfig.tls,
|
|
bdbId: dbConfig.bdb_id,
|
|
};
|
|
}
|
|
|
|
// TODO this should be moved in the tests utils package
|
|
export async function blockSetImmediate(fn: () => Promise<unknown>) {
|
|
let setImmediateStub: any;
|
|
|
|
try {
|
|
setImmediateStub = stub(global, "setImmediate");
|
|
setImmediateStub.callsFake(() => {
|
|
//Dont call the callback, effectively blocking execution
|
|
});
|
|
await fn();
|
|
} finally {
|
|
if (setImmediateStub) {
|
|
setImmediateStub.restore();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Factory class for creating and managing Redis clients
|
|
*/
|
|
export class ClientFactory {
|
|
private readonly clients = new Map<
|
|
string,
|
|
ReturnType<typeof createClient<any, any, any, any>>
|
|
>();
|
|
|
|
constructor(private readonly config: RedisConnectionConfig) {}
|
|
|
|
/**
|
|
* Creates a new client with the specified options and connects it to the database
|
|
* @param key - The key to store the client under
|
|
* @param options - Optional client options
|
|
* @returns The created and connected client
|
|
*/
|
|
async create(key: string, options: Partial<RedisClientOptions> = {}) {
|
|
const client = createClient({
|
|
socket: {
|
|
host: this.config.host,
|
|
port: this.config.port,
|
|
...(this.config.tls === true ? { tls: true } : {}),
|
|
},
|
|
password: this.config.password,
|
|
username: this.config.username,
|
|
RESP: 3,
|
|
maintPushNotifications: "auto",
|
|
maintMovingEndpointType: "auto",
|
|
...options,
|
|
});
|
|
|
|
client.on("error", (err: Error) => {
|
|
throw new Error(`Client error: ${err.message}`);
|
|
});
|
|
|
|
await client.connect();
|
|
|
|
this.clients.set(key, client);
|
|
|
|
return client;
|
|
}
|
|
|
|
/**
|
|
* Gets an existing client by key or the first one if no key is provided
|
|
* @param key - The key of the client to retrieve
|
|
* @returns The client if found, undefined otherwise
|
|
*/
|
|
get(key?: string) {
|
|
if (key) {
|
|
return this.clients.get(key);
|
|
}
|
|
|
|
// Get the first one if no key is provided
|
|
return this.clients.values().next().value;
|
|
}
|
|
|
|
/**
|
|
* Destroys all created clients
|
|
*/
|
|
destroyAll() {
|
|
this.clients.forEach((client) => {
|
|
if (client && client.isOpen) {
|
|
client.destroy();
|
|
}
|
|
});
|
|
}
|
|
}
|