1
0
mirror of https://github.com/redis/node-redis.git synced 2025-12-09 21:21:11 +03:00
Files
node-redis/packages/client/lib/tests/test-scenario/timeout-during-notifications.e2e.ts
kiryazovi-redis a64134c55f tests: fix flaky timeout assertion in maintenance notification test (#3143)
* Add debug info to assertion messages for root cause analysis

* Fix flaky timeout assertion in maintenance notification test

* Apply same tolerance fix to NORMAL_COMMAND_TIMEOUT assertions
2025-12-05 14:11:48 +02:00

213 lines
6.7 KiB
TypeScript

import assert from "node:assert";
import { FaultInjectorClient } from "./fault-injector-client";
import {
getDatabaseConfig,
getDatabaseConfigFromEnv,
getEnvConfig,
RedisConnectionConfig,
blockCommand,
createTestClient,
} from "./test-scenario.util";
import { createClient } from "../../..";
import { before } from "mocha";
import diagnostics_channel from "node:diagnostics_channel";
import { DiagnosticsEvent } from "../../client/enterprise-maintenance-manager";
describe("Timeout Handling During Notifications", () => {
let clientConfig: RedisConnectionConfig;
let faultInjectorClient: FaultInjectorClient;
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();
const redisConfig = getDatabaseConfigFromEnv(
envConfig.redisEndpointsConfigPath
);
clientConfig = getDatabaseConfig(redisConfig);
faultInjectorClient = new FaultInjectorClient(envConfig.faultInjectorUrl);
});
beforeEach(async () => {
client = await createTestClient(clientConfig, {
commandOptions: { timeout: NORMAL_COMMAND_TIMEOUT },
maintRelaxedCommandTimeout: RELAXED_COMMAND_TIMEOUT,
});
await client.flushAll();
});
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.2,
`Normal command should timeout within normal timeout ms`
);
assert.strictEqual(
error?.constructor?.name,
"TimeoutError",
"Command Timeout error should be TimeoutError"
);
// PART 2
// Command timeout during maintenance
const notifications: Array<DiagnosticsEvent["type"]> = [
"MOVING",
"MIGRATING",
];
const result: Record<
DiagnosticsEvent["type"],
{ error: any; duration: number }
> = {};
const onMessageHandler = createNotificationMessageHandler(
client,
result,
notifications
);
diagnostics_channel.subscribe("redis.maintenance", onMessageHandler);
const { action_id: bindAndMigrateActionId } =
await faultInjectorClient.migrateAndBindAction({
bdbId: clientConfig.bdbId,
clusterIndex: 0,
});
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. Got: ${JSON.stringify(result[notification])}`
);
assert.ok(
result[notification]?.duration >= RELAXED_COMMAND_TIMEOUT * 0.8 &&
result[notification]?.duration < RELAXED_COMMAND_TIMEOUT * 1.2,
`${notification} notification should timeout within relaxed timeout. Duration: ${result[notification]?.duration}, Expected: [${RELAXED_COMMAND_TIMEOUT * 0.8}, ${RELAXED_COMMAND_TIMEOUT * 1.2})`
);
assert.strictEqual(
result[notification]?.error?.constructor?.name,
"CommandTimeoutDuringMaintenanceError",
`${notification} notification error should be CommandTimeoutDuringMaintenanceError. Got: ${result[notification]?.error?.constructor?.name}`
);
});
});
it("should unrelax command timeout after MIGRATED and MOVING", async () => {
const { action_id: migrateActionId } =
await faultInjectorClient.triggerAction({
type: "migrate",
parameters: {
cluster_index: 0,
bdb_id: clientConfig.bdbId.toString(),
},
});
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 * 0.8 &&
durationMigrate < NORMAL_COMMAND_TIMEOUT * 1.2,
`Normal command should timeout within normal timeout ms. Duration: ${durationMigrate}, Expected: [${NORMAL_COMMAND_TIMEOUT * 0.8}, ${NORMAL_COMMAND_TIMEOUT * 1.2})`
);
assert.strictEqual(
errorMigrate?.constructor?.name,
"TimeoutError",
"Command Timeout error should be TimeoutError"
);
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 * 0.8 &&
durationBind < NORMAL_COMMAND_TIMEOUT * 1.2,
`Normal command should timeout within normal timeout ms. Duration: ${durationBind}, Expected: [${NORMAL_COMMAND_TIMEOUT * 0.8}, ${NORMAL_COMMAND_TIMEOUT * 1.2})`
);
assert.strictEqual(
errorBind?.constructor?.name,
"TimeoutError",
"Command Timeout error should be TimeoutError"
);
});
});