关于tornado.gen.coroutine的错误使用带来的bug分析
最近线上出了一个bug,让我来解决一下。大致逻辑是前端的一个ajax请求的response,其中body部分本来应该是一个json,却神秘失踪了。正常返回了200 ok,就是http response的body为空,导致前端处理有问题。经过分析,应该是最后在返回response的body之前,连接被关闭了。
简化逻辑的demo如下:
1 | class MainHandler(tornado.web.RequestHandler): |
原来的逻辑简化就是需要异步的抓取一个东西之后返回。正常的执行逻辑应该如下:
1 | get start |
但是因为第一个yield的缺失,导致执行的逻辑如下:
1 | get start |
也就意味着,在fetch_something
中,yield之后的逻辑在执行的时候,最上层调用的get方法已经执行完毕,这个连接已经被关闭掉了。
问题出在tornado.gen.coroutine
这个装饰器用的姿势不对,应该有的yield给丢了。
扒出tornado的这个装饰器的源码,核心部分如下:
1 | @functools.wraps(func) |
在分析这个装饰器之前,先要补一点python的基础知识。
首先是关于装饰器。如下代码:
1 | In [3]: def a(): |
当一个函数中有yield之后,在python里面调用这个方法,如上面的In [4]: a()
,已经不是直接运行这个方法了,而是返回一个generator。一定要记住,这个时候,这个方法还没有开始执行。实际开始执行这个generator可以使用如上的next驱动他,或者send,具体细节可以参详一些python基础语法书。
再来谈谈装饰器。看一下这个简单的demo:
1 | In [4]: def a(func): |
我们定义了一个装饰器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
虽然执行了,但是由于连接已经被关闭了,所以写的东西已经写不回去了。