I noticed some weird behavior with exceptions and transactions recently (in particular, a WSGI server which relies on a __del__ to close its client connection wasn't disconnecting from the client immediately when a conflict error was raised by ZODB, but would indeed disconnect if a conflict error had not been raised).
I tracked this down to the zope transaction module leaking frame stacks via retrieving traceback objects from sys.exc_info() without subsequently removing them from local scope. With the attached patch, the issue was solved. If no one complains, I'll apply it to the transaction trunk. - C
Index: transaction/_transaction.py =================================================================== --- transaction/_transaction.py (revision 116336) +++ transaction/_transaction.py (working copy) @@ -329,9 +329,15 @@ self._commitResources() self.status = Status.COMMITTED except: - t, v, tb = self._saveAndGetCommitishError() - self._callAfterCommitHooks(status=False) - raise t, v, tb + t = None + v = None + tb = None + try: + t, v, tb = self._saveAndGetCommitishError() + self._callAfterCommitHooks(status=False) + raise t, v, tb + finally: + del t, v, tb else: if self._manager: self._manager.free(self) @@ -343,14 +349,18 @@ self.status = Status.COMMITFAILED # Save the traceback for TransactionFailedError. ft = self._failure_traceback = StringIO() - t, v, tb = sys.exc_info() - # Record how we got into commit(). - traceback.print_stack(sys._getframe(1), None, ft) - # Append the stack entries from here down to the exception. - traceback.print_tb(tb, None, ft) - # Append the exception type and value. - ft.writelines(traceback.format_exception_only(t, v)) - return t, v, tb + try: + t, v, tb = sys.exc_info() + # Record how we got into commit(). + traceback.print_stack(sys._getframe(1), None, ft) + # Append the stack entries from here down to the exception. + traceback.print_tb(tb, None, ft) + # Append the exception type and value. + ft.writelines(traceback.format_exception_only(t, v)) + return t, v, tb + finally: + del t, v, tb + def _saveAndRaiseCommitishError(self): t, v, tb = self._saveAndGetCommitishError() @@ -442,10 +452,13 @@ # to revert the changes in each of the resource managers. t, v, tb = sys.exc_info() try: - self._cleanup(L) + try: + self._cleanup(L) + finally: + self._synchronizers.map(lambda s: s.afterCompletion(self)) + raise t, v, tb finally: - self._synchronizers.map(lambda s: s.afterCompletion(self)) - raise t, v, tb + del t, v, tb def _cleanup(self, L): # Called when an exception occurs during tpc_vote or tpc_finish. @@ -469,26 +482,33 @@ self._synchronizers.map(lambda s: s.beforeCompletion(self)) - tb = None - for rm in self._resources: - try: - rm.abort(self) - except: - if tb is None: - t, v, tb = sys.exc_info() - self.log.error("Failed to abort resource manager: %s", - rm, exc_info=sys.exc_info()) + try: - if self._manager: - self._manager.free(self) + t = None + v = None + tb = None + + for rm in self._resources: + try: + rm.abort(self) + except: + if tb is None: + t, v, tb = sys.exc_info() + self.log.error("Failed to abort resource manager: %s", + rm, exc_info=sys.exc_info()) - self._synchronizers.map(lambda s: s.afterCompletion(self)) + if self._manager: + self._manager.free(self) - self.log.debug("abort") + self._synchronizers.map(lambda s: s.afterCompletion(self)) - if tb is not None: - raise t, v, tb + self.log.debug("abort") + if tb is not None: + raise t, v, tb + finally: + del t, v, tb + def note(self, text): text = text.strip() if self.description: @@ -542,20 +562,26 @@ self.manager.tpc_vote(txn) def abort(self, txn): + t = None + v = None tb = None - for o in self.objects: - try: - self.manager.abort(o, txn) - except: - # Capture the first exception and re-raise it after - # aborting all the other objects. - if tb is None: - t, v, tb = sys.exc_info() - txn.log.error("Failed to abort object: %s", - object_hint(o), exc_info=sys.exc_info()) - if tb is not None: - raise t, v, tb + try: + for o in self.objects: + try: + self.manager.abort(o, txn) + except: + # Capture the first exception and re-raise it after + # aborting all the other objects. + if tb is None: + t, v, tb = sys.exc_info() + txn.log.error("Failed to abort object: %s", + object_hint(o), exc_info=sys.exc_info()) + if tb is not None: + raise t, v, tb + finally: + del t, v, tb + def rm_cmp(rm1, rm2): return cmp(rm1.sortKey(), rm2.sortKey())
_______________________________________________ For more information about ZODB, see the ZODB Wiki: http://www.zope.org/Wikis/ZODB/ ZODB-Dev mailing list - ZODB-Dev@zope.org https://mail.zope.org/mailman/listinfo/zodb-dev