一次tornado **list changed size during iteration**异常的处理

在使用tornado作为mqttconsumer来订阅消息,当接收到消息后使用tornado提供的AsyncHTTPClient进行异步HTTP请求的时候出现list changed size during iteration的异常

异常信息如下:

Exception in thread Tornado:
Traceback (most recent call last):
 ...
       tornado.ioloop.IOLoop.instance().start()
    File "/usr/lib64/python2.7/site-packages/tornado/ioloop.py", line 815, in start heapq.heappop(self._timeouts)
RuntimeError: list changed size during iteration

依赖的组件

  • tornado ==4.4.2
  • paho-mqtt==1.3.1

修改前

问题代码如下:

client = mqtt.Client(protocol=mqtt.MQTTv31)
 
def on_connect(client, userdata, flags, rc):
    
    gen_log.debug("Connected with result code " + str(rc))
    from threading import current_thread
    gen_log.debug('----------on_connect thread name:{}'.format(current_thread().getName()))     
    client.subscribe("chinasws")     
 
@coroutine
def on_message(client, userdata, msg):

    from threading import current_thread
    gen_log.debug('----------on_message thread name:{}'.format(current_thread().getName()))
    
    def on_response(response):
        gen_log.debug("on_response running result:{} message:{}".format(response.code,msg.payload))
        
    http_client=AsyncHTTPClient()
    http_request=HTTPRequest('http://192.168.2.40/?body={}'.format(random.randint(1,100000)))
    response=yield http_client.fetch(request=http_request)
    gen_log.debug("on_response running result:{} message:{}".format(response.code,msg.payload))


class PubMessageHandler(RequestHandler):
    
    @coroutine
    def get(self):
        topic=self.get_argument('topic')
        body=self.get_argument('body')
        #info=yield Task(client.publish,topic,payload=body,qos=1,retain=True)
        info=client.publish(topic,payload=body+'{}'.format(random.randint(1,10000)),qos=1,retain=True)
        self.write(str(info.mid))


@coroutine
def mqtt_subscribe():
    #host="192.168.2.40"
    host='iot.eclipse.org'
    gen_log.debug('connecting to {}'.format(host))
    #client.connect_async("iot.eclipse.org", 1883, 60)
    #client.connect("iot.eclipse.org", 1883, 60)
    client.connect(host, 1883, 60)
    client.on_connect = on_connect
    client.on_message = on_message
    gen_log.debug('connected to {}'.format(host))
    client.loop_start()

 def main():
    parse_command_line()
    handlers=[
        (r'/pub',PubMessageHandler),
    ]
    setting={
        'debug':True
    }
    from threading import current_thread
    gen_log.debug(current_thread().getName())
    app=Application(handlers=handlers,**setting)
    app.listen(11108)
    ioloop=IOLoop.current()
    ioloop.add_callback(mqtt_subscribe)
    gen_log.debug('server running at 11108')
    ioloop.start()

if __name__ == '__main__':
    main()

测试方法

python2.7 app.py --logging=debug #运行tornado服务器

输出

george@george-HP-Z230-SFF-Workstation:~/workspace/mqttdemo$ python2.7 app.py --logging=debug
[D 171113 08:51:38 app:100] MainThread
[D 171113 08:51:38 app:105] server running at 11108
[D 171113 08:51:38 app:64] connecting to 192.168.2.40
[D 171113 08:51:38 app:70] connected to 192.168.2.40
[D 171113 08:51:38 app:22] Connected with result code 0
[D 171113 08:51:38 app:24] ----------on_connect thread name:Thread-1
[D 171113 08:51:38 app:38] ----------on_message thread name:Thread-1
[D 171113 08:51:38 app:46] on_response running result:200 message:efgh9023

curl http://localhost:11108/pub?topic=chinasws\&body=aaaa #需进行压力测试话可以使用`ab`或`jmeter`等工具

在配置比较差的服务器上进行压力测试很容易出现list changed size during iteration的异常,通过观察服务器启动的输出发现 在on_connecton_message,两个方法是在线程Thread-1中运行,在on_message使用了tornado提供的AyncHTTPClient,而IOLoop是在线程MainThread中运行,结合https://github.com/tornadoweb/tornado/issues/1773的描述,基本断定是多线程互操作heapq导致的异常.

tornado推荐的用法是单线程事件循环的工作模式,所以解决该问题的思路是取消多线程的模式,利用tornado本身的事件循环来完成mqtt的订阅

修改后

代码如下

client = mqtt.Client(protocol=mqtt.MQTTv31)

# The callback for when the client receives a CONNACK response from the server.


def on_connect(client, userdata, flags, rc):
    
    gen_log.debug("Connected with result code " + str(rc))
    from threading import current_thread
    gen_log.debug('----------on_connect thread name:{}'.format(current_thread().getName()))
    # Subscribing in on_connect() means that if we lose the connection and
    # reconnect then subscriptions will be renewed.
    client.subscribe("chinasws")
    

    #client.subscribe("$SYS/#")

# The callback for when a PUBLISH message is received from the server.

@coroutine
def on_message(client, userdata, msg):

    from threading import current_thread
    gen_log.debug('----------on_message thread name:{}'.format(current_thread().getName()))
    gen_log.debug('----------on_message message body:{}'.format(msg.payload))
    
    def on_response(response):
        gen_log.debug("on_response running result:{} message:{}".format(response.code,msg.payload))
        
    http_client=AsyncHTTPClient()
    http_request=HTTPRequest('http://192.168.2.40/?body={}'.format(random.randint(1,100000)))
    response=yield http_client.fetch(request=http_request)
    gen_log.debug("on_response running result:{} message:{}".format(response.code,msg.payload))


class PubMessageHandler(RequestHandler):
    
    @coroutine
    def get(self):
        topic=self.get_argument('topic')
        body=self.get_argument('body')
        #info=yield Task(client.publish,topic,payload=body,qos=1,retain=True)
        #info=client.publish(topic,payload=body+'{}'.format(random.randint(1,10000)),qos=1,retain=True)
        info=client.publish(topic,payload=body+'{}'.format(random.randint(1,10000)))
        self.write(str(info.mid))


@coroutine
def mqtt_subscribe():
    host="192.168.2.40"
    #host='iot.eclipse.org'
    gen_log.debug('connecting to {}'.format(host))
    #client.connect_async("iot.eclipse.org", 1883, 60)
    #client.connect("iot.eclipse.org", 1883, 60)
    client.connect(host, 1883, 60)
    client.on_connect = on_connect
    client.on_message = on_message
    gen_log.debug('connected to {}'.format(host))
    #client.loop_start() 
    #关键修改
    def connection_read(fd,events):
        client.loop()    
    io_loop=IOLoop.current()
    io_loop.add_handler(client.socket().fileno(),connection_read,IOLoop.READ|IOLoop.WRITE)

    


def main():
    parse_command_line()
    handlers=[
        (r'/pub',PubMessageHandler),
    ]
    setting={
        'debug':True
    }
    from threading import current_thread
    gen_log.debug(current_thread().getName())
    app=Application(handlers=handlers,**setting)
    app.listen(11108)
    ioloop=IOLoop.current()
    ioloop.add_callback(mqtt_subscribe)
    gen_log.debug('server running at 11108')
    ioloop.start()

if __name__ == '__main__':
    main()

关键点

注释了mqtt loop_start的调用,增加了tornado 的事件注册

def connection_read(fd,events):
        client.loop()    
    io_loop=IOLoop.current()
    io_loop.add_handler(client.socket().fileno(),connection_read,IOLoop.READ|IOLoop.WRITE)
python2.7 app.py --logging=debug 

输出

george@george-HP-Z230-SFF-Workstation:~/workspace/mqttdemo$ python2.7 app.py --logging=debug
[D 171113 09:40:43 app:102] MainThread
[D 171113 09:40:43 app:107] server running at 11108
[D 171113 09:40:43 app:66] connecting to 192.168.2.40
[D 171113 09:40:43 app:72] connected to 192.168.2.40
[D 171113 09:40:43 app:22] Connected with result code 0
[D 171113 09:40:43 app:24] ----------on_connect thread name:MainThread
[D 171113 09:40:44 app:38] ----------on_message thread name:MainThread
[D 171113 09:40:44 app:39] ----------on_message message body:efgh1068
[I 171113 09:40:48 web:2063] 200 GET /pub?topic=chinasws&body=efgh (127.0.0.1) 2.01ms
[D 171113 09:40:48 app:38] ----------on_message thread name:MainThread
[D 171113 09:40:48 app:39] ----------on_message message body:efgh2680
[D 171113 09:40:49 app:47] on_response running result:200 message:efgh1068
[D 171113 09:40:55 app:47] on_response running result:200 message:efgh2680

通过调整代码后,从输出可以看出on_connecton_message都运行在线程MainThread中,修改后的代码在配置低的服务器上进行大量的压力测试未出现list changed size duration iternation的异常,至此解决了该问题

小结

tornado是一个比较灵活的应用程序框架,开发web站点或网络应用程序等都有相关模块提供支持,但是某些第三方的功能(如mqtt)需要第三方库(如paho-mqtt)进行支持,在集成第三方库的时候需要比较小心处理,有些第三方库本身没有提供对tornado的支持或官方文档没有相关指导说明,集成时要小心线程间的互操作问题。在本文的案例中如果在on_message中使用requests库放弃AsyncHTTPClient那么就不会出现list changed size duration iternation的异常。另外一个项目https://github.com/hulingfeng211/sshwall.git也是集成了paramiko,利用tornado实现webssh的功能的案例.

转载于:https://my.oschina.net/hulingfeng/blog/1572573

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值