Skip to content

Commit 1c9c907

Browse files
committed
feat: support client setinfo
1 parent 5befe70 commit 1c9c907

File tree

4 files changed

+235
-1
lines changed

4 files changed

+235
-1
lines changed

lib/redis/RedisOptions.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,20 @@ export interface CommonRedisOptions extends CommanderOptions {
4444
*/
4545
connectionName?: string;
4646

47+
/**
48+
* If true, skips setting library info via CLIENT SETINFO.
49+
* @link https://redis.io/docs/latest/commands/client-setinfo/
50+
* @default false
51+
*/
52+
disableClientInfo?: boolean;
53+
54+
/**
55+
* Tag to append to the library name in CLIENT SETINFO (ioredis(tag)).
56+
* @link https://redis.io/docs/latest/commands/client-setinfo/
57+
* @default undefined
58+
*/
59+
clientInfoTag?: string;
60+
4761
/**
4862
* If set, client will send AUTH command with the value of this option as the first argument when connected.
4963
* This is supported since Redis 6.
@@ -208,6 +222,8 @@ export const DEFAULT_REDIS_OPTIONS: RedisOptions = {
208222
keepAlive: 0,
209223
noDelay: true,
210224
connectionName: null,
225+
disableClientInfo: false,
226+
clientInfoTag: undefined,
211227
// Sentinel
212228
sentinels: null,
213229
name: null,

lib/redis/event_handler.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { MaxRetriesPerRequestError } from "../errors";
77
import { CommandItem, Respondable } from "../types";
88
import { Debug, noop, CONNECTION_CLOSED_ERROR_MSG } from "../utils";
99
import DataHandler from "../DataHandler";
10+
import { version } from "../../package.json";
1011

1112
const debug = Debug("connection");
1213

@@ -264,6 +265,21 @@ export function readyHandler(self) {
264265
self.client("setname", self.options.connectionName).catch(noop);
265266
}
266267

268+
if (!self.options.disableClientInfo) {
269+
debug("set the client info");
270+
self.client("SETINFO", "LIB-VER", version).catch(noop);
271+
272+
self
273+
.client(
274+
"SETINFO",
275+
"LIB-NAME",
276+
self.options.clientInfoTag
277+
? `ioredis(${self.options.clientInfoTag})`
278+
: "ioredis"
279+
)
280+
.catch(noop);
281+
}
282+
267283
if (self.options.readOnly) {
268284
debug("set the connection to readonly mode");
269285
self.readonly().catch(noop);

test/functional/client_info.ts

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
import { expect } from "chai";
2+
import Redis, { Cluster } from "../../lib";
3+
import MockServer from "../helpers/mock_server";
4+
5+
describe("clientInfo", function () {
6+
describe("Redis", function () {
7+
let redis: Redis;
8+
let mockServer: MockServer;
9+
let clientInfoCommands: Array<{ key: string; value: string }>;
10+
11+
beforeEach(() => {
12+
clientInfoCommands = [];
13+
mockServer = new MockServer(30001, (argv) => {
14+
if (
15+
argv[0].toLowerCase() === "client" &&
16+
argv[1].toLowerCase() === "setinfo"
17+
) {
18+
clientInfoCommands.push({
19+
key: argv[2],
20+
value: argv[3],
21+
});
22+
}
23+
});
24+
});
25+
26+
afterEach(() => {
27+
mockServer.disconnect();
28+
29+
if (redis && redis.status !== "end") {
30+
redis.disconnect();
31+
}
32+
});
33+
34+
it("should send client info by default", async () => {
35+
redis = new Redis({ port: 30001 });
36+
37+
// Wait for the client info to be sent, as it happens after the ready event
38+
await redis.ping();
39+
40+
expect(clientInfoCommands).to.have.length(2);
41+
42+
const libVerCommand = clientInfoCommands.find(
43+
(cmd) => cmd.key === "LIB-VER"
44+
);
45+
const libNameCommand = clientInfoCommands.find(
46+
(cmd) => cmd.key === "LIB-NAME"
47+
);
48+
49+
expect(libVerCommand).to.exist; // version will change over time
50+
expect(libNameCommand).to.exist;
51+
expect(libNameCommand?.value).to.equal("ioredis");
52+
});
53+
54+
it("should not send client info when disableClientInfo is true", async () => {
55+
redis = new Redis({ port: 30001, disableClientInfo: true });
56+
57+
// Wait for the client info to be sent, as it happens after the ready event
58+
await redis.ping();
59+
60+
expect(clientInfoCommands).to.have.length(0);
61+
});
62+
63+
it("should append tag to library name when clientInfoTag is set", async () => {
64+
redis = new Redis({ port: 30001, clientInfoTag: "tag-test" });
65+
66+
// Wait for the client info to be sent, as it happens after the ready event
67+
await redis.ping();
68+
69+
expect(clientInfoCommands).to.have.length(2);
70+
71+
const libNameCommand = clientInfoCommands.find(
72+
(cmd) => cmd.key === "LIB-NAME"
73+
);
74+
expect(libNameCommand).to.exist;
75+
expect(libNameCommand?.value).to.equal("ioredis(tag-test)");
76+
});
77+
78+
it("should send client info after reconnection", async () => {
79+
redis = new Redis({ port: 30001 });
80+
81+
// Wait for the client info to be sent, as it happens after the ready event
82+
await redis.ping();
83+
redis.disconnect();
84+
85+
// Make sure the client is disconnected
86+
await new Promise<void>((resolve) => {
87+
redis.once("end", () => {
88+
resolve();
89+
});
90+
});
91+
92+
await redis.connect();
93+
await redis.ping();
94+
95+
expect(clientInfoCommands).to.have.length(4);
96+
});
97+
});
98+
99+
describe("Error handling", () => {
100+
let mockServer: MockServer;
101+
let redis: Redis;
102+
103+
afterEach(() => {
104+
mockServer.disconnect();
105+
redis.disconnect();
106+
});
107+
108+
it("should handle server that doesn't support CLIENT SETINFO", async () => {
109+
mockServer = new MockServer(30002, (argv) => {
110+
if (
111+
argv[0].toLowerCase() === "client" &&
112+
argv[1].toLowerCase() === "setinfo"
113+
) {
114+
// Simulate older Redis version that doesn't support SETINFO
115+
return new Error("ERR unknown subcommand 'SETINFO'");
116+
}
117+
});
118+
119+
redis = new Redis({ port: 30002 });
120+
await redis.ping();
121+
122+
expect(redis.status).to.equal("ready");
123+
});
124+
});
125+
126+
describe("Cluster", () => {
127+
let cluster: Cluster;
128+
let mockServers: MockServer[];
129+
let clientInfoCommands: Array<{ key: string; value: string }>;
130+
const slotTable = [
131+
[0, 5000, ["127.0.0.1", 30001]],
132+
[5001, 9999, ["127.0.0.1", 30002]],
133+
[10000, 16383, ["127.0.0.1", 30003]],
134+
];
135+
136+
beforeEach(() => {
137+
clientInfoCommands = [];
138+
139+
// Create mock server that handles both cluster commands and client info
140+
const handler = (argv) => {
141+
if (argv[0] === "cluster" && argv[1] === "SLOTS") {
142+
return slotTable;
143+
}
144+
if (
145+
argv[0].toLowerCase() === "client" &&
146+
argv[1].toLowerCase() === "setinfo"
147+
) {
148+
clientInfoCommands.push({
149+
key: argv[2],
150+
value: argv[3],
151+
});
152+
}
153+
};
154+
155+
mockServers = [
156+
new MockServer(30001, handler),
157+
new MockServer(30002, handler),
158+
new MockServer(30003, handler),
159+
];
160+
});
161+
162+
afterEach(() => {
163+
mockServers.forEach((server) => server.disconnect());
164+
if (cluster) {
165+
cluster.disconnect();
166+
}
167+
});
168+
169+
it("should send client info by default", async () => {
170+
cluster = new Redis.Cluster([{ host: "127.0.0.1", port: 30001 }]);
171+
172+
// Wait for cluster to be ready and send a command to ensure connection
173+
await cluster.ping();
174+
175+
// Should have sent 2 SETINFO commands (LIB-VER and LIB-NAME)
176+
expect(clientInfoCommands).to.have.length.at.least(2);
177+
178+
const libVerCommand = clientInfoCommands.find(
179+
(cmd) => cmd.key === "LIB-VER"
180+
);
181+
const libNameCommand = clientInfoCommands.find(
182+
(cmd) => cmd.key === "LIB-NAME"
183+
);
184+
185+
expect(libVerCommand).to.exist;
186+
expect(libNameCommand).to.exist;
187+
expect(libNameCommand?.value).to.equal("ioredis");
188+
});
189+
190+
it("should propagate disableClientInfo to child nodes", async () => {
191+
cluster = new Redis.Cluster([{ host: "127.0.0.1", port: 30001 }], {
192+
redisOptions: {
193+
disableClientInfo: true,
194+
},
195+
});
196+
await cluster.ping();
197+
198+
expect(clientInfoCommands).to.have.length(0);
199+
});
200+
});
201+
});

tsconfig.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010
],
1111
"moduleResolution": "node",
1212
"module": "commonjs",
13-
"outDir": "./built"
13+
"outDir": "./built",
14+
"resolveJsonModule": true,
1415
},
1516
"include": ["./lib/**/*"]
1617
}

0 commit comments

Comments
 (0)