Skip to content

Commit a799d52

Browse files
committed
Don't use servers without the right replica set name, PYTHON-591.
1 parent 64619ed commit a799d52

File tree

5 files changed

+228
-15
lines changed

5 files changed

+228
-15
lines changed

doc/changelog.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
Changelog
22
=========
33

4+
Changes in Version 2.7
5+
----------------------
6+
7+
Version 2.7 drops support for replica sets running MongoDB versions older
8+
than 1.6.2.
9+
410
Changes in Version 2.6.3
511
------------------------
612

pymongo/mongo_client.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -645,10 +645,7 @@ def __try_node(self, node):
645645
# Check that this host is part of the given replica set.
646646
if self.__repl:
647647
set_name = response.get('setName')
648-
# The 'setName' field isn't returned by mongod before 1.6.2
649-
# so we can't assume that if it's missing this host isn't in
650-
# the specified set.
651-
if set_name and set_name != self.__repl:
648+
if set_name != self.__repl:
652649
raise ConfigurationError("%s:%d is not a member of "
653650
"replica set %s"
654651
% (node[0], node[1], self.__repl))

pymongo/mongo_replica_set_client.py

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -744,7 +744,7 @@ def __init__(self, hosts_or_uri=None, max_pool_size=100,
744744

745745
if _connect:
746746
try:
747-
self.refresh()
747+
self.refresh(initial=True)
748748
except AutoReconnect, e:
749749
# ConnectionFailure makes more sense here than AutoReconnect
750750
raise ConnectionFailure(str(e))
@@ -1113,7 +1113,7 @@ def __make_threadlocal(self):
11131113
else:
11141114
return threading.local()
11151115

1116-
def refresh(self):
1116+
def refresh(self, initial=False):
11171117
"""Iterate through the existing host list, or possibly the
11181118
seed list, to update the list of hosts and arbiters in this
11191119
replica set.
@@ -1153,15 +1153,17 @@ def refresh(self):
11531153
node, pool, response, MovingAverage([ping_time]), True)
11541154

11551155
# Check that this host is part of the given replica set.
1156-
set_name = response.get('setName')
1157-
# The 'setName' field isn't returned by mongod before 1.6.2
1158-
# so we can't assume that if it's missing this host isn't in
1159-
# the specified set.
1160-
if set_name and set_name != self.__name:
1161-
host, port = node
1162-
raise ConfigurationError("%s:%d is not a member of "
1163-
"replica set %s"
1164-
% (host, port, self.__name))
1156+
# Fail fast if we find a bad seed during __init__.
1157+
# Regular refreshes keep searching for valid nodes.
1158+
if response.get('setName') != self.__name:
1159+
if initial:
1160+
host, port = node
1161+
raise ConfigurationError("%s:%d is not a member of "
1162+
"replica set %s"
1163+
% (host, port, self.__name))
1164+
else:
1165+
continue
1166+
11651167
if "arbiters" in response:
11661168
arbiters = set([
11671169
_partition_node(h) for h in response["arbiters"]])
@@ -1202,10 +1204,19 @@ def refresh(self):
12021204
sock_info = self.__socket(member, force=True)
12031205
res, ping_time = self.__simple_command(
12041206
sock_info, 'admin', {'ismaster': 1})
1207+
1208+
if res.get('setName') != self.__name:
1209+
# Not a member of this set.
1210+
continue
1211+
12051212
member.pool.maybe_return_socket(sock_info)
12061213
new_member = member.clone_with(res, ping_time)
12071214
else:
12081215
res, connection_pool, ping_time = self.__is_master(host)
1216+
if res.get('setName') != self.__name:
1217+
# Not a member of this set.
1218+
continue
1219+
12091220
new_member = Member(
12101221
host, connection_pool, res, MovingAverage([ping_time]),
12111222
True)

test/test_client.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -882,6 +882,18 @@ def test_alive(self):
882882
client = MongoClient('doesnt exist', _connect=False)
883883
self.assertFalse(client.alive())
884884

885+
def test_replica_set(self):
886+
client = MongoClient(host, port)
887+
name = client.pymongo_test.command('ismaster').get('setName')
888+
if not name:
889+
raise SkipTest('Not connected to a replica set')
890+
891+
MongoClient(host, port, replicaSet=name) # No error.
892+
893+
self.assertRaises(
894+
ConnectionFailure,
895+
MongoClient, host, port, replicaSet='bad' + name)
896+
885897

886898
class TestClientLazyConnect(unittest.TestCase, _TestLazyConnectMixin):
887899
def _get_client(self, **kwargs):

test/test_replica_set_reconfig.py

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
# Copyright 2013 MongoDB, Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Test MongoReplicaSetClients and replica set configuration changes."""
16+
17+
import socket
18+
import sys
19+
import unittest
20+
21+
sys.path[0:0] = [""]
22+
23+
from pymongo import MongoClient
24+
from pymongo.errors import AutoReconnect
25+
from pymongo.mongo_replica_set_client import MongoReplicaSetClient
26+
from pymongo.pool import Pool
27+
from test import host as default_host, port as default_port
28+
29+
30+
class MockPool(Pool):
31+
def __init__(self, pair, *args, **kwargs):
32+
if pair:
33+
# RS client passes 'pair' to Pool's constructor.
34+
self.mock_host, self.mock_port = pair
35+
else:
36+
# MongoClient passes pair to get_socket() instead.
37+
self.mock_host, self.mock_port = None, None
38+
39+
Pool.__init__(
40+
self,
41+
pair=(default_host, default_port),
42+
max_size=None,
43+
net_timeout=None,
44+
conn_timeout=20,
45+
use_ssl=False,
46+
use_greenlets=False)
47+
48+
def get_socket(self, pair=None, force=False):
49+
sock_info = Pool.get_socket(self, (default_host, default_port), force)
50+
sock_info.host = self.mock_host or pair[0]
51+
return sock_info
52+
53+
54+
MOCK_HOSTS = ['a:27017', 'b:27017', 'c:27017']
55+
MOCK_PRIMARY = MOCK_HOSTS[0]
56+
MOCK_RS_NAME = 'rs'
57+
58+
59+
class MockClientBase(object):
60+
def __init__(self):
61+
self.mock_hosts = MOCK_HOSTS
62+
63+
# Hosts that should raise socket errors.
64+
self.mock_down_hosts = []
65+
66+
# Hosts that should respond to ismaster as if they're standalone.
67+
self.mock_standalone_hosts = []
68+
69+
def mock_is_master(self, host):
70+
if host in self.mock_down_hosts:
71+
raise socket.timeout('timed out')
72+
73+
if host in self.mock_standalone_hosts:
74+
return {'ismaster': True}
75+
76+
if host not in self.mock_hosts:
77+
# Host removed from set by a reconfig.
78+
return {'ismaster': False, 'secondary': False}
79+
80+
ismaster = host == MOCK_PRIMARY
81+
82+
# Simulate a replica set member.
83+
return {
84+
'ismaster': ismaster,
85+
'secondary': not ismaster,
86+
'setName': MOCK_RS_NAME,
87+
'hosts': self.mock_hosts}
88+
89+
def simple_command(self, sock_info, dbname, spec):
90+
# __simple_command is also used for authentication, but in this
91+
# test it's only used for ismaster.
92+
assert spec == {'ismaster': 1}
93+
response = self.mock_is_master('%s:%s' % (sock_info.host, 27017))
94+
ping_time = 10
95+
return response, ping_time
96+
97+
98+
class MockClient(MockClientBase, MongoClient):
99+
def __init__(self, hosts):
100+
MockClientBase.__init__(self)
101+
MongoClient.__init__(
102+
self,
103+
hosts,
104+
replicaSet=MOCK_RS_NAME,
105+
_pool_class=MockPool)
106+
107+
def _MongoClient__simple_command(self, sock_info, dbname, spec):
108+
return self.simple_command(sock_info, dbname, spec)
109+
110+
111+
class MockReplicaSetClient(MockClientBase, MongoReplicaSetClient):
112+
def __init__(self, hosts):
113+
MockClientBase.__init__(self)
114+
MongoReplicaSetClient.__init__(
115+
self,
116+
hosts,
117+
replicaSet=MOCK_RS_NAME)
118+
119+
def _MongoReplicaSetClient__is_master(self, host):
120+
response = self.mock_is_master('%s:%s' % host)
121+
connection_pool = MockPool(host)
122+
ping_time = 10
123+
return response, connection_pool, ping_time
124+
125+
def _MongoReplicaSetClient__simple_command(self, sock_info, dbname, spec):
126+
return self.simple_command(sock_info, dbname, spec)
127+
128+
129+
class TestSecondaryBecomesStandalone(unittest.TestCase):
130+
# An administrator removes a secondary from a 3-node set and
131+
# brings it back up as standalone, without updating the other
132+
# members' config. Verify we don't continue using it.
133+
def test_client(self):
134+
c = MockClient(','.join(MOCK_HOSTS))
135+
136+
# MongoClient connects to primary by default.
137+
self.assertEqual('a', c.host)
138+
self.assertEqual(27017, c.port)
139+
140+
# C is brought up as a standalone.
141+
c.mock_standalone_hosts.append('c:27017')
142+
143+
# Fail over.
144+
c.mock_down_hosts = ['a:27017', 'b:27017']
145+
146+
# Force reconnect.
147+
c.disconnect()
148+
149+
try:
150+
c.db.collection.find_one()
151+
except AutoReconnect, e:
152+
self.assertTrue('not a member of replica set' in str(e))
153+
else:
154+
self.fail("MongoClient didn't raise AutoReconnect")
155+
156+
self.assertEqual(None, c.host)
157+
self.assertEqual(None, c.port)
158+
159+
def test_replica_set_client(self):
160+
c = MockReplicaSetClient(','.join(MOCK_HOSTS))
161+
self.assertTrue(('c', 27017) in c.secondaries)
162+
163+
# C is brought up as a standalone.
164+
c.mock_standalone_hosts.append('c:27017')
165+
c.refresh()
166+
167+
self.assertEqual(('a', 27017), c.primary)
168+
self.assertEqual(set([('b', 27017)]), c.secondaries)
169+
170+
171+
class TestSecondaryRemoved(unittest.TestCase):
172+
# An administrator removes a secondary from a 3-node set *without*
173+
# restarting it as standalone.
174+
def test_replica_set_client(self):
175+
c = MockReplicaSetClient(','.join(MOCK_HOSTS))
176+
self.assertTrue(('c', 27017) in c.secondaries)
177+
178+
# C is removed.
179+
c.mock_hosts.remove('c:27017')
180+
c.refresh()
181+
182+
self.assertEqual(('a', 27017), c.primary)
183+
self.assertEqual(set([('b', 27017)]), c.secondaries)
184+
185+
186+
if __name__ == "__main__":
187+
unittest.main()

0 commit comments

Comments
 (0)