如何编写python龙卷风测试以断言ioloop已关闭?

时间:2019-12-11 16:38:57

标签: python tornado python-asyncio application-shutdown

我有一些代码将拦截SIGTERM / SIGINT,并指示龙卷风ioloop(是asyncio循环的包装器)等待飞行中的请求完成,然后关闭龙卷风ioloop(请参见下文)。

"""signals module provides helper functions for tornado graceful shutdown."""

import asyncio
import functools
import signal
import time

from tornado.httpserver import HTTPServer
from tornado.ioloop import IOLoop

SHUTDOWN_TIMEOUT = 30


def sig_handler(server: HTTPServer, timeout: int, sig, frame):
    """Schedules ioloop shutdown after specified timeout when TERM/INT signals received.

    In-flights tasks running on the asyncio event loop will be given the
    opportunity to finish before the loop is shutdown after specified timeout.

    Expects to be initiated using partial application:
        functools.partial(sig_handler, HTTPServer())
    This partial application is typically handled by signals.sig_listener.
    """

    io_loop = IOLoop.current()

    def stop_loop(deadline):
        now = time.time()
        tasks = asyncio.all_tasks()

        if now < deadline and len(tasks) > 0:
            # defer shutdown until all tasks have a chance to complete
            io_loop.add_timeout(now + 1, stop_loop, deadline)
        else:
            io_loop.stop()

    async def shutdown():
        # stop listening for new connections
        server.stop()

        # schedule ioloop shutdown
        stop_loop(time.time() + timeout)

    # execute callback on next event loop tick
    io_loop.add_callback_from_signal(shutdown)


def sig_listener(server: HTTPServer, timeout: int = 0):
    """Configures listeners for TERM/INT signals.

    Timeout should be a positive integer, otherwise a default will be provided.
    """

    if not timeout:
        timeout = SHUTDOWN_TIMEOUT

    p = functools.partial(sig_handler, server, timeout)
    signal.signal(signal.SIGTERM, p)
    signal.signal(signal.SIGINT, p)

此代码(见上文)工作正常,我已经对其进行了手动测试,但我不知道如何使用AsyncHTTPTestCase在自动化测试中实现同一目的,因为这会使异步请求同步+我们要关闭的ioloop与测试将要运行的ioloop相同。

以下代码是我目前拥有的...

import asyncio
import os
import signal
import threading
import time

import bf_metrics

import bf_tornado.handlers
import bf_tornado.signals

import tornado.gen
import tornado.ioloop
import tornado.testing
import tornado.web


class TestGracefulShutdown(tornado.testing.AsyncHTTPTestCase):
    def get_app(self):
        class FooHandler(bf_tornado.handlers.BaseHandler):
            metrics = bf_metrics.Metrics(namespace='foo', host='localhost')

            def get(self):
                asyncio.sleep(5)
                self.finish('OK')

        return tornado.web.Application([
            (r'/', FooHandler)
        ])

    def test_graceful_shutdown(self):
        # override AsyncHTTPTestCase default timeout of 5s
        os.environ["ASYNC_TEST_TIMEOUT"] = "10"

        shutdown_timeout = 8
        bf_tornado.signals.sig_listener(self.http_server, shutdown_timeout)

        pid = os.getpid()

        def trigger_signal():
            # defer SIGNINT long enough to allow HTTP request to tornado server
            time.sleep(2)
            os.kill(pid, signal.SIGINT)

        thread = threading.Thread(target=trigger_signal)
        thread.daemon = True
        thread.start()

        resp = self.fetch('/')
        assert resp.code == 200

        # ???
        #
        # WHAT ASSERTION DO WE USE HERE?
        #
        # WE CAN'T CHECK tornado.ioloop.IOLoop.current()
        # BECAUSE IT'LL STILL BE RUNNING AS PART OF THIS TEST.
        #
        # MAYBE WE COULD TEST asyncio.all_tasks() ? BUT WE WANT TO BE ABLE TO
        # SAY CONCLUSIVELY THAT THE IOLOOP WAS SHUTDOWN.
        #
        # ???

1 个答案:

答案 0 :(得分:0)

我认为您可以使用AsyncHTTPTestCase进行测试,因为事件循环的开始和停止对您而言是隐藏的(如果您不使用@gen_test,则IOLoop在大多数情况下已停止如果您确实使用@gen_test,则有一个IOLoop一直在运行,但是如果您停止它,则测试只会挂起。为了测试IOLoop是否停止,您的代码必须是调用IOLoop.start()的东西。

测试这种启动和关闭问题的最全面方法是将服务器作为子进程运行。这样,您既可以发送HTTP请求也可以发送信号,并看到该进程已退出。

在当前过程中,还有许多方法可以实现,但要在现实与性能之间进行各种折衷。例如,如下所示:

def test_graceful_shutdown(self):
    def trigger_signal():
        time.sleep(2)
        os.kill(os.getpid(), signal.SIGINT)
    thread = threading.Thread(target=trigger_signal)
    thread.start()

    io_loop = IOLoop()
    install_signal_handler(io_loop)
    def failure():
        self.fail("did not shut down within 5 seconds")
        io_loop.stop()
    io_loop.call_after(5, failure)
    io_loop.start()