Skip to content

Commit 3d1c206

Browse files
committed
PYTHON-924 - Use electionId to detect stale primaries.
1 parent 51cadce commit 3d1c206

17 files changed

+508
-26
lines changed

pymongo/ismaster.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,10 @@ def min_wire_version(self):
111111
def max_wire_version(self):
112112
return self._doc.get('maxWireVersion', common.MAX_WIRE_VERSION)
113113

114+
@property
115+
def election_id(self):
116+
return self._doc.get('electionId')
117+
114118
@property
115119
def is_writable(self):
116120
return self._is_writable

pymongo/server_description.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ class ServerDescription(object):
3232
'_address', '_server_type', '_all_hosts', '_tags', '_replica_set_name',
3333
'_primary', '_max_bson_size', '_max_message_size',
3434
'_max_write_batch_size', '_min_wire_version', '_max_wire_version',
35-
'_round_trip_time', '_is_writable', '_is_readable', '_error')
35+
'_round_trip_time', '_is_writable', '_is_readable', '_error',
36+
'_election_id')
3637

3738
def __init__(
3839
self,
@@ -54,6 +55,7 @@ def __init__(
5455
self._max_write_batch_size = ismaster.max_write_batch_size
5556
self._min_wire_version = ismaster.min_wire_version
5657
self._max_wire_version = ismaster.max_wire_version
58+
self._election_id = ismaster.election_id
5759
self._is_writable = ismaster.is_writable
5860
self._is_readable = ismaster.is_readable
5961
self._round_trip_time = round_trip_time
@@ -106,6 +108,10 @@ def min_wire_version(self):
106108
def max_wire_version(self):
107109
return self._max_wire_version
108110

111+
@property
112+
def election_id(self):
113+
return self._election_id
114+
109115
@property
110116
def round_trip_time(self):
111117
"""The current average latency or None."""

pymongo/topology.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@ def __init__(self, topology_settings):
4040
topology_description = TopologyDescription(
4141
topology_settings.get_topology_type(),
4242
topology_settings.get_server_descriptions(),
43-
topology_settings.replica_set_name)
43+
topology_settings.replica_set_name,
44+
None)
4445

4546
self._description = topology_description
4647
# Store the seed list to help diagnose errors in _error_message().

pymongo/topology_description.py

Lines changed: 49 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -28,18 +28,25 @@
2828

2929

3030
class TopologyDescription(object):
31-
def __init__(self, topology_type, server_descriptions, replica_set_name):
31+
def __init__(
32+
self,
33+
topology_type,
34+
server_descriptions,
35+
replica_set_name,
36+
max_election_id):
3237
"""Represent a topology of servers.
3338
3439
:Parameters:
3540
- `topology_type`: initial type
3641
- `server_descriptions`: dict of (address, ServerDescription) for
3742
all seeds
3843
- `replica_set_name`: replica set name or None
44+
- `max_election_id`: greatest electionId seen from a primary, or None
3945
"""
4046
self._topology_type = topology_type
4147
self._replica_set_name = replica_set_name
4248
self._server_descriptions = server_descriptions
49+
self._max_election_id = max_election_id
4350

4451
# Is PyMongo compatible with all servers' wire protocols?
4552
self._incompatible_err = None
@@ -96,7 +103,11 @@ def reset(self):
96103
sds = dict((address, ServerDescription(address))
97104
for address in self._server_descriptions)
98105

99-
return TopologyDescription(topology_type, sds, self._replica_set_name)
106+
return TopologyDescription(
107+
topology_type,
108+
sds,
109+
self._replica_set_name,
110+
self._max_election_id)
100111

101112
def server_descriptions(self):
102113
"""Dict of (address, ServerDescription)."""
@@ -111,6 +122,11 @@ def replica_set_name(self):
111122
"""The replica set name."""
112123
return self._replica_set_name
113124

125+
@property
126+
def max_election_id(self):
127+
"""Greatest electionId seen from a primary, or None."""
128+
return self._max_election_id
129+
114130
@property
115131
def known_servers(self):
116132
"""List of Servers of types besides Unknown."""
@@ -146,6 +162,7 @@ def updated_topology_description(topology_description, server_description):
146162
# TopologyDescription.
147163
topology_type = topology_description.topology_type
148164
set_name = topology_description.replica_set_name
165+
max_election_id = topology_description.max_election_id
149166
server_type = server_description.server_type
150167

151168
# Don't mutate the original dict of server descriptions; copy it.
@@ -156,7 +173,11 @@ def updated_topology_description(topology_description, server_description):
156173

157174
if topology_type == TOPOLOGY_TYPE.Single:
158175
# Single type never changes.
159-
return TopologyDescription(TOPOLOGY_TYPE.Single, sds, set_name)
176+
return TopologyDescription(
177+
TOPOLOGY_TYPE.Single,
178+
sds,
179+
set_name,
180+
max_election_id)
160181

161182
if topology_type == TOPOLOGY_TYPE.Unknown:
162183
if server_type == SERVER_TYPE.Standalone:
@@ -174,8 +195,8 @@ def updated_topology_description(topology_description, server_description):
174195
sds.pop(address)
175196

176197
elif server_type == SERVER_TYPE.RSPrimary:
177-
topology_type, set_name = _update_rs_from_primary(
178-
sds, set_name, server_description)
198+
topology_type, set_name, max_election_id = _update_rs_from_primary(
199+
sds, set_name, server_description, max_election_id)
179200

180201
elif server_type in (
181202
SERVER_TYPE.RSSecondary,
@@ -190,8 +211,8 @@ def updated_topology_description(topology_description, server_description):
190211
topology_type = _check_has_primary(sds)
191212

192213
elif server_type == SERVER_TYPE.RSPrimary:
193-
topology_type, set_name = _update_rs_from_primary(
194-
sds, set_name, server_description)
214+
topology_type, set_name, max_election_id = _update_rs_from_primary(
215+
sds, set_name, server_description, max_election_id)
195216

196217
elif server_type in (
197218
SERVER_TYPE.RSSecondary,
@@ -205,16 +226,21 @@ def updated_topology_description(topology_description, server_description):
205226
topology_type = _check_has_primary(sds)
206227

207228
# Return updated copy.
208-
return TopologyDescription(topology_type, sds, set_name)
229+
return TopologyDescription(topology_type, sds, set_name, max_election_id)
209230

210231

211-
def _update_rs_from_primary(sds, replica_set_name, server_description):
232+
def _update_rs_from_primary(
233+
sds,
234+
replica_set_name,
235+
server_description,
236+
max_election_id):
212237
"""Update topology description from a primary's ismaster response.
213238
214-
Pass in a dict of ServerDescriptions, current replica set name, and the
215-
ServerDescription we are processing.
239+
Pass in a dict of ServerDescriptions, current replica set name, the
240+
ServerDescription we are processing, and the TopologyDescription's
241+
max_election_id if any.
216242
217-
Returns (new topology type, new replica_set_name).
243+
Returns (new topology type, new replica_set_name, new max_election_id).
218244
"""
219245
if replica_set_name is None:
220246
replica_set_name = server_description.replica_set_name
@@ -223,7 +249,16 @@ def _update_rs_from_primary(sds, replica_set_name, server_description):
223249
# We found a primary but it doesn't have the replica_set_name
224250
# provided by the user.
225251
sds.pop(server_description.address)
226-
return _check_has_primary(sds), replica_set_name
252+
return _check_has_primary(sds), replica_set_name, max_election_id
253+
254+
if server_description.election_id is not None:
255+
if max_election_id and max_election_id > server_description.election_id:
256+
# Stale primary, set to type Unknown.
257+
address = server_description.address
258+
sds[address] = ServerDescription(address)
259+
return _check_has_primary(sds), replica_set_name, max_election_id
260+
261+
max_election_id = server_description.election_id
227262

228263
# We've heard from the primary. Is it the same primary as before?
229264
for server in sds.values():
@@ -247,7 +282,7 @@ def _update_rs_from_primary(sds, replica_set_name, server_description):
247282

248283
# If the host list differs from the seed list, we may not have a primary
249284
# after all.
250-
return _check_has_primary(sds), replica_set_name
285+
return _check_has_primary(sds), replica_set_name, max_election_id
251286

252287

253288
def _update_rs_with_primary_from_member(
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
{
2+
"description": "New primary with equal electionId",
3+
"phases": [
4+
{
5+
"outcome": {
6+
"servers": {
7+
"a:27017": {
8+
"electionId": null,
9+
"setName": null,
10+
"type": "Unknown"
11+
},
12+
"b:27017": {
13+
"electionId": {
14+
"$oid": "000000000000000000000001"
15+
},
16+
"setName": "rs",
17+
"type": "RSPrimary"
18+
}
19+
},
20+
"setName": "rs",
21+
"topologyType": "ReplicaSetWithPrimary"
22+
},
23+
"responses": [
24+
[
25+
"a:27017",
26+
{
27+
"electionId": {
28+
"$oid": "000000000000000000000001"
29+
},
30+
"hosts": [
31+
"a:27017",
32+
"b:27017"
33+
],
34+
"ismaster": true,
35+
"ok": 1,
36+
"setName": "rs"
37+
}
38+
],
39+
[
40+
"b:27017",
41+
{
42+
"electionId": {
43+
"$oid": "000000000000000000000001"
44+
},
45+
"hosts": [
46+
"a:27017",
47+
"b:27017"
48+
],
49+
"ismaster": true,
50+
"ok": 1,
51+
"setName": "rs"
52+
}
53+
]
54+
]
55+
}
56+
],
57+
"uri": "mongodb://a/?replicaSet=rs"
58+
}
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
{
2+
"description": "New primary with greater electionId",
3+
"phases": [
4+
{
5+
"outcome": {
6+
"servers": {
7+
"a:27017": {
8+
"electionId": {
9+
"$oid": "000000000000000000000001"
10+
},
11+
"setName": "rs",
12+
"type": "RSPrimary"
13+
},
14+
"b:27017": {
15+
"electionId": null,
16+
"setName": null,
17+
"type": "Unknown"
18+
}
19+
},
20+
"setName": "rs",
21+
"topologyType": "ReplicaSetWithPrimary"
22+
},
23+
"responses": [
24+
[
25+
"a:27017",
26+
{
27+
"electionId": {
28+
"$oid": "000000000000000000000001"
29+
},
30+
"hosts": [
31+
"a:27017",
32+
"b:27017"
33+
],
34+
"ismaster": true,
35+
"ok": 1,
36+
"setName": "rs"
37+
}
38+
]
39+
]
40+
},
41+
{
42+
"outcome": {
43+
"servers": {
44+
"a:27017": {
45+
"electionId": null,
46+
"setName": null,
47+
"type": "Unknown"
48+
},
49+
"b:27017": {
50+
"electionId": {
51+
"$oid": "000000000000000000000002"
52+
},
53+
"setName": "rs",
54+
"type": "RSPrimary"
55+
}
56+
},
57+
"setName": "rs",
58+
"topologyType": "ReplicaSetWithPrimary"
59+
},
60+
"responses": [
61+
[
62+
"b:27017",
63+
{
64+
"electionId": {
65+
"$oid": "000000000000000000000002"
66+
},
67+
"hosts": [
68+
"a:27017",
69+
"b:27017"
70+
],
71+
"ismaster": true,
72+
"ok": 1,
73+
"setName": "rs"
74+
}
75+
]
76+
]
77+
},
78+
{
79+
"outcome": {
80+
"servers": {
81+
"a:27017": {
82+
"electionId": null,
83+
"setName": null,
84+
"type": "Unknown"
85+
},
86+
"b:27017": {
87+
"electionId": {
88+
"$oid": "000000000000000000000002"
89+
},
90+
"setName": "rs",
91+
"type": "RSPrimary"
92+
}
93+
},
94+
"setName": "rs",
95+
"topologyType": "ReplicaSetWithPrimary"
96+
},
97+
"responses": [
98+
[
99+
"a:27017",
100+
{
101+
"electionId": {
102+
"$oid": "000000000000000000000001"
103+
},
104+
"hosts": [
105+
"a:27017",
106+
"b:27017"
107+
],
108+
"ismaster": true,
109+
"ok": 1,
110+
"setName": "rs"
111+
}
112+
]
113+
]
114+
}
115+
],
116+
"uri": "mongodb://a/?replicaSet=rs"
117+
}

0 commit comments

Comments
 (0)