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 |
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 |
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) |
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 |