1
0
mirror of https://github.com/redis/node-redis.git synced 2025-12-11 09:22:35 +03:00

refactor(test): improve test scenario reliability and maintainability (#3077)

* refactor(test): improve test scenario reliability and maintainability

* tests: add resp3 check test (#1)

* test: refactor connection handoff tests with enhanced spy utility (#2)

* test: add comprehensive push notification disabled scenarios (#3)

* tests: add params config tests (#4)

* tests: add feature enablement tests (#5)

---------

Co-authored-by: Nikolay Karadzhov <nikolay.karadzhov@redis.com>
This commit is contained in:
Pavel Pashov
2025-09-18 12:45:55 +03:00
committed by GitHub
parent 073db12dbb
commit b97bbbe8c6
10 changed files with 960 additions and 444 deletions

View File

@@ -2,22 +2,48 @@ import assert from "node:assert";
import { FaultInjectorClient } from "./fault-injector-client";
import {
ClientFactory,
getDatabaseConfig,
getDatabaseConfigFromEnv,
getEnvConfig,
RedisConnectionConfig,
blockSetImmediate
blockCommand,
createTestClient,
} from "./test-scenario.util";
import { createClient } from "../../..";
import { before } from "mocha";
import { TestCommandRunner } from "./test-command-runner";
import diagnostics_channel from "node:diagnostics_channel";
import { DiagnosticsEvent } from "../../client/enterprise-maintenance-manager";
describe("Timeout Handling During Notifications", () => {
let clientConfig: RedisConnectionConfig;
let clientFactory: ClientFactory;
let faultInjectorClient: FaultInjectorClient;
let defaultClient: ReturnType<typeof createClient<any, any, any, any>>;
let client: ReturnType<typeof createClient<any, any, any, any>>;
const NORMAL_COMMAND_TIMEOUT = 50;
const RELAXED_COMMAND_TIMEOUT = 2000;
/**
* Creates a handler for the `redis.maintenance` channel that will execute and block a command on the client
* when a notification is received and save the result in the `result` object.
* This is used to test that the command timeout is relaxed during notifications.
*/
const createNotificationMessageHandler = (
client: ReturnType<typeof createClient<any, any, any, any>>,
result: Record<DiagnosticsEvent["type"], { error: any; duration: number }>,
notifications: Array<DiagnosticsEvent["type"]>
) => {
return (message: unknown) => {
if (notifications.includes((message as DiagnosticsEvent).type)) {
setImmediate(async () => {
result[(message as DiagnosticsEvent).type] = await blockCommand(
async () => {
await client.set("key", "value");
}
);
});
}
};
};
before(() => {
const envConfig = getEnvConfig();
@@ -27,96 +53,64 @@ describe("Timeout Handling During Notifications", () => {
clientConfig = getDatabaseConfig(redisConfig);
faultInjectorClient = new FaultInjectorClient(envConfig.faultInjectorUrl);
clientFactory = new ClientFactory(clientConfig);
});
beforeEach(async () => {
defaultClient = await clientFactory.create("default");
await defaultClient.flushAll();
});
afterEach(async () => {
clientFactory.destroyAll();
});
it("should relax command timeout on MOVING, MIGRATING, and MIGRATED", async () => {
// PART 1
// Set very low timeout to trigger errors
const lowTimeoutClient = await clientFactory.create("lowTimeout", {
maintRelaxedCommandTimeout: 50,
client = await createTestClient(clientConfig, {
commandOptions: { timeout: NORMAL_COMMAND_TIMEOUT },
maintRelaxedCommandTimeout: RELAXED_COMMAND_TIMEOUT,
});
const { action_id: lowTimeoutBindAndMigrateActionId } =
await faultInjectorClient.migrateAndBindAction({
bdbId: clientConfig.bdbId,
clusterIndex: 0,
});
await client.flushAll();
});
const lowTimeoutWaitPromise = faultInjectorClient.waitForAction(
lowTimeoutBindAndMigrateActionId
afterEach(() => {
if (client && client.isOpen) {
client.destroy();
}
});
it("should relax command timeout on MOVING, MIGRATING", async () => {
// PART 1
// Normal command timeout
const { error, duration } = await blockCommand(async () => {
await client.set("key", "value");
});
assert.ok(
error instanceof Error,
"Command Timeout error should be instanceof Error"
);
assert.ok(
duration > NORMAL_COMMAND_TIMEOUT &&
duration < NORMAL_COMMAND_TIMEOUT * 1.1,
`Normal command should timeout within normal timeout ms`
);
const lowTimeoutCommandPromises =
await TestCommandRunner.fireCommandsUntilStopSignal(
lowTimeoutClient,
lowTimeoutWaitPromise
);
const lowTimeoutRejectedCommands = (
await Promise.all(lowTimeoutCommandPromises.commandPromises)
).filter((result) => result.status === "rejected");
assert.ok(lowTimeoutRejectedCommands.length > 0);
assert.strictEqual(
lowTimeoutRejectedCommands.filter((rejected) => {
return (
// TODO instanceof doesn't work for some reason
rejected.error.constructor.name ===
"CommandTimeoutDuringMaintananceError"
);
}).length,
lowTimeoutRejectedCommands.length
error?.constructor?.name,
"TimeoutError",
"Command Timeout error should be TimeoutError"
);
// PART 2
// Set high timeout to avoid errors
const highTimeoutClient = await clientFactory.create("highTimeout", {
maintRelaxedCommandTimeout: 10000,
});
// Command timeout during maintenance
const notifications: Array<DiagnosticsEvent["type"]> = [
"MOVING",
"MIGRATING",
];
const { action_id: highTimeoutBindAndMigrateActionId } =
await faultInjectorClient.migrateAndBindAction({
bdbId: clientConfig.bdbId,
clusterIndex: 0,
});
const result: Record<
DiagnosticsEvent["type"],
{ error: any; duration: number }
> = {};
const highTimeoutWaitPromise = faultInjectorClient.waitForAction(
highTimeoutBindAndMigrateActionId
const onMessageHandler = createNotificationMessageHandler(
client,
result,
notifications
);
const highTimeoutCommandPromises =
await TestCommandRunner.fireCommandsUntilStopSignal(
highTimeoutClient,
highTimeoutWaitPromise
);
const highTimeoutRejectedCommands = (
await Promise.all(highTimeoutCommandPromises.commandPromises)
).filter((result) => result.status === "rejected");
assert.strictEqual(highTimeoutRejectedCommands.length, 0);
});
it("should unrelax command timeout after MAINTENANCE", async () => {
const clientWithCommandTimeout = await clientFactory.create(
"clientWithCommandTimeout",
{
commandOptions: {
timeout: 100,
},
}
);
diagnostics_channel.subscribe("redis.maintenance", onMessageHandler);
const { action_id: bindAndMigrateActionId } =
await faultInjectorClient.migrateAndBindAction({
@@ -124,36 +118,173 @@ describe("Timeout Handling During Notifications", () => {
clusterIndex: 0,
});
const lowTimeoutWaitPromise = faultInjectorClient.waitForAction(
bindAndMigrateActionId
await faultInjectorClient.waitForAction(bindAndMigrateActionId);
diagnostics_channel.unsubscribe("redis.maintenance", onMessageHandler);
notifications.forEach((notification) => {
assert.ok(
result[notification]?.error instanceof Error,
`${notification} notification error should be instanceof Error`
);
assert.ok(
result[notification]?.duration > RELAXED_COMMAND_TIMEOUT &&
result[notification]?.duration < RELAXED_COMMAND_TIMEOUT * 1.1,
`${notification} notification should timeout within relaxed timeout`
);
assert.strictEqual(
result[notification]?.error?.constructor?.name,
"CommandTimeoutDuringMaintenanceError",
`${notification} notification error should be CommandTimeoutDuringMaintenanceError`
);
});
});
it("should unrelax command timeout after MIGRATED and MOVING", async () => {
const { action_id: migrateActionId } =
await faultInjectorClient.triggerAction({
type: "migrate",
parameters: {
cluster_index: 0,
},
});
await faultInjectorClient.waitForAction(migrateActionId);
// PART 1
// After migration
const { error: errorMigrate, duration: durationMigrate } =
await blockCommand(async () => {
await client.set("key", "value");
});
assert.ok(
errorMigrate instanceof Error,
"Command Timeout error should be instanceof Error"
);
assert.ok(
durationMigrate > NORMAL_COMMAND_TIMEOUT &&
durationMigrate < NORMAL_COMMAND_TIMEOUT * 1.1,
`Normal command should timeout within normal timeout ms`
);
assert.strictEqual(
errorMigrate?.constructor?.name,
"TimeoutError",
"Command Timeout error should be TimeoutError"
);
const relaxedTimeoutCommandPromises =
await TestCommandRunner.fireCommandsUntilStopSignal(
clientWithCommandTimeout,
lowTimeoutWaitPromise
);
const relaxedTimeoutRejectedCommands = (
await Promise.all(relaxedTimeoutCommandPromises.commandPromises)
).filter((result) => result.status === "rejected");
assert.ok(relaxedTimeoutRejectedCommands.length === 0);
const start = performance.now();
let error: any;
await blockSetImmediate(async () => {
try {
await clientWithCommandTimeout.set("key", "value");
} catch (err: any) {
error = err;
const { action_id: bindActionId } = await faultInjectorClient.triggerAction(
{
type: "bind",
parameters: {
bdb_id: clientConfig.bdbId.toString(),
cluster_index: 0,
},
}
);
await faultInjectorClient.waitForAction(bindActionId);
// PART 2
// After bind
const { error: errorBind, duration: durationBind } = await blockCommand(
async () => {
await client.set("key", "value");
}
);
assert.ok(
errorBind instanceof Error,
"Command Timeout error should be instanceof Error"
);
assert.ok(
durationBind > NORMAL_COMMAND_TIMEOUT &&
durationBind < NORMAL_COMMAND_TIMEOUT * 1.1,
`Normal command should timeout within normal timeout ms`
);
assert.strictEqual(
errorBind?.constructor?.name,
"TimeoutError",
"Command Timeout error should be TimeoutError"
);
});
it("should relax command timeout on FAILING_OVER", async () => {
const notifications: Array<DiagnosticsEvent["type"]> = ["FAILING_OVER"];
const result: Record<
DiagnosticsEvent["type"],
{ error: any; duration: number }
> = {};
const onMessageHandler = createNotificationMessageHandler(
client,
result,
notifications
);
diagnostics_channel.subscribe("redis.maintenance", onMessageHandler);
const { action_id: failoverActionId } =
await faultInjectorClient.triggerAction({
type: "failover",
parameters: {
bdb_id: clientConfig.bdbId.toString(),
cluster_index: 0,
},
});
await faultInjectorClient.waitForAction(failoverActionId);
diagnostics_channel.unsubscribe("redis.maintenance", onMessageHandler);
notifications.forEach((notification) => {
assert.ok(
result[notification]?.error instanceof Error,
`${notification} notification error should be instanceof Error`
);
assert.ok(
result[notification]?.duration > RELAXED_COMMAND_TIMEOUT &&
result[notification]?.duration < RELAXED_COMMAND_TIMEOUT * 1.1,
`${notification} notification should timeout within relaxed timeout`
);
assert.strictEqual(
result[notification]?.error?.constructor?.name,
"CommandTimeoutDuringMaintenanceError",
`${notification} notification error should be CommandTimeoutDuringMaintenanceError`
);
});
});
it("should unrelax command timeout after FAILED_OVER", async () => {
const { action_id: failoverActionId } =
await faultInjectorClient.triggerAction({
type: "failover",
parameters: {
bdb_id: clientConfig.bdbId.toString(),
cluster_index: 0,
},
});
await faultInjectorClient.waitForAction(failoverActionId);
const { error, duration } = await blockCommand(async () => {
await client.set("key", "value");
});
// Make sure it took less than 1sec to fail
assert.ok(performance.now() - start < 1000);
assert.ok(error instanceof Error);
assert.ok(error.constructor.name === "TimeoutError");
assert.ok(
error instanceof Error,
"Command Timeout error should be instanceof Error"
);
assert.ok(
duration > NORMAL_COMMAND_TIMEOUT &&
duration < NORMAL_COMMAND_TIMEOUT * 1.1,
`Normal command should timeout within normal timeout ms`
);
assert.strictEqual(
error?.constructor?.name,
"TimeoutError",
"Command Timeout error should be TimeoutError"
);
});
});