Как остановить веб-сервер Tornado?

Я немного поигрался с веб-сервером Tornado и пришел к выводу, что хочу остановить веб-сервер (для пример во время модульного тестирования). Следующий простой пример существует на веб-странице Tornado:

import tornado.ioloop
import tornado.web

class MainHandler(tornado.web.RequestHandler):
    def get(self):
        self.write("Hello, world")

application = tornado.web.Application([
    (r"/", MainHandler),
])

if __name__ == "__main__":
    application.listen(8888)
    tornado.ioloop.IOLoop.instance().start()

После вызова tornado.ioloop.IOLoop.instance().start() он блокирует программу (или текущий поток). Чтение исходного кода для объекта IOLoop дает этот пример в документации для stop функция:

To use asynchronous methods from otherwise-synchronous code (such as
unit tests), you can start and stop the event loop like this:
  ioloop = IOLoop()
  async_method(ioloop=ioloop, callback=ioloop.stop)
  ioloop.start()
ioloop.start() will return after async_method has run its callback,
whether that callback was invoked before or after ioloop.start.

Однако я понятия не имею, как интегрировать это в свою программу. На самом деле у меня есть класс, который инкапсулирует веб-сервер (имеющий собственные функции start и stop), но как только я вызову start, программа (или тесты), конечно, все равно заблокируется.

Я пытался запустить веб-сервер в другом процессе (используя пакет multiprocessing). Это класс, обертывающий веб-сервер:

class Server:
    def __init__(self, port=8888):
        self.application = tornado.web.Application([ (r"/", Handler) ])

        def server_thread(application, port):
            http_server = tornado.httpserver.HTTPServer(application)
            http_server.listen(port)
            tornado.ioloop.IOLoop.instance().start()

        self.process = Process(target=server_thread,
                               args=(self.application, port,))

    def start(self):
        self.process.start()

    def stop(self):
        ioloop = tornado.ioloop.IOLoop.instance()
        ioloop.add_callback(ioloop.stop)

Тем не менее, stop, по-видимому, не полностью останавливает веб-сервер, поскольку он все еще работает в следующем тесте, даже с этой тестовой настройкой:

def setup_method(self, _function):
    self.server = Server()
    self.server.start()
    time.sleep(0.5)  # Wait for web server to start

def teardown_method(self, _function):
    self.kstore.stop()
    time.sleep(0.5)

Как я могу запускать и останавливать веб-сервер Tornado из программы Python?


person Adam Lindberg    schedule 21.03.2011    source источник
comment
Более современный Tornado возвращает HTTP-сервер из app.listen(). у которого есть метод .close() для остановки сервера! См. раздел Остановка приложения торнадо.   -  person ti7    schedule 26.03.2021


Ответы (9)


Я только что столкнулся с этим и сам нашел эту проблему, и, используя информацию из этой темы, пришел к следующему. Я просто взял свой рабочий автономный код Tornado (скопированный из всех примеров) и переместил исходный код в функцию. Затем я назвал функцию потоковым потоком. Мой случай отличается тем, что вызов многопоточности был выполнен из моего существующего кода, где я только что импортировал подпрограммы startTornado и stopTornado.

Приведенное выше предложение, похоже, отлично сработало, поэтому я решил предоставить отсутствующий код примера. Я протестировал этот код под Linux в системе FC16 (и исправил свой первоначальный тип-о).

import tornado.ioloop, tornado.web

class Handler(tornado.web.RequestHandler):
    def get(self):
        self.write("Hello, world")

application = tornado.web.Application([ (r"/", Handler) ])

def startTornado():
    application.listen(8888)
    tornado.ioloop.IOLoop.instance().start()

def stopTornado():
    tornado.ioloop.IOLoop.instance().stop()

if __name__ == "__main__":
    import time, threading
    threading.Thread(target=startTornado).start()
    print "Your web server will self destruct in 2 minutes"
    time.sleep(120)
    stopTornado()

Надеюсь, это поможет следующему человеку.

person Robert Anderon    schedule 08.01.2012
comment
Это не потокобезопасно. Вероятно, проработает до пятничного вечера, когда вы собираетесь отправиться в пятинедельный отпуск. - person Schildmeijer; 07.07.2012
comment
Это не работает с многопроцессорностью. Проблема с методом ioloop.instance.stop() заключается в том, что он работает только в том случае, если вы запускаете его внутри потока, в котором был предыдущий метод ioloop.instance(). Если вы используете многопроцессорность, чтобы обернуть это, это просто не сработает. - person Nisan.H; 03.08.2012
comment
@Schildmeijer, не могли бы вы уточнить, почему и как он может сломаться? Спасибо! - person Zaar Hai; 10.06.2013
comment
Как отмечает Шилмейер, это не является потокобезопасным. Я разместил ответ для сохранения темы ниже. - person Zaar Hai; 26.06.2013
comment
@ZaarHai Можете ли вы уточнить разницу между ответом, безопасным для потоков, который вы опубликовали, и этим? Кажется, вы узнали ответ. - person Brōtsyorfuzthrāx; 13.09.2014
comment
Отличие состоит в том, что поточно-ориентированная версия вызывает IOLoop.add_callback, который является единственным методом для IOLoop, ориентированным на многопотоковое исполнение. Обратите внимание, что даже приведенный ниже потокобезопасный ответ, вероятно, не является отличным решением - вы не хотите использовать несколько потоков, подобных этому, если вам действительно это не нужно. - person Ben Darnell; 25.07.2016
comment
Когда я перехожу к коду для ioloop.py, я вижу ``` def stop(self): .... поднять NotImplementedError() ``` Это работает? - person Ozgur Ozturk; 20.10.2017
comment
IOLoop.instance() устарел в пользу IOLoop.current() из версии 5.0 на Python 3. - person Ninjakannon; 31.10.2019

Вот решение, как остановить Torando из другого потока. Schildmeijer дал хороший намек, но мне потребовалось некоторое время, чтобы понять окончательный работающий пример.

Пожалуйста, смотрите ниже:

import threading
import tornado.ioloop
import tornado.web
import time


class MainHandler(tornado.web.RequestHandler):
    def get(self):
        self.write("Hello, world!\n")

def start_tornado(*args, **kwargs):
    application = tornado.web.Application([
        (r"/", MainHandler),
    ])
    application.listen(8888)
    print "Starting Torando"
    tornado.ioloop.IOLoop.instance().start()
    print "Tornado finished"

def stop_tornado():
    ioloop = tornado.ioloop.IOLoop.instance()
    ioloop.add_callback(ioloop.stop)
    print "Asked Tornado to exit"

def main():

    t = threading.Thread(target=start_tornado)  
    t.start()

    time.sleep(5)

    stop_tornado()
    t.join()

if __name__ == "__main__":
    main()
person Zaar Hai    schedule 26.06.2013
comment
ioloop.add_callback(lambda x: x.stop(), ioloop) или проще: ioloop.add_callback(ioloop.stop) - person ereOn; 24.12.2015
comment
Это предназначено для фактической остановки веб-сервера? Похоже, порт остается открытым. - person rakslice; 14.06.2016

Если вы не хотите возиться с потоками, вы можете поймать сигнал прерывания клавиатуры:

try:
    tornado.ioloop.IOLoop.instance().start()
# signal : CTRL + BREAK on windows or CTRL + C on linux
except KeyboardInterrupt:
    tornado.ioloop.IOLoop.instance().stop()
person Alexandre Nucera    schedule 31.03.2016
comment
В Python 2.7/Windows 10: это полезно: CTRL-BREAK фактически вырвало меня из tornado.ioloop. Тем не менее, это определенно НЕ поднимало KeyboardInterrupt. На самом деле после CTRL-BREAK не было достигнуто заявлений НЕТ. Поэтому, что бы он ни делал, он убивает процесс жестко и быстро. - person Dan H; 21.05.2018
comment
Я полагаю, что SIGINT (Ctrl+C) и SIGBREAK (Ctrl+Break) обрабатываются по-разному. - person Ninjakannon; 25.01.2019

Чтобы остановить весь ioloop, вы просто вызываете метод ioloop.stop после завершения модульного теста. (Помните, что единственным (задокументированным) потокобезопасным методом является ioloop.add_callback, т.е. если модульные тесты выполняются другим потоком, вы можете обернуть вызов остановки в обратный вызов)

Если этого достаточно, чтобы остановить веб-сервер http, вы вызываете https://github.com/facebook/tornado/blob/master/tornado/httpserver.py. .stop() метод

person Schildmeijer    schedule 21.03.2011
comment
Но вызов ioloop.start() блокируется, так как же я могу вызвать ioloop.stop()? Должен ли я запускать ioloop.start() в другом потоке? - person Adam Lindberg; 21.03.2011
comment
Это одно решение. (не забудьте обернуть ioloop.stop обратным вызовом ioloop, чтобы избежать одновременных изменений). Другое решение — остановить ioloop из самого ioloop. - person Schildmeijer; 23.03.2011
comment
Извините, что беспокою вас, но я столкнулся с той же проблемой (я запускаю торнадо в потоке, но не могу его остановить). Я прочитал ваш ответ и комментарии, и я действительно не понял, как это сделать. Не могли бы вы опубликовать несколько строк кода, чтобы проиллюстрировать ваш подход? Спасибо. - person Alik; 26.07.2011
comment
@Konstantin Я думаю, что ответ Роберта Андерсона ниже подробно описывает решение, которое я выбрал. Дайте мне знать, если этого недостаточно. - person Adam Lindberg; 10.01.2012
comment
@Schildmeijer я не могу найти остановку метода в модуле httpsserver, python сообщает то же самое - person jondinham; 02.03.2012

Есть проблема с решением Zaar Hai, а именно то, что оно оставляет сокет открытым. Причина, по которой я искал решение для остановки Tornado, заключается в том, что я запускаю модульные тесты на своем сервере приложений, и мне нужен был способ запуска/остановки сервера между тестами, чтобы иметь четкое состояние (пустой сеанс и т. д.). Оставляя сокет открытым, второй тест всегда приводил к ошибке Address already in use. Итак, я придумал следующее:

import logging as log
from time import sleep
from threading import Thread

import tornado
from tornado.httpserver import HTTPServer


server = None
thread = None


def start_app():
    def start():
        global server
        server = HTTPServer(create_app())
        server.listen(TEST_PORT, TEST_HOST)
        tornado.ioloop.IOLoop.instance().start()
    global thread
    thread = Thread(target=start)
    thread.start()
    # wait for the server to fully initialize
    sleep(0.5)


def stop_app():
    server.stop()
    # silence StreamClosedError Tornado is throwing after it is stopped
    log.getLogger().setLevel(log.FATAL)
    ioloop = tornado.ioloop.IOLoop.instance()
    ioloop.add_callback(ioloop.stop)
    thread.join()

Таким образом, основная идея здесь состоит в том, чтобы сохранить ссылку на экземпляр HTTPServer и вызвать его метод stop(). И create_app() просто возвращает экземпляр Application, сконфигурированный с обработчиками. Теперь вы можете использовать эти методы в своих модульных тестах следующим образом:

class FoobarTest(unittest.TestCase):

    def setUp(self):
        start_app()

    def tearDown(self):
        stop_app()

    def test_foobar(self):
        # here the server is up and running, so you can make requests to it
        pass
person 3k-    schedule 22.09.2016
comment
server.stop() фактически закрывает сокет. Самый полезный ответ здесь для меня. - person sebix; 28.08.2019

Если вам нужно такое поведение для модульного тестирования, взгляните на tornado. тестирование.AsyncTestCase.

По умолчанию для каждого теста создается новый IOLoop, который доступен как self.io_loop. Этот IOLoop следует использовать при построении HTTP-клиентов/серверов и т. д. Если тестируемый код требует глобального IOLoop, подклассы должны переопределить get_new_ioloop, чтобы вернуть его.

Если вам нужно запустить и остановить IOLoop для какой-либо другой цели и по какой-то причине вы не можете вызвать ioloop.stop() из обратного вызова, возможна многопоточная реализация. Однако, чтобы избежать состояния гонки, вам необходимо синхронизировать доступ к ioloop, что на самом деле невозможно. Что-то вроде следующего приведет к взаимоблокировке:

Тема 1:

with lock:
    ioloop.start()

Тема 2:

with lock:
    ioloop.stop()

потому что поток 1 никогда не освободит блокировку (start() блокирует), а поток 2 будет ждать, пока блокировка не будет снята, чтобы остановить iloop.

Единственный способ сделать это для потока 2 — вызвать ioloop.add_callback(ioloop.stop), который вызовет stop() в потоке 1 в следующей итерации цикла событий. Будьте уверены, ioloop.add_callback() потокобезопасен.

person FactualHarmony    schedule 05.02.2014

У Ioloop.instance() Tornado возникают проблемы с остановкой из-за внешнего сигнала при запуске в multiprocessing.Process.

Единственное решение, которое я придумал, которое работает постоянно, — это использование Process.terminate():

import tornado.ioloop, tornado.web
import time
import multiprocessing

class Handler(tornado.web.RequestHandler):
    def get(self):
        self.write("Hello, world")

application = tornado.web.Application([ (r"/", Handler) ])

class TornadoStop(Exception):
    pass
def stop():
    raise TornadoStop
class worker(multiprocessing.Process):
    def __init__(self):
        multiprocessing.Process.__init__(self)
        application.listen(8888)
        self.ioloop = tornado.ioloop.IOLoop.instance()

    def run(self):
        self.ioloop.start()

    def stop(self, timeout = 0):
        self.ioloop.stop()
        time.sleep(timeout)
        self.terminate()



if __name__ == "__main__":

    w = worker()
    print 'starting server'
    w.start()
    t = 2
    print 'waiting {} seconds before stopping'.format(t)
    for i in range(t):
        time.sleep(1)
        print i
    print 'stopping'
    w.stop(1)
    print 'stopped'
person Nisan.H    schedule 03.08.2012

Мы хотим использовать multiprocessing.Process с tornado.ioloop.IOLoop, чтобы обойти cPython GIL для повышения производительности и независимости. Чтобы получить доступ к IOLoop, нам нужно использовать Queue для передачи сигнала выключения.

Вот минималистичный пример:

class Server(BokehServer)

    def start(self, signal=None):
        logger.info('Starting server on http://localhost:%d'
                    % (self.port))

        if signal is not None:
            def shutdown():
                if not signal.empty():
                    self.stop()
            tornado.ioloop.PeriodicCallback(shutdown, 1000).start()

        BokehServer.start(self)
        self.ioloop.start()

    def stop(self, *args, **kwargs):  # args important for signals
        logger.info('Stopping server...')
        BokehServer.stop(self)
        self.ioloop.stop()

Процесс

import multiprocessing as mp
import signal

from server import Server  # noqa

class ServerProcess(mp.Process):
    def __init__(self, *args, **kwargs):
        self.server = Server(*args, **kwargs)
        self.shutdown_signal = _mp.Queue(1)
        mp.Process.__init__(self)

        signal.signal(signal.SIGTERM, self.server.stop)
        signal.signal(signal.SIGINT, self.server.stop)

    def run(self):
        self.server.start(signal=self.shutdown_signal)

    def stop(self):
        self.shutdown_signal.put(True)

if __name__ == '__main__':
    p = ServerProcess()
    p.start()

Ваше здоровье!

person n1nj4    schedule 01.03.2017

Просто добавьте это перед start():

IOLoop.instance().add_timeout(10,IOLoop.instance().stop)

Он зарегистрирует функцию остановки как обратный вызов в цикле и запустит ее через 10 секунд после запуска.

person JulienFr    schedule 10.07.2013