]> git.ipfire.org Git - thirdparty/tornado.git/commitdiff
Coroutine exception __context__ leak in python 3 1333/head
authorCarl Sverre <accounts@carlsverre.com>
Thu, 12 Feb 2015 04:00:02 +0000 (20:00 -0800)
committerCarl Sverre <accounts@carlsverre.com>
Thu, 12 Feb 2015 19:33:09 +0000 (11:33 -0800)
In Python 3 every exception has an auto-attached __context__ exception if it was
raised while an existing exception was being handled.  This is often a very
useful property and allows users to see the underlying cause of many problems.

However, when dealing with async code via coroutines this feature can suddenly
become a pretty big issue.  Namely, if you generator.throw back into a coroutine
to raise an exception in the correct scope, and that coroutine raises an
exception at a later time you can have nested exceptions which shouldn't
logically be nested.

This patch resolves this issue in the coroutine code by moving the gen.throw
outside of the exception handler.  By doing this the user-code scope is able to
later raise exceptions without worrying about inheriting prior exception
context.

tornado/gen.py
tornado/test/gen_test.py

index 3d87d7177c12ad16f1365bf1ce80c9a2ac04974f..b405b2f7c239cac5d7d007c2312e9505f9513525 100644 (file)
@@ -824,13 +824,20 @@ class Runner(object):
                 self.future = None
                 try:
                     orig_stack_contexts = stack_context._state.contexts
+                    exc_info = None
+
                     try:
                         value = future.result()
                     except Exception:
                         self.had_exception = True
-                        yielded = self.gen.throw(*sys.exc_info())
+                        exc_info = sys.exc_info()
+
+                    if exc_info is not None:
+                        yielded = self.gen.throw(*exc_info)
+                        exc_info = None
                     else:
                         yielded = self.gen.send(value)
+
                     if stack_context._state.contexts is not orig_stack_contexts:
                         self.gen.throw(
                             stack_context.StackContextInconsistentError(
index 31acadd8b4e7578c136cace24b6b5a4d7699d993..15876d598700ae96065a4bbafd8cacc7f2f96784 100644 (file)
@@ -844,6 +844,29 @@ class GenCoroutineTest(AsyncTestCase):
         yield gen.sleep(0.01)
         self.finished = True
 
+    @skipBefore33
+    @gen_test
+    def test_py3_leak_exception_context(self):
+        class LeakedException(Exception):
+            pass
+
+        @gen.coroutine
+        def inner(iteration):
+            raise LeakedException(iteration)
+
+        try:
+            yield inner(1)
+        except LeakedException as e:
+            self.assertEqual(str(e), "1")
+            self.assertIsNone(e.__context__)
+
+        try:
+            yield inner(2)
+        except LeakedException as e:
+            self.assertEqual(str(e), "2")
+            self.assertIsNone(e.__context__)
+
+        self.finished = True
 
 class GenSequenceHandler(RequestHandler):
     @asynchronous