You've already forked node-redis
mirror of
https://github.com/redis/node-redis.git
synced 2025-12-09 21:21:11 +03:00
* 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
213 lines
6.7 KiB
TypeScript
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"
|
|
);
|
|
});
|
|
});
|