Source code for hurricane.management.commands.serve

import asyncio
import functools
import os
import signal
import time
from concurrent.futures.thread import ThreadPoolExecutor

import tornado.autoreload
import tornado.web
import tornado.wsgi
from django.core.management.base import BaseCommand

from hurricane.server import (
    check_db_and_migrations,
    command_task,
    logger,
    make_http_server_and_listen,
    make_probe_server,
    sanitize_probes,
    static_watch,
)
from hurricane.server.debugging import setup_debugging


[docs]class Command(BaseCommand): """ Start a Tornado-powered Django web server by using ``python manage.py serve <arguments>``. It can run Django management commands with the ``--command`` flag, that will be executed asynchronously. The application server will only be started upon successful execution of management commands. During execution of management commands the startup probe responds with a status 400. Arguments: - ``--static`` - serve collected static files - ``--media`` - serve media files - ``--autoreload`` - reload code on change - ``--debug`` - set Tornado's Debug flag - ``--port`` - the port for Tornado to listen on - ``--startup-probe`` - the exposed path (default is /startup) for probes to check startup - ``--readiness-probe`` - the exposed path (default is /ready) for probes to check readiness - ``--liveness-probe`` - the exposed path (default is /alive) for probes to check liveness - ``--probe-port`` - the port for Tornado probe route to listen on - ``--req-queue-len`` - threshold of length of queue of request, which is considered for readiness probe - ``--no-probe`` - disable probe endpoint - ``--no-metrics`` - disable metrics collection - ``--command`` - repetitive command for adding execution of management commands before serving - ``--check-migrations`` - check if all migrations were applied before starting application - ``--webhook-url``- If specified, webhooks will be sent to this url - ``--max-lifetime``- If specified, maximum requests after which pod is restarted - ``--static-watch`` - If specified, static files will be watched for changes and recollected """ help = "Start a Tornado-powered Django web server"
[docs] def add_arguments(self, parser): """ Defines arguments, that can be accepted with ``serve`` command. """ parser.add_argument("--static", action="store_true", help="Serve collected static files") parser.add_argument("--media", action="store_true", help="Serve media files") parser.add_argument("--autoreload", action="store_true", help="Reload code on change") parser.add_argument("--debug", action="store_true", help="Set Tornado's Debug flag") parser.add_argument("--port", type=int, default=8000, help="The port for Tornado to listen on") parser.add_argument( "--liveness-probe", type=str, default="/alive", help="The exposed path (default is /alive) for probes to check liveness", ) parser.add_argument( "--readiness-probe", type=str, default="/ready", help="The exposed path (default is /ready) for probes to check readiness", ) parser.add_argument( "--startup-probe", type=str, default="/startup", help="The exposed path (default is /startup) for probes to check startup", ) parser.add_argument( "--probe-port", type=int, help="The port for Tornado probe route to listen on", ) parser.add_argument("--req-queue-len", type=int, default=10, help="Length of the request queue") parser.add_argument("--no-probe", action="store_true", help="Disable probe endpoint") parser.add_argument("--no-metrics", action="store_true", help="Disable metrics collection") parser.add_argument("--command", type=str, action="append", nargs="+") parser.add_argument("--check-migrations", action="store_true", help="Check if migrations were applied") parser.add_argument( "--debugger", action="store_true", help="Open a debugger port according to the Debug Adapter Protocol" ) parser.add_argument("--debugger-port", type=int, default=5678, help="The port for the debug client to attach") parser.add_argument( "--webhook-url", type=str, help="Url for webhooks", ) parser.add_argument( "--max-lifetime", type=int, default=None, help="Maximum requests after which pod is restarted" ) parser.add_argument( "--static-watch", type=str, action="append", help="Watch files and run collectstatic if any file changed" ) parser.add_argument("--pycharm-host", type=str, default=None, help="The host of the pycharm debug server") parser.add_argument("--pycharm-port", type=int, default=None, help="The port of the pycharm debug server")
[docs] def handle(self, *args, **options): """ Defines functionalities for different arguments. After all arguments were processed, it starts the async event loop. """ start_time = time.time() logger.info("Tornado-powered Django web server") if options["autoreload"]: tornado.autoreload.start() if options["static_watch"] and len(options["static_watch"]): logger.info("Watching static files for any changes") for path in options["static_watch"]: if os.path.exists(path): logger.info("Watching path {}.".format(path)) tornado.autoreload.watch(path) else: logger.error("Tried to watch {}, but it does not exist.".format(path)) tornado.autoreload.add_reload_hook(static_watch) logger.info("Autoreload was performed") # set the probe port # the probe port by default is supposed to run the next port of the application probe_port = options["probe_port"] if options["probe_port"] else options["port"] + 1 # sanitize probes: returns regexps for probes in options and their representations for logging options, probe_representations = sanitize_probes(options) # set the probe routes # if probe port is set to the application's port, add it to the application's routes include_probe = False if not options["no_probe"]: if probe_port != options["port"]: logger.info( f"Starting probe application running on port {probe_port} with route liveness-probe: " f"{probe_representations['liveness_probe']}, " f"readiness-probe: {probe_representations['readiness_probe']}, " f"startup-probe: {probe_representations['startup_probe']}" ) probe_application = make_probe_server(options, self.check) probe_application.listen(probe_port) else: include_probe = True logger.info( f"Starting probe application with routes " f"liveness-probe: {probe_representations['liveness_probe']}, " f"readiness-probe: {probe_representations['readiness_probe']}, " f"startup-probe: {probe_representations['startup_probe']} " f"running integrated on port {probe_port}" ) else: logger.info("No probe application running") setup_debugging(options) loop = asyncio.get_event_loop() make_http_server_wrapper = functools.partial( make_http_server_and_listen, start_time=start_time, options=options, check=self.check, include_probe=include_probe, ) # all commands, that should be executed before starting http server should be added to this list exec_list = [] if options["command"]: # wrap function to use additional variables management_commands_wrapper = functools.partial( command_task, commands=options["command"], webhook_url=options["webhook_url"] or None, loop=loop, ) exec_list.append(management_commands_wrapper) if options["check_migrations"]: check_db_and_migrations_wrapper = functools.partial( check_db_and_migrations, webhook_url=options["webhook_url"] or None, loop=loop ) exec_list.append(check_db_and_migrations_wrapper) def bundle_func(exec_list, loop, make_http_server_wrapper): # executes functions from exec_list sequentially for func in exec_list: func(*args) # after all functions were executed http server is started in the main thread # call_soon_threadsafe puts http server into the main loop of the program loop.call_soon_threadsafe(make_http_server_wrapper) executor = ThreadPoolExecutor(max_workers=1) # bundle_func is executed in a separate thread. Main thread has an active probe server, which should not be # interrupted loop.run_in_executor(executor, bundle_func, exec_list, loop, make_http_server_wrapper) def ask_exit(signame): logger.info(f"Received signal {signame}. Shutting down now.") loop.stop() for signame in ("SIGINT", "SIGTERM"): loop.add_signal_handler(getattr(signal, signame), functools.partial(ask_exit, signame)) loop.run_forever()