Applied the mapper "configure mutex" against the declarative class mapping
process, to guard against the race which can occur if mappers are used
while dynamic module import schemes are still in the process of configuring
mappers for related classes. This does not guard against all possible race
conditions, such as if the concurrent import has not yet encountered the
dependent classes as of yet, however it guards against as much as possible
within the SQLAlchemy declarative process.
Fixes: #4686
Change-Id: I0349036b8078bd42265ab40862cfbfe5bf9d5b44
(cherry picked from commit
5039c6f01d0bd1f58f950e80cddf7472444a70a4)
--- /dev/null
+.. change::
+ :tags: bug, orm
+ :tickets: 4686
+
+ Applied the mapper "configure mutex" against the declarative class mapping
+ process, to guard against the race which can occur if mappers are used
+ while dynamic module import schemes are still in the process of configuring
+ mappers for related classes. This does not guard against all possible race
+ conditions, such as if the concurrent import has not yet encountered the
+ dependent classes as of yet, however it guards against as much as possible
+ within the SQLAlchemy declarative process.
from ...orm import class_mapper
from ...orm import exc as orm_exc
from ...orm import mapper
+from ...orm import mapperlib
from ...orm import synonym
from ...orm.attributes import QueryableAttribute
from ...orm.base import _is_mapped_class
cfg_cls = _DeferredMapperConfig
else:
cfg_cls = _MapperConfig
+
cfg_cls(cls_, classname, dict_)
def __init__(self, cls_, classname, dict_):
self._scan_attributes()
- clsregistry.add_class(self.classname, self.cls)
+ mapperlib._CONFIGURE_MUTEX.acquire()
+ try:
+ clsregistry.add_class(self.classname, self.cls)
- self._extract_mappable_attributes()
+ self._extract_mappable_attributes()
- self._extract_declared_columns()
+ self._extract_declared_columns()
- self._setup_table()
+ self._setup_table()
- self._setup_inheritance()
+ self._setup_inheritance()
- self._early_mapping()
+ self._early_mapping()
+ finally:
+ mapperlib._CONFIGURE_MUTEX.release()
def _early_mapping(self):
self.map()
--- /dev/null
+import random
+import threading
+import time
+
+from sqlalchemy import Column
+from sqlalchemy import ForeignKey
+from sqlalchemy import Integer
+from sqlalchemy import String
+from sqlalchemy.ext.declarative import declarative_base
+from sqlalchemy.ext.declarative import declared_attr
+from sqlalchemy.orm import clear_mappers
+from sqlalchemy.orm import relationship
+from sqlalchemy.orm import Session
+from sqlalchemy.testing import fixtures
+
+
+class ConcurrentUseDeclMappingTest(fixtures.TestBase):
+ @classmethod
+ def make_a(cls, Base):
+ class A(Base):
+ __tablename__ = "a"
+
+ id = Column(Integer, primary_key=True)
+ data = Column(String)
+ bs = relationship("B")
+
+ # need a strong ref so that the class is not gc'ed
+ cls.A = A
+
+ @classmethod
+ def query_a(cls, Base, result):
+ s = Session()
+ time.sleep(random.random() / 100)
+ A = cls.A
+ try:
+ s.query(A).join(A.bs)
+ except Exception as err:
+ result[0] = err
+ print(err)
+ else:
+ result[0] = True
+ print("worked")
+
+ @classmethod
+ def make_b(cls, Base):
+ class B(Base):
+ __tablename__ = "b"
+ id = Column(Integer, primary_key=True)
+
+ @declared_attr
+ def data(cls):
+ time.sleep(0.001)
+ return Column(String)
+
+ a_id = Column(ForeignKey("a.id"))
+
+ cls.B = B
+
+ def test_concurrent_create(self):
+ for i in range(50):
+ Base = declarative_base()
+ clear_mappers()
+
+ self.make_a(Base)
+ result = [False]
+ threads = [
+ threading.Thread(target=self.make_b, args=(Base,)),
+ threading.Thread(target=self.query_a, args=(Base, result)),
+ ]
+
+ for t in threads:
+ t.start()
+
+ for t in threads:
+ t.join()
+
+ if isinstance(result[0], Exception):
+ raise result[0]