Source code for hurricane.management.commands.serve

import asyncio
import functools
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,
)
from hurricane.server.debugging import setup_debugpy, setup_pycharm
from hurricane.webhooks import StartupWebhook
from hurricane.webhooks.base import WebhookStatus


[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 """ 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("--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() 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_debugpy(options) setup_pycharm(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()