server/session.py
brancholdstable
changeset 8123 a4e667270dd4
parent 7969 d43569aaf5d6
child 7970 83075d897943
equal deleted inserted replaced
7863:d8bb8f631d41 8123:a4e667270dd4
    59     description = []
    59     description = []
    60     for term in selected:
    60     for term in selected:
    61         description.append(term.get_type(solution, args))
    61         description.append(term.get_type(solution, args))
    62     return description
    62     return description
    63 
    63 
       
    64 def selection_idx_type(i, rqlst, args):
       
    65     """try to return type of term at index `i` of the rqlst's selection"""
       
    66     for select in rqlst.children:
       
    67         term = select.selection[i]
       
    68         for solution in select.solutions:
       
    69             try:
       
    70                 ttype = term.get_type(solution, args)
       
    71                 if ttype is not None:
       
    72                     return ttype
       
    73             except CoercionError:
       
    74                 return None
       
    75 
    64 @objectify_selector
    76 @objectify_selector
    65 def is_user_session(cls, req, **kwargs):
    77 def is_user_session(cls, req, **kwargs):
    66     """repository side only selector returning 1 if the session is a regular
    78     """repository side only selector returning 1 if the session is a regular
    67     user session and not an internal session
    79     user session and not an internal session
    68     """
    80     """
   123         self.session = session
   135         self.session = session
   124         self.mode = mode
   136         self.mode = mode
   125         self.categories = categories
   137         self.categories = categories
   126 
   138 
   127     def __enter__(self):
   139     def __enter__(self):
   128         self.oldmode = self.session.set_hooks_mode(self.mode)
   140         self.oldmode, self.changes = self.session.init_hooks_mode_categories(
   129         if self.mode is self.session.HOOKS_DENY_ALL:
   141             self.mode, self.categories)
   130             self.changes = self.session.enable_hook_categories(*self.categories)
       
   131         else:
       
   132             self.changes = self.session.disable_hook_categories(*self.categories)
       
   133 
   142 
   134     def __exit__(self, exctype, exc, traceback):
   143     def __exit__(self, exctype, exc, traceback):
   135         if self.changes:
   144         self.session.reset_hooks_mode_categories(self.oldmode, self.mode, self.changes)
   136             if self.mode is self.session.HOOKS_DENY_ALL:
   145 
   137                 self.session.disable_hook_categories(*self.changes)
   146 
   138             else:
       
   139                 self.session.enable_hook_categories(*self.changes)
       
   140         self.session.set_hooks_mode(self.oldmode)
       
   141 
       
   142 INDENT = ''
       
   143 class security_enabled(object):
   147 class security_enabled(object):
   144     """context manager to control security w/ session.execute, since by
   148     """context manager to control security w/ session.execute, since by
   145     default security is disabled on queries executed on the repository
   149     default security is disabled on queries executed on the repository
   146     side.
   150     side.
   147     """
   151     """
   149         self.session = session
   153         self.session = session
   150         self.read = read
   154         self.read = read
   151         self.write = write
   155         self.write = write
   152 
   156 
   153     def __enter__(self):
   157     def __enter__(self):
   154 #        global INDENT
   158         self.oldread, self.oldwrite = self.session.init_security(
   155         if self.read is not None:
   159             self.read, self.write)
   156             self.oldread = self.session.set_read_security(self.read)
       
   157 #            print INDENT + 'read', self.read, self.oldread
       
   158         if self.write is not None:
       
   159             self.oldwrite = self.session.set_write_security(self.write)
       
   160 #            print INDENT + 'write', self.write, self.oldwrite
       
   161 #        INDENT += '  '
       
   162 
   160 
   163     def __exit__(self, exctype, exc, traceback):
   161     def __exit__(self, exctype, exc, traceback):
   164 #        global INDENT
   162         self.session.reset_security(self.oldread, self.oldwrite)
   165 #        INDENT = INDENT[:-2]
       
   166         if self.read is not None:
       
   167             self.session.set_read_security(self.oldread)
       
   168 #            print INDENT + 'reset read to', self.oldread
       
   169         if self.write is not None:
       
   170             self.session.set_write_security(self.oldwrite)
       
   171 #            print INDENT + 'reset write to', self.oldwrite
       
   172 
   163 
   173 
   164 
   174 class TransactionData(object):
   165 class TransactionData(object):
   175     def __init__(self, txid):
   166     def __init__(self, txid):
   176         self.transactionid = txid
   167         self.transactionid = txid
       
   168         self.ctx_count = 0
       
   169 
   177 
   170 
   178 class Session(RequestSessionBase):
   171 class Session(RequestSessionBase):
   179     """tie session id, user, connections pool and other session data all
   172     """Repository usersession, tie a session id, user, connections set and
   180     together
   173     other session data all together.
       
   174 
       
   175     About session storage / transactions
       
   176     ------------------------------------
       
   177 
       
   178     Here is a description of internal session attributes. Besides :attr:`data`
       
   179     and :attr:`transaction_data`, you should not have to use attributes
       
   180     described here but higher level APIs.
       
   181 
       
   182       :attr:`data` is a dictionary containing shared data, used to communicate
       
   183       extra information between the client and the repository
       
   184 
       
   185       :attr:`_tx_data` is a dictionary of :class:`TransactionData` instance, one
       
   186       for each running transaction. The key is the transaction id. By default
       
   187       the transaction id is the thread name but it can be otherwise (per dbapi
       
   188       cursor for instance, or per thread name *from another process*).
       
   189 
       
   190       :attr:`__threaddata` is a thread local storage whose `txdata` attribute
       
   191       refers to the proper instance of :class:`TransactionData` according to the
       
   192       transaction.
       
   193 
       
   194       :attr:`_threads_in_transaction` is a set of (thread, connections set)
       
   195       referencing threads that currently hold a connections set for the session.
       
   196 
       
   197     You should not have to use neither :attr:`_txdata` nor :attr:`__threaddata`,
       
   198     simply access transaction data transparently through the :attr:`_threaddata`
       
   199     property. Also, you usually don't have to access it directly since current
       
   200     transaction's data may be accessed/modified through properties / methods:
       
   201 
       
   202       :attr:`transaction_data`, similarly to :attr:`data`, is a dictionary
       
   203       containing some shared data that should be cleared at the end of the
       
   204       transaction. Hooks and operations may put arbitrary data in there, and
       
   205       this may also be used as a communication channel between the client and
       
   206       the repository.
       
   207 
       
   208       :attr:`cnxset`, the connections set to use to execute queries on sources.
       
   209       During a transaction, the connection set may be freed so that is may be
       
   210       used by another session as long as no writing is done. This means we can
       
   211       have multiple sessions with a reasonably low connections set pool size.
       
   212 
       
   213       :attr:`mode`, string telling the connections set handling mode, may be one
       
   214       of 'read' (connections set may be freed), 'write' (some write was done in
       
   215       the connections set, it can't be freed before end of the transaction),
       
   216       'transaction' (we want to keep the connections set during all the
       
   217       transaction, with or without writing)
       
   218 
       
   219       :attr:`pending_operations`, ordered list of operations to be processed on
       
   220       commit/rollback
       
   221 
       
   222       :attr:`commit_state`, describing the transaction commit state, may be one
       
   223       of None (not yet committing), 'precommit' (calling precommit event on
       
   224       operations), 'postcommit' (calling postcommit event on operations),
       
   225       'uncommitable' (some :exc:`ValidationError` or :exc:`Unauthorized` error
       
   226       has been raised during the transaction and so it must be rollbacked).
       
   227 
       
   228       :attr:`read_security` and :attr:`write_security`, boolean flags telling if
       
   229       read/write security is currently activated.
       
   230 
       
   231       :attr:`hooks_mode`, may be either `HOOKS_ALLOW_ALL` or `HOOKS_DENY_ALL`.
       
   232 
       
   233       :attr:`enabled_hook_categories`, when :attr:`hooks_mode` is
       
   234       `HOOKS_DENY_ALL`, this set contains hooks categories that are enabled.
       
   235 
       
   236       :attr:`disabled_hook_categories`, when :attr:`hooks_mode` is
       
   237       `HOOKS_ALLOW_ALL`, this set contains hooks categories that are disabled.
       
   238 
       
   239 
       
   240       :attr:`running_dbapi_query`, boolean flag telling if the executing query
       
   241       is coming from a dbapi connection or is a query from within the repository
   181     """
   242     """
   182     is_internal_session = False
   243     is_internal_session = False
   183 
   244 
   184     def __init__(self, user, repo, cnxprops=None, _id=None):
   245     def __init__(self, user, repo, cnxprops=None, _id=None):
   185         super(Session, self).__init__(repo.vreg)
   246         super(Session, self).__init__(repo.vreg)
   244 
   305 
   245     def hijack_user(self, user):
   306     def hijack_user(self, user):
   246         """return a fake request/session using specified user"""
   307         """return a fake request/session using specified user"""
   247         session = Session(user, self.repo)
   308         session = Session(user, self.repo)
   248         threaddata = session._threaddata
   309         threaddata = session._threaddata
   249         threaddata.pool = self.pool
   310         threaddata.cnxset = self.cnxset
       
   311         # we attributed a connections set, need to update ctx_count else it will be freed
       
   312         # while undesired
       
   313         threaddata.ctx_count = 1
   250         # share pending_operations, else operation added in the hi-jacked
   314         # share pending_operations, else operation added in the hi-jacked
   251         # session such as SendMailOp won't ever be processed
   315         # session such as SendMailOp won't ever be processed
   252         threaddata.pending_operations = self.pending_operations
   316         threaddata.pending_operations = self.pending_operations
   253         # everything in transaction_data should be copied back but the entity
   317         # everything in transaction_data should be copied back but the entity
   254         # type cache we don't want to avoid security pb
   318         # type cache we don't want to avoid security pb
   386 
   450 
   387     def system_sql(self, sql, args=None, rollback_on_failure=True):
   451     def system_sql(self, sql, args=None, rollback_on_failure=True):
   388         """return a sql cursor on the system database"""
   452         """return a sql cursor on the system database"""
   389         if sql.split(None, 1)[0].upper() != 'SELECT':
   453         if sql.split(None, 1)[0].upper() != 'SELECT':
   390             self.mode = 'write'
   454             self.mode = 'write'
   391         source = self.pool.source('system')
   455         source = self.cnxset.source('system')
   392         try:
   456         try:
   393             return source.doexec(self, sql, args, rollback=rollback_on_failure)
   457             return source.doexec(self, sql, args, rollback=rollback_on_failure)
   394         except (source.OperationalError, source.InterfaceError):
   458         except (source.OperationalError, source.InterfaceError):
   395             if not rollback_on_failure:
   459             if not rollback_on_failure:
   396                 raise
   460                 raise
   397             source.warning("trying to reconnect")
   461             source.warning("trying to reconnect")
   398             self.pool.reconnect(source)
   462             self.cnxset.reconnect(source)
   399             return source.doexec(self, sql, args, rollback=rollback_on_failure)
   463             return source.doexec(self, sql, args, rollback=rollback_on_failure)
   400 
   464 
   401     def set_language(self, language):
   465     def set_language(self, language):
   402         """i18n configuration for translation"""
   466         """i18n configuration for translation"""
   403         language = language or self.user.property_value('ui.language')
   467         language = language or self.user.property_value('ui.language')
   443 
   507 
   444     DEFAULT_SECURITY = object() # evaluated to true by design
   508     DEFAULT_SECURITY = object() # evaluated to true by design
   445 
   509 
   446     def security_enabled(self, read=False, write=False):
   510     def security_enabled(self, read=False, write=False):
   447         return security_enabled(self, read=read, write=write)
   511         return security_enabled(self, read=read, write=write)
       
   512 
       
   513     def init_security(self, read, write):
       
   514         if read is None:
       
   515             oldread = None
       
   516         else:
       
   517             oldread = self.set_read_security(read)
       
   518         if write is None:
       
   519             oldwrite = None
       
   520         else:
       
   521             oldwrite = self.set_write_security(write)
       
   522         self._threaddata.ctx_count += 1
       
   523         return oldread, oldwrite
       
   524 
       
   525     def reset_security(self, read, write):
       
   526         txstore = self._threaddata
       
   527         txstore.ctx_count -= 1
       
   528         if txstore.ctx_count == 0:
       
   529             self._clear_thread_storage(txstore)
       
   530         else:
       
   531             if read is not None:
       
   532                 self.set_read_security(read)
       
   533             if write is not None:
       
   534                 self.set_write_security(write)
   448 
   535 
   449     @property
   536     @property
   450     def read_security(self):
   537     def read_security(self):
   451         """return a boolean telling if read security is activated or not"""
   538         """return a boolean telling if read security is activated or not"""
   452         txstore = self._threaddata
   539         txstore = self._threaddata
   496         txstore = self._threaddata
   583         txstore = self._threaddata
   497         if txstore is None:
   584         if txstore is None:
   498             return self.DEFAULT_SECURITY
   585             return self.DEFAULT_SECURITY
   499         try:
   586         try:
   500             return txstore.write_security
   587             return txstore.write_security
   501         except:
   588         except AttributeError:
   502             txstore.write_security = self.DEFAULT_SECURITY
   589             txstore.write_security = self.DEFAULT_SECURITY
   503             return txstore.write_security
   590             return txstore.write_security
   504 
   591 
   505     def set_write_security(self, activated):
   592     def set_write_security(self, activated):
   506         """[de]activate write security, returning the previous value set for
   593         """[de]activate write security, returning the previous value set for
   544         assert mode is self.HOOKS_ALLOW_ALL or mode is self.HOOKS_DENY_ALL
   631         assert mode is self.HOOKS_ALLOW_ALL or mode is self.HOOKS_DENY_ALL
   545         oldmode = getattr(self._threaddata, 'hooks_mode', self.HOOKS_ALLOW_ALL)
   632         oldmode = getattr(self._threaddata, 'hooks_mode', self.HOOKS_ALLOW_ALL)
   546         self._threaddata.hooks_mode = mode
   633         self._threaddata.hooks_mode = mode
   547         return oldmode
   634         return oldmode
   548 
   635 
       
   636     def init_hooks_mode_categories(self, mode, categories):
       
   637         oldmode = self.set_hooks_mode(mode)
       
   638         if mode is self.HOOKS_DENY_ALL:
       
   639             changes = self.enable_hook_categories(*categories)
       
   640         else:
       
   641             changes = self.disable_hook_categories(*categories)
       
   642         self._threaddata.ctx_count += 1
       
   643         return oldmode, changes
       
   644 
       
   645     def reset_hooks_mode_categories(self, oldmode, mode, categories):
       
   646         txstore = self._threaddata
       
   647         txstore.ctx_count -= 1
       
   648         if txstore.ctx_count == 0:
       
   649             self._clear_thread_storage(txstore)
       
   650         else:
       
   651             try:
       
   652                 if categories:
       
   653                     if mode is self.HOOKS_DENY_ALL:
       
   654                         return self.disable_hook_categories(*categories)
       
   655                     else:
       
   656                         return self.enable_hook_categories(*categories)
       
   657             finally:
       
   658                 self.set_hooks_mode(oldmode)
       
   659 
   549     @property
   660     @property
   550     def disabled_hook_categories(self):
   661     def disabled_hook_categories(self):
   551         try:
   662         try:
   552             return getattr(self._threaddata, 'disabled_hook_cats')
   663             return getattr(self._threaddata, 'disabled_hook_cats')
   553         except AttributeError:
   664         except AttributeError:
   567 
   678 
   568         - on HOOKS_DENY_ALL mode, ensure those categories are not enabled
   679         - on HOOKS_DENY_ALL mode, ensure those categories are not enabled
   569         - on HOOKS_ALLOW_ALL mode, ensure those categories are disabled
   680         - on HOOKS_ALLOW_ALL mode, ensure those categories are disabled
   570         """
   681         """
   571         changes = set()
   682         changes = set()
       
   683         self.pruned_hooks_cache.clear()
   572         if self.hooks_mode is self.HOOKS_DENY_ALL:
   684         if self.hooks_mode is self.HOOKS_DENY_ALL:
   573             enablecats = self.enabled_hook_categories
   685             enabledcats = self.enabled_hook_categories
   574             for category in categories:
   686             for category in categories:
   575                 if category in enablecats:
   687                 if category in enabledcats:
   576                     enablecats.remove(category)
   688                     enabledcats.remove(category)
   577                     changes.add(category)
   689                     changes.add(category)
   578         else:
   690         else:
   579             disablecats = self.disabled_hook_categories
   691             disabledcats = self.disabled_hook_categories
   580             for category in categories:
   692             for category in categories:
   581                 if category not in disablecats:
   693                 if category not in disabledcats:
   582                     disablecats.add(category)
   694                     disabledcats.add(category)
   583                     changes.add(category)
   695                     changes.add(category)
   584         return tuple(changes)
   696         return tuple(changes)
   585 
   697 
   586     def enable_hook_categories(self, *categories):
   698     def enable_hook_categories(self, *categories):
   587         """enable the given hook categories:
   699         """enable the given hook categories:
   588 
   700 
   589         - on HOOKS_DENY_ALL mode, ensure those categories are enabled
   701         - on HOOKS_DENY_ALL mode, ensure those categories are enabled
   590         - on HOOKS_ALLOW_ALL mode, ensure those categories are not disabled
   702         - on HOOKS_ALLOW_ALL mode, ensure those categories are not disabled
   591         """
   703         """
   592         changes = set()
   704         changes = set()
       
   705         self.pruned_hooks_cache.clear()
   593         if self.hooks_mode is self.HOOKS_DENY_ALL:
   706         if self.hooks_mode is self.HOOKS_DENY_ALL:
   594             enablecats = self.enabled_hook_categories
   707             enabledcats = self.enabled_hook_categories
   595             for category in categories:
   708             for category in categories:
   596                 if category not in enablecats:
   709                 if category not in enabledcats:
   597                     enablecats.add(category)
   710                     enabledcats.add(category)
   598                     changes.add(category)
   711                     changes.add(category)
   599         else:
   712         else:
   600             disablecats = self.disabled_hook_categories
   713             disabledcats = self.disabled_hook_categories
   601             for category in categories:
   714             for category in categories:
   602                 if category in self.disabled_hook_categories:
   715                 if category in disabledcats:
   603                     disablecats.remove(category)
   716                     disabledcats.remove(category)
   604                     changes.add(category)
   717                     changes.add(category)
   605         return tuple(changes)
   718         return tuple(changes)
   606 
   719 
   607     def is_hook_category_activated(self, category):
   720     def is_hook_category_activated(self, category):
   608         """return a boolean telling if the given category is currently activated
   721         """return a boolean telling if the given category is currently activated
   618         """
   731         """
   619         return self.is_hook_category_activated(hook.category)
   732         return self.is_hook_category_activated(hook.category)
   620 
   733 
   621     # connection management ###################################################
   734     # connection management ###################################################
   622 
   735 
   623     def keep_pool_mode(self, mode):
   736     def keep_cnxset_mode(self, mode):
   624         """set pool_mode, e.g. how the session will keep its pool:
   737         """set `mode`, e.g. how the session will keep its connections set:
   625 
   738 
   626         * if mode == 'write', the pool is freed after each ready query, but kept
   739         * if mode == 'write', the connections set is freed after each ready
   627           until the transaction's end (eg commit or rollback) when a write query
   740           query, but kept until the transaction's end (eg commit or rollback)
   628           is detected (eg INSERT/SET/DELETE queries)
   741           when a write query is detected (eg INSERT/SET/DELETE queries)
   629 
   742 
   630         * if mode == 'transaction', the pool is only freed after the
   743         * if mode == 'transaction', the connections set is only freed after the
   631           transaction's end
   744           transaction's end
   632 
   745 
   633         notice that a repository has a limited set of pools, and a session has to
   746         notice that a repository has a limited set of connections sets, and a
   634         wait for a free pool to run any rql query (unless it already has a pool
   747         session has to wait for a free connections set to run any rql query
   635         set).
   748         (unless it already has one set).
   636         """
   749         """
   637         assert mode in ('transaction', 'write')
   750         assert mode in ('transaction', 'write')
   638         if mode == 'transaction':
   751         if mode == 'transaction':
   639             self.default_mode = 'transaction'
   752             self.default_mode = 'transaction'
   640         else: # mode == 'write'
   753         else: # mode == 'write'
   653     def set_commit_state(self, value):
   766     def set_commit_state(self, value):
   654         self._threaddata.commit_state = value
   767         self._threaddata.commit_state = value
   655     commit_state = property(get_commit_state, set_commit_state)
   768     commit_state = property(get_commit_state, set_commit_state)
   656 
   769 
   657     @property
   770     @property
   658     def pool(self):
   771     def cnxset(self):
   659         """connections pool, set according to transaction mode for each query"""
   772         """connections set, set according to transaction mode for each query"""
   660         if self._closed:
   773         if self._closed:
   661             self.reset_pool(True)
   774             self.free_cnxset(True)
   662             raise Exception('try to access pool on a closed session')
   775             raise Exception('try to access connections set on a closed session %s' % self.id)
   663         return getattr(self._threaddata, 'pool', None)
   776         return getattr(self._threaddata, 'cnxset', None)
   664 
   777 
   665     def set_pool(self):
   778     def set_cnxset(self):
   666         """the session need a pool to execute some queries"""
   779         """the session need a connections set to execute some queries"""
   667         with self._closed_lock:
   780         with self._closed_lock:
   668             if self._closed:
   781             if self._closed:
   669                 self.reset_pool(True)
   782                 self.free_cnxset(True)
   670                 raise Exception('try to set pool on a closed session')
   783                 raise Exception('try to set connections set on a closed session %s' % self.id)
   671             if self.pool is None:
   784             if self.cnxset is None:
   672                 # get pool first to avoid race-condition
   785                 # get connections set first to avoid race-condition
   673                 self._threaddata.pool = pool = self.repo._get_pool()
   786                 self._threaddata.cnxset = cnxset = self.repo._get_cnxset()
       
   787                 self._threaddata.ctx_count += 1
   674                 try:
   788                 try:
   675                     pool.pool_set()
   789                     cnxset.cnxset_set()
   676                 except:
   790                 except Exception:
   677                     self._threaddata.pool = None
   791                     self._threaddata.cnxset = None
   678                     self.repo._free_pool(pool)
   792                     self.repo._free_cnxset(cnxset)
   679                     raise
   793                     raise
   680                 self._threads_in_transaction.add(
   794                 self._threads_in_transaction.add(
   681                     (threading.currentThread(), pool) )
   795                     (threading.currentThread(), cnxset) )
   682             return self._threaddata.pool
   796             return self._threaddata.cnxset
   683 
   797 
   684     def _free_thread_pool(self, thread, pool, force_close=False):
   798     def _free_thread_cnxset(self, thread, cnxset, force_close=False):
   685         try:
   799         try:
   686             self._threads_in_transaction.remove( (thread, pool) )
   800             self._threads_in_transaction.remove( (thread, cnxset) )
   687         except KeyError:
   801         except KeyError:
   688             # race condition on pool freeing (freed by commit or rollback vs
   802             # race condition on cnxset freeing (freed by commit or rollback vs
   689             # close)
   803             # close)
   690             pass
   804             pass
   691         else:
   805         else:
   692             if force_close:
   806             if force_close:
   693                 pool.reconnect()
   807                 cnxset.reconnect()
   694             else:
   808             else:
   695                 pool.pool_reset()
   809                 cnxset.cnxset_freed()
   696             # free pool once everything is done to avoid race-condition
   810             # free cnxset once everything is done to avoid race-condition
   697             self.repo._free_pool(pool)
   811             self.repo._free_cnxset(cnxset)
   698 
   812 
   699     def reset_pool(self, ignoremode=False):
   813     def free_cnxset(self, ignoremode=False):
   700         """the session is no longer using its pool, at least for some time"""
   814         """the session is no longer using its connections set, at least for some time"""
   701         # pool may be none if no operation has been done since last commit
   815         # cnxset may be none if no operation has been done since last commit
   702         # or rollback
   816         # or rollback
   703         pool = getattr(self._threaddata, 'pool', None)
   817         cnxset = getattr(self._threaddata, 'cnxset', None)
   704         if pool is not None and (ignoremode or self.mode == 'read'):
   818         if cnxset is not None and (ignoremode or self.mode == 'read'):
   705             # even in read mode, we must release the current transaction
   819             # even in read mode, we must release the current transaction
   706             self._free_thread_pool(threading.currentThread(), pool)
   820             self._free_thread_cnxset(threading.currentThread(), cnxset)
   707             del self._threaddata.pool
   821             del self._threaddata.cnxset
       
   822             self._threaddata.ctx_count -= 1
   708 
   823 
   709     def _touch(self):
   824     def _touch(self):
   710         """update latest session usage timestamp and reset mode to read"""
   825         """update latest session usage timestamp and reset mode to read"""
   711         self.timestamp = time()
   826         self.timestamp = time()
   712         self.local_perm_cache.clear() # XXX simply move in transaction_data, no?
   827         self.local_perm_cache.clear() # XXX simply move in transaction_data, no?
   768         return 'view'
   883         return 'view'
   769 
   884 
   770     def source_defs(self):
   885     def source_defs(self):
   771         return self.repo.source_defs()
   886         return self.repo.source_defs()
   772 
   887 
   773     def describe(self, eid):
   888     def describe(self, eid, asdict=False):
   774         """return a tuple (type, sourceuri, extid) for the entity with id <eid>"""
   889         """return a tuple (type, sourceuri, extid) for the entity with id <eid>"""
   775         return self.repo.type_and_source_from_eid(eid, self)
   890         metas = self.repo.type_and_source_from_eid(eid, self)
       
   891         if asdict:
       
   892             return dict(zip(('type', 'source', 'extid', 'asource'), metas)) 
       
   893        # XXX :-1 for cw compat, use asdict=True for full information
       
   894         return metas[:-1]
   776 
   895 
   777     # db-api like interface ###################################################
   896     # db-api like interface ###################################################
   778 
   897 
   779     def source_from_eid(self, eid):
   898     def source_from_eid(self, eid):
   780         """return the source where the entity with id <eid> is located"""
   899         """return the source where the entity with id <eid> is located"""
   791         self.timestamp = time() # update timestamp
   910         self.timestamp = time() # update timestamp
   792         rset = self._execute(self, rql, kwargs, build_descr)
   911         rset = self._execute(self, rql, kwargs, build_descr)
   793         rset.req = self
   912         rset.req = self
   794         return rset
   913         return rset
   795 
   914 
   796     def _clear_thread_data(self, reset_pool=True):
   915     def _clear_thread_data(self, free_cnxset=True):
   797         """remove everything from the thread local storage, except pool
   916         """remove everything from the thread local storage, except connections set
   798         which is explicitly removed by reset_pool, and mode which is set anyway
   917         which is explicitly removed by free_cnxset, and mode which is set anyway
   799         by _touch
   918         by _touch
   800         """
   919         """
   801         try:
   920         try:
   802             txstore = self.__threaddata.txdata
   921             txstore = self.__threaddata.txdata
   803         except AttributeError:
   922         except AttributeError:
   804             pass
   923             pass
   805         else:
   924         else:
   806             if reset_pool:
   925             if free_cnxset:
   807                 self._tx_data.pop(txstore.transactionid, None)
   926                 self.free_cnxset()
   808                 try:
   927                 if txstore.ctx_count == 0:
   809                     del self.__threaddata.txdata
   928                     self._clear_thread_storage(txstore)
   810                 except AttributeError:
   929                 else:
   811                     pass
   930                     self._clear_tx_storage(txstore)
   812             else:
   931             else:
   813                 for name in ('commit_state', 'transaction_data',
   932                 self._clear_tx_storage(txstore)
   814                              'pending_operations', '_rewriter'):
   933 
   815                     try:
   934     def _clear_thread_storage(self, txstore):
   816                         delattr(txstore, name)
   935         self._tx_data.pop(txstore.transactionid, None)
   817                     except AttributeError:
   936         try:
   818                         continue
   937             del self.__threaddata.txdata
   819 
   938         except AttributeError:
   820     def commit(self, reset_pool=True):
   939             pass
       
   940 
       
   941     def _clear_tx_storage(self, txstore):
       
   942         for name in ('commit_state', 'transaction_data',
       
   943                      'pending_operations', '_rewriter',
       
   944                      'pruned_hooks_cache'):
       
   945             try:
       
   946                 delattr(txstore, name)
       
   947             except AttributeError:
       
   948                 continue
       
   949 
       
   950     def commit(self, free_cnxset=True, reset_pool=None):
   821         """commit the current session's transaction"""
   951         """commit the current session's transaction"""
   822         if self.pool is None:
   952         if reset_pool is not None:
       
   953             warn('[3.13] use free_cnxset argument instead for reset_pool',
       
   954                  DeprecationWarning, stacklevel=2)
       
   955             free_cnxset = reset_pool
       
   956         if self.cnxset is None:
   823             assert not self.pending_operations
   957             assert not self.pending_operations
   824             self._clear_thread_data()
   958             self._clear_thread_data()
   825             self._touch()
   959             self._touch()
   826             self.debug('commit session %s done (no db activity)', self.id)
   960             self.debug('commit session %s done (no db activity)', self.id)
   827             return
   961             return
   845                         operation.processed = 'precommit'
   979                         operation.processed = 'precommit'
   846                         processed.append(operation)
   980                         processed.append(operation)
   847                         operation.handle_event('precommit_event')
   981                         operation.handle_event('precommit_event')
   848                     self.pending_operations[:] = processed
   982                     self.pending_operations[:] = processed
   849                     self.debug('precommit session %s done', self.id)
   983                     self.debug('precommit session %s done', self.id)
   850                 except:
   984                 except BaseException:
   851                     # if error on [pre]commit:
   985                     # if error on [pre]commit:
   852                     #
   986                     #
   853                     # * set .failed = True on the operation causing the failure
   987                     # * set .failed = True on the operation causing the failure
   854                     # * call revert<event>_event on processed operations
   988                     # * call revert<event>_event on processed operations
   855                     # * call rollback_event on *all* operations
   989                     # * call rollback_event on *all* operations
   860                     # and revertcommit, that will be enough in mont case.
   994                     # and revertcommit, that will be enough in mont case.
   861                     operation.failed = True
   995                     operation.failed = True
   862                     for operation in reversed(processed):
   996                     for operation in reversed(processed):
   863                         try:
   997                         try:
   864                             operation.handle_event('revertprecommit_event')
   998                             operation.handle_event('revertprecommit_event')
   865                         except:
   999                         except BaseException:
   866                             self.critical('error while reverting precommit',
  1000                             self.critical('error while reverting precommit',
   867                                           exc_info=True)
  1001                                           exc_info=True)
   868                     # XXX use slice notation since self.pending_operations is a
  1002                     # XXX use slice notation since self.pending_operations is a
   869                     # read-only property.
  1003                     # read-only property.
   870                     self.pending_operations[:] = processed + self.pending_operations
  1004                     self.pending_operations[:] = processed + self.pending_operations
   871                     self.rollback(reset_pool)
  1005                     self.rollback(free_cnxset)
   872                     raise
  1006                     raise
   873                 self.pool.commit()
  1007                 self.cnxset.commit()
   874                 self.commit_state = 'postcommit'
  1008                 self.commit_state = 'postcommit'
   875                 while self.pending_operations:
  1009                 while self.pending_operations:
   876                     operation = self.pending_operations.pop(0)
  1010                     operation = self.pending_operations.pop(0)
   877                     operation.processed = 'postcommit'
  1011                     operation.processed = 'postcommit'
   878                     try:
  1012                     try:
   879                         operation.handle_event('postcommit_event')
  1013                         operation.handle_event('postcommit_event')
   880                     except:
  1014                     except BaseException:
   881                         self.critical('error while postcommit',
  1015                         self.critical('error while postcommit',
   882                                       exc_info=sys.exc_info())
  1016                                       exc_info=sys.exc_info())
   883                 self.debug('postcommit session %s done', self.id)
  1017                 self.debug('postcommit session %s done', self.id)
   884                 return self.transaction_uuid(set=False)
  1018                 return self.transaction_uuid(set=False)
   885         finally:
  1019         finally:
   886             self._touch()
  1020             self._touch()
   887             if reset_pool:
  1021             if free_cnxset:
   888                 self.reset_pool(ignoremode=True)
  1022                 self.free_cnxset(ignoremode=True)
   889             self._clear_thread_data(reset_pool)
  1023             self._clear_thread_data(free_cnxset)
   890 
  1024 
   891     def rollback(self, reset_pool=True):
  1025     def rollback(self, free_cnxset=True, reset_pool=None):
   892         """rollback the current session's transaction"""
  1026         """rollback the current session's transaction"""
   893         # don't use self.pool, rollback may be called with _closed == True
  1027         if reset_pool is not None:
   894         pool = getattr(self._threaddata, 'pool', None)
  1028             warn('[3.13] use free_cnxset argument instead for reset_pool',
   895         if pool is None:
  1029                  DeprecationWarning, stacklevel=2)
       
  1030             free_cnxset = reset_pool
       
  1031         # don't use self.cnxset, rollback may be called with _closed == True
       
  1032         cnxset = getattr(self._threaddata, 'cnxset', None)
       
  1033         if cnxset is None:
   896             self._clear_thread_data()
  1034             self._clear_thread_data()
   897             self._touch()
  1035             self._touch()
   898             self.debug('rollback session %s done (no db activity)', self.id)
  1036             self.debug('rollback session %s done (no db activity)', self.id)
   899             return
  1037             return
   900         try:
  1038         try:
   902             with security_enabled(self, False, False):
  1040             with security_enabled(self, False, False):
   903                 while self.pending_operations:
  1041                 while self.pending_operations:
   904                     try:
  1042                     try:
   905                         operation = self.pending_operations.pop(0)
  1043                         operation = self.pending_operations.pop(0)
   906                         operation.handle_event('rollback_event')
  1044                         operation.handle_event('rollback_event')
   907                     except:
  1045                     except BaseException:
   908                         self.critical('rollback error', exc_info=sys.exc_info())
  1046                         self.critical('rollback error', exc_info=sys.exc_info())
   909                         continue
  1047                         continue
   910                 pool.rollback()
  1048                 cnxset.rollback()
   911                 self.debug('rollback for session %s done', self.id)
  1049                 self.debug('rollback for session %s done', self.id)
   912         finally:
  1050         finally:
   913             self._touch()
  1051             self._touch()
   914             if reset_pool:
  1052             if free_cnxset:
   915                 self.reset_pool(ignoremode=True)
  1053                 self.free_cnxset(ignoremode=True)
   916             self._clear_thread_data(reset_pool)
  1054             self._clear_thread_data(free_cnxset)
   917 
  1055 
   918     def close(self):
  1056     def close(self):
   919         """do not close pool on session close, since they are shared now"""
  1057         """do not close connections set on session close, since they are shared now"""
   920         with self._closed_lock:
  1058         with self._closed_lock:
   921             self._closed = True
  1059             self._closed = True
   922         # copy since _threads_in_transaction maybe modified while waiting
  1060         # copy since _threads_in_transaction maybe modified while waiting
   923         for thread, pool in self._threads_in_transaction.copy():
  1061         for thread, cnxset in self._threads_in_transaction.copy():
   924             if thread is threading.currentThread():
  1062             if thread is threading.currentThread():
   925                 continue
  1063                 continue
   926             self.info('waiting for thread %s', thread)
  1064             self.info('waiting for thread %s', thread)
   927             # do this loop/break instead of a simple join(10) in case thread is
  1065             # do this loop/break instead of a simple join(10) in case thread is
   928             # the main thread (in which case it will be removed from
  1066             # the main thread (in which case it will be removed from
   929             # self._threads_in_transaction but still be alive...)
  1067             # self._threads_in_transaction but still be alive...)
   930             for i in xrange(10):
  1068             for i in xrange(10):
   931                 thread.join(1)
  1069                 thread.join(1)
   932                 if not (thread.isAlive() and
  1070                 if not (thread.isAlive() and
   933                         (thread, pool) in self._threads_in_transaction):
  1071                         (thread, cnxset) in self._threads_in_transaction):
   934                     break
  1072                     break
   935             else:
  1073             else:
   936                 self.error('thread %s still alive after 10 seconds, will close '
  1074                 self.error('thread %s still alive after 10 seconds, will close '
   937                            'session anyway', thread)
  1075                            'session anyway', thread)
   938                 self._free_thread_pool(thread, pool, force_close=True)
  1076                 self._free_thread_cnxset(thread, cnxset, force_close=True)
   939         self.rollback()
  1077         self.rollback()
   940         del self.__threaddata
  1078         del self.__threaddata
   941         del self._tx_data
  1079         del self._tx_data
   942 
  1080 
   943     @property
  1081     @property
   960             return self._threaddata.pending_operations
  1098             return self._threaddata.pending_operations
   961         except AttributeError:
  1099         except AttributeError:
   962             self._threaddata.pending_operations = []
  1100             self._threaddata.pending_operations = []
   963             return self._threaddata.pending_operations
  1101             return self._threaddata.pending_operations
   964 
  1102 
       
  1103     @property
       
  1104     def pruned_hooks_cache(self):
       
  1105         try:
       
  1106             return self._threaddata.pruned_hooks_cache
       
  1107         except AttributeError:
       
  1108             self._threaddata.pruned_hooks_cache = {}
       
  1109             return self._threaddata.pruned_hooks_cache
       
  1110 
   965     def add_operation(self, operation, index=None):
  1111     def add_operation(self, operation, index=None):
   966         """add an observer"""
  1112         """add an operation"""
   967         assert self.commit_state != 'commit'
       
   968         if index is None:
  1113         if index is None:
   969             self.pending_operations.append(operation)
  1114             self.pending_operations.append(operation)
   970         else:
  1115         else:
   971             self.pending_operations.insert(index, operation)
  1116             self.pending_operations.insert(index, operation)
   972 
  1117 
  1021         # not so easy, looks for variable which changes from one solution
  1166         # not so easy, looks for variable which changes from one solution
  1022         # to another
  1167         # to another
  1023         unstables = rqlst.get_variable_indices()
  1168         unstables = rqlst.get_variable_indices()
  1024         basedescr = []
  1169         basedescr = []
  1025         todetermine = []
  1170         todetermine = []
  1026         sampleselect = rqlst.children[0]
  1171         for i in xrange(len(rqlst.children[0].selection)):
  1027         samplesols = sampleselect.solutions[0]
  1172             ttype = selection_idx_type(i, rqlst, args)
  1028         for i, term in enumerate(sampleselect.selection):
  1173             if ttype is None or ttype == 'Any':
  1029             try:
       
  1030                 ttype = term.get_type(samplesols, args)
       
  1031             except CoercionError:
       
  1032                 ttype = None
  1174                 ttype = None
  1033                 isfinal = True
  1175                 isfinal = True
  1034             else:
  1176             else:
  1035                 if ttype is None or ttype == 'Any':
  1177                 isfinal = ttype in BASE_TYPES
  1036                     ttype = None
       
  1037                     isfinal = True
       
  1038                 else:
       
  1039                     isfinal = ttype in BASE_TYPES
       
  1040             if ttype is None or i in unstables:
  1178             if ttype is None or i in unstables:
  1041                 basedescr.append(None)
  1179                 basedescr.append(None)
  1042                 todetermine.append( (i, isfinal) )
  1180                 todetermine.append( (i, isfinal) )
  1043             else:
  1181             else:
  1044                 basedescr.append(ttype)
  1182                 basedescr.append(ttype)
  1047         return self._build_descr(result, basedescr, todetermine)
  1185         return self._build_descr(result, basedescr, todetermine)
  1048 
  1186 
  1049     def _build_descr(self, result, basedescription, todetermine):
  1187     def _build_descr(self, result, basedescription, todetermine):
  1050         description = []
  1188         description = []
  1051         etype_from_eid = self.describe
  1189         etype_from_eid = self.describe
  1052         for row in result:
  1190         todel = []
       
  1191         for i, row in enumerate(result):
  1053             row_descr = basedescription[:]
  1192             row_descr = basedescription[:]
  1054             for index, isfinal in todetermine:
  1193             for index, isfinal in todetermine:
  1055                 value = row[index]
  1194                 value = row[index]
  1056                 if value is None:
  1195                 if value is None:
  1057                     # None value inserted by an outer join, no type
  1196                     # None value inserted by an outer join, no type
  1061                     row_descr[index] = etype_from_pyobj(value)
  1200                     row_descr[index] = etype_from_pyobj(value)
  1062                 else:
  1201                 else:
  1063                     try:
  1202                     try:
  1064                         row_descr[index] = etype_from_eid(value)[0]
  1203                         row_descr[index] = etype_from_eid(value)[0]
  1065                     except UnknownEid:
  1204                     except UnknownEid:
  1066                         self.critical('wrong eid %s in repository, you should '
  1205                         self.error('wrong eid %s in repository, you should '
  1067                                       'db-check the database' % value)
  1206                                    'db-check the database' % value)
  1068                         row_descr[index] = row[index] = None
  1207                         todel.append(i)
  1069             description.append(tuple(row_descr))
  1208                         break
       
  1209             else:
       
  1210                 description.append(tuple(row_descr))
       
  1211         for i in reversed(todel):
       
  1212             del result[i]
  1070         return description
  1213         return description
  1071 
  1214 
  1072     # deprecated ###############################################################
  1215     # deprecated ###############################################################
  1073 
  1216 
  1074     @deprecated('[3.13] use getattr(session.rtype_eids_rdef(rtype, eidfrom, eidto), prop)')
  1217     @deprecated('[3.13] use getattr(session.rtype_eids_rdef(rtype, eidfrom, eidto), prop)')
  1075     def schema_rproperty(self, rtype, eidfrom, eidto, rprop):
  1218     def schema_rproperty(self, rtype, eidfrom, eidto, rprop):
  1076         return getattr(self.rtype_eids_rdef(rtype, eidfrom, eidto), rprop)
  1219         return getattr(self.rtype_eids_rdef(rtype, eidfrom, eidto), rprop)
  1077 
  1220 
       
  1221     @property
       
  1222     @deprecated("[3.13] use .cnxset attribute instead of .pool")
       
  1223     def pool(self):
       
  1224         return self.cnxset
       
  1225 
       
  1226     @deprecated("[3.13] use .set_cnxset() method instead of .set_pool()")
       
  1227     def set_pool(self):
       
  1228         return self.set_cnxset()
       
  1229 
       
  1230     @deprecated("[3.13] use .free_cnxset() method instead of .reset_pool()")
       
  1231     def reset_pool(self):
       
  1232         return self.free_cnxset()
  1078 
  1233 
  1079     @deprecated("[3.7] execute is now unsafe by default in hooks/operation. You"
  1234     @deprecated("[3.7] execute is now unsafe by default in hooks/operation. You"
  1080                 " can also control security with the security_enabled context "
  1235                 " can also control security with the security_enabled context "
  1081                 "manager")
  1236                 "manager")
  1082     def unsafe_execute(self, rql, kwargs=None, eid_key=None, build_descr=True,
  1237     def unsafe_execute(self, rql, kwargs=None, eid_key=None, build_descr=True,
  1131 class InternalSession(Session):
  1286 class InternalSession(Session):
  1132     """special session created internaly by the repository"""
  1287     """special session created internaly by the repository"""
  1133     is_internal_session = True
  1288     is_internal_session = True
  1134     running_dbapi_query = False
  1289     running_dbapi_query = False
  1135 
  1290 
  1136     def __init__(self, repo, cnxprops=None):
  1291     def __init__(self, repo, cnxprops=None, safe=False):
  1137         super(InternalSession, self).__init__(InternalManager(), repo, cnxprops,
  1292         super(InternalSession, self).__init__(InternalManager(), repo, cnxprops,
  1138                                               _id='internal')
  1293                                               _id='internal')
  1139         self.user._cw = self # XXX remove when "vreg = user._cw.vreg" hack in entity.py is gone
  1294         self.user._cw = self # XXX remove when "vreg = user._cw.vreg" hack in entity.py is gone
  1140         self.cnxtype = 'inmemory'
  1295         self.cnxtype = 'inmemory'
  1141         self.disable_hook_categories('integrity')
  1296         if not safe:
  1142 
  1297             self.disable_hook_categories('integrity')
  1143     @property
  1298 
  1144     def pool(self):
  1299     @property
  1145         """connections pool, set according to transaction mode for each query"""
  1300     def cnxset(self):
       
  1301         """connections set, set according to transaction mode for each query"""
  1146         if self.repo.shutting_down:
  1302         if self.repo.shutting_down:
  1147             self.reset_pool(True)
  1303             self.free_cnxset(True)
  1148             raise ShuttingDown('repository is shutting down')
  1304             raise ShuttingDown('repository is shutting down')
  1149         return getattr(self._threaddata, 'pool', None)
  1305         return getattr(self._threaddata, 'cnxset', None)
  1150 
  1306 
  1151 
  1307 
  1152 class InternalManager(object):
  1308 class InternalManager(object):
  1153     """a manager user with all access rights used internally for task such as
  1309     """a manager user with all access rights used internally for task such as
  1154     bootstrapping the repository or creating regular users according to
  1310     bootstrapping the repository or creating regular users according to