Skip to content

Commit 5b49557

Browse files
committed
PYTHON-2268 Close clients in test suite
1 parent 4760d07 commit 5b49557

21 files changed

+128
-41
lines changed

pymongo/periodic_executor.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,10 @@ def __init__(self, interval, min_interval, target, name=None):
5050
self._thread_will_exit = False
5151
self._lock = threading.Lock()
5252

53+
def __repr__(self):
54+
return '<%s(name=%s) object at 0x%x>' % (
55+
self.__class__.__name__, self._name, id(self))
56+
5357
def open(self):
5458
"""Start. Multiple calls have no effect.
5559

pymongo/settings.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"""Represent MongoClient's configuration."""
1616

1717
import threading
18+
import traceback
1819

1920
from bson.objectid import ObjectId
2021
from pymongo import common, monitor, pool
@@ -60,6 +61,9 @@ def __init__(self,
6061
self._heartbeat_frequency = heartbeat_frequency
6162
self._direct = (len(self._seeds) == 1 and not replica_set_name)
6263
self._topology_id = ObjectId()
64+
# Store the allocation traceback to catch unclosed clients in the
65+
# test suite.
66+
self._stack = ''.join(traceback.format_stack())
6367

6468
@property
6569
def seeds(self):

test/__init__.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -795,6 +795,44 @@ def setup():
795795
warnings.simplefilter("always")
796796

797797

798+
def _get_executors(topology):
799+
executors = []
800+
for server in topology._servers.values():
801+
# Some MockMonitor do not have an _executor.
802+
executors.append(getattr(server._monitor, '_executor', None))
803+
executors.append(topology._Topology__events_executor)
804+
if topology._srv_monitor:
805+
executors.append(topology._srv_monitor._executor)
806+
return [e for e in executors if e is not None]
807+
808+
809+
def all_executors_stopped(topology):
810+
running = [e for e in _get_executors(topology) if not e._stopped]
811+
if running:
812+
print(' Topology %s has THREADS RUNNING: %s, created at: %s' % (
813+
topology, running, topology._settings._stack))
814+
return False
815+
return True
816+
817+
818+
def print_unclosed_clients():
819+
from pymongo.topology import Topology
820+
processed = set()
821+
# Call collect to manually cleanup any would-be gc'd clients to avoid
822+
# false positives.
823+
gc.collect()
824+
for obj in gc.get_objects():
825+
try:
826+
if isinstance(obj, Topology):
827+
# Avoid printing the same Topology multiple times.
828+
if obj._topology_id in processed:
829+
continue
830+
all_executors_stopped(obj)
831+
processed.add(obj._topology_id)
832+
except ReferenceError:
833+
pass
834+
835+
798836
def teardown():
799837
garbage = []
800838
for g in gc.garbage:
@@ -813,6 +851,10 @@ def teardown():
813851
c.drop_database("pymongo_test_bernie")
814852
c.close()
815853

854+
# Jython does not support gc.get_objects.
855+
if not sys.platform.startswith('java'):
856+
print_unclosed_clients()
857+
816858

817859
class PymongoTestRunner(unittest.TextTestRunner):
818860
def run(self, test):

test/pymongo_mocks.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,9 @@ def __init__(
6565
topology,
6666
pool,
6767
topology_settings):
68-
# MockMonitor gets a 'client' arg, regular monitors don't.
69-
self.client = client
68+
# MockMonitor gets a 'client' arg, regular monitors don't. Weakref it
69+
# to avoid cycles.
70+
self.client = weakref.proxy(client)
7071
Monitor.__init__(
7172
self,
7273
server_description,
@@ -75,8 +76,9 @@ def __init__(
7576
topology_settings)
7677

7778
def _check_once(self):
79+
client = self.client
7880
address = self._server_description.address
79-
response, rtt = self.client.mock_is_master('%s:%d' % address)
81+
response, rtt = client.mock_is_master('%s:%d' % address)
8082
return ServerDescription(address, IsMaster(response), rtt)
8183

8284

test/test_auth.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -695,8 +695,6 @@ def setUp(self):
695695
client_context.create_user('admin', 'admin', 'pass')
696696
client_context.create_user(
697697
'pymongo_test', 'user', 'pass', ['userAdmin', 'readWrite'])
698-
self.client = rs_or_single_client_noauth(
699-
username='admin', password='pass')
700698

701699
def tearDown(self):
702700
client_context.drop_user('pymongo_test', 'user')

test/test_client.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,10 @@ def setUpClass(cls):
101101
cls.client = rs_or_single_client(connect=False,
102102
serverSelectionTimeoutMS=100)
103103

104+
@classmethod
105+
def tearDownClass(cls):
106+
cls.client.close()
107+
104108
def test_keyword_arg_defaults(self):
105109
client = MongoClient(socketTimeoutMS=None,
106110
connectTimeoutMS=20000,

test/test_collation.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ def setUpClass(cls):
105105
def tearDownClass(cls):
106106
cls.warn_context.__exit__()
107107
cls.warn_context = None
108+
cls.client.close()
108109

109110
def tearDown(self):
110111
self.listener.results.clear()

test/test_command_monitoring_spec.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,10 @@ def setUpClass(cls):
4848
cls.listener = EventListener()
4949
cls.client = single_client(event_listeners=[cls.listener])
5050

51+
@classmethod
52+
def tearDownClass(cls):
53+
cls.client.close()
54+
5155
def tearDown(self):
5256
self.listener.results.clear()
5357

test/test_connections_survive_primary_stepdown_spec.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,10 @@ def setUpClass(cls):
5151
cls.coll = cls.db.get_collection(
5252
"step-down", write_concern=WriteConcern("majority"))
5353

54+
@classmethod
55+
def tearDownClass(cls):
56+
cls.client.close()
57+
5458
def setUp(self):
5559
# Note that all ops use same write-concern as self.db (majority).
5660
self.db.drop_collection("step-down")

test/test_custom_types.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -911,6 +911,7 @@ def create_targets(self, *args, **kwargs):
911911
kwargs['type_registry'] = codec_options.type_registry
912912
kwargs['document_class'] = codec_options.document_class
913913
self.watched_target = rs_client(*args, **kwargs)
914+
self.addCleanup(self.watched_target.close)
914915
self.input_target = self.watched_target[self.db.name].test
915916
# Insert a record to ensure db, coll are created.
916917
self.input_target.insert_one({'data': 'dummy'})

0 commit comments

Comments
 (0)