115 if self.write is not None: |
115 if self.write is not None: |
116 self.session.set_write_security(self.oldwrite) |
116 self.session.set_write_security(self.oldwrite) |
117 # print INDENT + 'reset write to', self.oldwrite |
117 # print INDENT + 'reset write to', self.oldwrite |
118 |
118 |
119 |
119 |
|
120 class TransactionData(object): |
|
121 def __init__(self, txid): |
|
122 self.transactionid = txid |
120 |
123 |
121 class Session(RequestSessionBase): |
124 class Session(RequestSessionBase): |
122 """tie session id, user, connections pool and other session data all |
125 """tie session id, user, connections pool and other session data all |
123 together |
126 together |
124 """ |
127 """ |
146 # and the rql server |
149 # and the rql server |
147 self.data = {} |
150 self.data = {} |
148 # i18n initialization |
151 # i18n initialization |
149 self.set_language(cnxprops.lang) |
152 self.set_language(cnxprops.lang) |
150 # internals |
153 # internals |
151 self._threaddata = threading.local() |
154 self._tx_data = {} |
|
155 self.__threaddata = threading.local() |
152 self._threads_in_transaction = set() |
156 self._threads_in_transaction = set() |
153 self._closed = False |
157 self._closed = False |
154 |
158 |
155 def __unicode__(self): |
159 def __unicode__(self): |
156 return '<%ssession %s (%s 0x%x)>' % ( |
160 return '<%ssession %s (%s 0x%x)>' % ( |
157 self.cnxtype, unicode(self.user.login), self.id, id(self)) |
161 self.cnxtype, unicode(self.user.login), self.id, id(self)) |
|
162 |
|
163 def set_tx_data(self, txid=None): |
|
164 if txid is None: |
|
165 txid = threading.currentThread().getName() |
|
166 try: |
|
167 self.__threaddata.txdata = self._tx_data[txid] |
|
168 except KeyError: |
|
169 self.__threaddata.txdata = self._tx_data[txid] = TransactionData(txid) |
|
170 |
|
171 @property |
|
172 def _threaddata(self): |
|
173 try: |
|
174 return self.__threaddata.txdata |
|
175 except AttributeError: |
|
176 self.set_tx_data() |
|
177 return self.__threaddata.txdata |
|
178 |
158 |
179 |
159 def hijack_user(self, user): |
180 def hijack_user(self, user): |
160 """return a fake request/session using specified user""" |
181 """return a fake request/session using specified user""" |
161 session = Session(user, self.repo) |
182 session = Session(user, self.repo) |
162 threaddata = session._threaddata |
183 threaddata = session._threaddata |
336 DEFAULT_SECURITY = object() # evaluated to true by design |
357 DEFAULT_SECURITY = object() # evaluated to true by design |
337 |
358 |
338 @property |
359 @property |
339 def read_security(self): |
360 def read_security(self): |
340 """return a boolean telling if read security is activated or not""" |
361 """return a boolean telling if read security is activated or not""" |
341 try: |
362 txstore = self._threaddata |
342 return self._threaddata.read_security |
363 if txstore is None: |
|
364 return self.DEFAULT_SECURITY |
|
365 try: |
|
366 return txstore.read_security |
343 except AttributeError: |
367 except AttributeError: |
344 self._threaddata.read_security = self.DEFAULT_SECURITY |
368 txstore.read_security = self.DEFAULT_SECURITY |
345 return self._threaddata.read_security |
369 return txstore.read_security |
346 |
370 |
347 def set_read_security(self, activated): |
371 def set_read_security(self, activated): |
348 """[de]activate read security, returning the previous value set for |
372 """[de]activate read security, returning the previous value set for |
349 later restoration. |
373 later restoration. |
350 |
374 |
351 you should usually use the `security_enabled` context manager instead |
375 you should usually use the `security_enabled` context manager instead |
352 of this to change security settings. |
376 of this to change security settings. |
353 """ |
377 """ |
354 oldmode = self.read_security |
378 txstore = self._threaddata |
355 self._threaddata.read_security = activated |
379 if txstore is None: |
|
380 return self.DEFAULT_SECURITY |
|
381 oldmode = getattr(txstore, 'read_security', self.DEFAULT_SECURITY) |
|
382 txstore.read_security = activated |
356 # dbapi_query used to detect hooks triggered by a 'dbapi' query (eg not |
383 # dbapi_query used to detect hooks triggered by a 'dbapi' query (eg not |
357 # issued on the session). This is tricky since we the execution model of |
384 # issued on the session). This is tricky since we the execution model of |
358 # a (write) user query is: |
385 # a (write) user query is: |
359 # |
386 # |
360 # repository.execute (security enabled) |
387 # repository.execute (security enabled) |
367 # so we can't rely on simply checking session.read_security, but |
394 # so we can't rely on simply checking session.read_security, but |
368 # recalling the first transition from DEFAULT_SECURITY to something |
395 # recalling the first transition from DEFAULT_SECURITY to something |
369 # else (False actually) is not perfect but should be enough |
396 # else (False actually) is not perfect but should be enough |
370 # |
397 # |
371 # also reset dbapi_query to true when we go back to DEFAULT_SECURITY |
398 # also reset dbapi_query to true when we go back to DEFAULT_SECURITY |
372 self._threaddata.dbapi_query = (oldmode is self.DEFAULT_SECURITY |
399 txstore.dbapi_query = (oldmode is self.DEFAULT_SECURITY |
373 or activated is self.DEFAULT_SECURITY) |
400 or activated is self.DEFAULT_SECURITY) |
374 return oldmode |
401 return oldmode |
375 |
402 |
376 @property |
403 @property |
377 def write_security(self): |
404 def write_security(self): |
378 """return a boolean telling if write security is activated or not""" |
405 """return a boolean telling if write security is activated or not""" |
379 try: |
406 txstore = self._threaddata |
380 return self._threaddata.write_security |
407 if txstore is None: |
|
408 return self.DEFAULT_SECURITY |
|
409 try: |
|
410 return txstore.write_security |
381 except: |
411 except: |
382 self._threaddata.write_security = self.DEFAULT_SECURITY |
412 txstore.write_security = self.DEFAULT_SECURITY |
383 return self._threaddata.write_security |
413 return txstore.write_security |
384 |
414 |
385 def set_write_security(self, activated): |
415 def set_write_security(self, activated): |
386 """[de]activate write security, returning the previous value set for |
416 """[de]activate write security, returning the previous value set for |
387 later restoration. |
417 later restoration. |
388 |
418 |
389 you should usually use the `security_enabled` context manager instead |
419 you should usually use the `security_enabled` context manager instead |
390 of this to change security settings. |
420 of this to change security settings. |
391 """ |
421 """ |
392 oldmode = self.write_security |
422 txstore = self._threaddata |
393 self._threaddata.write_security = activated |
423 if txstore is None: |
|
424 return self.DEFAULT_SECURITY |
|
425 oldmode = getattr(txstore, 'write_security', self.DEFAULT_SECURITY) |
|
426 txstore.write_security = activated |
394 return oldmode |
427 return oldmode |
395 |
428 |
396 @property |
429 @property |
397 def running_dbapi_query(self): |
430 def running_dbapi_query(self): |
398 """return a boolean telling if it's triggered by a db-api query or by |
431 """return a boolean telling if it's triggered by a db-api query or by |
565 |
598 |
566 def _touch(self): |
599 def _touch(self): |
567 """update latest session usage timestamp and reset mode to read""" |
600 """update latest session usage timestamp and reset mode to read""" |
568 self.timestamp = time() |
601 self.timestamp = time() |
569 self.local_perm_cache.clear() # XXX simply move in transaction_data, no? |
602 self.local_perm_cache.clear() # XXX simply move in transaction_data, no? |
570 self._threaddata.mode = self.default_mode |
|
571 |
603 |
572 # shared data handling ################################################### |
604 # shared data handling ################################################### |
573 |
605 |
574 def get_shared_data(self, key, default=None, pop=False): |
606 def get_shared_data(self, key, default=None, pop=False): |
575 """return value associated to `key` in session data""" |
607 """return value associated to `key` in session data""" |
655 self.timestamp = time() # update timestamp |
687 self.timestamp = time() # update timestamp |
656 rset = self._execute(self, rql, kwargs, build_descr) |
688 rset = self._execute(self, rql, kwargs, build_descr) |
657 rset.req = self |
689 rset.req = self |
658 return rset |
690 return rset |
659 |
691 |
660 def _clear_thread_data(self): |
692 def _clear_thread_data(self, reset_pool=True): |
661 """remove everything from the thread local storage, except pool |
693 """remove everything from the thread local storage, except pool |
662 which is explicitly removed by reset_pool, and mode which is set anyway |
694 which is explicitly removed by reset_pool, and mode which is set anyway |
663 by _touch |
695 by _touch |
664 """ |
696 """ |
665 store = self._threaddata |
697 try: |
666 for name in ('commit_state', 'transaction_data', 'pending_operations', |
698 txstore = self.__threaddata.txdata |
667 '_rewriter'): |
699 except AttributeError: |
668 try: |
700 pass |
669 delattr(store, name) |
701 else: |
670 except AttributeError: |
702 if reset_pool: |
671 pass |
703 self._tx_data.pop(txstore.transactionid, None) |
|
704 try: |
|
705 del self.__threaddata.txdata |
|
706 except AttributeError: |
|
707 pass |
|
708 else: |
|
709 for name in ('commit_state', 'transaction_data', |
|
710 'pending_operations', '_rewriter'): |
|
711 try: |
|
712 delattr(txstore, name) |
|
713 except AttributeError: |
|
714 continue |
672 |
715 |
673 def commit(self, reset_pool=True): |
716 def commit(self, reset_pool=True): |
674 """commit the current session's transaction""" |
717 """commit the current session's transaction""" |
675 if self.pool is None: |
718 if self.pool is None: |
676 assert not self.pending_operations |
719 assert not self.pending_operations |
678 self._touch() |
721 self._touch() |
679 self.debug('commit session %s done (no db activity)', self.id) |
722 self.debug('commit session %s done (no db activity)', self.id) |
680 return |
723 return |
681 if self.commit_state: |
724 if self.commit_state: |
682 return |
725 return |
683 # by default, operations are executed with security turned off |
726 # on rollback, an operation should have the following state |
684 with security_enabled(self, False, False): |
727 # information: |
685 # on rollback, an operation should have the following state |
728 # - processed by the precommit/commit event or not |
686 # information: |
729 # - if processed, is it the failed operation |
687 # - processed by the precommit/commit event or not |
730 try: |
688 # - if processed, is it the failed operation |
731 # by default, operations are executed with security turned off |
689 try: |
732 with security_enabled(self, False, False): |
690 for trstate in ('precommit', 'commit'): |
733 for trstate in ('precommit', 'commit'): |
691 processed = [] |
734 processed = [] |
692 self.commit_state = trstate |
735 self.commit_state = trstate |
693 try: |
736 try: |
694 while self.pending_operations: |
737 while self.pending_operations: |
728 except: |
771 except: |
729 self.critical('error while %sing', trstate, |
772 self.critical('error while %sing', trstate, |
730 exc_info=sys.exc_info()) |
773 exc_info=sys.exc_info()) |
731 self.info('%s session %s done', trstate, self.id) |
774 self.info('%s session %s done', trstate, self.id) |
732 return self.transaction_uuid(set=False) |
775 return self.transaction_uuid(set=False) |
733 finally: |
776 finally: |
734 self._clear_thread_data() |
777 self._touch() |
735 self._touch() |
778 if reset_pool: |
736 if reset_pool: |
779 self.reset_pool(ignoremode=True) |
737 self.reset_pool(ignoremode=True) |
780 self._clear_thread_data(reset_pool) |
738 |
781 |
739 def rollback(self, reset_pool=True): |
782 def rollback(self, reset_pool=True): |
740 """rollback the current session's transaction""" |
783 """rollback the current session's transaction""" |
741 if self.pool is None: |
784 if self.pool is None: |
742 assert not self.pending_operations |
|
743 self._clear_thread_data() |
785 self._clear_thread_data() |
744 self._touch() |
786 self._touch() |
745 self.debug('rollback session %s done (no db activity)', self.id) |
787 self.debug('rollback session %s done (no db activity)', self.id) |
746 return |
788 return |
747 # by default, operations are executed with security turned off |
789 try: |
748 with security_enabled(self, False, False): |
790 # by default, operations are executed with security turned off |
749 try: |
791 with security_enabled(self, False, False): |
750 while self.pending_operations: |
792 while self.pending_operations: |
751 try: |
793 try: |
752 operation = self.pending_operations.pop(0) |
794 operation = self.pending_operations.pop(0) |
753 operation.handle_event('rollback_event') |
795 operation.handle_event('rollback_event') |
754 except: |
796 except: |
755 self.critical('rollback error', exc_info=sys.exc_info()) |
797 self.critical('rollback error', exc_info=sys.exc_info()) |
756 continue |
798 continue |
757 self.pool.rollback() |
799 self.pool.rollback() |
758 self.debug('rollback for session %s done', self.id) |
800 self.debug('rollback for session %s done', self.id) |
759 finally: |
801 finally: |
760 self._clear_thread_data() |
802 self._touch() |
761 self._touch() |
803 if reset_pool: |
762 if reset_pool: |
804 self.reset_pool(ignoremode=True) |
763 self.reset_pool(ignoremode=True) |
805 self._clear_thread_data(reset_pool) |
764 |
806 |
765 def close(self): |
807 def close(self): |
766 """do not close pool on session close, since they are shared now""" |
808 """do not close pool on session close, since they are shared now""" |
767 self._closed = True |
809 self._closed = True |
768 # copy since _threads_in_transaction maybe modified while waiting |
810 # copy since _threads_in_transaction maybe modified while waiting |