Add ordering config

This commit is contained in:
Adam Hopkins 2022-12-11 10:59:30 +02:00
parent ae1669cd8f
commit 7f682cea02
No known key found for this signature in database
GPG Key ID: 9F85EE6C807303FB
7 changed files with 80 additions and 38 deletions

View File

@ -3,6 +3,7 @@ import os
import signal import signal
import sys import sys
from enum import Enum
from typing import Awaitable from typing import Awaitable
from multidict import CIMultiDict # type: ignore from multidict import CIMultiDict # type: ignore
@ -19,6 +20,31 @@ except ImportError:
pass 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(): def enable_windows_color_support():
import ctypes import ctypes

View File

@ -8,7 +8,7 @@ from pathlib import Path
from typing import Any, Callable, Dict, Optional, Sequence, Union from typing import Any, Callable, Dict, Optional, Sequence, Union
from warnings import filterwarnings 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.errorpages import DEFAULT_FORMAT, check_error_format
from sanic.helpers import Default, _default from sanic.helpers import Default, _default
from sanic.http import Http from sanic.http import Http
@ -63,6 +63,7 @@ DEFAULT_CONFIG = {
"REQUEST_MAX_SIZE": 100000000, # 100 megabytes "REQUEST_MAX_SIZE": 100000000, # 100 megabytes
"REQUEST_TIMEOUT": 60, # 60 seconds "REQUEST_TIMEOUT": 60, # 60 seconds
"RESPONSE_TIMEOUT": 60, # 60 seconds "RESPONSE_TIMEOUT": 60, # 60 seconds
"RESTART_ORDER": RestartOrder.SHUTDOWN_FIRST,
"TLS_CERT_PASSWORD": "", "TLS_CERT_PASSWORD": "",
"TOUCHUP": _default, "TOUCHUP": _default,
"USE_UVLOOP": _default, "USE_UVLOOP": _default,
@ -110,6 +111,7 @@ class Config(dict, metaclass=DescriptorMeta):
REQUEST_MAX_SIZE: int REQUEST_MAX_SIZE: int
REQUEST_TIMEOUT: int REQUEST_TIMEOUT: int
RESPONSE_TIMEOUT: int RESPONSE_TIMEOUT: int
RESTART_ORDER: Union[str, RestartOrder]
SERVER_NAME: str SERVER_NAME: str
TLS_CERT_PASSWORD: str TLS_CERT_PASSWORD: str
TOUCHUP: Union[Default, bool] TOUCHUP: Union[Default, bool]
@ -194,6 +196,10 @@ class Config(dict, metaclass=DescriptorMeta):
self.LOCAL_CERT_CREATOR = LocalCertCreator[ self.LOCAL_CERT_CREATOR = LocalCertCreator[
self.LOCAL_CERT_CREATOR.upper() 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": elif attr == "DEPRECATION_FILTER":
self._configure_warnings() self._configure_warnings()

View File

@ -1,19 +1,9 @@
from enum import Enum, auto from enum import auto
from sanic.compat import UpperStrEnum
class HTTPMethod(str, Enum): class HTTPMethod(UpperStrEnum):
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
GET = auto() GET = auto()
POST = auto() POST = auto()
@ -24,15 +14,19 @@ class HTTPMethod(str, Enum):
DELETE = auto() DELETE = auto()
class LocalCertCreator(str, Enum): class LocalCertCreator(UpperStrEnum):
def _generate_next_value_(name, start, count, last_values):
return name.upper()
AUTO = auto() AUTO = auto()
TRUSTME = auto() TRUSTME = auto()
MKCERT = auto() MKCERT = auto()
class RestartOrder(UpperStrEnum):
SHUTDOWN_FIRST = auto()
STARTUP_FIRST = auto()
HTTP_METHODS = tuple(HTTPMethod.__members__.values()) HTTP_METHODS = tuple(HTTPMethod.__members__.values())
SAFE_HTTP_METHODS = (HTTPMethod.GET, HTTPMethod.HEAD, HTTPMethod.OPTIONS) SAFE_HTTP_METHODS = (HTTPMethod.GET, HTTPMethod.HEAD, HTTPMethod.OPTIONS)
IDEMPOTENT_HTTP_METHODS = ( IDEMPOTENT_HTTP_METHODS = (

View File

@ -1,22 +1,10 @@
import logging import logging
import sys import sys
from enum import Enum from typing import Any, Dict
from typing import TYPE_CHECKING, Any, Dict
from warnings import warn from warnings import warn
from sanic.compat import is_atty from sanic.compat import StrEnum, 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
LOGGING_CONFIG_DEFAULTS: Dict[str, Any] = dict( # no cov LOGGING_CONFIG_DEFAULTS: Dict[str, Any] = dict( # no cov

View File

@ -814,6 +814,7 @@ class StartupMixin(metaclass=SanicMeta):
cls._get_context(), cls._get_context(),
(monitor_pub, monitor_sub), (monitor_pub, monitor_sub),
worker_state, worker_state,
primary.config.RESTART_ORDER,
) )
if cls.should_auto_reload(): if cls.should_auto_reload():
reload_dirs: Set[Path] = primary.state.reload_dirs.union( reload_dirs: Set[Path] = primary.state.reload_dirs.union(

View File

@ -5,6 +5,7 @@ from signal import signal as signal_func
from typing import List, Optional from typing import List, Optional
from sanic.compat import OS_IS_WINDOWS from sanic.compat import OS_IS_WINDOWS
from sanic.constants import RestartOrder
from sanic.exceptions import ServerKilled from sanic.exceptions import ServerKilled
from sanic.log import error_logger, logger from sanic.log import error_logger, logger
from sanic.worker.process import ProcessState, Worker, WorkerProcess from sanic.worker.process import ProcessState, Worker, WorkerProcess
@ -28,6 +29,7 @@ class WorkerManager:
context, context,
monitor_pubsub, monitor_pubsub,
worker_state, worker_state,
restart_order: RestartOrder,
): ):
self.num_server = number self.num_server = number
self.context = context self.context = context
@ -37,6 +39,7 @@ class WorkerManager:
self.worker_state = worker_state self.worker_state = worker_state
self.worker_state[self.MAIN_IDENT] = {"pid": self.pid} self.worker_state[self.MAIN_IDENT] = {"pid": self.pid}
self.terminated = False self.terminated = False
self.restart_order = restart_order
if number == 0: if number == 0:
raise RuntimeError("Cannot serve with no workers") raise RuntimeError("Cannot serve with no workers")
@ -55,7 +58,14 @@ class WorkerManager:
def manage(self, ident, func, kwargs, transient=False): def manage(self, ident, func, kwargs, transient=False):
container = self.transient if transient else self.durable container = self.transient if transient else self.durable
container.append( 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): def run(self):

View File

@ -7,6 +7,7 @@ from signal import SIGINT
from threading import Thread from threading import Thread
from typing import Any, Dict, Set from typing import Any, Dict, Set
from sanic.constants import RestartOrder
from sanic.log import Colors, logger from sanic.log import Colors, logger
@ -28,13 +29,22 @@ class ProcessState(IntEnum):
class WorkerProcess: class WorkerProcess:
SERVER_LABEL = "Server" 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.state = ProcessState.IDLE
self.factory = factory self.factory = factory
self.name = name self.name = name
self.target = target self.target = target
self.kwargs = kwargs self.kwargs = kwargs
self.worker_state = worker_state self.worker_state = worker_state
self.restart_order = restart_order
if self.name not in self.worker_state: if self.name not in self.worker_state:
self.worker_state[self.name] = { self.worker_state[self.name] = {
"server": self.SERVER_LABEL in self.name "server": self.SERVER_LABEL in self.name
@ -96,8 +106,11 @@ class WorkerProcess:
self.name, self.name,
self.pid, self.pid,
) )
self._old_process = self._current_process
self.set_state(ProcessState.RESTARTING, force=True) 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( self.kwargs.update(
{"config": {k.upper(): v for k, v in kwargs.items()}} {"config": {k.upper(): v for k, v in kwargs.items()}}
) )
@ -107,8 +120,9 @@ class WorkerProcess:
except AttributeError: except AttributeError:
raise RuntimeError("Restart failed") raise RuntimeError("Restart failed")
termination_thread = Thread(target=self.wait_to_terminate) if self.restart_order is RestartOrder.STARTUP_FIRST:
termination_thread.start() termination_thread = Thread(target=self.wait_to_terminate)
termination_thread.start()
self.worker_state[self.name] = { self.worker_state[self.name] = {
**self.worker_state[self.name], **self.worker_state[self.name],
@ -118,7 +132,7 @@ class WorkerProcess:
} }
def wait_to_terminate(self): def wait_to_terminate(self):
# TODO: Add a timeout # TODO: Add a timeout?
while self.state is not ProcessState.ACKED: while self.state is not ProcessState.ACKED:
... ...
else: else:
@ -163,6 +177,7 @@ class Worker:
server_settings, server_settings,
context: BaseContext, context: BaseContext,
worker_state: Dict[str, Any], worker_state: Dict[str, Any],
restart_order: RestartOrder,
): ):
self.ident = f"{self.WORKER_PREFIX}{ident}" self.ident = f"{self.WORKER_PREFIX}{ident}"
self.context = context self.context = context
@ -170,6 +185,7 @@ class Worker:
self.server_settings = server_settings self.server_settings = server_settings
self.worker_state = worker_state self.worker_state = worker_state
self.processes: Set[WorkerProcess] = set() self.processes: Set[WorkerProcess] = set()
self.restart_order = restart_order
self.create_process() self.create_process()
def create_process(self) -> WorkerProcess: def create_process(self) -> WorkerProcess:
@ -179,6 +195,7 @@ class Worker:
target=self.serve, target=self.serve,
kwargs={**self.server_settings}, kwargs={**self.server_settings},
worker_state=self.worker_state, worker_state=self.worker_state,
restart_order=self.restart_order,
) )
self.processes.add(process) self.processes.add(process)
return process return process