From 7f682cea02003f8d1d6f9fc6872d30bf259ef166 Mon Sep 17 00:00:00 2001 From: Adam Hopkins Date: Sun, 11 Dec 2022 10:59:30 +0200 Subject: [PATCH] Add ordering config --- sanic/compat.py | 26 ++++++++++++++++++++++++++ sanic/config.py | 8 +++++++- sanic/constants.py | 28 +++++++++++----------------- sanic/log.py | 16 ++-------------- sanic/mixins/startup.py | 1 + sanic/worker/manager.py | 12 +++++++++++- sanic/worker/process.py | 27 ++++++++++++++++++++++----- 7 files changed, 80 insertions(+), 38 deletions(-) diff --git a/sanic/compat.py b/sanic/compat.py index 4ea2ed91..7bb002bc 100644 --- a/sanic/compat.py +++ b/sanic/compat.py @@ -3,6 +3,7 @@ import os import signal import sys +from enum import Enum from typing import Awaitable from multidict import CIMultiDict # type: ignore @@ -19,6 +20,31 @@ except ImportError: pass +# Python 3.11 changed the way Enum formatting works for mixed-in types. +if sys.version_info < (3, 11, 0): + + class StrEnum(str, Enum): + pass + +else: + from enum import StrEnum # type: ignore # noqa + + +class UpperStrEnum(StrEnum): + def _generate_next_value_(name, start, count, last_values): + return name.upper() + + def __eq__(self, value: object) -> bool: + value = str(value).upper() + return super().__eq__(value) + + def __hash__(self) -> int: + return hash(self.value) + + def __str__(self) -> str: + return self.value + + def enable_windows_color_support(): import ctypes diff --git a/sanic/config.py b/sanic/config.py index dc14d710..79d81701 100644 --- a/sanic/config.py +++ b/sanic/config.py @@ -8,7 +8,7 @@ from pathlib import Path from typing import Any, Callable, Dict, Optional, Sequence, Union from warnings import filterwarnings -from sanic.constants import LocalCertCreator +from sanic.constants import LocalCertCreator, RestartOrder from sanic.errorpages import DEFAULT_FORMAT, check_error_format from sanic.helpers import Default, _default from sanic.http import Http @@ -63,6 +63,7 @@ DEFAULT_CONFIG = { "REQUEST_MAX_SIZE": 100000000, # 100 megabytes "REQUEST_TIMEOUT": 60, # 60 seconds "RESPONSE_TIMEOUT": 60, # 60 seconds + "RESTART_ORDER": RestartOrder.SHUTDOWN_FIRST, "TLS_CERT_PASSWORD": "", "TOUCHUP": _default, "USE_UVLOOP": _default, @@ -110,6 +111,7 @@ class Config(dict, metaclass=DescriptorMeta): REQUEST_MAX_SIZE: int REQUEST_TIMEOUT: int RESPONSE_TIMEOUT: int + RESTART_ORDER: Union[str, RestartOrder] SERVER_NAME: str TLS_CERT_PASSWORD: str TOUCHUP: Union[Default, bool] @@ -194,6 +196,10 @@ class Config(dict, metaclass=DescriptorMeta): self.LOCAL_CERT_CREATOR = LocalCertCreator[ self.LOCAL_CERT_CREATOR.upper() ] + elif attr == "RESTART_ORDER" and not isinstance( + self.RESTART_ORDER, RestartOrder + ): + self.RESTART_ORDER = RestartOrder[self.RESTART_ORDER.upper()] elif attr == "DEPRECATION_FILTER": self._configure_warnings() diff --git a/sanic/constants.py b/sanic/constants.py index 988d8bae..74ce2403 100644 --- a/sanic/constants.py +++ b/sanic/constants.py @@ -1,19 +1,9 @@ -from enum import Enum, auto +from enum import auto + +from sanic.compat import UpperStrEnum -class HTTPMethod(str, Enum): - def _generate_next_value_(name, start, count, last_values): - return name.upper() - - def __eq__(self, value: object) -> bool: - value = str(value).upper() - return super().__eq__(value) - - def __hash__(self) -> int: - return hash(self.value) - - def __str__(self) -> str: - return self.value +class HTTPMethod(UpperStrEnum): GET = auto() POST = auto() @@ -24,15 +14,19 @@ class HTTPMethod(str, Enum): DELETE = auto() -class LocalCertCreator(str, Enum): - def _generate_next_value_(name, start, count, last_values): - return name.upper() +class LocalCertCreator(UpperStrEnum): AUTO = auto() TRUSTME = auto() MKCERT = auto() +class RestartOrder(UpperStrEnum): + + SHUTDOWN_FIRST = auto() + STARTUP_FIRST = auto() + + HTTP_METHODS = tuple(HTTPMethod.__members__.values()) SAFE_HTTP_METHODS = (HTTPMethod.GET, HTTPMethod.HEAD, HTTPMethod.OPTIONS) IDEMPOTENT_HTTP_METHODS = ( diff --git a/sanic/log.py b/sanic/log.py index f6781e6d..119379e6 100644 --- a/sanic/log.py +++ b/sanic/log.py @@ -1,22 +1,10 @@ import logging import sys -from enum import Enum -from typing import TYPE_CHECKING, Any, Dict +from typing import Any, Dict from warnings import warn -from sanic.compat import is_atty - - -# Python 3.11 changed the way Enum formatting works for mixed-in types. -if sys.version_info < (3, 11, 0): - - class StrEnum(str, Enum): - pass - -else: - if not TYPE_CHECKING: - from enum import StrEnum +from sanic.compat import StrEnum, is_atty LOGGING_CONFIG_DEFAULTS: Dict[str, Any] = dict( # no cov diff --git a/sanic/mixins/startup.py b/sanic/mixins/startup.py index 78abb884..43c1aaac 100644 --- a/sanic/mixins/startup.py +++ b/sanic/mixins/startup.py @@ -814,6 +814,7 @@ class StartupMixin(metaclass=SanicMeta): cls._get_context(), (monitor_pub, monitor_sub), worker_state, + primary.config.RESTART_ORDER, ) if cls.should_auto_reload(): reload_dirs: Set[Path] = primary.state.reload_dirs.union( diff --git a/sanic/worker/manager.py b/sanic/worker/manager.py index bba4ecaf..f013e8b7 100644 --- a/sanic/worker/manager.py +++ b/sanic/worker/manager.py @@ -5,6 +5,7 @@ from signal import signal as signal_func from typing import List, Optional from sanic.compat import OS_IS_WINDOWS +from sanic.constants import RestartOrder from sanic.exceptions import ServerKilled from sanic.log import error_logger, logger from sanic.worker.process import ProcessState, Worker, WorkerProcess @@ -28,6 +29,7 @@ class WorkerManager: context, monitor_pubsub, worker_state, + restart_order: RestartOrder, ): self.num_server = number self.context = context @@ -37,6 +39,7 @@ class WorkerManager: self.worker_state = worker_state self.worker_state[self.MAIN_IDENT] = {"pid": self.pid} self.terminated = False + self.restart_order = restart_order if number == 0: raise RuntimeError("Cannot serve with no workers") @@ -55,7 +58,14 @@ class WorkerManager: def manage(self, ident, func, kwargs, transient=False): container = self.transient if transient else self.durable container.append( - Worker(ident, func, kwargs, self.context, self.worker_state) + Worker( + ident, + func, + kwargs, + self.context, + self.worker_state, + self.restart_order, + ) ) def run(self): diff --git a/sanic/worker/process.py b/sanic/worker/process.py index d19e3cd4..09e12b95 100644 --- a/sanic/worker/process.py +++ b/sanic/worker/process.py @@ -7,6 +7,7 @@ from signal import SIGINT from threading import Thread from typing import Any, Dict, Set +from sanic.constants import RestartOrder from sanic.log import Colors, logger @@ -28,13 +29,22 @@ class ProcessState(IntEnum): class WorkerProcess: SERVER_LABEL = "Server" - def __init__(self, factory, name, target, kwargs, worker_state): + def __init__( + self, + factory, + name, + target, + kwargs, + worker_state, + restart_order: RestartOrder, + ): self.state = ProcessState.IDLE self.factory = factory self.name = name self.target = target self.kwargs = kwargs self.worker_state = worker_state + self.restart_order = restart_order if self.name not in self.worker_state: self.worker_state[self.name] = { "server": self.SERVER_LABEL in self.name @@ -96,8 +106,11 @@ class WorkerProcess: self.name, self.pid, ) - self._old_process = self._current_process self.set_state(ProcessState.RESTARTING, force=True) + if self.restart_order is RestartOrder.SHUTDOWN_FIRST: + self._current_process.terminate() + else: + self._old_process = self._current_process self.kwargs.update( {"config": {k.upper(): v for k, v in kwargs.items()}} ) @@ -107,8 +120,9 @@ class WorkerProcess: except AttributeError: raise RuntimeError("Restart failed") - termination_thread = Thread(target=self.wait_to_terminate) - termination_thread.start() + if self.restart_order is RestartOrder.STARTUP_FIRST: + termination_thread = Thread(target=self.wait_to_terminate) + termination_thread.start() self.worker_state[self.name] = { **self.worker_state[self.name], @@ -118,7 +132,7 @@ class WorkerProcess: } def wait_to_terminate(self): - # TODO: Add a timeout + # TODO: Add a timeout? while self.state is not ProcessState.ACKED: ... else: @@ -163,6 +177,7 @@ class Worker: server_settings, context: BaseContext, worker_state: Dict[str, Any], + restart_order: RestartOrder, ): self.ident = f"{self.WORKER_PREFIX}{ident}" self.context = context @@ -170,6 +185,7 @@ class Worker: self.server_settings = server_settings self.worker_state = worker_state self.processes: Set[WorkerProcess] = set() + self.restart_order = restart_order self.create_process() def create_process(self) -> WorkerProcess: @@ -179,6 +195,7 @@ class Worker: target=self.serve, kwargs={**self.server_settings}, worker_state=self.worker_state, + restart_order=self.restart_order, ) self.processes.add(process) return process