Skip to content

Commit f7eae99

Browse files
committed
PYTHON-2433 Fix Python 3 ServerDescription/Exception memory leak (#520)
When the SDAM monitor check fails, a ServerDescription is created from the exception. This exception is kept alive via the ServerDescription.error field. Unfortunately, the exception's traceback contains a reference to the previous ServerDescription. Altogether this means that each consecutively failing check leaks memory by building an ever growing chain of ServerDescription -> Exception -> Traceback -> Frame -> ServerDescription -> ... objects. This change breaks the chain and prevents the memory leak by clearing the Exception's __traceback__, __context__, and __cause__ fields. (cherry picked from commit 6c92e6c)
1 parent fa44639 commit f7eae99

File tree

2 files changed

+40
-0
lines changed

2 files changed

+40
-0
lines changed

pymongo/monitor.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
import threading
1919
import weakref
2020

21+
from bson.py3compat import PY3
22+
2123
from pymongo import common, periodic_executor
2224
from pymongo.errors import (NotMasterError,
2325
OperationFailure,
@@ -30,6 +32,14 @@
3032
from pymongo.srv_resolver import _SrvResolver
3133

3234

35+
def _sanitize(error):
36+
"""PYTHON-2433 Clear error traceback info."""
37+
if PY3:
38+
error.__traceback__ = None
39+
error.__context__ = None
40+
error.__cause__ = None
41+
42+
3343
class MonitorBase(object):
3444
def __init__(self, topology, name, interval, min_interval):
3545
"""Base class to do periodic work on a background thread.
@@ -169,6 +179,7 @@ def _run(self):
169179
try:
170180
self._server_description = self._check_server()
171181
except _OperationCancelled as exc:
182+
_sanitize(exc)
172183
# Already closed the connection, wait for the next check.
173184
self._server_description = ServerDescription(
174185
self._server_description.address, error=exc)
@@ -212,6 +223,7 @@ def _check_server(self):
212223
except ReferenceError:
213224
raise
214225
except Exception as error:
226+
_sanitize(error)
215227
sd = self._server_description
216228
address = sd.address
217229
duration = _time() - start

test/test_client.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
from pymongo.driver_info import DriverInfo
5858
from pymongo.pool import SocketInfo, _METADATA
5959
from pymongo.read_preferences import ReadPreference
60+
from pymongo.server_description import ServerDescription
6061
from pymongo.server_selectors import (any_server_selector,
6162
writable_server_selector)
6263
from pymongo.server_type import SERVER_TYPE
@@ -1614,6 +1615,33 @@ def test_direct_connection(self):
16141615
with self.assertRaises(ConfigurationError):
16151616
MongoClient(['host1', 'host2'], directConnection=True)
16161617

1618+
def test_continuous_network_errors(self):
1619+
def server_description_count():
1620+
i = 0
1621+
for obj in gc.get_objects():
1622+
try:
1623+
if isinstance(obj, ServerDescription):
1624+
i += 1
1625+
except ReferenceError:
1626+
pass
1627+
return i
1628+
gc.collect()
1629+
with client_knobs(min_heartbeat_interval=0.003):
1630+
client = MongoClient(
1631+
'invalid:27017',
1632+
heartbeatFrequencyMS=3,
1633+
serverSelectionTimeoutMS=100)
1634+
initial_count = server_description_count()
1635+
self.addCleanup(client.close)
1636+
with self.assertRaises(ServerSelectionTimeoutError):
1637+
client.test.test.find_one()
1638+
gc.collect()
1639+
final_count = server_description_count()
1640+
# If a bug like PYTHON-2433 is reintroduced then too many
1641+
# ServerDescriptions will be kept alive and this test will fail:
1642+
# AssertionError: 4 != 22 within 5 delta (18 difference)
1643+
self.assertAlmostEqual(initial_count, final_count, delta=5)
1644+
16171645

16181646
class TestExhaustCursor(IntegrationTest):
16191647
"""Test that clients properly handle errors from exhaust cursors."""

0 commit comments

Comments
 (0)