:version: 0.7.10
:released:
+ .. change::
+ :tags: orm, bug
+ :tickets: 2650
+
+ Fixed potential memory leak which could occur if an
+ arbitrary number of :class:`.sessionmaker` objects
+ were created. The anonymous subclass created by
+ the sessionmaker, when dereferenced, would not be garbage
+ collected due to remaining class-level references from the
+ event package. This issue also applies to any custom system
+ that made use of ad-hoc subclasses in conjunction with
+ an event dispatcher.
+
.. change::
:tags: orm, bug
:tickets: 2640
.. changelog::
:version: 0.8.0
+ .. change::
+ :tags: orm, bug
+ :tickets: 2650
+
+ Fixed potential memory leak which could occur if an
+ arbitrary number of :class:`.sessionmaker` objects
+ were created. The anonymous subclass created by
+ the sessionmaker, when dereferenced, would not be garbage
+ collected due to remaining class-level references from the
+ event package. This issue also applies to any custom system
+ that made use of ad-hoc subclasses in conjunction with
+ an event dispatcher. Also in 0.7.10.
+
.. change::
:tags: mssql, bug
from . import util, exc
from itertools import chain
+import weakref
CANCEL = util.symbol('CANCEL')
NO_RETVAL = util.symbol('NO_RETVAL')
def __init__(self, fn):
self.__name__ = fn.__name__
self.__doc__ = fn.__doc__
- self._clslevel = util.defaultdict(list)
- self._empty_listeners = {}
+ self._clslevel = weakref.WeakKeyDictionary()
+ self._empty_listeners = weakref.WeakKeyDictionary()
def _contains(self, cls, evt):
- return evt in self._clslevel[cls]
+ return cls in self._clslevel and \
+ evt in self._clslevel[cls]
def insert(self, obj, target, propagate):
assert isinstance(target, type), \
if cls is not target and cls not in self._clslevel:
self.update_subclass(cls)
else:
+ if cls not in self._clslevel:
+ self._clslevel[cls] = []
self._clslevel[cls].insert(0, obj)
def append(self, obj, target, propagate):
if cls is not target and cls not in self._clslevel:
self.update_subclass(cls)
else:
+ if cls not in self._clslevel:
+ self._clslevel[cls] = []
self._clslevel[cls].append(obj)
def update_subclass(self, target):
+ if target not in self._clslevel:
+ self._clslevel[target] = []
clslevel = self._clslevel[target]
for cls in target.__mro__[1:]:
if cls in self._clslevel:
while stack:
cls = stack.pop(0)
stack.extend(cls.__subclasses__())
- self._clslevel[cls].remove(obj)
+ if cls in self._clslevel:
+ self._clslevel[cls].remove(obj)
def clear(self):
"""Clear all class level listeners"""
from sqlalchemy.testing import eq_
from sqlalchemy.orm import mapper, relationship, create_session, \
- clear_mappers, sessionmaker, class_mapper, aliased,\
+ clear_mappers, sessionmaker, aliased,\
Session, subqueryload
from sqlalchemy.orm.mapper import _mapper_registry
from sqlalchemy.orm.session import _sessions
-import operator
from sqlalchemy import testing
from sqlalchemy.testing import engines
from sqlalchemy import MetaData, Integer, String, ForeignKey, \
- PickleType, create_engine, Unicode
-from sqlalchemy.testing.schema import Table, Column
+ Unicode, select
import sqlalchemy as sa
+from sqlalchemy.testing.schema import Table, Column
from sqlalchemy.sql import column
from sqlalchemy.processors import to_decimal_processor_factory, \
to_unicode_processor_factory
from sqlalchemy.testing.util import gc_collect
from sqlalchemy.util.compat import decimal
import gc
-import weakref
from sqlalchemy.testing import fixtures
+import weakref
class A(fixtures.ComparableEntity):
pass
# run the test 50 times. if length of gc.get_objects()
# keeps growing, assert false
+ def get_objects_skipping_sqlite_issue():
+ # pysqlite keeps adding weakref objects which only
+ # get reset after 220 iterations, which is too long
+ # to run lots of these tests, so just filter them
+ # out.
+ return [o for o in gc.get_objects()
+ if not isinstance(o, weakref.ref)]
+
def profile(*args):
gc_collect()
samples = [0 for x in range(0, times)]
for x in range(0, times):
func(*args)
gc_collect()
- samples[x] = len(gc.get_objects())
+ samples[x] = len(get_objects_skipping_sqlite_issue())
print "sample gc sizes:", samples
del m1, m2, m3
assert_no_mappers()
+ def test_sessionmaker(self):
+ @profile_memory()
+ def go():
+ sessmaker = sessionmaker(bind=testing.db)
+ sess = sessmaker()
+ r = sess.execute(select([1]))
+ r.close()
+ sess.close()
+ del sess
+ del sessmaker
+ go()
+
@testing.crashes('sqlite', ':memory: connection not suitable here')
def test_orm_many_engines(self):
metadata = MetaData(testing.db)
# pysqlite clearing out it's internal buffer and allow
# the test to pass
@testing.emits_warning()
- @profile_memory(times=220)
+ @profile_memory()
def go():
# execute with a non-unicode object. a warning is emitted,
metadata.drop_all()
assert_no_mappers()
- @testing.fails_if(lambda : testing.db.dialect.name == 'sqlite' \
- and testing.db.dialect.dbapi.version > '2.5')
@testing.provide_metadata
def test_key_fallback_result(self):
e = testing.db
# in pysqlite itself. background at:
# http://thread.gmane.org/gmane.comp.python.db.pysqlite.user/2290
- @testing.fails_if(lambda : testing.db.dialect.name == 'sqlite' \
- and testing.db.dialect.dbapi.version > '2.5')
def test_join_cache(self):
metadata = MetaData(testing.db)
table1 = Table('table1', metadata, Column('id', Integer,
from sqlalchemy.testing import eq_, assert_raises, assert_raises_message, \
is_, is_not_
-from sqlalchemy import event, exc, util
+from sqlalchemy import event, exc
from sqlalchemy.testing import fixtures
+from sqlalchemy.testing.util import gc_collect
class EventsTest(fixtures.TestBase):
"""Test class- and instance-level event registration."""
listen, "event_one", self.Target
)
+class SubclassGrowthTest(fixtures.TestBase):
+ """test that ad-hoc subclasses are garbage collected."""
+
+ def setUp(self):
+ class TargetEvents(event.Events):
+ def some_event(self, x, y):
+ pass
+
+ class Target(object):
+ dispatch = event.dispatcher(TargetEvents)
+
+ self.Target = Target
+
+ def test_subclass(self):
+ class SubTarget(self.Target):
+ pass
+
+ st = SubTarget()
+ st.dispatch.some_event(1, 2)
+ del st
+ del SubTarget
+ gc_collect()
+ eq_(self.Target.__subclasses__(), [])
+
+
class ListenOverrideTest(fixtures.TestBase):
"""Test custom listen functions which change the listener function signature."""