Forked from wonderbeyond/graceful_shutdown_tornado_web_server.py
Last active
February 25, 2021 14:59
-
-
Save rekolae/4522ce70d4d6a4b7a7d6da135e7efdd2 to your computer and use it in GitHub Desktop.
The example to how to shutdown tornado web server gracefully...
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| """ | |
| Initializes and starts a server and listens to requests on a configured port. Performs a graceful shutdown | |
| when a terminate or keyboardInterrupt signal is caught. | |
| """ | |
| # STD lib imports | |
| import sys | |
| import time | |
| import signal | |
| import asyncio | |
| import logging | |
| from functools import partial | |
| # 3rd-party lib imports | |
| import tornado.httpserver | |
| import tornado.ioloop | |
| import tornado.options | |
| import tornado.web | |
| from tornado.options import define, options | |
| define("ip", default="127.0.0.1", help="IP address of server", type=str) | |
| define("port", default=8888, help="Run on the given port", type=int) | |
| MAX_WAIT_SECONDS_BEFORE_SHUTDOWN = 3 | |
| class MainHandler(tornado.web.RequestHandler): | |
| def get(self): | |
| self.write("Hello world!") | |
| def sig_handler(application: tornado.httpserver.HTTPServer, sig: signal, _frame): | |
| """ | |
| Called when an appropriate signal was detected, handles shutting down the running tornado web application. | |
| The function parameter "_frame" is not used for anything, because it contains stack execution info etc | |
| that is not relevant in this case -> ignore it. | |
| :param application: tornado.httpserver.HTTPServer object | |
| :param sig: Signal that is being handled | |
| :param _frame: Current stack frame | |
| """ | |
| # Get current IOLoop | |
| io_loop = tornado.ioloop.IOLoop.instance() | |
| def stop_loop(app: tornado.httpserver.HTTPServer, deadline: float): | |
| """ | |
| Stop the IOLoop after all connections are closed or after deadline was reached (even though there | |
| still might be active connections). | |
| :param app: tornado.httpserver.HTTPServer object | |
| :param deadline: deadline for shutting down the application | |
| """ | |
| now = time.time() | |
| # Check if there are tasks still in queue | |
| tasks = [t for t in asyncio.all_tasks() if t is not asyncio.current_task() and not t.done()] | |
| if now < deadline and len(tasks) > 0: | |
| logging.info("Awaiting %s pending tasks: %s", len(tasks), tasks) | |
| io_loop.add_timeout(now + 1, stop_loop, app, deadline) | |
| return | |
| # Check if there are active connections to the application | |
| pending_connections = len(app._connections) | |
| if now < deadline and pending_connections > 0: | |
| logging.info("Waiting on %s connections to complete", pending_connections) | |
| io_loop.add_timeout(now + 1, stop_loop, app, deadline) | |
| else: | |
| logging.warning("Continuing with %s connections open", pending_connections) | |
| logging.info("Stopping IOLoop") | |
| io_loop.stop() | |
| logging.info("Shutdown complete") | |
| def shutdown(): | |
| """ | |
| Start shutdown sequence for the application | |
| """ | |
| logging.info(f"Shutting down within %s seconds", MAX_WAIT_SECONDS_BEFORE_SHUTDOWN) | |
| try: | |
| stop_loop(application, time.time() + MAX_WAIT_SECONDS_BEFORE_SHUTDOWN) | |
| except BaseException as error: | |
| logging.exception("Error while trying to shutdown Tornado") | |
| raise error | |
| logging.warning('Caught signal: %s', signal.Signals(sig).name) | |
| # Add callback to shutdown function | |
| io_loop.add_callback_from_signal(shutdown) | |
| def main() -> int: | |
| """ | |
| Main function that initializes and starts the application | |
| :return: Return code | |
| """ | |
| # Setup logging | |
| logging.basicConfig(level=logging.INFO) | |
| logging.info("Starting server at %s:%s", options.ip, options.port) | |
| # Initialize server | |
| tornado.options.parse_command_line() | |
| application = tornado.web.Application([ | |
| (r"/", MainHandler), | |
| ]) | |
| server = tornado.httpserver.HTTPServer(application) | |
| server.listen(options.port) | |
| # Catch termination (SIGTERM) and keyboard interrupt (SIGINT) signals and pass them to the signal handler | |
| partial_sig_handler = partial(sig_handler, server) | |
| signal.signal(signal.SIGTERM, partial_sig_handler) | |
| signal.signal(signal.SIGINT, partial_sig_handler) | |
| # Start the event loop for the tornado application | |
| tornado.ioloop.IOLoop.current().start() | |
| return 0 | |
| if __name__ == "__main__": | |
| sys.exit(main()) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment