Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion tornado/gen.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ def get(self):
import sys
import textwrap
import types
import weakref

from tornado.concurrent import Future, TracebackFuture, is_future, chain_future
from tornado.ioloop import IOLoop
Expand Down Expand Up @@ -244,6 +245,24 @@ def coroutine(func, replace_callback=True):
"""
return _make_coroutine_wrapper(func, replace_callback=True)

# Ties lifetime of runners to their result futures. Github Issue #1769
# Generators, like any object in Python, must be strong referenced
# in order to not be cleaned up by the garbage collector. When using
# coroutines, the Runner object is what strong-refs the inner
# generator. However, the only item that strong-reffed the Runner
# was the last Future that the inner generator yielded (via the
# Future's internal done_callback list). Usually this is enough, but
# it is also possible for this Future to not have any strong references
# other than other objects referenced by the Runner object (usually
# when using other callback patterns and/or weakrefs). In this
# situation, if a garbage collection ran, a cycle would be detected and
# Runner objects could be destroyed along with their inner generators
# and everything in their local scope.
# This map provides strong references to Runner objects as long as
# their result future objects also have strong references (typically
# from the parent coroutine's Runner). This keeps the coroutine's
# Runner alive.
_futures_to_runners = weakref.WeakKeyDictionary()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a blank line after this definition, and expand the comment. Something like:
Unlike regular stack frames, idle coroutines are not anchored to a garbage-collection root. Object references point in the opposite direction from what you'd expect, with child frames holding references to the parent frames that they will awaken when completed, but there is not necessarily anything keeping the child frames alive (In most cases the innermost child frame will be registered as a callback on the IOLoop, but other patterns are possible especially when weak references are used). This map provides hard references in the other direction, so that as long as the Future returned by a coroutine is referenced (typically by the parent's Runner), this coroutine's Runner will be kept alive.


def _make_coroutine_wrapper(func, replace_callback):
"""The inner workings of ``@gen.coroutine`` and ``@gen.engine``.
Expand Down Expand Up @@ -294,7 +313,7 @@ def wrapper(*args, **kwargs):
except Exception:
future.set_exc_info(sys.exc_info())
else:
Runner(result, future, yielded)
_futures_to_runners[future] = Runner(result, future, yielded)
try:
return future
finally:
Expand Down
25 changes: 24 additions & 1 deletion tornado/test/gen_test.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import absolute_import, division, print_function, with_statement

import gc
import contextlib
import datetime
import functools
Expand All @@ -25,7 +26,6 @@
except ImportError:
futures = None


class GenEngineTest(AsyncTestCase):
def setUp(self):
super(GenEngineTest, self).setUp()
Expand Down Expand Up @@ -1368,5 +1368,28 @@ def test_no_ref(self):
gen.WaitIterator(gen.sleep(0)).next())


class RunnerGCTest(AsyncTestCase):
"""Github issue 1769: Runner objects can get GCed unexpectedly"""
@gen_test
def test_gc(self):
"""Runners shouldn't GC if future is alive"""
# Create the weakref
weakref_scope = [None]
def callback():
gc.collect(2)
weakref_scope[0]().set_result(123)

@gen.coroutine
def tester():
fut = Future()
weakref_scope[0] = weakref.ref(fut)
self.io_loop.add_callback(callback)
yield fut

yield gen.with_timeout(
datetime.timedelta(seconds=0.2),
tester()
)

if __name__ == '__main__':
unittest.main()