Skip to content

Commit a80169d

Browse files
authored
PYTHON-2463 Do not allow a MongoClient to be reused after it is closed (#737)
1 parent 1115522 commit a80169d

13 files changed

+79
-140
lines changed

doc/changelog.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,9 @@ Breaking Changes in 4.0
143143
opposed to
144144
the previous syntax which was simply ``if collection:`` or ``if database:``.
145145
You must now explicitly compare with None.
146+
- :class:`~pymongo.mongo_client.MongoClient` cannot execute any operations
147+
after being closed. The previous behavior would simply reconnect. However,
148+
now you must create a new instance.
146149
- Classes :class:`~bson.int64.Int64`, :class:`~bson.min_key.MinKey`,
147150
:class:`~bson.max_key.MaxKey`, :class:`~bson.timestamp.Timestamp`,
148151
:class:`~bson.regex.Regex`, and :class:`~bson.dbref.DBRef` all implement

doc/migrate-to-pymongo4.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,12 @@ can be changed to this::
180180
now defaults to ``False`` instead of ``True``. ``json_util.loads`` now
181181
decodes datetime as naive by default.
182182

183+
MongoClient cannot execute operations after ``close()``
184+
.......................................................
185+
186+
:class:`~pymongo.mongo_client.MongoClient` cannot execute any operations
187+
after being closed. The previous behavior would simply reconnect. However,
188+
now you must create a new instance.
183189

184190
Database
185191
--------

pymongo/mongo_client.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -704,7 +704,6 @@ def __init__(
704704
self.__kill_cursors_queue = []
705705

706706
self._event_listeners = options.pool_options.event_listeners
707-
708707
super(MongoClient, self).__init__(options.codec_options,
709708
options.read_preference,
710709
options.write_concern,
@@ -1127,10 +1126,10 @@ def close(self):
11271126
sending one or more endSessions commands.
11281127
11291128
Close all sockets in the connection pools and stop the monitor threads.
1130-
If this instance is used again it will be automatically re-opened and
1131-
the threads restarted unless auto encryption is enabled. A client
1132-
enabled with auto encryption cannot be used again after being closed;
1133-
any attempt will raise :exc:`~.errors.InvalidOperation`.
1129+
1130+
.. versionchanged:: 4.0
1131+
Once closed, the client cannot be used again and any attempt will
1132+
raise :exc:`~pymongo.errors.InvalidOperation`.
11341133
11351134
.. versionchanged:: 3.6
11361135
End all server sessions created by this client.

pymongo/topology.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@
3333
OperationFailure,
3434
PyMongoError,
3535
ServerSelectionTimeoutError,
36-
WriteError)
36+
WriteError,
37+
InvalidOperation)
3738
from pymongo.hello import Hello
3839
from pymongo.monitor import SrvMonitor
3940
from pymongo.pool import PoolOptions
@@ -112,6 +113,7 @@ def __init__(self, topology_settings):
112113
# Store the seed list to help diagnose errors in _error_message().
113114
self._seed_addresses = list(topology_description.server_descriptions())
114115
self._opened = False
116+
self._closed = False
115117
self._lock = threading.Lock()
116118
self._condition = self._settings.condition_class(self._lock)
117119
self._servers = {}
@@ -461,7 +463,9 @@ def update_pool(self, all_credentials):
461463
raise
462464

463465
def close(self):
464-
"""Clear pools and terminate monitors. Topology reopens on demand."""
466+
"""Clear pools and terminate monitors. Topology does not reopen on
467+
demand. Any further operations will raise
468+
:exc:`~.errors.InvalidOperation`. """
465469
with self._lock:
466470
for server in self._servers.values():
467471
server.close()
@@ -477,6 +481,7 @@ def close(self):
477481
self._srv_monitor.close()
478482

479483
self._opened = False
484+
self._closed = True
480485

481486
# Publish only after releasing the lock.
482487
if self._publish_tp:
@@ -550,6 +555,11 @@ def _ensure_opened(self):
550555
551556
Hold the lock when calling this.
552557
"""
558+
if self._closed:
559+
raise InvalidOperation("Once a MongoClient is closed, "
560+
"all operations will fail. Please create "
561+
"a new client object if you wish to "
562+
"reconnect.")
553563
if not self._opened:
554564
self._opened = True
555565
self._update_servers()

test/test_client.py

Lines changed: 41 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,8 @@
5050
NetworkTimeout,
5151
OperationFailure,
5252
ServerSelectionTimeoutError,
53-
WriteConcernError)
53+
WriteConcernError,
54+
InvalidOperation)
5455
from pymongo.hello import HelloCompat
5556
from pymongo.mongo_client import MongoClient
5657
from pymongo.monitoring import (ServerHeartbeatListener,
@@ -772,28 +773,22 @@ def test_drop_database(self):
772773
self.assertNotIn("pymongo_test2", dbs)
773774

774775
def test_close(self):
775-
coll = self.client.pymongo_test.bar
776-
777-
self.client.close()
778-
self.client.close()
779-
780-
coll.count_documents({})
781-
782-
self.client.close()
783-
self.client.close()
784-
785-
coll.count_documents({})
776+
test_client = rs_or_single_client()
777+
coll = test_client.pymongo_test.bar
778+
test_client.close()
779+
self.assertRaises(InvalidOperation, coll.count_documents, {})
786780

787781
def test_close_kills_cursors(self):
788782
if sys.platform.startswith('java'):
789783
# We can't figure out how to make this test reliable with Jython.
790784
raise SkipTest("Can't test with Jython")
785+
test_client = rs_or_single_client()
791786
# Kill any cursors possibly queued up by previous tests.
792787
gc.collect()
793-
self.client._process_periodic_tasks()
788+
test_client._process_periodic_tasks()
794789

795790
# Add some test data.
796-
coll = self.client.pymongo_test.test_close_kills_cursors
791+
coll = test_client.pymongo_test.test_close_kills_cursors
797792
docs_inserted = 1000
798793
coll.insert_many([{"i": i} for i in range(docs_inserted)])
799794

@@ -811,13 +806,13 @@ def test_close_kills_cursors(self):
811806
gc.collect()
812807

813808
# Close the client and ensure the topology is closed.
814-
self.assertTrue(self.client._topology._opened)
815-
self.client.close()
816-
self.assertFalse(self.client._topology._opened)
817-
809+
self.assertTrue(test_client._topology._opened)
810+
test_client.close()
811+
self.assertFalse(test_client._topology._opened)
812+
test_client = rs_or_single_client()
818813
# The killCursors task should not need to re-open the topology.
819-
self.client._process_periodic_tasks()
820-
self.assertFalse(self.client._topology._opened)
814+
test_client._process_periodic_tasks()
815+
self.assertTrue(test_client._topology._opened)
821816

822817
def test_close_stops_kill_cursors_thread(self):
823818
client = rs_client()
@@ -828,12 +823,9 @@ def test_close_stops_kill_cursors_thread(self):
828823
client.close()
829824
self.assertTrue(client._kill_cursors_executor._stopped)
830825

831-
# Reusing the closed client should restart the thread.
832-
client.admin.command('ping')
833-
self.assertFalse(client._kill_cursors_executor._stopped)
834-
835-
# Again, closing the client should stop the thread.
836-
client.close()
826+
# Reusing the closed client should raise an InvalidOperation error.
827+
self.assertRaises(InvalidOperation, client.admin.command, 'ping')
828+
# Thread is still stopped.
837829
self.assertTrue(client._kill_cursors_executor._stopped)
838830

839831
def test_uri_connect_option(self):
@@ -1128,12 +1120,13 @@ def test_contextlib(self):
11281120

11291121
with contextlib.closing(client):
11301122
self.assertEqual("bar", client.pymongo_test.test.find_one()["foo"])
1131-
self.assertEqual(1, len(get_pool(client).sockets))
1132-
self.assertEqual(0, len(get_pool(client).sockets))
1133-
1123+
with self.assertRaises(InvalidOperation):
1124+
client.pymongo_test.test.find_one()
1125+
client = rs_or_single_client()
11341126
with client as client:
11351127
self.assertEqual("bar", client.pymongo_test.test.find_one()["foo"])
1136-
self.assertEqual(0, len(get_pool(client).sockets))
1128+
with self.assertRaises(InvalidOperation):
1129+
client.pymongo_test.test.find_one()
11371130

11381131
def test_interrupt_signal(self):
11391132
if sys.platform.startswith('java'):
@@ -1787,35 +1780,26 @@ def test_max_bson_size(self):
17871780
class TestMongoClientFailover(MockClientTest):
17881781

17891782
def test_discover_primary(self):
1790-
# Disable background refresh.
1791-
with client_knobs(heartbeat_frequency=999999):
1792-
c = MockClient(
1793-
standalones=[],
1794-
members=['a:1', 'b:2', 'c:3'],
1795-
mongoses=[],
1796-
host='b:2', # Pass a secondary.
1797-
replicaSet='rs')
1798-
self.addCleanup(c.close)
1799-
1800-
wait_until(lambda: len(c.nodes) == 3, 'connect')
1801-
self.assertEqual(c.address, ('a', 1))
1802-
1803-
# Fail over.
1804-
c.kill_host('a:1')
1805-
c.mock_primary = 'b:2'
1806-
1807-
c.close()
1808-
self.assertEqual(0, len(c.nodes))
1809-
1810-
t = c._get_topology()
1811-
t.select_servers(writable_server_selector) # Reconnect.
1812-
self.assertEqual(c.address, ('b', 2))
1783+
c = MockClient(
1784+
standalones=[],
1785+
members=['a:1', 'b:2', 'c:3'],
1786+
mongoses=[],
1787+
host='b:2', # Pass a secondary.
1788+
replicaSet='rs',
1789+
heartbeatFrequencyMS=500)
1790+
self.addCleanup(c.close)
18131791

1814-
# a:1 not longer in nodes.
1815-
self.assertLess(len(c.nodes), 3)
1792+
wait_until(lambda: len(c.nodes) == 3, 'connect')
18161793

1817-
# c:3 is rediscovered.
1818-
t.select_server_by_address(('c', 3))
1794+
self.assertEqual(c.address, ('a', 1))
1795+
# Fail over.
1796+
c.kill_host('a:1')
1797+
c.mock_primary = 'b:2'
1798+
wait_until(lambda: c.address == ('b', 2), "wait for server "
1799+
"address to be "
1800+
"updated")
1801+
# a:1 not longer in nodes.
1802+
self.assertLess(len(c.nodes), 3)
18191803

18201804
def test_reconnect(self):
18211805
# Verify the node list isn't forgotten during a network failure.

test/test_load_balancer.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -62,10 +62,6 @@ def test_unpin_committed_transaction(self):
6262
self.assertEqual(pool.active_sockets, 1) # Still pinned.
6363
self.assertEqual(pool.active_sockets, 0) # Unpinned.
6464

65-
def test_client_can_be_reopened(self):
66-
self.client.close()
67-
self.db.test.find_one({})
68-
6965
@client_context.require_failCommand_fail_point
7066
def test_cursor_gc(self):
7167
def create_resource(coll):

test/test_mongos_load_balancing.py

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -92,21 +92,6 @@ def test_lazy_connect(self):
9292
do_simple_op(client, nthreads)
9393
wait_until(lambda: len(client.nodes) == 3, 'connect to all mongoses')
9494

95-
def test_reconnect(self):
96-
nthreads = 10
97-
client = connected(self.mock_client())
98-
99-
# connected() ensures we've contacted at least one mongos. Wait for
100-
# all of them.
101-
wait_until(lambda: len(client.nodes) == 3, 'connect to all mongoses')
102-
103-
# Trigger reconnect.
104-
client.close()
105-
do_simple_op(client, nthreads)
106-
107-
wait_until(lambda: len(client.nodes) == 3,
108-
'reconnect to all mongoses')
109-
11095
def test_failover(self):
11196
nthreads = 10
11297
client = connected(self.mock_client(localThresholdMS=0.001))

test/test_pooling.py

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -102,12 +102,6 @@ def run_mongo_thread(self):
102102
raise AssertionError("Should have raised DuplicateKeyError")
103103

104104

105-
class Disconnect(MongoThread):
106-
def run_mongo_thread(self):
107-
for _ in range(N):
108-
self.client.close()
109-
110-
111105
class SocketGetter(MongoThread):
112106
"""Utility for TestPooling.
113107
@@ -198,9 +192,6 @@ def test_max_pool_size_validation(self):
198192
def test_no_disconnect(self):
199193
run_cases(self.c, [NonUnique, Unique, InsertOneAndFind])
200194

201-
def test_disconnect(self):
202-
run_cases(self.c, [InsertOneAndFind, Disconnect, Unique])
203-
204195
def test_pool_reuses_open_socket(self):
205196
# Test Pool's _check_closed() method doesn't close a healthy socket.
206197
cx_pool = self.create_pool(max_pool_size=10)

test/test_raw_bson.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
from bson.raw_bson import RawBSONDocument, DEFAULT_RAW_BSON_OPTIONS
2626
from bson.son import SON
2727
from test import client_context, unittest
28+
from test.utils import rs_or_single_client
2829
from test.test_client import IntegrationTest
2930

3031

@@ -43,6 +44,7 @@ class TestRawBSONDocument(IntegrationTest):
4344
@classmethod
4445
def setUpClass(cls):
4546
super(TestRawBSONDocument, cls).setUpClass()
47+
client_context.client = rs_or_single_client()
4648
cls.client = client_context.client
4749

4850
def tearDown(self):

test/test_replica_set_reconfig.py

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818

1919
sys.path[0:0] = [""]
2020

21-
from pymongo.errors import ConnectionFailure, AutoReconnect
21+
from pymongo.errors import ConnectionFailure, ServerSelectionTimeoutError
2222
from pymongo import ReadPreference
2323
from test import unittest, client_context, client_knobs, MockClientTest
2424
from test.pymongo_mocks import MockClient
@@ -42,13 +42,10 @@ def test_client(self):
4242
mongoses=[],
4343
host='a:1,b:2,c:3',
4444
replicaSet='rs',
45-
serverSelectionTimeoutMS=100)
45+
serverSelectionTimeoutMS=100,
46+
connect=False)
4647
self.addCleanup(c.close)
4748

48-
# MongoClient connects to primary by default.
49-
wait_until(lambda: c.address is not None, 'connect to primary')
50-
self.assertEqual(c.address, ('a', 1))
51-
5249
# C is brought up as a standalone.
5350
c.mock_members.remove('c:3')
5451
c.mock_standalones.append('c:3')
@@ -57,14 +54,15 @@ def test_client(self):
5754
c.kill_host('a:1')
5855
c.kill_host('b:2')
5956

60-
# Force reconnect.
61-
c.close()
62-
63-
with self.assertRaises(AutoReconnect):
57+
with self.assertRaises(ServerSelectionTimeoutError):
6458
c.db.command('ping')
65-
6659
self.assertEqual(c.address, None)
6760

61+
# Client can still discover the primary node
62+
c.revive_host('a:1')
63+
wait_until(lambda: c.address is not None, 'connect to primary')
64+
self.assertEqual(c.address, ('a', 1))
65+
6866
def test_replica_set_client(self):
6967
c = MockClient(
7068
standalones=[],
@@ -158,7 +156,6 @@ def test_client(self):
158156
c.mock_members.append('c:3')
159157
c.mock_hello_hosts.append('c:3')
160158

161-
c.close()
162159
c.db.command('ping')
163160

164161
self.assertEqual(c.address, ('a', 1))

0 commit comments

Comments
 (0)