Tornado在6.0版本中, 已经完全切换到了asyncio, 但是我在使用IPython embed调试时并不顺利, 本篇博客主要介绍一下如何在新版的Tornado中继续使用IPython.

出现的问题

以下面的代码为例, 请求127.0.0.1:8888时会出现RuntimeError: This event loop is already running

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import tornado.ioloop
import tornado.web


class MainHandler(tornado.web.RequestHandler):
def get(self):
# ---------- XXX: Can't GIT add [START] ---------- #
import IPython
IPython.embed(using=False)
# ---------- XXX: Can't GIT add [END] ---------- #
self.write("Hello, world")


def make_app():
return tornado.web.Application([
(r"/", MainHandler),
])


if __name__ == "__main__":
app = make_app()
app.listen(8888)
tornado.ioloop.IOLoop.current().start()

以下为详细的堆栈信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
Python 3.8.1 (default, Jan 22 2020, 06:38:00) 
Type 'copyright', 'credits' or 'license' for more information
IPython 7.12.0 -- An enhanced Interactive Python. Type '?' for help.

ERROR:tornado.application:Uncaught exception GET / (127.0.0.1)
HTTPServerRequest(protocol='http', host='127.0.0.1:8888', method='GET', uri='/', version='HTTP/1.1', remote_ip='127.0.0.1')
Traceback (most recent call last):
File "/usr/lib/python3.8/site-packages/tornado/web.py", line 1697, in _execute
result = method(*self.path_args, **self.path_kwargs)
File "main.py", line 11, in get
IPython.embed(using=False)
File "/usr/lib/python3.8/site-packages/IPython/terminal/embed.py", line 388, in embed
shell(header=header, stack_depth=2, compile_flags=compile_flags,
File "/usr/lib/python3.8/site-packages/IPython/terminal/embed.py", line 228, in __call__
self.mainloop(local_ns, module, stack_depth=stack_depth,
File "/usr/lib/python3.8/site-packages/IPython/terminal/embed.py", line 324, in mainloop
self.interact()
File "/usr/lib/python3.8/site-packages/IPython/terminal/interactiveshell.py", line 541, in interact
code = self.prompt_for_code()
File "/usr/lib/python3.8/site-packages/IPython/terminal/interactiveshell.py", line 467, in prompt_for_code
text = self.pt_app.prompt(
File "/usr/lib/python3.8/site-packages/prompt_toolkit/shortcuts/prompt.py", line 997, in prompt
return self.app.run()
File "/usr/lib/python3.8/site-packages/prompt_toolkit/application/application.py", line 810, in run
return loop.run_until_complete(self.run_async(pre_run=pre_run))
File "/usr/lib/python3.8/asyncio/base_events.py", line 599, in run_until_complete
self.run_forever()
File "/usr/lib/python3.8/asyncio/base_events.py", line 554, in run_forever
raise RuntimeError('This event loop is already running')
RuntimeError: This event loop is already running
ERROR:tornado.access:500 GET / (127.0.0.1) 718.57ms

解决方案

我在StackOverflow上找到了相关的问题: RuntimeError: This event loop is already running in python

出现报错的问题是asyncio不支持嵌套, 一个可行的方案是使用nest_asyncio将其patch, 允许asyncio嵌套.

1
2
3
# pip install nest_asyncio
import nest_asyncio
nest_asyncio.apply()

相应代码的改动如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import os

# patch要放在所有语句之前, 防止其他库引用了未被patch的库
# 为了安全, 只在DEBUG模式下打开, 使用时需要: DEBUG=true python main.py
if os.environ.get("DEBUG") == "true":
print("In nest asyncio mode")
import nest_asyncio
nest_asyncio.apply()

import tornado.ioloop
import tornado.web


class MainHandler(tornado.web.RequestHandler):
def get(self):
# ---------- XXX: Can't GIT add [START] ---------- #
import IPython
IPython.embed(using=False)
# ---------- XXX: Can't GIT add [END] ---------- #
self.write("Hello, world")


def make_app():
return tornado.web.Application([
(r"/", MainHandler),
])


if __name__ == "__main__":
app = make_app()
app.listen(8888)
tornado.ioloop.IOLoop.current().start()

之后就可以愉快的使用IPython了.

nest_asyncio源码的一点阅读

大家有兴趣可以查看nest_asyncio的相关代码, 代码量不多, 只有一个文件, 主要pathc的部分为原函数中对多项任务的报错部分, 并且保留了执行时的上下文信息.

比如patch_loop中,

将原始的RuntimeError去掉, 并且将上下文环境进行了还原.

总结

说真的, 我并不想将nest_asyncio这样的策略应用到线上环境, 我只想用来作为程序的调试时使用.

我没有去深究到底是重新创建一个新的event loop好还是使得原有的eventloop可以嵌套更好, 毕竟我的问题已经解决了, 我仍然可以愉快的使用IPython进行调试, 这就足够了.