Skip to content

Commit 8d503d6

Browse files
committed
PYTHON-2433 Fix Python 3 ServerDescription/Exception memory leak
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.
1 parent 1d651b9 commit 8d503d6

File tree

2 files changed

+34
-0
lines changed

2 files changed

+34
-0
lines changed

pymongo/server_description.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"""Represent one server the driver is connected to."""
1616

1717
from bson import EPOCH_NAIVE
18+
from bson.py3compat import PY3
1819
from pymongo.server_type import SERVER_TYPE
1920
from pymongo.ismaster import IsMaster
2021
from pymongo.monotonic import time as _time
@@ -69,6 +70,11 @@ def __init__(
6970
self._me = ismaster.me
7071
self._last_update_time = _time()
7172
self._error = error
73+
# PYTHON-2433 Clear error traceback info.
74+
if error and PY3:
75+
error.__traceback__ = None
76+
error.__context__ = None
77+
error.__cause__ = None
7278
self._topology_version = ismaster.topology_version
7379
if error:
7480
if hasattr(error, 'details') and isinstance(error.details, dict):

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)