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:
parent
fa44639ba1
commit
f7eae9922f
@ -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
|
||||
|
||||
@ -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."""
|
||||
|
||||
Loading…
Reference in New Issue
Block a user