Skip to content

Commit 2e49ba1

Browse files
rbagdshalevr
andauthored
Use a weak reference to sqlalchemy Engine to avoid memory leak (#1771)
* Use a weak reference to sqlalchemy Engine to avoid memory leak Closes #1761 By using a weak reference to the `Engine` object, we can avoid the memory leak as disposed `Engines` get properly deallocated. Whenever `SQLAlchemy` is uninstrumented, we only trigger a removal for those event listeners which are listening for objects that haven't been garbage-collected yet. * Made a mistake in resolving the weak reference * Fixed formatting issues * Updated changelog * Added unit test to check that engine was garbage collected * Do not save engine in EngineTracer to avoid memory leak * Add an empty line to satisfy black formatter * Fix isort complaints * Fixed the issue when pool name is not set and =None * Fix formatting issue * Rebased after changes in a recent commit * Updated PR number in changelog --------- Co-authored-by: Shalev Roda <[email protected]>
1 parent a45c9c3 commit 2e49ba1

File tree

3 files changed

+60
-18
lines changed

3 files changed

+60
-18
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
4040
- `opentelemetry-instrumentation-system-metrics` Add `process.` prefix to `runtime.memory`, `runtime.cpu.time`, and `runtime.gc_count`. Change `runtime.memory` from count to UpDownCounter. ([#1735](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1735))
4141
- Add request and response hooks for GRPC instrumentation (client only)
4242
([#1706](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1706))
43+
- Fix memory leak in SQLAlchemy instrumentation where disposed `Engine` does not get garbage collected
44+
([#1771](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1771)
4345
- `opentelemetry-instrumentation-pymemcache` Update instrumentation to support pymemcache >4
4446
([#1764](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1764))
4547
- `opentelemetry-instrumentation-confluent-kafka` Add support for higher versions of confluent_kafka

instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/engine.py

+35-18
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
# limitations under the License.
1414
import os
1515
import re
16+
import weakref
1617

1718
from sqlalchemy.event import ( # pylint: disable=no-name-in-module
1819
listen,
@@ -99,11 +100,11 @@ def __init__(
99100
commenter_options=None,
100101
):
101102
self.tracer = tracer
102-
self.engine = engine
103103
self.connections_usage = connections_usage
104104
self.vendor = _normalize_vendor(engine.name)
105105
self.enable_commenter = enable_commenter
106106
self.commenter_options = commenter_options if commenter_options else {}
107+
self._engine_attrs = _get_attributes_from_engine(engine)
107108
self._leading_comment_remover = re.compile(r"^/\*.*?\*/")
108109

109110
self._register_event_listener(
@@ -118,23 +119,11 @@ def __init__(
118119
self._register_event_listener(engine, "checkin", self._pool_checkin)
119120
self._register_event_listener(engine, "checkout", self._pool_checkout)
120121

121-
def _get_connection_string(self):
122-
drivername = self.engine.url.drivername or ""
123-
host = self.engine.url.host or ""
124-
port = self.engine.url.port or ""
125-
database = self.engine.url.database or ""
126-
return f"{drivername}://{host}:{port}/{database}"
127-
128-
def _get_pool_name(self):
129-
if self.engine.pool.logging_name is not None:
130-
return self.engine.pool.logging_name
131-
return self._get_connection_string()
132-
133122
def _add_idle_to_connection_usage(self, value):
134123
self.connections_usage.add(
135124
value,
136125
attributes={
137-
"pool.name": self._get_pool_name(),
126+
**self._engine_attrs,
138127
"state": "idle",
139128
},
140129
)
@@ -143,7 +132,7 @@ def _add_used_to_connection_usage(self, value):
143132
self.connections_usage.add(
144133
value,
145134
attributes={
146-
"pool.name": self._get_pool_name(),
135+
**self._engine_attrs,
147136
"state": "used",
148137
},
149138
)
@@ -169,12 +158,21 @@ def _pool_checkout(
169158
@classmethod
170159
def _register_event_listener(cls, target, identifier, func, *args, **kw):
171160
listen(target, identifier, func, *args, **kw)
172-
cls._remove_event_listener_params.append((target, identifier, func))
161+
cls._remove_event_listener_params.append(
162+
(weakref.ref(target), identifier, func)
163+
)
173164

174165
@classmethod
175166
def remove_all_event_listeners(cls):
176-
for remove_params in cls._remove_event_listener_params:
177-
remove(*remove_params)
167+
for (
168+
weak_ref_target,
169+
identifier,
170+
func,
171+
) in cls._remove_event_listener_params:
172+
# Remove an event listener only if saved weak reference points to an object
173+
# which has not been garbage collected
174+
if weak_ref_target() is not None:
175+
remove(weak_ref_target(), identifier, func)
178176
cls._remove_event_listener_params.clear()
179177

180178
def _operation_name(self, db_name, statement):
@@ -300,3 +298,22 @@ def _get_attributes_from_cursor(vendor, cursor, attrs):
300298
if info.port:
301299
attrs[SpanAttributes.NET_PEER_PORT] = int(info.port)
302300
return attrs
301+
302+
303+
def _get_connection_string(engine):
304+
drivername = engine.url.drivername or ""
305+
host = engine.url.host or ""
306+
port = engine.url.port or ""
307+
database = engine.url.database or ""
308+
return f"{drivername}://{host}:{port}/{database}"
309+
310+
311+
def _get_attributes_from_engine(engine):
312+
"""Set metadata attributes of the database engine"""
313+
attrs = {}
314+
315+
attrs["pool.name"] = getattr(
316+
getattr(engine, "pool", None), "logging_name", None
317+
) or _get_connection_string(engine)
318+
319+
return attrs

instrumentation/opentelemetry-instrumentation-sqlalchemy/tests/test_sqlalchemy.py

+23
Original file line numberDiff line numberDiff line change
@@ -307,3 +307,26 @@ def test_no_op_tracer_provider(self):
307307
cnx.execute("SELECT 1 + 1;").fetchall()
308308
spans = self.memory_exporter.get_finished_spans()
309309
self.assertEqual(len(spans), 0)
310+
311+
def test_no_memory_leakage_if_engine_diposed(self):
312+
SQLAlchemyInstrumentor().instrument()
313+
import gc
314+
import weakref
315+
316+
from sqlalchemy import create_engine
317+
318+
callback = mock.Mock()
319+
320+
def make_shortlived_engine():
321+
engine = create_engine("sqlite:///:memory:")
322+
# Callback will be called if engine is deallocated during garbage
323+
# collection
324+
weakref.finalize(engine, callback)
325+
with engine.connect() as conn:
326+
conn.execute("SELECT 1 + 1;").fetchall()
327+
328+
for _ in range(0, 5):
329+
make_shortlived_engine()
330+
331+
gc.collect()
332+
assert callback.call_count == 5

0 commit comments

Comments
 (0)