最近线上出了一个bug,让我来解决一下。大致逻辑是前端的一个ajax请求的response,其中body部分本来应该是一个json,却神秘失踪了。正常返回了200 ok,就是http response的body为空,导致前端处理有问题。经过分析,应该是最后在返回response的body之前,连接被关闭了。

简化逻辑的demo如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class MainHandler(tornado.web.RequestHandler):

@tornado.gen.coroutine
def get(self):
print 'get start'
yield self.fetch_something() # 原来错误的逻辑就是这里的yield忘了写了
print 'get over'

@tornado.gen.coroutine
def fetch_something(self):
print 'fetch start'
http_client = AsyncHTTPClient()
response = yield http_client.fetch("http://www.baidu.com")
print 'fetch over'
self.write(response.body)

原来的逻辑简化就是需要异步的抓取一个东西之后返回。正常的执行逻辑应该如下:

1
2
3
4
get start
fetch start
fetch over
get over

但是因为第一个yield的缺失,导致执行的逻辑如下:

1
2
3
4
get start
fetch start
get over
fetch over

也就意味着,在fetch_something中,yield之后的逻辑在执行的时候,最上层调用的get方法已经执行完毕,这个连接已经被关闭掉了。

问题出在tornado.gen.coroutine这个装饰器用的姿势不对,应该有的yield给丢了。

扒出tornado的这个装饰器的源码,核心部分如下:

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
33
34
35
36
37
38
39
40
@functools.wraps(func)
def wrapper(*args, **kwargs):
future = TracebackFuture() # 0

if replace_callback and 'callback' in kwargs:
callback = kwargs.pop('callback')
IOLoop.current().add_future(
future, lambda future: callback(future.result()))

try:
result = func(*args, **kwargs) # 1
except (Return, StopIteration) as e:
result = getattr(e, 'value', None)
except Exception:
future.set_exc_info(sys.exc_info())
return future
else:
if isinstance(result, types.GeneratorType): # 2
try:
orig_stack_contexts = stack_context._state.contexts
yielded = next(result) # 3
if stack_context._state.contexts is not orig_stack_contexts:
yielded = TracebackFuture()
yielded.set_exception(
stack_context.StackContextInconsistentError(
'stack_context inconsistency (probably caused '
'by yield within a "with StackContext" block)'))
except (StopIteration, Return) as e:
future.set_result(getattr(e, 'value', None))
except Exception:
future.set_exc_info(sys.exc_info())
else:
Runner(result, future, yielded)
try:
return future
finally:
future = None
future.set_result(result)
return future
return wrapper

在分析这个装饰器之前,先要补一点python的基础知识。

首先是关于装饰器。如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
In [3]: def a():
...: print 1
...: yield 2
...: print 3
...:

In [4]: a()
Out[4]: <generator object a at 0x10db4b4b0>

In [6]: next(a())
1
Out[6]: 2

当一个函数中有yield之后,在python里面调用这个方法,如上面的In [4]: a(),已经不是直接运行这个方法了,而是返回一个generator。一定要记住,这个时候,这个方法还没有开始执行。实际开始执行这个generator可以使用如上的next驱动他,或者send,具体细节可以参详一些python基础语法书。

再来谈谈装饰器。看一下这个简单的demo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
In [4]: def a(func):
...: def wrapper(*args, **kwargs):
...: print 'a:1'
...: func(*args, **kwargs)
...: print 'a:2'
...: return wrapper
...:

In [5]: @a
...: def b():
...: print 'b'
...:

In [6]: b()
a:1
b
a:2

我们定义了一个装饰器a和使用了装饰器a的方法b。装饰器最主要的作用就是在被装饰的方法前后可以执行一些逻辑。如上,在b执行的前后分别执行了一些装饰器a中的逻辑。

关于生成器和装饰器就讲到这里。基础语法不明白的可以去看看雨痕大叔写的python学习笔记

我们回到tornado.gen.coroutine这个装饰器。这个装饰器调用的逻辑在1处。那么之前之后都做了什么呢?

首先是0处,创建了一个future对象。并在wrapper的最后返回了这个对象。

在1处,调用了被装饰的方法。这里分两种情况,返回的result是一个GeneratorType或者不是。如果不是,那么下面2处的if判断自然不成功,所以不会进入if的逻辑。然后就是直接返回了。如果是一个GeneratorType,那么进入if之后的逻辑,重要的是3处,使用next启动了一个Generator的执行逻辑。

我们来根据最上面我们自己的demo跟踪一下执行逻辑。首先是调用get。当然由于这个get也有@tornado.gen.coroutine装饰器,所以这个装饰器前面的逻辑自然也会执行。到@tornado.gen.coroutine的1处,由于get在原先错误的用法中,没有写yield,所以自然是直接执行这个代码。首先print 'get start',然后调用self.fetch_something()。注意,这个fetch_something也是有@tornado.gen.coroutine装饰器的。所以也要进入这个装饰器的执行逻辑中。但是到1处,因为fetch_something中是有yield的,所以这个result会是一个GeneratorType。这样子,后面2处的if就会进入。然后就到3处,这时,fetch_something就会真正的开始执行。所以就执行到了print 'fetch start'这句。接下来就到了yield http_client.fetch("http://www.baidu.com")这句,因为yield的存在,fetch_something的执行权主动被交出了。

这时,执行逻辑就回到了调用self.fetch_something()的地方。所以接下来执行的就是print 'get over',再接着get方法就执行完了,在tornado中,这个request的连接就被关闭了。

在之后,当http_client.fetch("http://www.baidu.com")真正的抓取到数据之后,执行逻辑被调度回来,执行了接下来的print 'fetch over'。这时,下面的self.write虽然执行了,但是由于连接已经被关闭了,所以写的东西已经写不回去了。