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 6c92e6c67e)
This commit is contained in:
Shane Harvey 2020-11-20 18:58:47 -08:00 committed by Shane Harvey
parent fa44639ba1
commit f7eae9922f
2 changed files with 40 additions and 0 deletions

View File

@ -18,6 +18,8 @@ import atexit
import threading
import weakref
from bson.py3compat import PY3
from pymongo import common, periodic_executor
from pymongo.errors import (NotMasterError,
OperationFailure,
@ -30,6 +32,14 @@ from pymongo.server_description import ServerDescription
from pymongo.srv_resolver import _SrvResolver
def _sanitize(error):
"""PYTHON-2433 Clear error traceback info."""
if PY3:
error.__traceback__ = None
error.__context__ = None
error.__cause__ = None
class MonitorBase(object):
def __init__(self, topology, name, interval, min_interval):
"""Base class to do periodic work on a background thread.
@ -169,6 +179,7 @@ class Monitor(MonitorBase):
try:
self._server_description = self._check_server()
except _OperationCancelled as exc:
_sanitize(exc)
# Already closed the connection, wait for the next check.
self._server_description = ServerDescription(
self._server_description.address, error=exc)
@ -212,6 +223,7 @@ class Monitor(MonitorBase):
except ReferenceError:
raise
except Exception as error:
_sanitize(error)
sd = self._server_description
address = sd.address
duration = _time() - start

View File

@ -57,6 +57,7 @@ from pymongo.monotonic import time as monotonic_time
from pymongo.driver_info import DriverInfo
from pymongo.pool import SocketInfo, _METADATA
from pymongo.read_preferences import ReadPreference
from pymongo.server_description import ServerDescription
from pymongo.server_selectors import (any_server_selector,
writable_server_selector)
from pymongo.server_type import SERVER_TYPE
@ -1614,6 +1615,33 @@ class TestClient(IntegrationTest):
with self.assertRaises(ConfigurationError):
MongoClient(['host1', 'host2'], directConnection=True)
def test_continuous_network_errors(self):
def server_description_count():
i = 0
for obj in gc.get_objects():
try:
if isinstance(obj, ServerDescription):
i += 1
except ReferenceError:
pass
return i
gc.collect()
with client_knobs(min_heartbeat_interval=0.003):
client = MongoClient(
'invalid:27017',
heartbeatFrequencyMS=3,
serverSelectionTimeoutMS=100)
initial_count = server_description_count()
self.addCleanup(client.close)
with self.assertRaises(ServerSelectionTimeoutError):
client.test.test.find_one()
gc.collect()
final_count = server_description_count()
# If a bug like PYTHON-2433 is reintroduced then too many
# ServerDescriptions will be kept alive and this test will fail:
# AssertionError: 4 != 22 within 5 delta (18 difference)
self.assertAlmostEqual(initial_count, final_count, delta=5)
class TestExhaustCursor(IntegrationTest):
"""Test that clients properly handle errors from exhaust cursors."""