diff --git a/lib/mongo/cluster/topology/replica_set.rb b/lib/mongo/cluster/topology/replica_set.rb index ccbb673be9..590e2189c4 100644 --- a/lib/mongo/cluster/topology/replica_set.rb +++ b/lib/mongo/cluster/topology/replica_set.rb @@ -68,6 +68,7 @@ def elect_primary(description, servers) end end update_max_election_id(description) + update_max_set_version(description) end else log_warn( @@ -88,7 +89,8 @@ def elect_primary(description, servers) # @since 2.0.0 def initialize(options, seeds = []) @options = options - @max_election_id = 0 + @max_election_id = nil + @max_set_version = nil end # A replica set topology is a replica set. @@ -222,14 +224,29 @@ def standalone_discovered; self; end private def update_max_election_id(description) - if description.election_id && description.election_id > @max_election_id + if description.election_id && + (@max_election_id.nil? || + description.election_id > @max_election_id) @max_election_id = description.election_id end end + def update_max_set_version(description) + if description.set_version && + (@max_set_version.nil? || + description.set_version > @max_set_version) + @max_set_version = description.set_version + end + end + def detect_stale_primary!(description) - if description.election_id && description.election_id < @max_election_id - description.unknown! + if description.election_id && description.set_version + if @max_set_version && @max_election_id && + (description.set_version < @max_set_version || + (description.set_version == @max_set_version && + description.election_id < @max_election_id)) + description.unknown! + end end end diff --git a/lib/mongo/server/description.rb b/lib/mongo/server/description.rb index ddcac09de0..c51b1653ca 100644 --- a/lib/mongo/server/description.rb +++ b/lib/mongo/server/description.rb @@ -129,11 +129,16 @@ class Description # @since 2.0.0 TAGS = 'tags'.freeze - # Constant for reading electionID info from config. + # Constant for reading electionId info from config. # # @since 2.1.0 ELECTION_ID = 'electionId'.freeze + # Constant for reading setVersion info from config. + # + # @since 2.2.2 + SET_VERSION = 'setVersion'.freeze + # Constant for reading localTime info from config. # # @since 2.1.0 @@ -142,7 +147,7 @@ class Description # Fields to exclude when comparing two descriptions. # # @since 2.0.6 - EXCLUDE_FOR_COMPARISON = [ LOCAL_TIME, ELECTION_ID ].freeze + EXCLUDE_FOR_COMPARISON = [ LOCAL_TIME, ELECTION_ID, SET_VERSION ].freeze # @return [ Address ] address The server's address. attr_reader :address @@ -343,6 +348,18 @@ def election_id config[ELECTION_ID] end + # Get the setVersion from the config. + # + # @example Get the setVersion. + # description.set_version + # + # @return [ Integer ] The set version. + # + # @since 2.2.2 + def set_version + config[SET_VERSION] + end + # Is the server a mongos? # # @example Is the server a mongos? diff --git a/spec/support/sdam/rs/equal_electionids.yml b/spec/support/sdam/rs/equal_electionids.yml index a7f6e1a539..271e4fa0c4 100644 --- a/spec/support/sdam/rs/equal_electionids.yml +++ b/spec/support/sdam/rs/equal_electionids.yml @@ -12,6 +12,7 @@ phases: [ ismaster: true, hosts: ["a:27017", "b:27017"], setName: "rs", + setVersion: 1, electionId: {"$oid": "000000000000000000000001"} }], ["b:27017", { @@ -19,6 +20,7 @@ phases: [ ismaster: true, hosts: ["a:27017", "b:27017"], setName: "rs", + setVersion: 1, electionId: {"$oid": "000000000000000000000001"} }] ], @@ -29,11 +31,13 @@ phases: [ "a:27017": { type: "Unknown", setName: , + setVersion: , electionId: }, "b:27017": { type: "RSPrimary", setName: "rs", + setVersion: 1, electionId: {"$oid": "000000000000000000000001"} } }, diff --git a/spec/support/sdam/rs/new_primary_new_electionid.yml b/spec/support/sdam/rs/new_primary_new_electionid.yml index f38906fc14..e7b0d1d670 100644 --- a/spec/support/sdam/rs/new_primary_new_electionid.yml +++ b/spec/support/sdam/rs/new_primary_new_electionid.yml @@ -1,4 +1,4 @@ -description: "New primary with greater electionId" +description: "New primary with greater setVersion and electionId" uri: "mongodb://a/?replicaSet=rs" @@ -12,6 +12,7 @@ phases: [ ismaster: true, hosts: ["a:27017", "b:27017"], setName: "rs", + setVersion: 1, electionId: {"$oid": "000000000000000000000001"} }] ], @@ -21,6 +22,7 @@ phases: [ "a:27017": { type: "RSPrimary", setName: "rs", + setVersion: 1, electionId: {"$oid": "000000000000000000000001"} }, "b:27017": { @@ -42,6 +44,7 @@ phases: [ ismaster: true, hosts: ["a:27017", "b:27017"], setName: "rs", + setVersion: 1, electionId: {"$oid": "000000000000000000000002"} }] ], @@ -56,6 +59,7 @@ phases: [ "b:27017": { type: "RSPrimary", setName: "rs", + setVersion: 1, electionId: {"$oid": "000000000000000000000002"} } }, @@ -72,6 +76,7 @@ phases: [ ismaster: true, hosts: ["a:27017", "b:27017"], setName: "rs", + setVersion: 1, electionId: {"$oid": "000000000000000000000001"} }] ], @@ -85,6 +90,7 @@ phases: [ "b:27017": { type: "RSPrimary", setName: "rs", + setVersion: 1, electionId: {"$oid": "000000000000000000000002"} } }, diff --git a/spec/support/sdam/rs/new_primary_new_setversion.yml b/spec/support/sdam/rs/new_primary_new_setversion.yml new file mode 100644 index 0000000000..df83bedcee --- /dev/null +++ b/spec/support/sdam/rs/new_primary_new_setversion.yml @@ -0,0 +1,101 @@ +description: "New primary with greater setVersion" + +uri: "mongodb://a/?replicaSet=rs" + +phases: [ + + # Primary A is discovered and tells us about B. + { + responses: [ + ["a:27017", { + ok: 1, + ismaster: true, + hosts: ["a:27017", "b:27017"], + setName: "rs", + setVersion: 1, + electionId: {"$oid": "000000000000000000000001"} + }] + ], + + outcome: { + servers: { + "a:27017": { + type: "RSPrimary", + setName: "rs", + setVersion: 1, + electionId: {"$oid": "000000000000000000000001"} + }, + "b:27017": { + type: "Unknown", + setName: , + electionId: + } + }, + topologyType: "ReplicaSetWithPrimary", + setName: "rs", + } + }, + + # RS is reconfigured and B is elected. + { + responses: [ + ["b:27017", { + ok: 1, + ismaster: true, + hosts: ["a:27017", "b:27017"], + setName: "rs", + setVersion: 2, + electionId: {"$oid": "000000000000000000000001"} + }] + ], + + outcome: { + servers: { + "a:27017": { + type: "Unknown", + setName: , + electionId: + }, + "b:27017": { + type: "RSPrimary", + setName: "rs", + setVersion: 2, + electionId: {"$oid": "000000000000000000000001"} + } + }, + topologyType: "ReplicaSetWithPrimary", + setName: "rs", + } + }, + + # A still claims to be primary but it's ignored. + { + responses: [ + ["a:27017", { + ok: 1, + ismaster: true, + hosts: ["a:27017", "b:27017"], + setName: "rs", + setVersion: 1, + electionId: {"$oid": "000000000000000000000001"} + }] + ], + outcome: { + servers: { + "a:27017": { + type: "Unknown", + setName: , + electionId: + }, + "b:27017": { + type: "RSPrimary", + setName: "rs", + setVersion: 2, + electionId: {"$oid": "000000000000000000000002"} + } + }, + topologyType: "ReplicaSetWithPrimary", + setName: "rs", + } + } +] diff --git a/spec/support/sdam/rs/null_election_id.yml b/spec/support/sdam/rs/null_election_id.yml index 7608c6e4c5..d990d28a0e 100644 --- a/spec/support/sdam/rs/null_election_id.yml +++ b/spec/support/sdam/rs/null_election_id.yml @@ -11,6 +11,7 @@ phases: [ ok: 1, ismaster: true, hosts: ["a:27017", "b:27017", "c:27017"], + setVersion: 1, setName: "rs" }] ], @@ -20,6 +21,7 @@ phases: [ "a:27017": { type: "RSPrimary", setName: "rs", + setVersion: 1, electionId: }, "b:27017": { @@ -46,6 +48,7 @@ phases: [ ismaster: true, hosts: ["a:27017", "b:27017", "c:27017"], setName: "rs", + setVersion: 1, electionId: {"$oid": "000000000000000000000002"} }] ], @@ -60,6 +63,7 @@ phases: [ "b:27017": { type: "RSPrimary", setName: "rs", + setVersion: 1, electionId: {"$oid": "000000000000000000000002"} }, "c:27017": { @@ -80,6 +84,7 @@ phases: [ ok: 1, ismaster: true, hosts: ["a:27017", "b:27017", "c:27017"], + setVersion: 1, setName: "rs" }] ], @@ -88,6 +93,7 @@ phases: [ "a:27017": { type: "RSPrimary", setName: "rs", + setVersion: 1, electionId: }, "b:27017": { @@ -106,7 +112,7 @@ phases: [ } }, - # But we remember A's electionId, so when we finally hear from C + # But we remember B's electionId, so when we finally hear from C # claiming it is primary, we ignore it due to its outdated electionId { responses: [ @@ -115,6 +121,7 @@ phases: [ ismaster: true, hosts: ["a:27017", "b:27017", "c:27017"], setName: "rs", + setVersion: 1, electionId: {"$oid": "000000000000000000000001"} }] ], diff --git a/spec/support/sdam/rs/primary_disconnect_electionid.yml b/spec/support/sdam/rs/primary_disconnect_electionid.yml index 3d009a7eaa..0c4db3dd4b 100644 --- a/spec/support/sdam/rs/primary_disconnect_electionid.yml +++ b/spec/support/sdam/rs/primary_disconnect_electionid.yml @@ -1,4 +1,4 @@ -description: "Disconnected from primary, reject stale primary" +description: "Disconnected from primary, reject primary with stale electionId" uri: "mongodb://a/?replicaSet=rs" @@ -12,6 +12,7 @@ phases: [ ismaster: true, hosts: ["a:27017", "b:27017"], setName: "rs", + setVersion: 1, electionId: {"$oid": "000000000000000000000001"} }], ["b:27017", { @@ -19,6 +20,7 @@ phases: [ ismaster: true, hosts: ["a:27017", "b:27017"], setName: "rs", + setVersion: 1, electionId: {"$oid": "000000000000000000000002"} }] ], @@ -33,6 +35,7 @@ phases: [ "b:27017": { type: "RSPrimary", setName: "rs", + setVersion: 1, electionId: {"$oid": "000000000000000000000002"} } }, @@ -72,6 +75,7 @@ phases: [ ismaster: true, hosts: ["a:27017", "b:27017"], setName: "rs", + setVersion: 1, electionId: {"$oid": "000000000000000000000001"} }] ], @@ -101,6 +105,7 @@ phases: [ ismaster: true, hosts: ["a:27017", "b:27017"], setName: "rs", + setVersion: 1, electionId: {"$oid": "000000000000000000000003"} }] ], @@ -109,6 +114,7 @@ phases: [ "a:27017": { type: "RSPrimary", setName: "rs", + setVersion: 1, electionId: {"$oid": "000000000000000000000003"} }, "b:27017": { @@ -120,5 +126,35 @@ phases: [ topologyType: "ReplicaSetWithPrimary", setName: "rs", } + }, + + # B comes back as secondary. + { + responses: [ + ["b:27017", { + ok: 1, + ismaster: false, + secondary: true, + hosts: ["a:27017", "b:27017"], + setName: "rs", + setVersion: 2 + }] + ], + outcome: { + servers: { + "a:27017": { + type: "RSPrimary", + setName: "rs", + setVersion: 1, + electionId: {"$oid": "000000000000000000000002"} + }, + "b:27017": { + type: "RSSecondary", + setName: "rs" + } + }, + topologyType: "ReplicaSetWithPrimary", + setName: "rs", + } } ] diff --git a/spec/support/sdam/rs/primary_disconnect_setversion.yml b/spec/support/sdam/rs/primary_disconnect_setversion.yml new file mode 100644 index 0000000000..89daf3a951 --- /dev/null +++ b/spec/support/sdam/rs/primary_disconnect_setversion.yml @@ -0,0 +1,160 @@ +description: "Disconnected from primary, reject primary with stale setVersion" + +uri: "mongodb://a/?replicaSet=rs" + +phases: [ + + # A is elected, then B after a reconfig. + { + responses: [ + ["a:27017", { + ok: 1, + ismaster: true, + hosts: ["a:27017", "b:27017"], + setName: "rs", + setVersion: 1, + electionId: {"$oid": "000000000000000000000001"} + }], + ["b:27017", { + ok: 1, + ismaster: true, + hosts: ["a:27017", "b:27017"], + setName: "rs", + setVersion: 2, + electionId: {"$oid": "000000000000000000000001"} + }] + ], + + outcome: { + servers: { + "a:27017": { + type: "Unknown", + setName: , + electionId: + }, + "b:27017": { + type: "RSPrimary", + setName: "rs", + setVersion: 2, + electionId: {"$oid": "000000000000000000000001"} + } + }, + topologyType: "ReplicaSetWithPrimary", + setName: "rs", + } + }, + + # Disconnected from B. + { + responses: [ + ["b:27017", {}] + ], + outcome: { + servers: { + "a:27017": { + type: "Unknown", + setName: , + electionId: + }, + "b:27017": { + type: "Unknown", + setName: , + electionId: + } + }, + topologyType: "ReplicaSetNoPrimary", + setName: "rs", + } + }, + + # A still claims to be primary but it's ignored. + { + responses: [ + ["a:27017", { + ok: 1, + ismaster: true, + hosts: ["a:27017", "b:27017"], + setName: "rs", + setVersion: 1, + electionId: {"$oid": "000000000000000000000001"} + }] + ], + outcome: { + servers: { + "a:27017": { + type: "Unknown", + setName: , + electionId: + }, + "b:27017": { + type: "Unknown", + setName: , + electionId: + } + }, + topologyType: "ReplicaSetNoPrimary", + setName: "rs", + } + }, + + # Now A is re-elected. + { + responses: [ + ["a:27017", { + ok: 1, + ismaster: true, + hosts: ["a:27017", "b:27017"], + setName: "rs", + setVersion: 2, + electionId: {"$oid": "000000000000000000000002"} + }] + ], + outcome: { + servers: { + "a:27017": { + type: "RSPrimary", + setName: "rs", + setVersion: 2, + electionId: {"$oid": "000000000000000000000002"} + }, + "b:27017": { + type: "Unknown", + setName: , + electionId: + } + }, + topologyType: "ReplicaSetWithPrimary", + setName: "rs", + } + }, + + # B comes back as secondary. + { + responses: [ + ["b:27017", { + ok: 1, + ismaster: false, + secondary: true, + hosts: ["a:27017", "b:27017"], + setName: "rs", + setVersion: 2 + }] + ], + outcome: { + servers: { + "a:27017": { + type: "RSPrimary", + setName: "rs", + setVersion: 2, + electionId: {"$oid": "000000000000000000000002"} + }, + "b:27017": { + type: "RSSecondary", + setName: "rs" + } + }, + topologyType: "ReplicaSetWithPrimary", + setName: "rs", + } + } +] diff --git a/spec/support/sdam/rs/set_version_without_electionid.yml b/spec/support/sdam/rs/set_version_without_electionid.yml new file mode 100644 index 0000000000..2b11050c22 --- /dev/null +++ b/spec/support/sdam/rs/set_version_without_electionid.yml @@ -0,0 +1,69 @@ +description: "setVersion is ignored if there is no electionId" + +uri: "mongodb://a/?replicaSet=rs" + +phases: [ + + # Primary A is discovered and tells us about B. + { + responses: [ + ["a:27017", { + ok: 1, + ismaster: true, + hosts: ["a:27017", "b:27017"], + setName: "rs", + setVersion: 2 + }] + ], + + outcome: { + servers: { + "a:27017": { + type: "RSPrimary", + setName: "rs", + setVersion: 2 , + electionId: + }, + "b:27017": { + type: "Unknown", + setName: , + electionId: + } + }, + topologyType: "ReplicaSetWithPrimary", + setName: "rs", + } + }, + + # B is elected, its setVersion is older but we believe it anyway, because + # setVersion is only used in conjunction with electionId. + { + responses: [ + ["b:27017", { + ok: 1, + ismaster: true, + hosts: ["a:27017", "b:27017"], + setName: "rs", + setVersion: 1 + }] + ], + + outcome: { + servers: { + "a:27017": { + type: "Unknown", + setName: , + electionId: + }, + "b:27017": { + type: "RSPrimary", + setName: "rs", + setVersion: 1, + electionId: + } + }, + topologyType: "ReplicaSetWithPrimary", + setName: "rs", + } + } +] diff --git a/spec/support/sdam/rs/setversion_without_electionid.yml b/spec/support/sdam/rs/setversion_without_electionid.yml new file mode 100644 index 0000000000..2b11050c22 --- /dev/null +++ b/spec/support/sdam/rs/setversion_without_electionid.yml @@ -0,0 +1,69 @@ +description: "setVersion is ignored if there is no electionId" + +uri: "mongodb://a/?replicaSet=rs" + +phases: [ + + # Primary A is discovered and tells us about B. + { + responses: [ + ["a:27017", { + ok: 1, + ismaster: true, + hosts: ["a:27017", "b:27017"], + setName: "rs", + setVersion: 2 + }] + ], + + outcome: { + servers: { + "a:27017": { + type: "RSPrimary", + setName: "rs", + setVersion: 2 , + electionId: + }, + "b:27017": { + type: "Unknown", + setName: , + electionId: + } + }, + topologyType: "ReplicaSetWithPrimary", + setName: "rs", + } + }, + + # B is elected, its setVersion is older but we believe it anyway, because + # setVersion is only used in conjunction with electionId. + { + responses: [ + ["b:27017", { + ok: 1, + ismaster: true, + hosts: ["a:27017", "b:27017"], + setName: "rs", + setVersion: 1 + }] + ], + + outcome: { + servers: { + "a:27017": { + type: "Unknown", + setName: , + electionId: + }, + "b:27017": { + type: "RSPrimary", + setName: "rs", + setVersion: 1, + electionId: + } + }, + topologyType: "ReplicaSetWithPrimary", + setName: "rs", + } + } +] diff --git a/spec/support/sdam/rs/use_setversion_without_electionid.yml b/spec/support/sdam/rs/use_setversion_without_electionid.yml new file mode 100644 index 0000000000..505ed0b4dc --- /dev/null +++ b/spec/support/sdam/rs/use_setversion_without_electionid.yml @@ -0,0 +1,99 @@ +description: "Record max setVersion, even from primary without electionId" + +uri: "mongodb://a/?replicaSet=rs" + +phases: [ + + # Primary A has setVersion and electionId, tells us about B. + { + responses: [ + ["a:27017", { + ok: 1, + ismaster: true, + hosts: ["a:27017", "b:27017"], + setName: "rs", + setVersion: 1, + electionId: {"$oid": "000000000000000000000001"} + }] + ], + + outcome: { + servers: { + "a:27017": { + type: "RSPrimary", + setName: "rs", + setVersion: 1, + electionId: {"$oid": "000000000000000000000001"} + }, + "b:27017": { + type: "Unknown", + setName: , + electionId: + } + }, + topologyType: "ReplicaSetWithPrimary", + setName: "rs", + } + }, + + # Reconfig the set and elect B, it has a new setVersion but no electionId. + { + responses: [ + ["b:27017", { + ok: 1, + ismaster: true, + hosts: ["a:27017", "b:27017"], + setName: "rs", + setVersion: 2 + }] + ], + + outcome: { + servers: { + "a:27017": { + type: "Unknown", + setName: , + electionId: + }, + "b:27017": { + type: "RSPrimary", + setName: "rs", + setVersion: 2 + } + }, + topologyType: "ReplicaSetWithPrimary", + setName: "rs", + } + }, + + # Delayed response from A, reporting its reelection. Its setVersion shows + # the election preceded B's so we ignore it. + { + responses: [ + ["a:27017", { + ok: 1, + ismaster: true, + hosts: ["a:27017", "b:27017"], + setName: "rs", + setVersion: 1, + electionId: {"$oid": "000000000000000000000002"} + }] + ], + outcome: { + servers: { + "a:27017": { + type: "Unknown", + setName: , + electionId: + }, + "b:27017": { + type: "RSPrimary", + setName: "rs", + setVersion: 2 + } + }, + topologyType: "ReplicaSetWithPrimary", + setName: "rs", + } + } +]