Skip to content

Session and SessionTransaction ref cycle #5074

Closed
@carsonip

Description

@carsonip
Contributor

SQLAlchemy version: 1.3

objgraph:
sessiontransaction

MCVE in CycleTest:

diff --git a/test/aaa_profiling/test_memusage.py b/test/aaa_profiling/test_memusage.py
index be825151d..8867f8447 100644
--- a/test/aaa_profiling/test_memusage.py
+++ b/test/aaa_profiling/test_memusage.py
@@ -1396,3 +1396,14 @@ class CycleTest(_fixtures.FixtureTest):
             visit_binary_product(visit, expr)
 
         go()
+
+    def test_session_transaction(self):
+        User, Address = self.classes("User", "Address")
+        configure_mappers()
+
+        @assert_cycles()
+        def go():
+            s = Session()
+            s.query(User).exists()
+
+        go()

Session creates SessionTransaction which has a ref to Session.

This ref cycle is particularly problematic because gc spills weakref WeakValueDictionary.remove all over the call stacks randomly. The WeakValueDictionary is created in orm.session

_sessions = weakref.WeakValueDictionary()

I have no idea how to fix this yet, but this issue is quite critical in my opinion. Thanks!

Activity

carsonip

carsonip commented on Jan 2, 2020

@carsonip
ContributorAuthor

A new SessionTransaction is started when Session.close() is called: (orm/session.py:595)

        if self._parent is None:
            if not self.session.autocommit:
                self.session.begin()

Although the old SessionTransaction.session = None (orm/session.py:598), a new cycle is created again.

If we are not fixing this, do we have a way to avoid this cycle in application code?

added
bugSomething isn't working
performancewhere performance can be improved. add "bug" only if it's a performance degradation
on Jan 2, 2020
added this to the 1.3.x milestone on Jan 2, 2020
zzzeek

zzzeek commented on Jan 2, 2020

@zzzeek
Member

I see that you mentioned the issue of weakref.refs being cleaned previously but I'm not understanding the problem right now. are you suggesting that WeakValueDictionary simply should not be used because the simple fact that weakrefs point to things that are removed is a performance issue ? can you show the reproduction case ?

zzzeek

zzzeek commented on Jan 2, 2020

@zzzeek
Member

trying the test now.

carsonip

carsonip commented on Jan 2, 2020

@carsonip
ContributorAuthor

I see that you mentioned the issue of weakref.refs being cleaned previously but I'm not understanding the problem right now. are you suggesting that WeakValueDictionary simply should not be used because the simple fact that weakrefs point to things that are removed is a performance issue ? can you show the reproduction case ?

They are designed to be cleaned. There is no performance problem. The problem I am facing is that they are not being cleaned when Session is closed, but only during gc because Session has a ref cycle. And due to the fact that gc may happen any time (especially when many objects are allocated), it messes up the call stack, showing random WeakValueDictionary.remove at the top of any call stacks.

zzzeek

zzzeek commented on Jan 2, 2020

@zzzeek
Member

here's the fix:


diff --git a/lib/sqlalchemy/orm/session.py b/lib/sqlalchemy/orm/session.py
index 3c58b4260..b23fca57a 100644
--- a/lib/sqlalchemy/orm/session.py
+++ b/lib/sqlalchemy/orm/session.py
@@ -1317,6 +1317,8 @@ class Session(_SessionClassMethods):
             for transaction in self.transaction._iterate_self_and_parents():
                 transaction.close(invalidate)
 
+        self.transaction = None  # remove cycles
+
     def expunge_all(self):
         """Remove all object instances from this ``Session``.
 

and here's the test:

    def test_session_transaction(self):
        @assert_cycles()
        def go():
            s = Session()
            s.close()

        go()

is there a reason s.close() would not be called on a Session before it is to be discarded?

carsonip

carsonip commented on Jan 2, 2020

@carsonip
ContributorAuthor

here's the fix:


diff --git a/lib/sqlalchemy/orm/session.py b/lib/sqlalchemy/orm/session.py
index 3c58b4260..b23fca57a 100644
--- a/lib/sqlalchemy/orm/session.py
+++ b/lib/sqlalchemy/orm/session.py
@@ -1317,6 +1317,8 @@ class Session(_SessionClassMethods):
             for transaction in self.transaction._iterate_self_and_parents():
                 transaction.close(invalidate)
 
+        self.transaction = None  # remove cycles
+
     def expunge_all(self):
         """Remove all object instances from this ``Session``.
 

and here's the test:

    def test_session_transaction(self):
        @assert_cycles()
        def go():
            s = Session()
            s.close()

        go()

is there a reason s.close() would not be called on a Session before it is to be discarded?

I do call s.close() in finally, so there is no problem. But I have tried to do self.transaction = None. It fails some tests in the sqlalchemy test suite.

zzzeek

zzzeek commented on Jan 2, 2020

@zzzeek
Member

oh ok, if the new transaction replaced itself then that would happen, let's see

carsonip

carsonip commented on Jan 2, 2020

@carsonip
ContributorAuthor

self.transaction = None would fix this issue and I feel like it is the right fix. But I just don't have enough understanding to fix the tests. :(
Not sure if there are better fixes.

zzzeek

zzzeek commented on Jan 2, 2020

@zzzeek
Member

Ok right there's the "autobegin" thing that has to be adjusted here.

zzzeek

zzzeek commented on Jan 2, 2020

@zzzeek
Member

I can adjust this but I don't think it's appropriate for 1.3 because it is a change in documented behavior. "If this session were created with autocommit=False, a new transaction is immediately begun." that has to change

zzzeek

zzzeek commented on Jan 2, 2020

@zzzeek
Member

so in your application, make your own:

def close_session(sess):
   sess.close()
   sess.transaction = None

in 1.4 you won't need the above but it will also be harmless

13 remaining items

Loading
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingormperformancewhere performance can be improved. add "bug" only if it's a performance degradation

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

      Development

      No branches or pull requests

        Participants

        @zzzeek@sqla-tester@carsonip

        Issue actions

          Session and SessionTransaction ref cycle · Issue #5074 · sqlalchemy/sqlalchemy