Skip to content

Commit 2d8e730

Browse files
committed
feat: RedisOptions.familyFallback
Signed-off-by: David Bauch <[email protected]>
1 parent 2628db0 commit 2d8e730

File tree

3 files changed

+161
-1
lines changed

3 files changed

+161
-1
lines changed

lib/redis/RedisOptions.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,9 @@ export interface CommonRedisOptions extends CommanderOptions {
192192

193193
export type RedisOptions = CommonRedisOptions &
194194
SentinelConnectionOptions &
195-
StandaloneConnectionOptions;
195+
StandaloneConnectionOptions & {
196+
familyFallback?: boolean;
197+
};
196198

197199
export const DEFAULT_REDIS_OPTIONS: RedisOptions = {
198200
// Connection

lib/redis/event_handler.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,11 @@ export function closeHandler(self) {
198198
self.setStatus("reconnecting", retryDelay);
199199
self.reconnectTimeout = setTimeout(function () {
200200
self.reconnectTimeout = null;
201+
if (self.options.familyFallback === true) {
202+
// alternate between 4 and 6
203+
const family = self.options.family;
204+
self.options.family = family === 6 ? 4 : 6;
205+
}
201206
self.connect().catch(noop);
202207
}, retryDelay);
203208

test/unit/redis.ts

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,159 @@ describe("Redis", () => {
128128
});
129129
});
130130

131+
describe(".options.familyFallback", () => {
132+
it("should fail to connect to invalid host", (done) => {
133+
let errorEmitted = false;
134+
135+
const redis = new Redis({
136+
host: "invalid-hostname",
137+
retryStrategy: null,
138+
reconnectOnError: () => false,
139+
});
140+
141+
redis.on("error", (err) => {
142+
if (!errorEmitted) {
143+
try {
144+
expect(err).to.be.instanceOf(Error);
145+
expect(err.message).to.contain("ENOTFOUND");
146+
done();
147+
} catch (assertionError) {
148+
done(assertionError);
149+
}
150+
}
151+
});
152+
});
153+
154+
it("should connect via IPv6 if family is 0", (done) => {
155+
const redis = new Redis({
156+
host: "localhost",
157+
family: 0,
158+
retryStrategy: null,
159+
reconnectOnError: () => false,
160+
});
161+
162+
redis.on("connect", () => {
163+
try {
164+
const remoteAddress = redis.stream.remoteAddress;
165+
const remotePort = redis.stream.remotePort;
166+
expect(redis.options.family).to.equal(0);
167+
expect(remoteAddress).not.to.equal("127.0.0.1");
168+
expect(remoteAddress).to.equal("::1");
169+
expect(remotePort).to.equal(6379);
170+
done();
171+
} catch (err) {
172+
done(err);
173+
} finally {
174+
redis.disconnect();
175+
}
176+
});
177+
178+
redis.on("error", done);
179+
});
180+
181+
it("should continue to connect via family 0 after connection failure", (done) => {
182+
let errorEmitted = false;
183+
let attempts = 0;
184+
185+
const redis = new Redis({
186+
host: "invalid-hostname",
187+
family: 0,
188+
// Make the test faster by reducing the initial delay
189+
retryStrategy: (times) => (times > 1 ? null : 10), // Only retry once after 10ms
190+
});
191+
192+
redis.on("close", () => {
193+
try {
194+
const family = redis.options.family;
195+
if (attempts === 0) {
196+
expect(family).to.equal(0);
197+
} else {
198+
expect(family).to.equal(6);
199+
}
200+
attempts++;
201+
} catch (err) {
202+
done(err);
203+
} finally {
204+
if (attempts === 2) {
205+
redis.disconnect();
206+
done();
207+
}
208+
}
209+
});
210+
211+
redis.on("error", (err) => {
212+
if (!errorEmitted) {
213+
try {
214+
expect(err).to.be.instanceOf(Error);
215+
expect(err.message).to.contain("ENOTFOUND");
216+
errorEmitted = true;
217+
} catch (assertionError) {
218+
done(assertionError);
219+
}
220+
}
221+
});
222+
});
223+
224+
it("should attempt to reconnect with another family", (done) => {
225+
let attempts = 0;
226+
227+
const redis = new Redis({
228+
familyFallback: true,
229+
host: "invalid-hostname",
230+
// Make the test faster by reducing the initial delay
231+
retryStrategy: (times) => (times > 1 ? null : 10), // Only retry once after 10ms
232+
});
233+
234+
redis.on("close", () => {
235+
try {
236+
expect(redis.options.familyFallback).to.equal(true);
237+
let testFamily = attempts === 0 ? 4 : 6;
238+
const family = redis.options.family;
239+
// testFamily should be 4 on the first attempt and 6 on the second
240+
expect(family).to.equal(testFamily);
241+
testFamily = family === 6 ? 4 : 6;
242+
} catch (err) {
243+
done(err);
244+
} finally {
245+
if (attempts++ === 1) {
246+
done();
247+
redis.disconnect();
248+
}
249+
}
250+
});
251+
});
252+
253+
it("should attempt to reconnect with another family (custom)", (done) => {
254+
let attempts = 0;
255+
let testFamily = 6;
256+
257+
const redis = new Redis({
258+
familyFallback: true,
259+
host: "invalid-hostname",
260+
family: testFamily,
261+
// Make the test faster by reducing the initial delay
262+
retryStrategy: (times) => (times > 1 ? null : 10), // Only retry once after 10ms
263+
});
264+
265+
redis.on("close", () => {
266+
try {
267+
expect(redis.options.familyFallback).to.equal(true);
268+
const family = redis.options.family;
269+
// testFamily should be 6 on the first attempt and 4 on the second
270+
expect(family).to.equal(testFamily);
271+
testFamily = family === 6 ? 4 : 6;
272+
} catch (err) {
273+
done(err);
274+
} finally {
275+
if (attempts++ === 1) {
276+
done();
277+
redis.disconnect();
278+
}
279+
}
280+
});
281+
});
282+
});
283+
131284
describe("#end", () => {
132285
it("should redirect to #disconnect", (done) => {
133286
const redis = new Redis({ lazyConnect: true });

0 commit comments

Comments
 (0)