backport stable
authorSylvain Thénault <sylvain.thenault@logilab.fr>
Mon, 21 Jun 2010 15:34:46 +0200
changeset 5815 282194aa43f3
parent 5814 51cc4b61f9ae (current diff)
parent 5813 0b250d72fcfa (diff)
child 5816 5d72fbba92e9
backport stable
cwconfig.py
dbapi.py
devtools/testlib.py
hooks/security.py
server/repository.py
server/session.py
--- a/cwconfig.py	Mon Jun 21 15:32:58 2010 +0200
+++ b/cwconfig.py	Mon Jun 21 15:34:46 2010 +0200
@@ -668,6 +668,7 @@
         self.load_defaults()
         # will be properly initialized later by _gettext_init
         self.translations = {'en': (unicode, lambda ctx, msgid: unicode(msgid) )}
+        self._site_loaded = set()
         # don't register ReStructured Text directives by simple import, avoid pb
         # with eg sphinx.
         # XXX should be done properly with a function from cw.uicfg
@@ -696,7 +697,7 @@
             init_log(self.debugmode, syslog, logthreshold, logfile, self.log_format,
                      rotation_parameters={'when': 'W6', # every sunday
                                           'interval': 1,
-                                          'backupCount': 52,})
+                                          'backupCount': 52})
         else:
             init_log(self.debugmode, syslog, logthreshold, logfile, self.log_format)
         # configure simpleTal logger
@@ -708,6 +709,34 @@
         """
         return []
 
+    apphome = None
+
+    def load_site_cubicweb(self, paths=None):
+        """load instance's specific site_cubicweb file"""
+        if paths is None:
+            paths = self.cubes_path()
+            if self.apphome is not None:
+                paths = [self.apphome] + paths
+        for path in reversed(paths):
+            sitefile = join(path, 'site_cubicweb.py')
+            if exists(sitefile) and not sitefile in self._site_loaded:
+                self._load_site_cubicweb(sitefile)
+                self._site_loaded.add(sitefile)
+            else:
+                sitefile = join(path, 'site_erudi.py')
+                if exists(sitefile) and not sitefile in self._site_loaded:
+                    self._load_site_cubicweb(sitefile)
+                    self._site_loaded.add(sitefile)
+                    self.warning('[3.5] site_erudi.py is deprecated, should be '
+                                 'renamed to site_cubicweb.py')
+
+    def _load_site_cubicweb(self, sitefile):
+        # XXX extrapath argument to load_module_from_file only in lgc > 0.50.2
+        from logilab.common.modutils import load_module_from_modpath, modpath_from_file
+        module = load_module_from_modpath(modpath_from_file(sitefile, self.extrapath))
+        self.info('%s loaded', sitefile)
+        return module
+
     def eproperty_definitions(self):
         cfg = self.persistent_options_configuration()
         for section, options in cfg.options_by_section():
@@ -889,7 +918,6 @@
         self.appid = appid
         CubicWebNoAppConfiguration.__init__(self, debugmode)
         self._cubes = None
-        self._site_loaded = set()
         self.load_file_configuration(self.main_config_file())
 
     def adjust_sys_path(self):
@@ -964,37 +992,6 @@
             infos.append('%s-%s' % (pkg, version))
         return md5.new(';'.join(infos)).hexdigest()
 
-    def load_site_cubicweb(self):
-        """load instance's specific site_cubicweb file"""
-        paths = self.cubes_path()
-        if self.apphome is not None:
-            paths = [self.apphome] + paths
-        for path in reversed(paths):
-            sitefile = join(path, 'site_cubicweb.py')
-            if exists(sitefile) and not sitefile in self._site_loaded:
-                self._load_site_cubicweb(sitefile)
-                self._site_loaded.add(sitefile)
-            else:
-                sitefile = join(path, 'site_erudi.py')
-                if exists(sitefile) and not sitefile in self._site_loaded:
-                    self._load_site_cubicweb(sitefile)
-                    self._site_loaded.add(sitefile)
-                    self.warning('[3.5] site_erudi.py is deprecated, should be '
-                                 'renamed to site_cubicweb.py')
-
-    def _load_site_cubicweb(self, sitefile):
-        # XXX extrapath argument to load_module_from_file only in lgc > 0.46
-        from logilab.common.modutils import load_module_from_modpath, modpath_from_file
-        def load_module_from_file(filepath, path=None, use_sys=1, extrapath=None):
-            return load_module_from_modpath(modpath_from_file(filepath, extrapath),
-                                            path, use_sys)
-        module = load_module_from_file(sitefile, extrapath=self.extrapath)
-        self.info('%s loaded', sitefile)
-        # cube specific options
-        if getattr(module, 'options', None):
-            self.register_options(module.options)
-            self.load_defaults()
-
     def load_configuration(self):
         """load instance's configuration files"""
         super(CubicWebConfiguration, self).load_configuration()
@@ -1002,6 +999,13 @@
             # init gettext
             self._gettext_init()
 
+    def _load_site_cubicweb(self, sitefile):
+        # overriden to register cube specific options
+        mod = super(CubicWebConfiguration, self)._load_site_cubicweb(sitefile)
+        if getattr(mod, 'options', None):
+            self.register_options(module.options)
+            self.load_defaults()
+
     def init_log(self, logthreshold=None, force=False):
         """init the log service"""
         if not force and hasattr(self, '_logging_initialized'):
@@ -1096,7 +1100,8 @@
             SMTP_LOCK.release()
         return True
 
-set_log_methods(CubicWebConfiguration, logging.getLogger('cubicweb.configuration'))
+set_log_methods(CubicWebNoAppConfiguration,
+                logging.getLogger('cubicweb.configuration'))
 
 # alias to get a configuration instance from an instance id
 instance_configuration = CubicWebConfiguration.config_for
--- a/dbapi.py	Mon Jun 21 15:32:58 2010 +0200
+++ b/dbapi.py	Mon Jun 21 15:34:46 2010 +0200
@@ -24,6 +24,7 @@
 
 __docformat__ = "restructuredtext en"
 
+from threading import currentThread
 from logging import getLogger
 from time import time, clock
 from itertools import count
@@ -400,6 +401,9 @@
         """no effect"""
         pass
 
+    def _txid(self):
+        return self.connection._txid(self)
+
     def execute(self, rql, args=None, eid_key=None, build_descr=True):
         """execute a rql query, return resulting rows and their description in
         a :class:`~cubicweb.rset.ResultSet` object
@@ -435,7 +439,8 @@
             warn('[3.8] eid_key is deprecated, you can safely remove this argument',
                  DeprecationWarning, stacklevel=2)
         # XXX use named argument for build_descr in case repo is < 3.8
-        rset = self._repo.execute(self._sessid, rql, args, build_descr=build_descr)
+        rset = self._repo.execute(self._sessid, rql, args,
+                                  build_descr=build_descr, txid=self._txid())
         rset.req = self.req
         return rset
 
@@ -490,6 +495,9 @@
             self.rollback()
             return False #propagate the exception
 
+    def _txid(self, cursor=None): # XXX could now handle various isolation level!
+        return currentThread().getName()
+
     def request(self):
         return DBAPIRequest(self.vreg, DBAPISession(self))
 
@@ -551,18 +559,12 @@
             esubpath = list(subpath)
             esubpath.remove('views')
             esubpath.append(join('web', 'views'))
-        cubes = reversed([config.cube_dir(p) for p in cubes])
-        vpath = config.build_vregistry_path(cubes, evobjpath=esubpath,
+        cubespath = [config.cube_dir(p) for p in cubes]
+        config.load_site_cubicweb(cubespath)
+        vpath = config.build_vregistry_path(reversed(cubespath),
+                                            evobjpath=esubpath,
                                             tvobjpath=subpath)
         self.vreg.register_objects(vpath)
-        if self._cnxtype == 'inmemory':
-            # should reinit hooks manager as well
-            hm, config = self._repo.hm, self._repo.config
-            hm.set_schema(hm.schema) # reset structure
-            hm.register_system_hooks(config)
-            # instance specific hooks
-            if self._repo.config.instance_hooks:
-                hm.register_hooks(config.load_hooks(self.vreg))
 
     def use_web_compatible_requests(self, baseurl, sitetitle=None):
         """monkey patch DBAPIRequest to fake a cw.web.request, so you should
@@ -632,7 +634,7 @@
     def describe(self, eid):
         if self._closed is not None:
             raise ProgrammingError('Closed connection')
-        return self._repo.describe(self.sessionid, eid)
+        return self._repo.describe(self.sessionid, eid, txid=self._txid())
 
     def close(self):
         """Close the connection now (rather than whenever __del__ is called).
@@ -645,7 +647,7 @@
         """
         if self._closed:
             raise ProgrammingError('Connection is already closed')
-        self._repo.close(self.sessionid)
+        self._repo.close(self.sessionid, txid=self._txid())
         del self._repo # necessary for proper garbage collection
         self._closed = 1
 
@@ -659,7 +661,7 @@
         """
         if not self._closed is None:
             raise ProgrammingError('Connection is already closed')
-        return self._repo.commit(self.sessionid)
+        return self._repo.commit(self.sessionid, txid=self._txid())
 
     def rollback(self):
         """This method is optional since not all databases provide transaction
@@ -672,7 +674,7 @@
         """
         if not self._closed is None:
             raise ProgrammingError('Connection is already closed')
-        self._repo.rollback(self.sessionid)
+        self._repo.rollback(self.sessionid, txid=self._txid())
 
     def cursor(self, req=None):
         """Return a new Cursor Object using the connection.
@@ -713,6 +715,7 @@
           and set to false.
         """
         txinfos = self._repo.undoable_transactions(self.sessionid, ueid,
+                                                   txid=self._txid(),
                                                    **actionfilters)
         if req is None:
             req = self.request()
@@ -727,7 +730,8 @@
         allowed (eg not in managers group and the transaction doesn't belong to
         him).
         """
-        txinfo = self._repo.transaction_info(self.sessionid, txuuid)
+        txinfo = self._repo.transaction_info(self.sessionid, txuuid,
+                                             txid=self._txid())
         if req is None:
             req = self.request()
         txinfo.req = req
@@ -743,7 +747,8 @@
         session's user is not allowed (eg not in managers group and the
         transaction doesn't belong to him).
         """
-        return self._repo.transaction_actions(self.sessionid, txuuid, public)
+        return self._repo.transaction_actions(self.sessionid, txuuid, public,
+                                              txid=self._txid())
 
     def undo_transaction(self, txuuid):
         """Undo the given transaction. Return potential restoration errors.
@@ -752,4 +757,5 @@
         allowed (eg not in managers group and the transaction doesn't belong to
         him).
         """
-        return self._repo.undo_transaction(self.sessionid, txuuid)
+        return self._repo.undo_transaction(self.sessionid, txuuid,
+                                           txid=self._txid())
--- a/hooks/security.py	Mon Jun 21 15:32:58 2010 +0200
+++ b/hooks/security.py	Mon Jun 21 15:34:46 2010 +0200
@@ -18,6 +18,7 @@
 """Security hooks: check permissions to add/delete/update entities according to
 the user connected to a session
 """
+
 __docformat__ = "restructuredtext en"
 
 from cubicweb import Unauthorized
--- a/server/repository.py	Mon Jun 21 15:32:58 2010 +0200
+++ b/server/repository.py	Mon Jun 21 15:34:46 2010 +0200
@@ -575,7 +575,8 @@
         session.commit()
         return session.id
 
-    def execute(self, sessionid, rqlstring, args=None, build_descr=True):
+    def execute(self, sessionid, rqlstring, args=None, build_descr=True,
+                txid=None):
         """execute a RQL query
 
         * rqlstring should be an unicode string or a plain ascii string
@@ -583,7 +584,7 @@
         * build_descr is a flag indicating if the description should be
           built on select queries
         """
-        session = self._get_session(sessionid, setpool=True)
+        session = self._get_session(sessionid, setpool=True, txid=txid)
         try:
             try:
                 rset = self.querier.execute(session, rqlstring, args,
@@ -611,9 +612,9 @@
         finally:
             session.reset_pool()
 
-    def describe(self, sessionid, eid):
+    def describe(self, sessionid, eid, txid=None):
         """return a tuple (type, source, extid) for the entity with id <eid>"""
-        session = self._get_session(sessionid, setpool=True)
+        session = self._get_session(sessionid, setpool=True, txid=txid)
         try:
             return self.type_and_source_from_eid(eid, session)
         finally:
@@ -639,32 +640,36 @@
         session = self._get_session(sessionid, setpool=False)
         session.set_shared_data(key, value, querydata)
 
-    def commit(self, sessionid):
+    def commit(self, sessionid, txid=None):
         """commit transaction for the session with the given id"""
         self.debug('begin commit for session %s', sessionid)
         try:
-            return self._get_session(sessionid).commit()
+            session = self._get_session(sessionid)
+            session.set_tx_data(txid)
+            return session.commit()
         except (ValidationError, Unauthorized):
             raise
         except:
             self.exception('unexpected error')
             raise
 
-    def rollback(self, sessionid):
+    def rollback(self, sessionid, txid=None):
         """commit transaction for the session with the given id"""
         self.debug('begin rollback for session %s', sessionid)
         try:
-            self._get_session(sessionid).rollback()
+            session = self._get_session(sessionid)
+            session.set_tx_data(txid)
+            session.rollback()
         except:
             self.exception('unexpected error')
             raise
 
-    def close(self, sessionid, checkshuttingdown=True):
+    def close(self, sessionid, txid=None, checkshuttingdown=True):
         """close the session with the given id"""
-        session = self._get_session(sessionid, setpool=True,
+        session = self._get_session(sessionid, setpool=True, txid=txid,
                                     checkshuttingdown=checkshuttingdown)
         # operation uncommited before close are rollbacked before hook is called
-        session.rollback()
+        session.rollback(reset_pool=False)
         self.hm.call_hooks('session_close', session)
         # commit session at this point in case write operation has been done
         # during `session_close` hooks
@@ -695,34 +700,35 @@
         for prop, value in props.items():
             session.change_property(prop, value)
 
-    def undoable_transactions(self, sessionid, ueid=None, **actionfilters):
+    def undoable_transactions(self, sessionid, ueid=None, txid=None,
+                              **actionfilters):
         """See :class:`cubicweb.dbapi.Connection.undoable_transactions`"""
-        session = self._get_session(sessionid, setpool=True)
+        session = self._get_session(sessionid, setpool=True, txid=txid)
         try:
             return self.system_source.undoable_transactions(session, ueid,
                                                             **actionfilters)
         finally:
             session.reset_pool()
 
-    def transaction_info(self, sessionid, txuuid):
+    def transaction_info(self, sessionid, txuuid, txid=None):
         """See :class:`cubicweb.dbapi.Connection.transaction_info`"""
-        session = self._get_session(sessionid, setpool=True)
+        session = self._get_session(sessionid, setpool=True, txid=txid)
         try:
             return self.system_source.tx_info(session, txuuid)
         finally:
             session.reset_pool()
 
-    def transaction_actions(self, sessionid, txuuid, public=True):
+    def transaction_actions(self, sessionid, txuuid, public=True, txid=None):
         """See :class:`cubicweb.dbapi.Connection.transaction_actions`"""
-        session = self._get_session(sessionid, setpool=True)
+        session = self._get_session(sessionid, setpool=True, txid=txid)
         try:
             return self.system_source.tx_actions(session, txuuid, public)
         finally:
             session.reset_pool()
 
-    def undo_transaction(self, sessionid, txuuid):
+    def undo_transaction(self, sessionid, txuuid, txid=None):
         """See :class:`cubicweb.dbapi.Connection.undo_transaction`"""
-        session = self._get_session(sessionid, setpool=True)
+        session = self._get_session(sessionid, setpool=True, txid=txid)
         try:
             return self.system_source.undo_transaction(session, txuuid)
         finally:
@@ -785,7 +791,8 @@
         session.set_pool()
         return session
 
-    def _get_session(self, sessionid, setpool=False, checkshuttingdown=True):
+    def _get_session(self, sessionid, setpool=False, txid=None,
+                     checkshuttingdown=True):
         """return the user associated to the given session identifier"""
         if checkshuttingdown and self._shutting_down:
             raise Exception('Repository is shutting down')
@@ -794,6 +801,7 @@
         except KeyError:
             raise BadConnectionId('No such session %s' % sessionid)
         if setpool:
+            session.set_tx_data(txid) # must be done before set_pool
             session.set_pool()
         return session
 
--- a/server/session.py	Mon Jun 21 15:32:58 2010 +0200
+++ b/server/session.py	Mon Jun 21 15:34:46 2010 +0200
@@ -117,6 +117,9 @@
 #            print INDENT + 'reset write to', self.oldwrite
 
 
+class TransactionData(object):
+    def __init__(self, txid):
+        self.transactionid = txid
 
 class Session(RequestSessionBase):
     """tie session id, user, connections pool and other session data all
@@ -148,7 +151,8 @@
         # i18n initialization
         self.set_language(cnxprops.lang)
         # internals
-        self._threaddata = threading.local()
+        self._tx_data = {}
+        self.__threaddata = threading.local()
         self._threads_in_transaction = set()
         self._closed = False
 
@@ -156,6 +160,23 @@
         return '<%ssession %s (%s 0x%x)>' % (
             self.cnxtype, unicode(self.user.login), self.id, id(self))
 
+    def set_tx_data(self, txid=None):
+        if txid is None:
+            txid = threading.currentThread().getName()
+        try:
+            self.__threaddata.txdata = self._tx_data[txid]
+        except KeyError:
+            self.__threaddata.txdata = self._tx_data[txid] = TransactionData(txid)
+
+    @property
+    def _threaddata(self):
+        try:
+            return self.__threaddata.txdata
+        except AttributeError:
+            self.set_tx_data()
+            return self.__threaddata.txdata
+
+
     def hijack_user(self, user):
         """return a fake request/session using specified user"""
         session = Session(user, self.repo)
@@ -339,11 +360,14 @@
     @property
     def read_security(self):
         """return a boolean telling if read security is activated or not"""
+        txstore = self._threaddata
+        if txstore is None:
+            return self.DEFAULT_SECURITY
         try:
-            return self._threaddata.read_security
+            return txstore.read_security
         except AttributeError:
-            self._threaddata.read_security = self.DEFAULT_SECURITY
-            return self._threaddata.read_security
+            txstore.read_security = self.DEFAULT_SECURITY
+            return txstore.read_security
 
     def set_read_security(self, activated):
         """[de]activate read security, returning the previous value set for
@@ -352,8 +376,11 @@
         you should usually use the `security_enabled` context manager instead
         of this to change security settings.
         """
-        oldmode = self.read_security
-        self._threaddata.read_security = activated
+        txstore = self._threaddata
+        if txstore is None:
+            return self.DEFAULT_SECURITY
+        oldmode = getattr(txstore, 'read_security', self.DEFAULT_SECURITY)
+        txstore.read_security = activated
         # dbapi_query used to detect hooks triggered by a 'dbapi' query (eg not
         # issued on the session). This is tricky since we the execution model of
         # a (write) user query is:
@@ -370,18 +397,21 @@
         # else (False actually) is not perfect but should be enough
         #
         # also reset dbapi_query to true when we go back to DEFAULT_SECURITY
-        self._threaddata.dbapi_query = (oldmode is self.DEFAULT_SECURITY
-                                        or activated is self.DEFAULT_SECURITY)
+        txstore.dbapi_query = (oldmode is self.DEFAULT_SECURITY
+                               or activated is self.DEFAULT_SECURITY)
         return oldmode
 
     @property
     def write_security(self):
         """return a boolean telling if write security is activated or not"""
+        txstore = self._threaddata
+        if txstore is None:
+            return self.DEFAULT_SECURITY
         try:
-            return self._threaddata.write_security
+            return txstore.write_security
         except:
-            self._threaddata.write_security = self.DEFAULT_SECURITY
-            return self._threaddata.write_security
+            txstore.write_security = self.DEFAULT_SECURITY
+            return txstore.write_security
 
     def set_write_security(self, activated):
         """[de]activate write security, returning the previous value set for
@@ -390,8 +420,11 @@
         you should usually use the `security_enabled` context manager instead
         of this to change security settings.
         """
-        oldmode = self.write_security
-        self._threaddata.write_security = activated
+        txstore = self._threaddata
+        if txstore is None:
+            return self.DEFAULT_SECURITY
+        oldmode = getattr(txstore, 'write_security', self.DEFAULT_SECURITY)
+        txstore.write_security = activated
         return oldmode
 
     @property
@@ -568,7 +601,6 @@
         """update latest session usage timestamp and reset mode to read"""
         self.timestamp = time()
         self.local_perm_cache.clear() # XXX simply move in transaction_data, no?
-        self._threaddata.mode = self.default_mode
 
     # shared data handling ###################################################
 
@@ -648,18 +680,29 @@
         rset.req = self
         return rset
 
-    def _clear_thread_data(self):
+    def _clear_thread_data(self, reset_pool=True):
         """remove everything from the thread local storage, except pool
         which is explicitly removed by reset_pool, and mode which is set anyway
         by _touch
         """
-        store = self._threaddata
-        for name in ('commit_state', 'transaction_data', 'pending_operations',
-                     '_rewriter'):
-            try:
-                delattr(store, name)
-            except AttributeError:
-                pass
+        try:
+            txstore = self.__threaddata.txdata
+        except AttributeError:
+            pass
+        else:
+            if reset_pool:
+                self._tx_data.pop(txstore.transactionid, None)
+                try:
+                    del self.__threaddata.txdata
+                except AttributeError:
+                    pass
+            else:
+                for name in ('commit_state', 'transaction_data',
+                             'pending_operations', '_rewriter'):
+                    try:
+                        delattr(txstore, name)
+                    except AttributeError:
+                        continue
 
     def commit(self, reset_pool=True):
         """commit the current session's transaction"""
@@ -671,13 +714,13 @@
             return
         if self.commit_state:
             return
-        # by default, operations are executed with security turned off
-        with security_enabled(self, False, False):
-            # on rollback, an operation should have the following state
-            # information:
-            # - processed by the precommit/commit event or not
-            # - if processed, is it the failed operation
-            try:
+        # on rollback, an operation should have the following state
+        # information:
+        # - processed by the precommit/commit event or not
+        # - if processed, is it the failed operation
+        try:
+            # by default, operations are executed with security turned off
+            with security_enabled(self, False, False):
                 for trstate in ('precommit', 'commit'):
                     processed = []
                     self.commit_state = trstate
@@ -721,23 +764,22 @@
                                       exc_info=sys.exc_info())
                 self.info('%s session %s done', trstate, self.id)
                 return self.transaction_uuid(set=False)
-            finally:
-                self._clear_thread_data()
-                self._touch()
-                if reset_pool:
-                    self.reset_pool(ignoremode=True)
+        finally:
+            self._touch()
+            if reset_pool:
+                self.reset_pool(ignoremode=True)
+            self._clear_thread_data(reset_pool)
 
     def rollback(self, reset_pool=True):
         """rollback the current session's transaction"""
         if self.pool is None:
-            assert not self.pending_operations
             self._clear_thread_data()
             self._touch()
             self.debug('rollback session %s done (no db activity)', self.id)
             return
-        # by default, operations are executed with security turned off
-        with security_enabled(self, False, False):
-            try:
+        try:
+            # by default, operations are executed with security turned off
+            with security_enabled(self, False, False):
                 while self.pending_operations:
                     try:
                         operation = self.pending_operations.pop(0)
@@ -747,11 +789,11 @@
                         continue
                 self.pool.rollback()
                 self.debug('rollback for session %s done', self.id)
-            finally:
-                self._clear_thread_data()
-                self._touch()
-                if reset_pool:
-                    self.reset_pool(ignoremode=True)
+        finally:
+            self._touch()
+            if reset_pool:
+                self.reset_pool(ignoremode=True)
+            self._clear_thread_data(reset_pool)
 
     def close(self):
         """do not close pool on session close, since they are shared now"""
@@ -773,7 +815,8 @@
                 self.error('thread %s still alive after 10 seconds, will close '
                            'session anyway', thread)
         self.rollback()
-        del self._threaddata
+        del self.__threaddata
+        del self._tx_data
 
     # transaction data/operations management ##################################