server/session.py
changeset 9020 cb87e831c183
parent 9016 0368b94921ed
child 9021 d8806996ac01
equal deleted inserted replaced
9019:e08f9c55dab5 9020:cb87e831c183
   119     methods.
   119     methods.
   120     """
   120     """
   121     def __init__(self, session, mode, *categories):
   121     def __init__(self, session, mode, *categories):
   122         assert mode in (HOOKS_ALLOW_ALL, HOOKS_DENY_ALL)
   122         assert mode in (HOOKS_ALLOW_ALL, HOOKS_DENY_ALL)
   123         self.session = session
   123         self.session = session
   124         self.tx = session._tx
   124         self.cnx = session._cnx
   125         self.mode = mode
   125         self.mode = mode
   126         self.categories = categories
   126         self.categories = categories
   127         self.oldmode = None
   127         self.oldmode = None
   128         self.changes = ()
   128         self.changes = ()
   129 
   129 
   130     def __enter__(self):
   130     def __enter__(self):
   131         self.oldmode = self.tx.hooks_mode
   131         self.oldmode = self.cnx.hooks_mode
   132         self.tx.hooks_mode = self.mode
   132         self.cnx.hooks_mode = self.mode
   133         if self.mode is HOOKS_DENY_ALL:
   133         if self.mode is HOOKS_DENY_ALL:
   134             self.changes = self.tx.enable_hook_categories(*self.categories)
   134             self.changes = self.cnx.enable_hook_categories(*self.categories)
   135         else:
   135         else:
   136             self.changes = self.tx.disable_hook_categories(*self.categories)
   136             self.changes = self.cnx.disable_hook_categories(*self.categories)
   137         self.tx.ctx_count += 1
   137         self.cnx.ctx_count += 1
   138 
   138 
   139     def __exit__(self, exctype, exc, traceback):
   139     def __exit__(self, exctype, exc, traceback):
   140         self.tx.ctx_count -= 1
   140         self.cnx.ctx_count -= 1
   141         if self.tx.ctx_count == 0:
   141         if self.cnx.ctx_count == 0:
   142             self.session._clear_thread_storage(self.tx)
   142             self.session._clear_thread_storage(self.cnx)
   143         else:
   143         else:
   144             try:
   144             try:
   145                 if self.categories:
   145                 if self.categories:
   146                     if self.mode is HOOKS_DENY_ALL:
   146                     if self.mode is HOOKS_DENY_ALL:
   147                         self.tx.disable_hook_categories(*self.categories)
   147                         self.cnx.disable_hook_categories(*self.categories)
   148                     else:
   148                     else:
   149                         self.tx.enable_hook_categories(*self.categories)
   149                         self.cnx.enable_hook_categories(*self.categories)
   150             finally:
   150             finally:
   151                 self.tx.hooks_mode = self.oldmode
   151                 self.cnx.hooks_mode = self.oldmode
   152 
   152 
   153 @deprecated('[3.17] use <object>.security_enabled instead')
   153 @deprecated('[3.17] use <object>.security_enabled instead')
   154 def security_enabled(obj, *args, **kwargs):
   154 def security_enabled(obj, *args, **kwargs):
   155     return obj.security_enabled(*args, **kwargs)
   155     return obj.security_enabled(*args, **kwargs)
   156 
   156 
   160     By default security is disabled on queries executed on the repository
   160     By default security is disabled on queries executed on the repository
   161     side.
   161     side.
   162     """
   162     """
   163     def __init__(self, session, read=None, write=None):
   163     def __init__(self, session, read=None, write=None):
   164         self.session = session
   164         self.session = session
   165         self.tx = session._tx
   165         self.cnx = session._cnx
   166         self.read = read
   166         self.read = read
   167         self.write = write
   167         self.write = write
   168         self.oldread = None
   168         self.oldread = None
   169         self.oldwrite = None
   169         self.oldwrite = None
   170 
   170 
   171     def __enter__(self):
   171     def __enter__(self):
   172         if self.read is None:
   172         if self.read is None:
   173             self.oldread = None
   173             self.oldread = None
   174         else:
   174         else:
   175             self.oldread = self.tx.read_security
   175             self.oldread = self.cnx.read_security
   176             self.tx.read_security = self.read
   176             self.cnx.read_security = self.read
   177         if self.write is None:
   177         if self.write is None:
   178             self.oldwrite = None
   178             self.oldwrite = None
   179         else:
   179         else:
   180             self.oldwrite = self.tx.write_security
   180             self.oldwrite = self.cnx.write_security
   181             self.tx.write_security = self.write
   181             self.cnx.write_security = self.write
   182         self.tx.ctx_count += 1
   182         self.cnx.ctx_count += 1
   183 
   183 
   184     def __exit__(self, exctype, exc, traceback):
   184     def __exit__(self, exctype, exc, traceback):
   185         self.tx.ctx_count -= 1
   185         self.cnx.ctx_count -= 1
   186         if self.tx.ctx_count == 0:
   186         if self.cnx.ctx_count == 0:
   187             self.session._clear_thread_storage(self.tx)
   187             self.session._clear_thread_storage(self.cnx)
   188         else:
   188         else:
   189             if self.oldread is not None:
   189             if self.oldread is not None:
   190                 self.tx.read_security = self.oldread
   190                 self.cnx.read_security = self.oldread
   191             if self.oldwrite is not None:
   191             if self.oldwrite is not None:
   192                 self.tx.write_security = self.oldwrite
   192                 self.cnx.write_security = self.oldwrite
   193 
   193 
   194 HOOKS_ALLOW_ALL = object()
   194 HOOKS_ALLOW_ALL = object()
   195 HOOKS_DENY_ALL = object()
   195 HOOKS_DENY_ALL = object()
   196 DEFAULT_SECURITY = object() # evaluated to true by design
   196 DEFAULT_SECURITY = object() # evaluated to true by design
   197 
   197 
   198 class SessionClosedError(RuntimeError):
   198 class SessionClosedError(RuntimeError):
   199     pass
   199     pass
   200 
   200 
   201 class CnxSetTracker(object):
   201 class CnxSetTracker(object):
   202     """Keep track of which transaction use which cnxset.
   202     """Keep track of which connection use which cnxset.
   203 
   203 
   204     There should be one of this object per session plus one another for
   204     There should be one of this object per session plus one another for
   205     internal session.
   205     internal session.
   206 
   206 
   207     Session object are responsible of creating their CnxSetTracker object.
   207     Session object are responsible of creating their CnxSetTracker object.
   208 
   208 
   209     Transaction should use the :meth:`record` and :meth:`forget` to inform the
   209     Connection should use the :meth:`record` and :meth:`forget` to inform the
   210     tracker of cnxset they have acquired.
   210     tracker of cnxset they have acquired.
   211 
   211 
   212     .. automethod:: cubicweb.server.session.CnxSetTracker.record
   212     .. automethod:: cubicweb.server.session.CnxSetTracker.record
   213     .. automethod:: cubicweb.server.session.CnxSetTracker.forget
   213     .. automethod:: cubicweb.server.session.CnxSetTracker.forget
   214 
   214 
   230         self._condition.__enter__()
   230         self._condition.__enter__()
   231 
   231 
   232     def __exit__(self, *args):
   232     def __exit__(self, *args):
   233         self._condition.__exit__(*args)
   233         self._condition.__exit__(*args)
   234 
   234 
   235     def record(self, txid, cnxset):
   235     def record(self, cnxid, cnxset):
   236         """Inform the tracker that a txid have acquired a cnxset
   236         """Inform the tracker that a cnxid have acquired a cnxset
   237 
   237 
   238         This methode is to be used by Transaction object.
   238         This methode is to be used by Connection object.
   239 
   239 
   240         This method fails when:
   240         This method fails when:
   241         - The txid already have a recorded cnxset.
   241         - The cnxid already have a recorded cnxset.
   242         - The tracker is not active anymore.
   242         - The tracker is not active anymore.
   243 
   243 
   244         Notes about the caller:
   244         Notes about the caller:
   245         (1) It is responsible for retrieving a cnxset.
   245         (1) It is responsible for retrieving a cnxset.
   246         (2) It must be prepared to release the cnxset if the
   246         (2) It must be prepared to release the cnxset if the
   263         """
   263         """
   264         # dubious since the caller is suppose to have acquired it anyway.
   264         # dubious since the caller is suppose to have acquired it anyway.
   265         with self._condition:
   265         with self._condition:
   266             if not self._active:
   266             if not self._active:
   267                 raise SessionClosedError('Closed')
   267                 raise SessionClosedError('Closed')
   268             old = self._record.get(txid)
   268             old = self._record.get(cnxid)
   269             if old is not None:
   269             if old is not None:
   270                 raise ValueError('"%s" already have a cnx_set (%r)'
   270                 raise ValueError('"%s" already have a cnx_set (%r)'
   271                                  % (txid, old))
   271                                  % (cnxid, old))
   272             self._record[txid] = cnxset
   272             self._record[cnxid] = cnxset
   273 
   273 
   274     def forget(self, txid, cnxset):
   274     def forget(self, cnxid, cnxset):
   275         """Inform the tracker that a txid have release a cnxset
   275         """Inform the tracker that a cnxid have release a cnxset
   276 
   276 
   277         This methode is to be used by Transaction object.
   277         This methode is to be used by Connection object.
   278 
   278 
   279         This method fails when:
   279         This method fails when:
   280         - The cnxset for the txid does not match the recorded one.
   280         - The cnxset for the cnxid does not match the recorded one.
   281 
   281 
   282         Notes about the caller:
   282         Notes about the caller:
   283         (1) It is responsible for releasing the cnxset.
   283         (1) It is responsible for releasing the cnxset.
   284         (2) It should acquire the tracker lock during the operation to ensure
   284         (2) It should acquire the tracker lock during the operation to ensure
   285             the internal tracker state is always accurate regarding its own state.
   285             the internal tracker state is always accurate regarding its own state.
   295                 cnxset_tracker.forget(caller.id, cnxset)
   295                 cnxset_tracker.forget(caller.id, cnxset)
   296         finally:
   296         finally:
   297             cnxset = repo._free_cnxset(cnxset) # (1)
   297             cnxset = repo._free_cnxset(cnxset) # (1)
   298         """
   298         """
   299         with self._condition:
   299         with self._condition:
   300             old = self._record.get(txid, None)
   300             old = self._record.get(cnxid, None)
   301             if old is not cnxset:
   301             if old is not cnxset:
   302                 raise ValueError('recorded cnxset for "%s" mismatch: %r != %r'
   302                 raise ValueError('recorded cnxset for "%s" mismatch: %r != %r'
   303                                  % (txid, old, cnxset))
   303                                  % (cnxid, old, cnxset))
   304             self._record.pop(txid)
   304             self._record.pop(cnxid)
   305             self._condition.notify_all()
   305             self._condition.notify_all()
   306 
   306 
   307     def close(self):
   307     def close(self):
   308         """Marks the tracker as inactive.
   308         """Marks the tracker as inactive.
   309 
   309 
   317     def wait(self, timeout=10):
   317     def wait(self, timeout=10):
   318         """Wait for all recorded cnxset to be released
   318         """Wait for all recorded cnxset to be released
   319 
   319 
   320         This methode is to be used by Session object.
   320         This methode is to be used by Session object.
   321 
   321 
   322         returns a tuple of transaction id that remains open.
   322         returns a tuple of connection id that remains open.
   323         """
   323         """
   324         with self._condition:
   324         with self._condition:
   325             if  self._active:
   325             if  self._active:
   326                 raise RuntimeError('Cannot wait on active tracker.'
   326                 raise RuntimeError('Cannot wait on active tracker.'
   327                                    ' Call tracker.close() first')
   327                                    ' Call tracker.close() first')
   329                 start = time()
   329                 start = time()
   330                 self._condition.wait(timeout)
   330                 self._condition.wait(timeout)
   331                 timeout -= time() - start
   331                 timeout -= time() - start
   332             return tuple(self._record)
   332             return tuple(self._record)
   333 
   333 
   334 class Transaction(object):
   334 class Connection(object):
   335     """Repository Transaction
   335     """Repository Connection
   336 
   336 
   337     Holds all transaction related data
   337     Holds all connection related data
   338 
   338 
   339     Database connections resource:
   339     Database connections resource:
   340 
   340 
   341       :attr:`running_dbapi_query`, boolean flag telling if the executing query
   341       :attr:`running_dbapi_query`, boolean flag telling if the executing query
   342       is coming from a dbapi connection or is a query from within the repository
   342       is coming from a dbapi connection or is a query from within the repository
   343 
   343 
   344       :attr:`cnxset`, the connections set to use to execute queries on sources.
   344       :attr:`cnxset`, the connections set to use to execute queries on sources.
   345       If the transaction is read only, the connection set may be freed between
   345       If the transaction is read only, the connection set may be freed between
   346       actual query. This allows multiple transaction with a reasonable low
   346       actual query. This allows multiple connection with a reasonable low
   347       connection set pool size. control mechanism is detailed below
   347       connection set pool size. control mechanism is detailed below
   348 
   348 
   349     .. automethod:: cubicweb.server.session.Transaction.set_cnxset
   349     .. automethod:: cubicweb.server.session.Connection.set_cnxset
   350     .. automethod:: cubicweb.server.session.Transaction.free_cnxset
   350     .. automethod:: cubicweb.server.session.Connection.free_cnxset
   351 
   351 
   352       :attr:`mode`, string telling the connections set handling mode, may be one
   352       :attr:`mode`, string telling the connections set handling mode, may be one
   353       of 'read' (connections set may be freed), 'write' (some write was done in
   353       of 'read' (connections set may be freed), 'write' (some write was done in
   354       the connections set, it can't be freed before end of the transaction),
   354       the connections set, it can't be freed before end of the transaction),
   355       'transaction' (we want to keep the connections set during all the
   355       'transaction' (we want to keep the connections set during all the
   386       :attr:`read_security` and :attr:`write_security`, boolean flags telling if
   386       :attr:`read_security` and :attr:`write_security`, boolean flags telling if
   387       read/write security is currently activated.
   387       read/write security is currently activated.
   388 
   388 
   389     """
   389     """
   390 
   390 
   391     def __init__(self, txid, session, rewriter):
   391     def __init__(self, cnxid, session, rewriter):
   392         #: transaction unique id
   392         #: connection unique id
   393         self.transactionid = txid
   393         self.connectionid = cnxid
   394         #: reentrance handling
   394         #: reentrance handling
   395         self.ctx_count = 0
   395         self.ctx_count = 0
   396 
   396 
   397         #: server.Repository object
   397         #: server.Repository object
   398         self.repo = session.repo
   398         self.repo = session.repo
   402         self.mode = session.default_mode
   402         self.mode = session.default_mode
   403         #: connection set used to execute queries on sources
   403         #: connection set used to execute queries on sources
   404         self._cnxset = None
   404         self._cnxset = None
   405         #: CnxSetTracker used to report cnxset usage
   405         #: CnxSetTracker used to report cnxset usage
   406         self._cnxset_tracker = session._cnxset_tracker
   406         self._cnxset_tracker = session._cnxset_tracker
   407         #: is this transaction from a client or internal to the repo
   407         #: is this connection from a client or internal to the repo
   408         self.running_dbapi_query = True
   408         self.running_dbapi_query = True
   409 
   409 
   410         #: dict containing arbitrary data cleared at the end of the transaction
   410         #: dict containing arbitrary data cleared at the end of the transaction
   411         self.data = {}
   411         self.data = {}
   412         #: ordered list of operations to be processed on commit/rollback
   412         #: ordered list of operations to be processed on commit/rollback
   460             if new_cnxset is old_cnxset:
   460             if new_cnxset is old_cnxset:
   461                 return #nothing to do
   461                 return #nothing to do
   462             if old_cnxset is not None:
   462             if old_cnxset is not None:
   463                 self._cnxset = None
   463                 self._cnxset = None
   464                 self.ctx_count -= 1
   464                 self.ctx_count -= 1
   465                 self._cnxset_tracker.forget(self.transactionid, old_cnxset)
   465                 self._cnxset_tracker.forget(self.connectionid, old_cnxset)
   466             if new_cnxset is not None:
   466             if new_cnxset is not None:
   467                 self._cnxset_tracker.record(self.transactionid, new_cnxset)
   467                 self._cnxset_tracker.record(self.connectionid, new_cnxset)
   468                 self._cnxset = new_cnxset
   468                 self._cnxset = new_cnxset
   469                 self.ctx_count += 1
   469                 self.ctx_count += 1
   470 
   470 
   471     def set_cnxset(self):
   471     def set_cnxset(self):
   472         """the transaction need a connections set to execute some queries"""
   472         """the connection need a connections set to execute some queries"""
   473         if self.cnxset is None:
   473         if self.cnxset is None:
   474             cnxset = self.repo._get_cnxset()
   474             cnxset = self.repo._get_cnxset()
   475             try:
   475             try:
   476                 self.cnxset = cnxset
   476                 self.cnxset = cnxset
   477                 try:
   477                 try:
   483                 self.repo._free_cnxset(cnxset)
   483                 self.repo._free_cnxset(cnxset)
   484                 raise
   484                 raise
   485         return self.cnxset
   485         return self.cnxset
   486 
   486 
   487     def free_cnxset(self, ignoremode=False):
   487     def free_cnxset(self, ignoremode=False):
   488         """the transaction is no longer using its connections set, at least for some time"""
   488         """the connection is no longer using its connections set, at least for some time"""
   489         # cnxset may be none if no operation has been done since last commit
   489         # cnxset may be none if no operation has been done since last commit
   490         # or rollback
   490         # or rollback
   491         cnxset = self.cnxset
   491         cnxset = self.cnxset
   492         if cnxset is not None and (ignoremode or self.mode == 'read'):
   492         if cnxset is not None and (ignoremode or self.mode == 'read'):
   493             try:
   493             try:
   497                 self.repo._free_cnxset(cnxset)
   497                 self.repo._free_cnxset(cnxset)
   498 
   498 
   499 
   499 
   500     # Entity cache management #################################################
   500     # Entity cache management #################################################
   501     #
   501     #
   502     # The transaction entity cache as held in tx.data it is removed at end the
   502     # The connection entity cache as held in cnx.data it is removed at end the
   503     # end of the transaction (commit and rollback)
   503     # end of the connection (commit and rollback)
   504     #
   504     #
   505     # XXX transaction level caching may be a pb with multiple repository
   505     # XXX connection level caching may be a pb with multiple repository
   506     # instances, but 1. this is probably not the only one :$ and 2. it may be
   506     # instances, but 1. this is probably not the only one :$ and 2. it may be
   507     # an acceptable risk. Anyway we could activate it or not according to a
   507     # an acceptable risk. Anyway we could activate it or not according to a
   508     # configuration option
   508     # configuration option
   509 
   509 
   510     def set_entity_cache(self, entity):
   510     def set_entity_cache(self, entity):
   511         """Add `entity` to the transaction entity cache"""
   511         """Add `entity` to the connection entity cache"""
   512         ecache = self.data.setdefault('ecache', {})
   512         ecache = self.data.setdefault('ecache', {})
   513         ecache.setdefault(entity.eid, entity)
   513         ecache.setdefault(entity.eid, entity)
   514 
   514 
   515     def entity_cache(self, eid):
   515     def entity_cache(self, eid):
   516         """get cache entity for `eid`"""
   516         """get cache entity for `eid`"""
   692         subjtype = self.repo.type_and_source_from_eid(eidfrom, self)[0]
   692         subjtype = self.repo.type_and_source_from_eid(eidfrom, self)[0]
   693         objtype = self.repo.type_and_source_from_eid(eidto, self)[0]
   693         objtype = self.repo.type_and_source_from_eid(eidto, self)[0]
   694         return self.vreg.schema.rschema(rtype).rdefs[(subjtype, objtype)]
   694         return self.vreg.schema.rschema(rtype).rdefs[(subjtype, objtype)]
   695 
   695 
   696 
   696 
   697 def tx_attr(attr_name, writable=False):
   697 def cnx_attr(attr_name, writable=False):
   698     """return a property to forward attribute access to transaction.
   698     """return a property to forward attribute access to connection.
   699 
   699 
   700     This is to be used by session"""
   700     This is to be used by session"""
   701     args = {}
   701     args = {}
   702     def attr_from_tx(session):
   702     def attr_from_cnx(session):
   703         return getattr(session._tx, attr_name)
   703         return getattr(session._cnx, attr_name)
   704     args['fget'] = attr_from_tx
   704     args['fget'] = attr_from_cnx
   705     if writable:
   705     if writable:
   706         def write_attr(session, value):
   706         def write_attr(session, value):
   707             return setattr(session._tx, attr_name, value)
   707             return setattr(session._cnx, attr_name, value)
   708         args['fset'] = write_attr
   708         args['fset'] = write_attr
   709     return property(**args)
   709     return property(**args)
   710 
   710 
   711 def tx_meth(meth_name):
   711 def cnx_meth(meth_name):
   712     """return a function forwarding calls to transaction.
   712     """return a function forwarding calls to connection.
   713 
   713 
   714     This is to be used by session"""
   714     This is to be used by session"""
   715     def meth_from_tx(session, *args, **kwargs):
   715     def meth_from_cnx(session, *args, **kwargs):
   716         return getattr(session._tx, meth_name)(*args, **kwargs)
   716         return getattr(session._cnx, meth_name)(*args, **kwargs)
   717     return meth_from_tx
   717     return meth_from_cnx
   718 
   718 
   719 
   719 
   720 class Session(RequestSessionBase):
   720 class Session(RequestSessionBase):
   721     """Repository user session
   721     """Repository user session
   722 
   722 
   734     described here but higher level APIs.
   734     described here but higher level APIs.
   735 
   735 
   736       :attr:`data` is a dictionary containing shared data, used to communicate
   736       :attr:`data` is a dictionary containing shared data, used to communicate
   737       extra information between the client and the repository
   737       extra information between the client and the repository
   738 
   738 
   739       :attr:`_txs` is a dictionary of :class:`TransactionData` instance, one
   739       :attr:`_cnxs` is a dictionary of :class:`Connection` instance, one
   740       for each running transaction. The key is the transaction id. By default
   740       for each running connection. The key is the connection id. By default
   741       the transaction id is the thread name but it can be otherwise (per dbapi
   741       the connection id is the thread name but it can be otherwise (per dbapi
   742       cursor for instance, or per thread name *from another process*).
   742       cursor for instance, or per thread name *from another process*).
   743 
   743 
   744       :attr:`__threaddata` is a thread local storage whose `tx` attribute
   744       :attr:`__threaddata` is a thread local storage whose `cnx` attribute
   745       refers to the proper instance of :class:`Transaction` according to the
   745       refers to the proper instance of :class:`Connection` according to the
   746       transaction.
   746       connection.
   747 
   747 
   748     You should not have to use neither :attr:`_tx` nor :attr:`__threaddata`,
   748     You should not have to use neither :attr:`_cnx` nor :attr:`__threaddata`,
   749     simply access transaction data transparently through the :attr:`_tx`
   749     simply access connection data transparently through the :attr:`_cnx`
   750     property. Also, you usually don't have to access it directly since current
   750     property. Also, you usually don't have to access it directly since current
   751     transaction's data may be accessed/modified through properties / methods:
   751     connection's data may be accessed/modified through properties / methods:
   752 
   752 
   753       :attr:`transaction_data`, similarly to :attr:`data`, is a dictionary
   753       :attr:`connection_data`, similarly to :attr:`data`, is a dictionary
   754       containing some shared data that should be cleared at the end of the
   754       containing some shared data that should be cleared at the end of the
   755       transaction. Hooks and operations may put arbitrary data in there, and
   755       connection. Hooks and operations may put arbitrary data in there, and
   756       this may also be used as a communication channel between the client and
   756       this may also be used as a communication channel between the client and
   757       the repository.
   757       the repository.
   758 
   758 
   759     .. automethod:: cubicweb.server.session.Session.get_shared_data
   759     .. automethod:: cubicweb.server.session.Session.get_shared_data
   760     .. automethod:: cubicweb.server.session.Session.set_shared_data
   760     .. automethod:: cubicweb.server.session.Session.set_shared_data
   761     .. automethod:: cubicweb.server.session.Session.added_in_transaction
   761     .. automethod:: cubicweb.server.session.Session.added_in_transaction
   762     .. automethod:: cubicweb.server.session.Session.deleted_in_transaction
   762     .. automethod:: cubicweb.server.session.Session.deleted_in_transaction
   763 
   763 
   764     Transaction state information:
   764     Connection state information:
   765 
   765 
   766       :attr:`running_dbapi_query`, boolean flag telling if the executing query
   766       :attr:`running_dbapi_query`, boolean flag telling if the executing query
   767       is coming from a dbapi connection or is a query from within the repository
   767       is coming from a dbapi connection or is a query from within the repository
   768 
   768 
   769       :attr:`cnxset`, the connections set to use to execute queries on sources.
   769       :attr:`cnxset`, the connections set to use to execute queries on sources.
   845         # and the rql server
   845         # and the rql server
   846         self.data = {}
   846         self.data = {}
   847         # i18n initialization
   847         # i18n initialization
   848         self.set_language(user.prefered_language())
   848         self.set_language(user.prefered_language())
   849         ### internals
   849         ### internals
   850         # Transaction of this section
   850         # Connection of this section
   851         self._txs = {}
   851         self._cnxs = {}
   852         # Data local to the thread
   852         # Data local to the thread
   853         self.__threaddata = threading.local()
   853         self.__threaddata = threading.local()
   854         self._cnxset_tracker = CnxSetTracker()
   854         self._cnxset_tracker = CnxSetTracker()
   855         self._closed = False
   855         self._closed = False
   856         self._lock = threading.RLock()
   856         self._lock = threading.RLock()
   857 
   857 
   858     def __unicode__(self):
   858     def __unicode__(self):
   859         return '<session %s (%s 0x%x)>' % (
   859         return '<session %s (%s 0x%x)>' % (
   860             unicode(self.user.login), self.id, id(self))
   860             unicode(self.user.login), self.id, id(self))
   861 
   861 
   862     def get_tx(self, txid):
   862     def get_cnx(self, cnxid):
   863         """return the <txid> transaction attached to this session
   863         """return the <cnxid> connection attached to this session
   864 
   864 
   865         Transaction is created if necessary"""
   865         Connection is created if necessary"""
   866         with self._lock: # no transaction exist with the same id
   866         with self._lock: # no connection exist with the same id
   867             try:
   867             try:
   868                 if self.closed:
   868                 if self.closed:
   869                     raise SessionClosedError('try to access connections set on a closed session %s' % self.id)
   869                     raise SessionClosedError('try to access connections set on a closed session %s' % self.id)
   870                 tx = self._txs[txid]
   870                 cnx = self._cnxs[cnxid]
   871             except KeyError:
   871             except KeyError:
   872                 rewriter = RQLRewriter(self)
   872                 rewriter = RQLRewriter(self)
   873                 tx = Transaction(txid, self, rewriter)
   873                 cnx = Connection(cnxid, self, rewriter)
   874                 self._txs[txid] = tx
   874                 self._cnxs[cnxid] = cnx
   875         return tx
   875         return cnx
   876 
   876 
   877     def set_tx(self, txid=None):
   877     def set_cnx(self, cnxid=None):
   878         """set the default transaction of the current thread to <txid>
   878         """set the default connection of the current thread to <cnxid>
   879 
   879 
   880         Transaction is created if necessary"""
   880         Connection is created if necessary"""
   881         if txid is None:
   881         if cnxid is None:
   882             txid = threading.currentThread().getName()
   882             cnxid = threading.currentThread().getName()
   883         self.__threaddata.tx = self.get_tx(txid)
   883         self.__threaddata.cnx = self.get_cnx(cnxid)
   884 
   884 
   885     @property
   885     @property
   886     def _tx(self):
   886     def _cnx(self):
   887         """default transaction for current session in current thread"""
   887         """default connection for current session in current thread"""
   888         try:
   888         try:
   889             return self.__threaddata.tx
   889             return self.__threaddata.cnx
   890         except AttributeError:
   890         except AttributeError:
   891             self.set_tx()
   891             self.set_cnx()
   892             return self.__threaddata.tx
   892             return self.__threaddata.cnx
   893 
   893 
   894     def get_option_value(self, option, foreid=None):
   894     def get_option_value(self, option, foreid=None):
   895         return self.repo.get_option_value(option, foreid)
   895         return self.repo.get_option_value(option, foreid)
   896 
   896 
   897     def transaction(self, free_cnxset=True):
   897     def transaction(self, free_cnxset=True):
  1030             entity._cw_related_cache['%s_%s' % (rtype, role)] = (
  1030             entity._cw_related_cache['%s_%s' % (rtype, role)] = (
  1031                 rset, tuple(entities))
  1031                 rset, tuple(entities))
  1032 
  1032 
  1033     # resource accessors ######################################################
  1033     # resource accessors ######################################################
  1034 
  1034 
  1035     system_sql = tx_meth('system_sql')
  1035     system_sql = cnx_meth('system_sql')
  1036     deleted_in_transaction = tx_meth('deleted_in_transaction')
  1036     deleted_in_transaction = cnx_meth('deleted_in_transaction')
  1037     added_in_transaction = tx_meth('added_in_transaction')
  1037     added_in_transaction = cnx_meth('added_in_transaction')
  1038     rtype_eids_rdef = tx_meth('rtype_eids_rdef')
  1038     rtype_eids_rdef = cnx_meth('rtype_eids_rdef')
  1039 
  1039 
  1040     # security control #########################################################
  1040     # security control #########################################################
  1041 
  1041 
  1042 
  1042 
  1043     def security_enabled(self, read=None, write=None):
  1043     def security_enabled(self, read=None, write=None):
  1044         return _security_enabled(self, read=read, write=write)
  1044         return _security_enabled(self, read=read, write=write)
  1045 
  1045 
  1046     read_security = tx_attr('read_security', writable=True)
  1046     read_security = cnx_attr('read_security', writable=True)
  1047     write_security = tx_attr('write_security', writable=True)
  1047     write_security = cnx_attr('write_security', writable=True)
  1048     running_dbapi_query = tx_attr('running_dbapi_query')
  1048     running_dbapi_query = cnx_attr('running_dbapi_query')
  1049 
  1049 
  1050     # hooks activation control #################################################
  1050     # hooks activation control #################################################
  1051     # all hooks should be activated during normal execution
  1051     # all hooks should be activated during normal execution
  1052 
  1052 
  1053     def allow_all_hooks_but(self, *categories):
  1053     def allow_all_hooks_but(self, *categories):
  1054         return _hooks_control(self, HOOKS_ALLOW_ALL, *categories)
  1054         return _hooks_control(self, HOOKS_ALLOW_ALL, *categories)
  1055     def deny_all_hooks_but(self, *categories):
  1055     def deny_all_hooks_but(self, *categories):
  1056         return _hooks_control(self, HOOKS_DENY_ALL, *categories)
  1056         return _hooks_control(self, HOOKS_DENY_ALL, *categories)
  1057 
  1057 
  1058     hooks_mode = tx_attr('hooks_mode')
  1058     hooks_mode = cnx_attr('hooks_mode')
  1059 
  1059 
  1060     disabled_hook_categories = tx_attr('disabled_hook_cats')
  1060     disabled_hook_categories = cnx_attr('disabled_hook_cats')
  1061     enabled_hook_categories = tx_attr('enabled_hook_cats')
  1061     enabled_hook_categories = cnx_attr('enabled_hook_cats')
  1062     disable_hook_categories = tx_meth('disable_hook_categories')
  1062     disable_hook_categories = cnx_meth('disable_hook_categories')
  1063     enable_hook_categories = tx_meth('enable_hook_categories')
  1063     enable_hook_categories = cnx_meth('enable_hook_categories')
  1064     is_hook_category_activated = tx_meth('is_hook_category_activated')
  1064     is_hook_category_activated = cnx_meth('is_hook_category_activated')
  1065     is_hook_activated = tx_meth('is_hook_activated')
  1065     is_hook_activated = cnx_meth('is_hook_activated')
  1066 
  1066 
  1067     # connection management ###################################################
  1067     # connection management ###################################################
  1068 
  1068 
  1069     def keep_cnxset_mode(self, mode):
  1069     def keep_cnxset_mode(self, mode):
  1070         """set `mode`, e.g. how the session will keep its connections set:
  1070         """set `mode`, e.g. how the session will keep its connections set:
  1084         if mode == 'transaction':
  1084         if mode == 'transaction':
  1085             self.default_mode = 'transaction'
  1085             self.default_mode = 'transaction'
  1086         else: # mode == 'write'
  1086         else: # mode == 'write'
  1087             self.default_mode = 'read'
  1087             self.default_mode = 'read'
  1088 
  1088 
  1089     mode = tx_attr('mode', writable=True)
  1089     mode = cnx_attr('mode', writable=True)
  1090     commit_state = tx_attr('commit_state', writable=True)
  1090     commit_state = cnx_attr('commit_state', writable=True)
  1091 
  1091 
  1092     @property
  1092     @property
  1093     def cnxset(self):
  1093     def cnxset(self):
  1094         """connections set, set according to transaction mode for each query"""
  1094         """connections set, set according to transaction mode for each query"""
  1095         if self._closed:
  1095         if self._closed:
  1096             self.free_cnxset(True)
  1096             self.free_cnxset(True)
  1097             raise SessionClosedError('try to access connections set on a closed session %s' % self.id)
  1097             raise SessionClosedError('try to access connections set on a closed session %s' % self.id)
  1098         return self._tx.cnxset
  1098         return self._cnx.cnxset
  1099 
  1099 
  1100     def set_cnxset(self):
  1100     def set_cnxset(self):
  1101         """the session need a connections set to execute some queries"""
  1101         """the session need a connections set to execute some queries"""
  1102         with self._lock: # can probably be removed
  1102         with self._lock: # can probably be removed
  1103             if self._closed:
  1103             if self._closed:
  1104                 self.free_cnxset(True)
  1104                 self.free_cnxset(True)
  1105                 raise SessionClosedError('try to set connections set on a closed session %s' % self.id)
  1105                 raise SessionClosedError('try to set connections set on a closed session %s' % self.id)
  1106             return self._tx.set_cnxset()
  1106             return self._cnx.set_cnxset()
  1107     free_cnxset = tx_meth('free_cnxset')
  1107     free_cnxset = cnx_meth('free_cnxset')
  1108 
  1108 
  1109     def _touch(self):
  1109     def _touch(self):
  1110         """update latest session usage timestamp and reset mode to read"""
  1110         """update latest session usage timestamp and reset mode to read"""
  1111         self.timestamp = time()
  1111         self.timestamp = time()
  1112         self.local_perm_cache.clear() # XXX simply move in tx.data, no?
  1112         self.local_perm_cache.clear() # XXX simply move in cnx.data, no?
  1113 
  1113 
  1114     # shared data handling ###################################################
  1114     # shared data handling ###################################################
  1115 
  1115 
  1116     def get_shared_data(self, key, default=None, pop=False, txdata=False):
  1116     def get_shared_data(self, key, default=None, pop=False, txdata=False):
  1117         """return value associated to `key` in session data"""
  1117         """return value associated to `key` in session data"""
  1118         if txdata:
  1118         if txdata:
  1119             data = self._tx.data
  1119             data = self._cnx.data
  1120         else:
  1120         else:
  1121             data = self.data
  1121             data = self.data
  1122         if pop:
  1122         if pop:
  1123             return data.pop(key, default)
  1123             return data.pop(key, default)
  1124         else:
  1124         else:
  1125             return data.get(key, default)
  1125             return data.get(key, default)
  1126 
  1126 
  1127     def set_shared_data(self, key, value, txdata=False):
  1127     def set_shared_data(self, key, value, txdata=False):
  1128         """set value associated to `key` in session data"""
  1128         """set value associated to `key` in session data"""
  1129         if txdata:
  1129         if txdata:
  1130             self._tx.data[key] = value
  1130             self._cnx.data[key] = value
  1131         else:
  1131         else:
  1132             self.data[key] = value
  1132             self.data[key] = value
  1133 
  1133 
  1134     # server-side service call #################################################
  1134     # server-side service call #################################################
  1135 
  1135 
  1143     @property
  1143     @property
  1144     def cursor(self):
  1144     def cursor(self):
  1145         """return a rql cursor"""
  1145         """return a rql cursor"""
  1146         return self
  1146         return self
  1147 
  1147 
  1148     set_entity_cache  = tx_meth('set_entity_cache')
  1148     set_entity_cache  = cnx_meth('set_entity_cache')
  1149     entity_cache      = tx_meth('entity_cache')
  1149     entity_cache      = cnx_meth('entity_cache')
  1150     cache_entities    = tx_meth('cached_entities')
  1150     cache_entities    = cnx_meth('cached_entities')
  1151     drop_entity_cache = tx_meth('drop_entity_cache')
  1151     drop_entity_cache = cnx_meth('drop_entity_cache')
  1152 
  1152 
  1153     def from_controller(self):
  1153     def from_controller(self):
  1154         """return the id (string) of the controller issuing the request (no
  1154         """return the id (string) of the controller issuing the request (no
  1155         sense here, always return 'view')
  1155         sense here, always return 'view')
  1156         """
  1156         """
  1157         return 'view'
  1157         return 'view'
  1158 
  1158 
  1159     source_defs = tx_meth('source_defs')
  1159     source_defs = cnx_meth('source_defs')
  1160     describe = tx_meth('describe')
  1160     describe = cnx_meth('describe')
  1161     source_from_eid = tx_meth('source_from_eid')
  1161     source_from_eid = cnx_meth('source_from_eid')
  1162 
  1162 
  1163 
  1163 
  1164     def execute(self, rql, kwargs=None, eid_key=None, build_descr=True):
  1164     def execute(self, rql, kwargs=None, eid_key=None, build_descr=True):
  1165         """db-api like method directly linked to the querier execute method.
  1165         """db-api like method directly linked to the querier execute method.
  1166 
  1166 
  1178         """remove everything from the thread local storage, except connections set
  1178         """remove everything from the thread local storage, except connections set
  1179         which is explicitly removed by free_cnxset, and mode which is set anyway
  1179         which is explicitly removed by free_cnxset, and mode which is set anyway
  1180         by _touch
  1180         by _touch
  1181         """
  1181         """
  1182         try:
  1182         try:
  1183             tx = self.__threaddata.tx
  1183             cnx = self.__threaddata.cnx
  1184         except AttributeError:
  1184         except AttributeError:
  1185             pass
  1185             pass
  1186         else:
  1186         else:
  1187             if free_cnxset:
  1187             if free_cnxset:
  1188                 self.free_cnxset()
  1188                 self.free_cnxset()
  1189                 if tx.ctx_count == 0:
  1189                 if cnx.ctx_count == 0:
  1190                     self._clear_thread_storage(tx)
  1190                     self._clear_thread_storage(cnx)
  1191                 else:
  1191                 else:
  1192                     self._clear_tx_storage(tx)
  1192                     self._clear_cnx_storage(cnx)
  1193             else:
  1193             else:
  1194                 self._clear_tx_storage(tx)
  1194                 self._clear_cnx_storage(cnx)
  1195 
  1195 
  1196     def _clear_thread_storage(self, tx):
  1196     def _clear_thread_storage(self, cnx):
  1197         self._txs.pop(tx.transactionid, None)
  1197         self._cnxs.pop(cnx.connectionid, None)
  1198         try:
  1198         try:
  1199             del self.__threaddata.tx
  1199             del self.__threaddata.cnx
  1200         except AttributeError:
  1200         except AttributeError:
  1201             pass
  1201             pass
  1202 
  1202 
  1203     def _clear_tx_storage(self, tx):
  1203     def _clear_cnx_storage(self, cnx):
  1204         tx.clear()
  1204         cnx.clear()
  1205         tx._rewriter = RQLRewriter(self)
  1205         cnx._rewriter = RQLRewriter(self)
  1206 
  1206 
  1207     def commit(self, free_cnxset=True, reset_pool=None):
  1207     def commit(self, free_cnxset=True, reset_pool=None):
  1208         """commit the current session's transaction"""
  1208         """commit the current session's transaction"""
  1209         if reset_pool is not None:
  1209         if reset_pool is not None:
  1210             warn('[3.13] use free_cnxset argument instead for reset_pool',
  1210             warn('[3.13] use free_cnxset argument instead for reset_pool',
  1297         if reset_pool is not None:
  1297         if reset_pool is not None:
  1298             warn('[3.13] use free_cnxset argument instead for reset_pool',
  1298             warn('[3.13] use free_cnxset argument instead for reset_pool',
  1299                  DeprecationWarning, stacklevel=2)
  1299                  DeprecationWarning, stacklevel=2)
  1300             free_cnxset = reset_pool
  1300             free_cnxset = reset_pool
  1301         # don't use self.cnxset, rollback may be called with _closed == True
  1301         # don't use self.cnxset, rollback may be called with _closed == True
  1302         cnxset = self._tx.cnxset
  1302         cnxset = self._cnx.cnxset
  1303         if cnxset is None:
  1303         if cnxset is None:
  1304             self._clear_thread_data()
  1304             self._clear_thread_data()
  1305             self._touch()
  1305             self._touch()
  1306             self.debug('rollback session %s done (no db activity)', self.id)
  1306             self.debug('rollback session %s done (no db activity)', self.id)
  1307             return
  1307             return
  1328         tracker = self._cnxset_tracker
  1328         tracker = self._cnxset_tracker
  1329         with self._lock:
  1329         with self._lock:
  1330             self._closed = True
  1330             self._closed = True
  1331         tracker.close()
  1331         tracker.close()
  1332         self.rollback()
  1332         self.rollback()
  1333         self.info('waiting for open transaction of session: %s', self)
  1333         self.info('waiting for open connection of session: %s', self)
  1334         timeout = 10
  1334         timeout = 10
  1335         pendings = tracker.wait(timeout)
  1335         pendings = tracker.wait(timeout)
  1336         if pendings:
  1336         if pendings:
  1337             self.error('%i transaction still alive after 10 seconds, will close '
  1337             self.error('%i connection still alive after 10 seconds, will close '
  1338                        'session anyway', len(pendings))
  1338                        'session anyway', len(pendings))
  1339             for txid in pendings:
  1339             for cnxid in pendings:
  1340                 tx = self._txs.get(txid)
  1340                 cnx = self._cnxs.get(cnxid)
  1341                 if tx is not None:
  1341                 if cnx is not None:
  1342                     # drop tx.cnxset
  1342                     # drop cnx.cnxset
  1343                     with tracker:
  1343                     with tracker:
  1344                         try:
  1344                         try:
  1345                             cnxset = tx.cnxset
  1345                             cnxset = cnx.cnxset
  1346                             if cnxset is None:
  1346                             if cnxset is None:
  1347                                 continue
  1347                                 continue
  1348                             tx.cnxset = None
  1348                             cnx.cnxset = None
  1349                         except RuntimeError:
  1349                         except RuntimeError:
  1350                             msg = 'issue while force free of cnxset in %s'
  1350                             msg = 'issue while force free of cnxset in %s'
  1351                             self.error(msg, tx)
  1351                             self.error(msg, cnx)
  1352                     # cnxset.reconnect() do an hard reset of the cnxset
  1352                     # cnxset.reconnect() do an hard reset of the cnxset
  1353                     # it force it to be freed
  1353                     # it force it to be freed
  1354                     cnxset.reconnect()
  1354                     cnxset.reconnect()
  1355                     self.repo._free_cnxset(cnxset)
  1355                     self.repo._free_cnxset(cnxset)
  1356         del self.__threaddata
  1356         del self.__threaddata
  1357         del self._txs
  1357         del self._cnxs
  1358 
  1358 
  1359     @property
  1359     @property
  1360     def closed(self):
  1360     def closed(self):
  1361         return not hasattr(self, '_txs')
  1361         return not hasattr(self, '_cnxs')
  1362 
  1362 
  1363     # transaction data/operations management ##################################
  1363     # transaction data/operations management ##################################
  1364 
  1364 
  1365     transaction_data = tx_attr('data')
  1365     transaction_data = cnx_attr('data')
  1366     pending_operations = tx_attr('pending_operations')
  1366     pending_operations = cnx_attr('pending_operations')
  1367     pruned_hooks_cache = tx_attr('pruned_hooks_cache')
  1367     pruned_hooks_cache = cnx_attr('pruned_hooks_cache')
  1368     add_operation      = tx_meth('add_operation')
  1368     add_operation      = cnx_meth('add_operation')
  1369 
  1369 
  1370     # undo support ############################################################
  1370     # undo support ############################################################
  1371 
  1371 
  1372     ertype_supports_undo = tx_meth('ertype_supports_undo')
  1372     ertype_supports_undo = cnx_meth('ertype_supports_undo')
  1373     transaction_inc_action_counter = tx_meth('transaction_inc_action_counter')
  1373     transaction_inc_action_counter = cnx_meth('transaction_inc_action_counter')
  1374 
  1374 
  1375     def transaction_uuid(self, set=True):
  1375     def transaction_uuid(self, set=True):
  1376         try:
  1376         try:
  1377             return self._tx.transaction_uuid(set=set)
  1377             return self._cnx.transaction_uuid(set=set)
  1378         except KeyError:
  1378         except KeyError:
  1379             self._tx.data['tx_uuid'] = uuid = uuid4().hex
  1379             self._cnx.data['tx_uuid'] = uuid = uuid4().hex
  1380             self.repo.system_source.start_undoable_transaction(self, uuid)
  1380             self.repo.system_source.start_undoable_transaction(self, uuid)
  1381             return uuid
  1381             return uuid
  1382 
  1382 
  1383     # querier helpers #########################################################
  1383     # querier helpers #########################################################
  1384 
  1384 
  1385     rql_rewriter = tx_attr('_rewriter')
  1385     rql_rewriter = cnx_attr('_rewriter')
  1386 
  1386 
  1387     # deprecated ###############################################################
  1387     # deprecated ###############################################################
  1388 
  1388 
  1389     @deprecated('[3.13] use getattr(session.rtype_eids_rdef(rtype, eidfrom, eidto), prop)')
  1389     @deprecated('[3.13] use getattr(session.rtype_eids_rdef(rtype, eidfrom, eidto), prop)')
  1390     def schema_rproperty(self, rtype, eidfrom, eidto, rprop):
  1390     def schema_rproperty(self, rtype, eidfrom, eidto, rprop):
  1435     def cnxset(self):
  1435     def cnxset(self):
  1436         """connections set, set according to transaction mode for each query"""
  1436         """connections set, set according to transaction mode for each query"""
  1437         if self.repo.shutting_down:
  1437         if self.repo.shutting_down:
  1438             self.free_cnxset(True)
  1438             self.free_cnxset(True)
  1439             raise ShuttingDown('repository is shutting down')
  1439             raise ShuttingDown('repository is shutting down')
  1440         return self._tx.cnxset
  1440         return self._cnx.cnxset
  1441 
  1441 
  1442 
  1442 
  1443 class InternalManager(object):
  1443 class InternalManager(object):
  1444     """a manager user with all access rights used internally for task such as
  1444     """a manager user with all access rights used internally for task such as
  1445     bootstrapping the repository or creating regular users according to
  1445     bootstrapping the repository or creating regular users according to