Skip to content

Commit f28c0aa

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

File tree

7 files changed

+420
-7
lines changed

7 files changed

+420
-7
lines changed

lib/mongo/cluster/topology/replica_set.rb

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,11 +60,14 @@ 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+
unless detect_stale_primary!(description)
64+
log_debug([ "Server #{description.address.to_s} elected as primary in #{replica_set_name}." ])
65+
servers.each do |server|
66+
if server.primary? && server.address != description.address
67+
server.description.unknown!
68+
end
6769
end
70+
@max_election_id = description.election_id
6871
end
6972
else
7073
log_warn([
@@ -84,6 +87,7 @@ def elect_primary(description, servers)
8487
# @since 2.0.0
8588
def initialize(options, seeds = [])
8689
@options = options
90+
@max_election_id = 0
8791
end
8892

8993
# A replica set topology is a replica set.
@@ -215,6 +219,12 @@ def standalone_discovered; self; end
215219

216220
private
217221

222+
def detect_stale_primary!(description)
223+
if description.election_id && description.election_id < @max_election_id
224+
description.unknown!
225+
end
226+
end
227+
218228
def has_primary?(servers)
219229
servers.find { |s| s.primary? }
220230
end

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: 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+
]

0 commit comments

Comments
 (0)