Skip to content

Commit 4f3a427

Browse files
committed
RUBY-912 Use electionId from server descriptions to detect stale primaries
1 parent 08d8433 commit 4f3a427

File tree

7 files changed

+326
-7
lines changed

7 files changed

+326
-7
lines changed

lib/mongo/cluster/topology/replica_set.rb

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,11 +60,16 @@ def display_name
6060
# @return [ ReplicaSet ] The topology.
6161
def elect_primary(description, servers)
6262
if description.replica_set_name == replica_set_name
63-
log_debug([ "Server #{description.address.to_s} elected as primary in #{replica_set_name}." ])
64-
servers.each do |server|
65-
if server.primary? && server.address != description.address
66-
server.description.unknown!
63+
if description.primary? && description.election_id && description.election_id < @max_election_id
64+
description.unknown!
65+
else
66+
log_debug([ "Server #{description.address.to_s} elected as primary in #{replica_set_name}." ])
67+
servers.each do |server|
68+
if server.primary? && server.address != description.address
69+
server.description.unknown!
70+
end
6771
end
72+
@max_election_id = description.election_id
6873
end
6974
else
7075
log_warn([
@@ -84,6 +89,7 @@ def elect_primary(description, servers)
8489
# @since 2.0.0
8590
def initialize(options, seeds = [])
8691
@options = options
92+
@max_election_id = 0
8793
end
8894

8995
# A replica set topology is a replica set.

lib/mongo/server/description.rb

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,10 +124,20 @@ class Description
124124
# @since 2.0.0
125125
TAGS = 'tags'.freeze
126126

127+
# Constant for reading electionID info from config.
128+
#
129+
# @since 2.1.0
130+
ELECTION_ID = 'electionId'.freeze
131+
132+
# Constant for reading localTime info from config.
133+
#
134+
# @since 2.1.0
135+
LOCAL_TIME = 'localTime'.freeze
136+
127137
# Fields to exclude when comparing two descriptions.
128138
#
129139
# @since 2.0.6
130-
EXCLUDE_FOR_COMPARISON = [ 'localTime' ]
140+
EXCLUDE_FOR_COMPARISON = [ LOCAL_TIME, ELECTION_ID ]
131141

132142
# @return [ Address ] address The server's address.
133143
attr_reader :address
@@ -304,6 +314,18 @@ def tags
304314
config[TAGS] || {}
305315
end
306316

317+
# Get the electionId from the config.
318+
#
319+
# @example Get the electionId.
320+
# description.election_id
321+
#
322+
# @return [ BSON::ObjectId ] The election id.
323+
#
324+
# @since 2.1.0
325+
def election_id
326+
config[ELECTION_ID]
327+
end
328+
307329
# Is the server a mongos?
308330
#
309331
# @example Is the server a mongos?
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
description: "New primary with equal electionId"
2+
3+
uri: "mongodb://a/?replicaSet=rs"
4+
5+
phases: [
6+
7+
# A and B claim to be primaries, with equal electionIds.
8+
{
9+
responses: [
10+
["a:27017", {
11+
ok: 1,
12+
ismaster: true,
13+
hosts: ["a:27017", "b:27017"],
14+
setName: "rs",
15+
electionId: {"$oid": "000000000000000000000001"}
16+
}],
17+
["b:27017", {
18+
ok: 1,
19+
ismaster: true,
20+
hosts: ["a:27017", "b:27017"],
21+
setName: "rs",
22+
electionId: {"$oid": "000000000000000000000001"}
23+
}]
24+
],
25+
26+
# No choice but to believe the latter response.
27+
outcome: {
28+
servers: {
29+
"a:27017": {
30+
type: "Unknown",
31+
setName: ,
32+
electionId: ,
33+
},
34+
"b:27017": {
35+
type: "RSPrimary",
36+
setName: "rs",
37+
electionId: {"$oid": "000000000000000000000001"}
38+
}
39+
},
40+
topologyType: "ReplicaSetWithPrimary",
41+
setName: "rs",
42+
maxElectionId: {"$oid": "000000000000000000000001"}
43+
}
44+
}
45+
]
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
description: "Ignore a secondary's electionId"
2+
3+
# Only primaries currently report electionId, make sure we're future-proof
4+
5+
uri: "mongodb://a/?replicaSet=rs"
6+
7+
phases: [
8+
9+
{
10+
responses: [
11+
["a:27017", {
12+
ok: 1,
13+
ismaster: false,
14+
secondary: true,
15+
hosts: ["a:27017"],
16+
setName: "rs",
17+
electionId: {"$oid": "000000000000000000000001"}
18+
}]
19+
],
20+
21+
outcome: {
22+
servers: {
23+
"a:27017": {
24+
type: "RSSecondary",
25+
setName: "rs",
26+
electionId: {"$oid": "000000000000000000000001"}
27+
}
28+
},
29+
topologyType: "ReplicaSetNoPrimary",
30+
setName: "rs",
31+
maxElectionId:
32+
}
33+
}
34+
]
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
description: "New primary with greater electionId"
2+
3+
uri: "mongodb://a/?replicaSet=rs"
4+
5+
phases: [
6+
7+
# Primary A is discovered and tells us about B.
8+
{
9+
responses: [
10+
["a:27017", {
11+
ok: 1,
12+
ismaster: true,
13+
hosts: ["a:27017", "b:27017"],
14+
setName: "rs",
15+
electionId: {"$oid": "000000000000000000000001"}
16+
}]
17+
],
18+
19+
outcome: {
20+
servers: {
21+
"a:27017": {
22+
type: "RSPrimary",
23+
setName: "rs",
24+
electionId: {"$oid": "000000000000000000000001"}
25+
},
26+
"b:27017": {
27+
type: "Unknown",
28+
setName: ,
29+
electionId:
30+
}
31+
},
32+
topologyType: "ReplicaSetWithPrimary",
33+
setName: "rs",
34+
maxElectionId: {"$oid": "000000000000000000000001"}
35+
}
36+
},
37+
38+
# B is elected.
39+
{
40+
responses: [
41+
["b:27017", {
42+
ok: 1,
43+
ismaster: true,
44+
hosts: ["a:27017", "b:27017"],
45+
setName: "rs",
46+
electionId: {"$oid": "000000000000000000000002"}
47+
}]
48+
],
49+
50+
outcome: {
51+
servers: {
52+
"a:27017": {
53+
type: "Unknown",
54+
setName: ,
55+
electionId:
56+
},
57+
"b:27017": {
58+
type: "RSPrimary",
59+
setName: "rs",
60+
electionId: {"$oid": "000000000000000000000002"}
61+
}
62+
},
63+
topologyType: "ReplicaSetWithPrimary",
64+
setName: "rs",
65+
maxElectionId: {"$oid": "000000000000000000000002"}
66+
}
67+
},
68+
69+
# A still claims to be primary but it's ignored.
70+
{
71+
responses: [
72+
["a:27017", {
73+
ok: 1,
74+
ismaster: true,
75+
hosts: ["a:27017", "b:27017"],
76+
setName: "rs",
77+
electionId: {"$oid": "000000000000000000000001"}
78+
}]
79+
],
80+
outcome: {
81+
servers: {
82+
"a:27017": {
83+
type: "Unknown",
84+
setName: ,
85+
electionId:
86+
},
87+
"b:27017": {
88+
type: "RSPrimary",
89+
setName: "rs",
90+
electionId: {"$oid": "000000000000000000000002"}
91+
}
92+
},
93+
topologyType: "ReplicaSetWithPrimary",
94+
setName: "rs",
95+
maxElectionId: {"$oid": "000000000000000000000002"}
96+
}
97+
}
98+
]
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
description: "Primary with no electionId, then a primary with electionId"
2+
3+
uri: "mongodb://a/?replicaSet=rs"
4+
5+
phases: [
6+
7+
# Primary A has no electionId.
8+
{
9+
responses: [
10+
["a:27017", {
11+
ok: 1,
12+
ismaster: true,
13+
hosts: ["a:27017", "b:27017"],
14+
setName: "rs"
15+
}]
16+
],
17+
18+
outcome: {
19+
servers: {
20+
"a:27017": {
21+
type: "RSPrimary",
22+
setName: "rs",
23+
electionId:
24+
},
25+
"b:27017": {
26+
type: "Unknown",
27+
setName: ,
28+
electionId:
29+
}
30+
},
31+
topologyType: "ReplicaSetWithPrimary",
32+
setName: "rs",
33+
maxElectionId:
34+
}
35+
},
36+
37+
# B is elected, it has an electionId.
38+
{
39+
responses: [
40+
["b:27017", {
41+
ok: 1,
42+
ismaster: true,
43+
hosts: ["a:27017", "b:27017"],
44+
setName: "rs",
45+
electionId: {"$oid": "000000000000000000000001"}
46+
}]
47+
],
48+
49+
outcome: {
50+
servers: {
51+
"a:27017": {
52+
type: "Unknown",
53+
setName: ,
54+
electionId:
55+
},
56+
"b:27017": {
57+
type: "RSPrimary",
58+
setName: "rs",
59+
electionId: {"$oid": "000000000000000000000001"}
60+
}
61+
},
62+
topologyType: "ReplicaSetWithPrimary",
63+
setName: "rs",
64+
maxElectionId: {"$oid": "000000000000000000000001"}
65+
}
66+
},
67+
68+
# A still claims to be primary, no electionId, we have to trust it.
69+
{
70+
responses: [
71+
["a:27017", {
72+
ok: 1,
73+
ismaster: true,
74+
hosts: ["a:27017", "b:27017"],
75+
setName: "rs"
76+
}]
77+
],
78+
outcome: {
79+
servers: {
80+
"a:27017": {
81+
type: "RSPrimary",
82+
setName: "rs",
83+
electionId:
84+
},
85+
"b:27017": {
86+
type: "Unknown",
87+
setName: ,
88+
electionId:
89+
}
90+
},
91+
topologyType: "ReplicaSetWithPrimary",
92+
setName: "rs",
93+
# But we remember old electionId.
94+
maxElectionId: {"$oid": "000000000000000000000001"}
95+
}
96+
}
97+
]

spec/support/server_discovery_and_monitoring.rb

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,15 @@ def find_server(client, uri)
4545
client.cluster.instance_variable_get(:@servers).detect{ |s| s.address.to_s == uri }
4646
end
4747

48+
# Helper to convert an extended JSON ObjectId to BSON::ObjectId.
49+
#
50+
# @since 2.1.0
51+
def self.convert_oids(docs)
52+
docs.each do |doc |
53+
doc['electionId'] = BSON::ObjectId.from_string(doc['electionId']['$oid']) if doc['electionId']
54+
end
55+
end
56+
4857
# Represents a specification.
4958
#
5059
# @since 2.0.0
@@ -102,7 +111,7 @@ class Phase
102111
# @since 2.0.0
103112
def initialize(phase, uri)
104113
@phase = phase
105-
@responses = @phase['responses'].map{ |response| Response.new(response, uri) }
114+
@responses = @phase['responses'].map{ |response| Response.new(SDAM::convert_oids(response), uri) }
106115
@outcome = Outcome.new(@phase['outcome'])
107116
end
108117
end
@@ -158,10 +167,18 @@ class Outcome
158167
#
159168
# @since 2.0.0
160169
def initialize(outcome)
161-
@servers = outcome['servers']
170+
@servers = process_servers(outcome['servers'])
162171
@set_name = outcome['setName']
163172
@topology_type = outcome['topologyType']
164173
end
174+
175+
private
176+
177+
def process_servers(servers)
178+
servers.each do |s|
179+
SDAM::convert_oids([ s[1] ])
180+
end
181+
end
165182
end
166183
end
167184
end

0 commit comments

Comments
 (0)