Skip to content

Commit f02e383

Browse files
authoredJun 5, 2021
fix(SENTINEL): actively failover detection under an option (#1363)
1 parent c87ea2a commit f02e383

File tree

4 files changed

+212
-196
lines changed

4 files changed

+212
-196
lines changed
 

‎lib/connectors/SentinelConnector/index.ts

+4
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ export interface ISentinelConnectionOptions extends ITcpConnectionOptions {
5252
natMap?: INatMap;
5353
updateSentinels?: boolean;
5454
sentinelMaxConnections?: number;
55+
failoverDetector?: boolean;
5556
}
5657

5758
export default class SentinelConnector extends AbstractConnector {
@@ -321,6 +322,9 @@ export default class SentinelConnector extends AbstractConnector {
321322
}
322323

323324
private async initFailoverDetector(): Promise<void> {
325+
if (!this.options.failoverDetector) {
326+
return;
327+
}
324328
// Move the current sentinel to the first position
325329
this.sentinelIterator.reset(true);
326330

‎lib/redis/RedisOptions.ts

+1
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ export const DEFAULT_REDIS_OPTIONS: IRedisOptions = {
6363
natMap: null,
6464
enableTLSForSentinelMode: false,
6565
updateSentinels: true,
66+
failoverDetector: false,
6667
// Status
6768
username: null,
6869
password: null,

‎lib/redis/index.ts

+5
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,11 @@ const debug = Debug("redis");
9898
* @param {NatMap} [options.natMap=null] NAT map for sentinel connector.
9999
* @param {boolean} [options.updateSentinels=true] - Update the given `sentinels` list with new IP
100100
* addresses when communicating with existing sentinels.
101+
* @param {boolean} [options.failoverDetector=false] - Detect failover actively by subscribing to the
102+
* related channels. With this option disabled, ioredis is still able to detect failovers because Redis
103+
* Sentinel will disconnect all clients whenever a failover happens, so ioredis will reconnect to the new
104+
* master. This option is useful when you want to detect failover quicker, but it will create more TCP
105+
* connections to Redis servers in order to subscribe to related channels.
101106
* @param {boolean} [options.enableAutoPipelining=false] - When enabled, all commands issued during an event loop
102107
* iteration are automatically wrapped in a pipeline and sent to the server at the same time.
103108
* This can dramatically improve performance.

‎test/functional/sentinel.ts

+202-196
Original file line numberDiff line numberDiff line change
@@ -556,242 +556,248 @@ describe("sentinel", function () {
556556
});
557557
});
558558

559-
it("should connect to new master after +switch-master", async function () {
560-
const sentinel = new MockServer(27379, function (argv) {
561-
if (argv[0] === "sentinel" && argv[1] === "get-master-addr-by-name") {
562-
return ["127.0.0.1", "17380"];
563-
}
564-
});
565-
const master = new MockServer(17380);
566-
const newMaster = new MockServer(17381);
567-
568-
const redis = new Redis({
569-
sentinels: [{ host: "127.0.0.1", port: 27379 }],
570-
name: "master",
571-
});
572-
573-
await Promise.all([
574-
once(master, "connect"),
575-
once(redis, "failoverSubscribed"),
576-
]);
559+
describe("failoverDetector", () => {
560+
it("should connect to new master after +switch-master", async function () {
561+
const sentinel = new MockServer(27379, function (argv) {
562+
if (argv[0] === "sentinel" && argv[1] === "get-master-addr-by-name") {
563+
return ["127.0.0.1", "17380"];
564+
}
565+
});
566+
const master = new MockServer(17380);
567+
const newMaster = new MockServer(17381);
577568

578-
sentinel.handler = function (argv) {
579-
if (argv[0] === "sentinel" && argv[1] === "get-master-addr-by-name") {
580-
return ["127.0.0.1", "17381"];
581-
}
582-
};
569+
const redis = new Redis({
570+
sentinels: [{ host: "127.0.0.1", port: 27379 }],
571+
failoverDetector: true,
572+
name: "master",
573+
});
583574

584-
sentinel.broadcast([
585-
"message",
586-
"+switch-master",
587-
"master 127.0.0.1 17380 127.0.0.1 17381",
588-
]);
575+
await Promise.all([
576+
once(master, "connect"),
577+
once(redis, "failoverSubscribed"),
578+
]);
589579

590-
await Promise.all([
591-
once(redis, "close"), // Wait until disconnects from old master
592-
once(master, "disconnect"),
593-
once(newMaster, "connect"),
594-
]);
580+
sentinel.handler = function (argv) {
581+
if (argv[0] === "sentinel" && argv[1] === "get-master-addr-by-name") {
582+
return ["127.0.0.1", "17381"];
583+
}
584+
};
595585

596-
redis.disconnect(); // Disconnect from new master
586+
sentinel.broadcast([
587+
"message",
588+
"+switch-master",
589+
"master 127.0.0.1 17380 127.0.0.1 17381",
590+
]);
597591

598-
await Promise.all([
599-
sentinel.disconnectPromise(),
600-
master.disconnectPromise(),
601-
newMaster.disconnectPromise(),
602-
]);
603-
});
592+
await Promise.all([
593+
once(redis, "close"), // Wait until disconnects from old master
594+
once(master, "disconnect"),
595+
once(newMaster, "connect"),
596+
]);
604597

605-
it("should detect failover from secondary sentinel", async function () {
606-
const sentinel1 = new MockServer(27379, function (argv) {
607-
if (argv[0] === "sentinel" && argv[1] === "get-master-addr-by-name") {
608-
return ["127.0.0.1", "17380"];
609-
}
610-
});
611-
const sentinel2 = new MockServer(27380);
612-
const master = new MockServer(17380);
613-
const newMaster = new MockServer(17381);
598+
redis.disconnect(); // Disconnect from new master
614599

615-
const redis = new Redis({
616-
sentinels: [
617-
{ host: "127.0.0.1", port: 27379 },
618-
{ host: "127.0.0.1", port: 27380 },
619-
],
620-
name: "master",
600+
await Promise.all([
601+
sentinel.disconnectPromise(),
602+
master.disconnectPromise(),
603+
newMaster.disconnectPromise(),
604+
]);
621605
});
622606

623-
await Promise.all([
624-
once(master, "connect"),
625-
once(redis, "failoverSubscribed"),
626-
]);
607+
it("should detect failover from secondary sentinel", async function () {
608+
const sentinel1 = new MockServer(27379, function (argv) {
609+
if (argv[0] === "sentinel" && argv[1] === "get-master-addr-by-name") {
610+
return ["127.0.0.1", "17380"];
611+
}
612+
});
613+
const sentinel2 = new MockServer(27380);
614+
const master = new MockServer(17380);
615+
const newMaster = new MockServer(17381);
616+
617+
const redis = new Redis({
618+
sentinels: [
619+
{ host: "127.0.0.1", port: 27379 },
620+
{ host: "127.0.0.1", port: 27380 },
621+
],
622+
name: "master",
623+
failoverDetector: true,
624+
});
627625

628-
// In this test, only the first sentinel is used to resolve the master
629-
sentinel1.handler = function (argv) {
630-
if (argv[0] === "sentinel" && argv[1] === "get-master-addr-by-name") {
631-
return ["127.0.0.1", "17381"];
632-
}
633-
};
626+
await Promise.all([
627+
once(master, "connect"),
628+
once(redis, "failoverSubscribed"),
629+
]);
634630

635-
// But only the second sentinel broadcasts +switch-master
636-
sentinel2.broadcast([
637-
"message",
638-
"+switch-master",
639-
"master 127.0.0.1 17380 127.0.0.1 17381",
640-
]);
631+
// In this test, only the first sentinel is used to resolve the master
632+
sentinel1.handler = function (argv) {
633+
if (argv[0] === "sentinel" && argv[1] === "get-master-addr-by-name") {
634+
return ["127.0.0.1", "17381"];
635+
}
636+
};
641637

642-
await Promise.all([
643-
once(redis, "close"), // Wait until disconnects from old master
644-
once(master, "disconnect"),
645-
once(newMaster, "connect"),
646-
]);
638+
// But only the second sentinel broadcasts +switch-master
639+
sentinel2.broadcast([
640+
"message",
641+
"+switch-master",
642+
"master 127.0.0.1 17380 127.0.0.1 17381",
643+
]);
647644

648-
redis.disconnect(); // Disconnect from new master
645+
await Promise.all([
646+
once(redis, "close"), // Wait until disconnects from old master
647+
once(master, "disconnect"),
648+
once(newMaster, "connect"),
649+
]);
649650

650-
await Promise.all([
651-
sentinel1.disconnectPromise(),
652-
sentinel2.disconnectPromise(),
653-
master.disconnectPromise(),
654-
newMaster.disconnectPromise(),
655-
]);
656-
});
657-
658-
it("should detect failover when some sentinels fail", async function () {
659-
// Will disconnect before failover
660-
const sentinel1 = new MockServer(27379, function (argv) {
661-
if (argv[0] === "sentinel" && argv[1] === "get-master-addr-by-name") {
662-
return ["127.0.0.1", "17380"];
663-
}
664-
});
651+
redis.disconnect(); // Disconnect from new master
665652

666-
// Will emit an error before failover
667-
let sentinel2Socket: Socket | null = null;
668-
const sentinel2 = new MockServer(27380, function (argv, socket) {
669-
sentinel2Socket = socket;
653+
await Promise.all([
654+
sentinel1.disconnectPromise(),
655+
sentinel2.disconnectPromise(),
656+
master.disconnectPromise(),
657+
newMaster.disconnectPromise(),
658+
]);
670659
});
671660

672-
// Fails to subscribe
673-
const sentinel3 = new MockServer(27381, function (argv, socket, flags) {
674-
if (argv[0] === "subscribe") {
675-
triggerParseError(socket);
676-
}
677-
});
678-
679-
// The only sentinel that can successfully publish the failover message
680-
const sentinel4 = new MockServer(27382);
661+
it("should detect failover when some sentinels fail", async function () {
662+
// Will disconnect before failover
663+
const sentinel1 = new MockServer(27379, function (argv) {
664+
if (argv[0] === "sentinel" && argv[1] === "get-master-addr-by-name") {
665+
return ["127.0.0.1", "17380"];
666+
}
667+
});
681668

682-
const master = new MockServer(17380);
683-
const newMaster = new MockServer(17381);
669+
// Will emit an error before failover
670+
let sentinel2Socket: Socket | null = null;
671+
const sentinel2 = new MockServer(27380, function (argv, socket) {
672+
sentinel2Socket = socket;
673+
});
684674

685-
const redis = new Redis({
686-
sentinels: [
687-
{ host: "127.0.0.1", port: 27379 },
688-
{ host: "127.0.0.1", port: 27380 },
689-
{ host: "127.0.0.1", port: 27381 },
690-
{ host: "127.0.0.1", port: 27382 },
691-
],
692-
name: "master",
693-
});
675+
// Fails to subscribe
676+
const sentinel3 = new MockServer(27381, function (argv, socket, flags) {
677+
if (argv[0] === "subscribe") {
678+
triggerParseError(socket);
679+
}
680+
});
694681

695-
await Promise.all([
696-
once(master, "connect"),
682+
// The only sentinel that can successfully publish the failover message
683+
const sentinel4 = new MockServer(27382);
684+
685+
const master = new MockServer(17380);
686+
const newMaster = new MockServer(17381);
687+
688+
const redis = new Redis({
689+
sentinels: [
690+
{ host: "127.0.0.1", port: 27379 },
691+
{ host: "127.0.0.1", port: 27380 },
692+
{ host: "127.0.0.1", port: 27381 },
693+
{ host: "127.0.0.1", port: 27382 },
694+
],
695+
name: "master",
696+
failoverDetector: true,
697+
});
697698

698-
// Must resolve even though subscribing to sentinel3 fails
699-
once(redis, "failoverSubscribed"),
700-
]);
699+
await Promise.all([
700+
once(master, "connect"),
701701

702-
// Fail sentinels 1 and 2
703-
await sentinel1.disconnectPromise();
704-
triggerParseError(sentinel2Socket);
702+
// Must resolve even though subscribing to sentinel3 fails
703+
once(redis, "failoverSubscribed"),
704+
]);
705705

706-
sentinel4.handler = function (argv) {
707-
if (argv[0] === "sentinel" && argv[1] === "get-master-addr-by-name") {
708-
return ["127.0.0.1", "17381"];
709-
}
710-
};
706+
// Fail sentinels 1 and 2
707+
await sentinel1.disconnectPromise();
708+
triggerParseError(sentinel2Socket);
711709

712-
sentinel4.broadcast([
713-
"message",
714-
"+switch-master",
715-
"master 127.0.0.1 17380 127.0.0.1 17381",
716-
]);
710+
sentinel4.handler = function (argv) {
711+
if (argv[0] === "sentinel" && argv[1] === "get-master-addr-by-name") {
712+
return ["127.0.0.1", "17381"];
713+
}
714+
};
717715

718-
await Promise.all([
719-
once(redis, "close"), // Wait until disconnects from old master
720-
once(master, "disconnect"),
721-
once(newMaster, "connect"),
722-
]);
716+
sentinel4.broadcast([
717+
"message",
718+
"+switch-master",
719+
"master 127.0.0.1 17380 127.0.0.1 17381",
720+
]);
723721

724-
redis.disconnect(); // Disconnect from new master
722+
await Promise.all([
723+
once(redis, "close"), // Wait until disconnects from old master
724+
once(master, "disconnect"),
725+
once(newMaster, "connect"),
726+
]);
725727

726-
await Promise.all([
727-
// sentinel1 is already disconnected
728-
sentinel2.disconnectPromise(),
729-
sentinel3.disconnectPromise(),
730-
sentinel4.disconnectPromise(),
731-
master.disconnectPromise(),
732-
newMaster.disconnectPromise(),
733-
]);
734-
});
728+
redis.disconnect(); // Disconnect from new master
735729

736-
it("should detect failover after sentinel disconnects and reconnects", async function () {
737-
const sentinel = new MockServer(27379, function (argv) {
738-
if (argv[0] === "sentinel" && argv[1] === "get-master-addr-by-name") {
739-
return ["127.0.0.1", "17380"];
740-
}
730+
await Promise.all([
731+
// sentinel1 is already disconnected
732+
sentinel2.disconnectPromise(),
733+
sentinel3.disconnectPromise(),
734+
sentinel4.disconnectPromise(),
735+
master.disconnectPromise(),
736+
newMaster.disconnectPromise(),
737+
]);
741738
});
742739

743-
const master = new MockServer(17380);
744-
const newMaster = new MockServer(17381);
740+
it("should detect failover after sentinel disconnects and reconnects", async function () {
741+
const sentinel = new MockServer(27379, function (argv) {
742+
if (argv[0] === "sentinel" && argv[1] === "get-master-addr-by-name") {
743+
return ["127.0.0.1", "17380"];
744+
}
745+
});
745746

746-
const redis = new Redis({
747-
sentinels: [{ host: "127.0.0.1", port: 27379 }],
748-
name: "master",
749-
sentinelReconnectStrategy: () => 1000,
750-
});
747+
const master = new MockServer(17380);
748+
const newMaster = new MockServer(17381);
751749

752-
await Promise.all([
753-
once(master, "connect"),
754-
once(redis, "failoverSubscribed"),
755-
]);
750+
const redis = new Redis({
751+
sentinels: [{ host: "127.0.0.1", port: 27379 }],
752+
name: "master",
753+
sentinelReconnectStrategy: () => 1000,
754+
failoverDetector: true,
755+
});
756756

757-
await sentinel.disconnectPromise();
757+
await Promise.all([
758+
once(master, "connect"),
759+
once(redis, "failoverSubscribed"),
760+
]);
758761

759-
sentinel.handler = function (argv) {
760-
if (argv[0] === "sentinel" && argv[1] === "get-master-addr-by-name") {
761-
return ["127.0.0.1", "17381"];
762-
}
763-
if (argv[0] === "subscribe") {
764-
sentinel.emit("test:resubscribed"); // Custom event only used in tests
765-
}
766-
};
762+
await sentinel.disconnectPromise();
767763

768-
sentinel.connect();
764+
sentinel.handler = function (argv) {
765+
if (argv[0] === "sentinel" && argv[1] === "get-master-addr-by-name") {
766+
return ["127.0.0.1", "17381"];
767+
}
768+
if (argv[0] === "subscribe") {
769+
sentinel.emit("test:resubscribed"); // Custom event only used in tests
770+
}
771+
};
769772

770-
const clock = sinon.useFakeTimers();
771-
await once(redis, "sentinelReconnecting"); // Wait for the timeout to be set
772-
clock.tick(1000);
773-
clock.restore();
774-
await once(sentinel, "test:resubscribed");
773+
sentinel.connect();
775774

776-
sentinel.broadcast([
777-
"message",
778-
"+switch-master",
779-
"master 127.0.0.1 17380 127.0.0.1 17381",
780-
]);
775+
const clock = sinon.useFakeTimers();
776+
await once(redis, "sentinelReconnecting"); // Wait for the timeout to be set
777+
clock.tick(1000);
778+
clock.restore();
779+
await once(sentinel, "test:resubscribed");
781780

782-
await Promise.all([
783-
once(redis, "close"), // Wait until disconnects from old master
784-
once(master, "disconnect"),
785-
once(newMaster, "connect"),
786-
]);
781+
sentinel.broadcast([
782+
"message",
783+
"+switch-master",
784+
"master 127.0.0.1 17380 127.0.0.1 17381",
785+
]);
787786

788-
redis.disconnect(); // Disconnect from new master
787+
await Promise.all([
788+
once(redis, "close"), // Wait until disconnects from old master
789+
once(master, "disconnect"),
790+
once(newMaster, "connect"),
791+
]);
789792

790-
await Promise.all([
791-
sentinel.disconnectPromise(),
792-
master.disconnectPromise(),
793-
newMaster.disconnectPromise(),
794-
]);
793+
redis.disconnect(); // Disconnect from new master
794+
795+
await Promise.all([
796+
sentinel.disconnectPromise(),
797+
master.disconnectPromise(),
798+
newMaster.disconnectPromise(),
799+
]);
800+
});
795801
});
796802
});
797803
});

0 commit comments

Comments
 (0)
Please sign in to comment.