Repository: tornadoweb/tornado

Issue:
 'HTTPClient' object has no attribute '_closed'  in tornado5.0
about httpclient.HTTPClient()
I can't execute http_client = httpclient.HTTPClient() in tornado==5.0
running this will throw Exception this：
Traceback (most recent call last):
File "/Users/zhonghao/anaconda3/envs/tornado_test/lib/python3.6/site-packages/tornado/httpclient.py", line 85, in del
self.close()
File "/Users/zhonghao/anaconda3/envs/tornado_test/lib/python3.6/site-packages/tornado/httpclient.py", line 89, in close
if not self._closed:
AttributeError: 'HTTPClient' object has no attribute '_closed'

but in tornado==4.5.2
it works fine, this issue on macOS，can you tell me why？


Discussion:
Is this the only error in your logs? It looks like there was an exception in the constructor which kept the `_closed` attribute from being set, and then this exception occurred in the destructor trying to clean up. The first exception is the real error. 
you are right，the whole error log is 

> /Users/zhonghao/anaconda3/envs/tornado_test/bin/python3.6 /Users/zhonghao/Desktop/tornado_test/async_webserver/5.1.1_sync_rate.py
> [E 180325 12:21:13 web:1621] Uncaught exception GET / (::1)
>     HTTPServerRequest(protocol='http', host='localhost:8000', method='GET', uri='/', version='HTTP/1.1', remote_ip='::1')
>     Traceback (most recent call last):
>       File "/Users/zhonghao/anaconda3/envs/tornado_test/lib/python3.6/site-packages/tornado/web.py", line 1541, in _execute
>         result = method(*self.path_args, **self.path_kwargs)
>       File "/Users/zhonghao/Desktop/tornado_test/async_webserver/5.1.1_sync_rate.py", line 23, in get
>         client = httpclient.HTTPClient()
>       File "/Users/zhonghao/anaconda3/envs/tornado_test/lib/python3.6/site-packages/tornado/httpclient.py", line 81, in __init__
>         gen.coroutine(lambda: async_client_class(**kwargs)))
>       File "/Users/zhonghao/anaconda3/envs/tornado_test/lib/python3.6/site-packages/tornado/ioloop.py", line 577, in run_sync
>         self.start()
>       File "/Users/zhonghao/anaconda3/envs/tornado_test/lib/python3.6/site-packages/tornado/platform/asyncio.py", line 112, in start
>         self.asyncio_loop.run_forever()
>       File "/Users/zhonghao/anaconda3/envs/tornado_test/lib/python3.6/asyncio/base_events.py", line 411, in run_forever
>         **'Cannot run the event loop while another loop is running')**
>     **RuntimeError: Cannot run the event loop while another loop is running**
> [E 180325 12:21:13 web:2106] 500 GET / (::1) 3.77ms
> Exception ignored in: <bound method HTTPClient.__del__ of <tornado.httpclient.HTTPClient object at 0x106864588>>
> Traceback (most recent call last):
>   File "/Users/zhonghao/anaconda3/envs/tornado_test/lib/python3.6/site-packages/tornado/httpclient.py", line 85, in __del__
>     self.close()
>   File "/Users/zhonghao/anaconda3/envs/tornado_test/lib/python3.6/site-packages/tornado/httpclient.py", line 89, in close
>     if not self._closed:
> AttributeError: 'HTTPClient' object has no attribute '_closed'
 
and my demo code is this:
```
class IndexHandler(tornado.web.RequestHandler):
    def get(self):
        client = httpclient.HTTPClient()
        self.write('done')
if __name__ == "__main__":
    tornado.options.parse_command_line()
    app = tornado.web.Application(
        handlers=[(r"/", IndexHandler)],)
    http_server = tornado.httpserver.HTTPServer(app)
    http_server.listen(options.port)
    tornado.ioloop.IOLoop.instance().start()
```

note: I use python3.6
OK. The real error, "RuntimeError: Cannot run the event loop while another loop is running", is a limitation of `asyncio`. You can't use the synchronous `HTTPClient` in an asynchronous Tornado application when `asyncio` is used. You have to use `AsyncHTTPClient` instead.
why the  error not in tornado4.5? what's the difference between 4.5 and 5, may be I should  read 5.0 document seriously.    : (
thanks for your reply

In Tornado 5.0, asyncio is used automatically when it is available. In 4.5 you had to opt in to asyncio integration separately. 
I get it
thank you again

Resolving patch:
diff --git a/tornado/httpclient.py b/tornado/httpclient.py
--- a/tornado/httpclient.py
+++ b/tornado/httpclient.py
@@ -54,8 +54,10 @@
 class HTTPClient(object):
     """A blocking HTTP client.
 
-    This interface is provided for convenience and testing; most applications
-    that are running an IOLoop will want to use `AsyncHTTPClient` instead.
+    This interface is provided to make it easier to share code between
+    synchronous and asynchronous applications. Applications that are
+    running an `.IOLoop` must use `AsyncHTTPClient` instead.
+
     Typical usage looks like this::
 
         http_client = httpclient.HTTPClient()
@@ -70,8 +72,19 @@ class HTTPClient(object):
             # Other errors are possible, such as IOError.
             print("Error: " + str(e))
         http_client.close()
+
+    .. versionchanged:: 5.0
+
+       Due to limitations in `asyncio`, it is no longer possible to
+       use the synchronous ``HTTPClient`` while an `.IOLoop` is running.
+       Use `AsyncHTTPClient` instead.
+
     """
     def __init__(self, async_client_class=None, **kwargs):
+        # Initialize self._closed at the beginning of the constructor
+        # so that an exception raised here doesn't lead to confusing
+        # failures in __del__.
+        self._closed = True
         self._io_loop = IOLoop(make_current=False)
         if async_client_class is None:
             async_client_class = AsyncHTTPClient
diff --git a/tornado/ioloop.py b/tornado/ioloop.py
--- a/tornado/ioloop.py
+++ b/tornado/ioloop.py
@@ -47,7 +47,6 @@
 import time
 import traceback
 import math
-import weakref
 
 from tornado.concurrent import Future, is_future, chain_future, future_set_exc_info, future_add_done_callback  # noqa: E501
 from tornado.log import app_log, gen_log
@@ -185,7 +184,7 @@ def connection_ready(sock, fd, events):
     _current = threading.local()
 
     # In Python 3, _ioloop_for_asyncio maps from asyncio loops to IOLoops.
-    _ioloop_for_asyncio = weakref.WeakKeyDictionary()
+    _ioloop_for_asyncio = dict()
 
     @classmethod
     def configure(cls, impl, **kwargs):
diff --git a/tornado/platform/asyncio.py b/tornado/platform/asyncio.py
--- a/tornado/platform/asyncio.py
+++ b/tornado/platform/asyncio.py
@@ -38,6 +38,20 @@ def initialize(self, asyncio_loop, **kwargs):
         self.readers = set()
         self.writers = set()
         self.closing = False
+        # If an asyncio loop was closed through an asyncio interface
+        # instead of IOLoop.close(), we'd never hear about it and may
+        # have left a dangling reference in our map. In case an
+        # application (or, more likely, a test suite) creates and
+        # destroys a lot of event loops in this way, check here to
+        # ensure that we don't have a lot of dead loops building up in
+        # the map.
+        #
+        # TODO(bdarnell): consider making self.asyncio_loop a weakref
+        # for AsyncIOMainLoop and make _ioloop_for_asyncio a
+        # WeakKeyDictionary.
+        for loop in list(IOLoop._ioloop_for_asyncio):
+            if loop.is_closed():
+                del IOLoop._ioloop_for_asyncio[loop]
         IOLoop._ioloop_for_asyncio[asyncio_loop] = self
         super(BaseAsyncIOLoop, self).initialize(**kwargs)
 
@@ -49,6 +63,7 @@ def close(self, all_fds=False):
             if all_fds:
                 self.close_fd(fileobj)
         self.asyncio_loop.close()
+        del IOLoop._ioloop_for_asyncio[self.asyncio_loop]
 
     def add_handler(self, fd, handler, events):
         fd, fileobj = self.split_fd(fd)


Test patch:
diff --git a/tornado/test/asyncio_test.py b/tornado/test/asyncio_test.py
--- a/tornado/test/asyncio_test.py
+++ b/tornado/test/asyncio_test.py
@@ -122,6 +122,44 @@ async def native_coroutine_with_adapter2():
             42)
 
 
+@unittest.skipIf(asyncio is None, "asyncio module not present")
+class LeakTest(unittest.TestCase):
+    def setUp(self):
+        # Trigger a cleanup of the mapping so we start with a clean slate.
+        AsyncIOLoop().close()
+        # If we don't clean up after ourselves other tests may fail on
+        # py34.
+        self.orig_policy = asyncio.get_event_loop_policy()
+        asyncio.set_event_loop_policy(asyncio.DefaultEventLoopPolicy())
+
+    def tearDown(self):
+        asyncio.get_event_loop().close()
+        asyncio.set_event_loop_policy(self.orig_policy)
+
+    def test_ioloop_close_leak(self):
+        orig_count = len(IOLoop._ioloop_for_asyncio)
+        for i in range(10):
+            # Create and close an AsyncIOLoop using Tornado interfaces.
+            loop = AsyncIOLoop()
+            loop.close()
+        new_count = len(IOLoop._ioloop_for_asyncio) - orig_count
+        self.assertEqual(new_count, 0)
+
+    def test_asyncio_close_leak(self):
+        orig_count = len(IOLoop._ioloop_for_asyncio)
+        for i in range(10):
+            # Create and close an AsyncIOMainLoop using asyncio interfaces.
+            loop = asyncio.new_event_loop()
+            loop.call_soon(IOLoop.current)
+            loop.call_soon(loop.stop)
+            loop.run_forever()
+            loop.close()
+        new_count = len(IOLoop._ioloop_for_asyncio) - orig_count
+        # Because the cleanup is run on new loop creation, we have one
+        # dangling entry in the map (but only one).
+        self.assertEqual(new_count, 1)
+
+
 @unittest.skipIf(asyncio is None, "asyncio module not present")
 class AnyThreadEventLoopPolicyTest(unittest.TestCase):
     def setUp(self):