Sanic Server WorkerManager refactor (#2499)

Co-authored-by: Néstor Pérez <25409753+prryplatypus@users.noreply.github.com>
This commit is contained in:
Adam Hopkins
2022-09-18 17:17:23 +03:00
committed by GitHub
parent d352a4155e
commit 4726cf1910
73 changed files with 3929 additions and 940 deletions

View File

@@ -1,10 +1,10 @@
import logging
import os
import shutil
import sys
from argparse import ArgumentParser, RawTextHelpFormatter
from importlib import import_module
from pathlib import Path
from functools import partial
from textwrap import indent
from typing import Any, List, Union
@@ -12,7 +12,8 @@ from sanic.app import Sanic
from sanic.application.logo import get_logo
from sanic.cli.arguments import Group
from sanic.log import error_logger
from sanic.simple import create_simple_server
from sanic.worker.inspector import inspect
from sanic.worker.loader import AppLoader
class SanicArgumentParser(ArgumentParser):
@@ -66,13 +67,17 @@ Or, a path to a directory to run as a simple HTTP server:
instance.attach()
self.groups.append(instance)
def run(self):
# This is to provide backwards compat -v to display version
legacy_version = len(sys.argv) == 2 and sys.argv[-1] == "-v"
parse_args = ["--version"] if legacy_version else None
def run(self, parse_args=None):
legacy_version = False
if not parse_args:
parsed, unknown = self.parser.parse_known_args()
# This is to provide backwards compat -v to display version
legacy_version = len(sys.argv) == 2 and sys.argv[-1] == "-v"
parse_args = ["--version"] if legacy_version else None
elif parse_args == ["-v"]:
parse_args = ["--version"]
if not legacy_version:
parsed, unknown = self.parser.parse_known_args(args=parse_args)
if unknown and parsed.factory:
for arg in unknown:
if arg.startswith("--"):
@@ -80,20 +85,47 @@ Or, a path to a directory to run as a simple HTTP server:
self.args = self.parser.parse_args(args=parse_args)
self._precheck()
app_loader = AppLoader(
self.args.module,
self.args.factory,
self.args.simple,
self.args,
)
try:
app = self._get_app()
app = self._get_app(app_loader)
kwargs = self._build_run_kwargs()
except ValueError:
error_logger.exception("Failed to run app")
except ValueError as e:
error_logger.exception(f"Failed to run app: {e}")
else:
for http_version in self.args.http:
app.prepare(**kwargs, version=http_version)
if self.args.inspect or self.args.inspect_raw or self.args.trigger:
os.environ["SANIC_IGNORE_PRODUCTION_WARNING"] = "true"
else:
for http_version in self.args.http:
app.prepare(**kwargs, version=http_version)
Sanic.serve()
if self.args.inspect or self.args.inspect_raw or self.args.trigger:
action = self.args.trigger or (
"raw" if self.args.inspect_raw else "pretty"
)
inspect(
app.config.INSPECTOR_HOST,
app.config.INSPECTOR_PORT,
action,
)
del os.environ["SANIC_IGNORE_PRODUCTION_WARNING"]
return
if self.args.single:
serve = Sanic.serve_single
elif self.args.legacy:
serve = Sanic.serve_legacy
else:
serve = partial(Sanic.serve, app_loader=app_loader)
serve(app)
def _precheck(self):
# # Custom TLS mismatch handling for better diagnostics
# Custom TLS mismatch handling for better diagnostics
if self.main_process and (
# one of cert/key missing
bool(self.args.cert) != bool(self.args.key)
@@ -113,58 +145,14 @@ Or, a path to a directory to run as a simple HTTP server:
)
error_logger.error(message)
sys.exit(1)
if self.args.inspect or self.args.inspect_raw:
logging.disable(logging.CRITICAL)
def _get_app(self):
def _get_app(self, app_loader: AppLoader):
try:
module_path = os.path.abspath(os.getcwd())
if module_path not in sys.path:
sys.path.append(module_path)
if self.args.simple:
path = Path(self.args.module)
app = create_simple_server(path)
else:
delimiter = ":" if ":" in self.args.module else "."
module_name, app_name = self.args.module.rsplit(delimiter, 1)
if module_name == "" and os.path.isdir(self.args.module):
raise ValueError(
"App not found.\n"
" Please use --simple if you are passing a "
"directory to sanic.\n"
f" eg. sanic {self.args.module} --simple"
)
if app_name.endswith("()"):
self.args.factory = True
app_name = app_name[:-2]
module = import_module(module_name)
app = getattr(module, app_name, None)
if self.args.factory:
try:
app = app(self.args)
except TypeError:
app = app()
app_type_name = type(app).__name__
if not isinstance(app, Sanic):
if callable(app):
solution = f"sanic {self.args.module} --factory"
raise ValueError(
"Module is not a Sanic app, it is a "
f"{app_type_name}\n"
" If this callable returns a "
f"Sanic instance try: \n{solution}"
)
raise ValueError(
f"Module is not a Sanic app, it is a {app_type_name}\n"
f" Perhaps you meant {self.args.module}:app?"
)
app = app_loader.load()
except ImportError as e:
if module_name.startswith(e.name):
if app_loader.module_name.startswith(e.name): # type: ignore
error_logger.error(
f"No module named {e.name} found.\n"
" Example File: project/sanic_server.py -> app\n"
@@ -190,6 +178,7 @@ Or, a path to a directory to run as a simple HTTP server:
elif len(ssl) == 1 and ssl[0] is not None:
# Use only one cert, no TLSSelector.
ssl = ssl[0]
kwargs = {
"access_log": self.args.access_log,
"coffee": self.args.coffee,
@@ -204,6 +193,8 @@ Or, a path to a directory to run as a simple HTTP server:
"verbosity": self.args.verbosity or 0,
"workers": self.args.workers,
"auto_tls": self.args.auto_tls,
"single_process": self.args.single,
"legacy": self.args.legacy,
}
for maybe_arg in ("auto_reload", "dev"):

View File

@@ -30,7 +30,7 @@ class Group:
instance = cls(parser, cls.name)
return instance
def add_bool_arguments(self, *args, **kwargs):
def add_bool_arguments(self, *args, nullable=False, **kwargs):
group = self.container.add_mutually_exclusive_group()
kwargs["help"] = kwargs["help"].capitalize()
group.add_argument(*args, action="store_true", **kwargs)
@@ -38,6 +38,9 @@ class Group:
group.add_argument(
"--no-" + args[0][2:], *args[1:], action="store_false", **kwargs
)
if nullable:
params = {args[0][2:].replace("-", "_"): None}
group.set_defaults(**params)
def prepare(self, args) -> None:
...
@@ -67,7 +70,8 @@ class ApplicationGroup(Group):
name = "Application"
def attach(self):
self.container.add_argument(
group = self.container.add_mutually_exclusive_group()
group.add_argument(
"--factory",
action="store_true",
help=(
@@ -75,7 +79,7 @@ class ApplicationGroup(Group):
"i.e. a () -> <Sanic app> callable"
),
)
self.container.add_argument(
group.add_argument(
"-s",
"--simple",
dest="simple",
@@ -85,6 +89,32 @@ class ApplicationGroup(Group):
"a directory\n(module arg should be a path)"
),
)
group.add_argument(
"--inspect",
dest="inspect",
action="store_true",
help=("Inspect the state of a running instance, human readable"),
)
group.add_argument(
"--inspect-raw",
dest="inspect_raw",
action="store_true",
help=("Inspect the state of a running instance, JSON output"),
)
group.add_argument(
"--trigger-reload",
dest="trigger",
action="store_const",
const="reload",
help=("Trigger worker processes to reload"),
)
group.add_argument(
"--trigger-shutdown",
dest="trigger",
action="store_const",
const="shutdown",
help=("Trigger all processes to shutdown"),
)
class HTTPVersionGroup(Group):
@@ -207,8 +237,22 @@ class WorkerGroup(Group):
action="store_true",
help="Set the number of workers to max allowed",
)
group.add_argument(
"--single-process",
dest="single",
action="store_true",
help="Do not use multiprocessing, run server in a single process",
)
self.container.add_argument(
"--legacy",
action="store_true",
help="Use the legacy server manager",
)
self.add_bool_arguments(
"--access-logs", dest="access_log", help="display access logs"
"--access-logs",
dest="access_log",
help="display access logs",
default=None,
)