-
Notifications
You must be signed in to change notification settings - Fork 624
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix sqlalchemy uninstrument #1581
Fix sqlalchemy uninstrument #1581
Conversation
073a31a
to
34218a9
Compare
34218a9
to
0bac6a0
Compare
Will it work if you create the engine separately?
When you instrument like this the engine will not be saved as part of the instrumentation, and the engine wont be un-wrapped as well (I am not sure if this is a problem, just raising a discussion) There are tests that create the engine separately, maybe you can check the uninstrument() on them |
@@ -158,17 +160,22 @@ def _instrument(self, **kwargs): | |||
"create_async_engine", | |||
_wrap_create_async_engine(tracer_provider, enable_commenter), | |||
) | |||
|
|||
self.engines = [] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there any reason to keep list of engines, why is it necessary to keep all the engines in array?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can create one engine or engines, so if I keep them in array it will be easier to clean up
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No problem, We should consider to remove the engine from that list when python finalize the engine by the garbage collector or when the user deleting the engine, otherwise potentially a memory leak could happen
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i wonder if we should implement call uninstrument logic when the garbage collector trying to collect the engine
https://docs.python.org/3.6/library/weakref.html#finalizer-objects
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought about this but the problem is after the instrumentation the EngineTracer returns to the user and we can't be sure the user delete it and when the garbage collector works
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for the comments
@@ -111,6 +114,11 @@ def __init__( | |||
listen(engine, "after_cursor_execute", _after_cur_exec) | |||
listen(engine, "handle_error", _handle_error) | |||
|
|||
def remove_event_listeners(self): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
its a private method should start with _
prefix
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's not a private method, I need to use this function from sqlalchemy
@@ -248,6 +248,7 @@ def test_uninstrument(self): | |||
|
|||
self.memory_exporter.clear() | |||
SQLAlchemyInstrumentor().uninstrument() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please dont call uninstrument
on a new SQLAlchemyInstrumentor instance,
suggested code:
def test_uninstrument(self):
engine = create_engine("sqlite:///:memory:")
instrumentor = SQLAlchemyInstrumentor()
instrumentor.instrument(
engine=engine,
tracer_provider=self.tracer_provider,
)
cnx = engine.connect()
cnx.execute("SELECT 1 + 1;").fetchall()
spans = self.memory_exporter.get_finished_spans()
self.assertEqual(len(spans), 2)
# first span - the connection to the db
self.assertEqual(spans[0].name, "connect")
self.assertEqual(spans[0].kind, trace.SpanKind.CLIENT)
# second span - the query itself
self.assertEqual(spans[1].name, "SELECT :memory:")
self.assertEqual(spans[1].kind, trace.SpanKind.CLIENT)
self.memory_exporter.clear()
instrumentor.uninstrument()
cnx.execute("SELECT 2 + 2;").fetchall()
spans = self.memory_exporter.get_finished_spans()
self.assertEqual(len(spans), 0)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure, can you explain what's the difference?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We dont want to create additional instrumentor class with new connection and new engine,
we would like to see that we remove listener events for specific instrumentation with specific engine
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It will not create another sqlalchemy instrumentor, if I understand this correctly it's like Singelton:
Line 50 in c92ba14
if cls._instance is None: |
What do you think?
You need to pass engine/engines to SQLAlchemyInstrumentor to create EngineTracer, if not there is no EngineTracer to create so my fix will not change anything |
Not necessarily, |
You are right the create engine wrapper creates EngineTracer and no one manages this instance, I will fix this |
0c17709
to
0a7cbdf
Compare
I refactor the code so when EngineTracer registers to the event listener those params will be saved in list of tuples, |
…ve them when uninstument
0a7cbdf
to
8edb72a
Compare
@@ -95,6 +98,8 @@ def _wrap_connect_internal(func, module, args, kwargs): | |||
|
|||
|
|||
class EngineTracer: | |||
_remove_event_listener_params = [] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion: maybe drop the 'remove'
like, call it event_listener_params
, or even event_listeners
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
but the params are for the remove function
I called the list like this so it will be clear that I keep the params only for remove
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i agree with the event_listeners
name convention because that what you store in this list
and it would be better name to use: unregister_event_listeners
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not exactly... I don't store the event listeners params, its without args and kwargs, for exmaple its without(retval=True)
I store the remove event listernes params
I agree with you It wasn't clear to me too, but @avzis is right it's possible to create an engine after sqlalchemy instrumentation, we don't want to miss engines that were created after the instrumentation. |
@devopsmetis I agree that this is a weird behavior, And I also agree that this discussion is not related to this PR, if you think that it is problematic I suggest you open another issue and discuss it |
Bug fix for sqlalchemy uninstrument, inside the EngineTracer we call listen on events but no one removes the event listeners after the uninstrument
Fixes #1163
Type of change
How Has This Been Tested?
Does This PR Require a Core Repo Change?
Checklist: