Compare commits
10 Commits
main
...
monitor-re
Author | SHA1 | Date | |
---|---|---|---|
|
f83553be9e | ||
|
0c3527b8b2 | ||
|
232bbce1e0 | ||
|
f034a31d29 | ||
|
3536c0af27 | ||
|
0e7ee94574 | ||
|
4e4e2b036b | ||
|
7f682cea02 | ||
|
ae1669cd8f | ||
|
3c4c136090 |
|
@ -8,11 +8,6 @@ from typing import TYPE_CHECKING
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from sanic import Sanic
|
from sanic import Sanic
|
||||||
|
|
||||||
try:
|
|
||||||
from sanic_ext import Extend # type: ignore
|
|
||||||
except ImportError:
|
|
||||||
...
|
|
||||||
|
|
||||||
|
|
||||||
def setup_ext(app: Sanic, *, fail: bool = False, **kwargs):
|
def setup_ext(app: Sanic, *, fail: bool = False, **kwargs):
|
||||||
if not app.config.AUTO_EXTEND:
|
if not app.config.AUTO_EXTEND:
|
||||||
|
@ -33,7 +28,7 @@ def setup_ext(app: Sanic, *, fail: bool = False, **kwargs):
|
||||||
return
|
return
|
||||||
|
|
||||||
if not getattr(app, "_ext", None):
|
if not getattr(app, "_ext", None):
|
||||||
Ext: Extend = getattr(sanic_ext, "Extend")
|
Ext = getattr(sanic_ext, "Extend")
|
||||||
app._ext = Ext(app, **kwargs)
|
app._ext = Ext(app, **kwargs)
|
||||||
|
|
||||||
return app.ext
|
return app.ext
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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 = (
|
||||||
|
|
16
sanic/log.py
16
sanic/log.py
|
@ -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
|
||||||
|
|
|
@ -41,6 +41,7 @@ from sanic.application.motd import MOTD
|
||||||
from sanic.application.state import ApplicationServerInfo, Mode, ServerStage
|
from sanic.application.state import ApplicationServerInfo, Mode, ServerStage
|
||||||
from sanic.base.meta import SanicMeta
|
from sanic.base.meta import SanicMeta
|
||||||
from sanic.compat import OS_IS_WINDOWS, is_atty
|
from sanic.compat import OS_IS_WINDOWS, is_atty
|
||||||
|
from sanic.constants import RestartOrder
|
||||||
from sanic.exceptions import ServerKilled
|
from sanic.exceptions import ServerKilled
|
||||||
from sanic.helpers import Default
|
from sanic.helpers import Default
|
||||||
from sanic.http.constants import HTTP
|
from sanic.http.constants import HTTP
|
||||||
|
@ -814,6 +815,7 @@ class StartupMixin(metaclass=SanicMeta):
|
||||||
cls._get_context(),
|
cls._get_context(),
|
||||||
(monitor_pub, monitor_sub),
|
(monitor_pub, monitor_sub),
|
||||||
worker_state,
|
worker_state,
|
||||||
|
cast(RestartOrder, 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(
|
||||||
|
|
|
@ -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
|
||||||
|
@ -18,6 +19,7 @@ else:
|
||||||
|
|
||||||
class WorkerManager:
|
class WorkerManager:
|
||||||
THRESHOLD = 300 # == 30 seconds
|
THRESHOLD = 300 # == 30 seconds
|
||||||
|
MAIN_IDENT = "Sanic-Main"
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
|
@ -27,6 +29,7 @@ class WorkerManager:
|
||||||
context,
|
context,
|
||||||
monitor_pubsub,
|
monitor_pubsub,
|
||||||
worker_state,
|
worker_state,
|
||||||
|
restart_order: RestartOrder = RestartOrder.SHUTDOWN_FIRST,
|
||||||
):
|
):
|
||||||
self.num_server = number
|
self.num_server = number
|
||||||
self.context = context
|
self.context = context
|
||||||
|
@ -34,8 +37,9 @@ class WorkerManager:
|
||||||
self.durable: List[Worker] = []
|
self.durable: List[Worker] = []
|
||||||
self.monitor_publisher, self.monitor_subscriber = monitor_pubsub
|
self.monitor_publisher, self.monitor_subscriber = monitor_pubsub
|
||||||
self.worker_state = worker_state
|
self.worker_state = worker_state
|
||||||
self.worker_state["Sanic-Main"] = {"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")
|
||||||
|
@ -54,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):
|
||||||
|
@ -122,11 +133,18 @@ class WorkerManager:
|
||||||
process_names=process_names,
|
process_names=process_names,
|
||||||
reloaded_files=reloaded_files,
|
reloaded_files=reloaded_files,
|
||||||
)
|
)
|
||||||
|
self._sync_states()
|
||||||
except InterruptedError:
|
except InterruptedError:
|
||||||
if not OS_IS_WINDOWS:
|
if not OS_IS_WINDOWS:
|
||||||
raise
|
raise
|
||||||
break
|
break
|
||||||
|
|
||||||
|
def _sync_states(self):
|
||||||
|
for process in self.processes:
|
||||||
|
state = self.worker_state[process.name].get("state")
|
||||||
|
if state and process.state.name != state:
|
||||||
|
process.set_state(ProcessState[state], True)
|
||||||
|
|
||||||
def wait_for_ack(self): # no cov
|
def wait_for_ack(self): # no cov
|
||||||
misses = 0
|
misses = 0
|
||||||
message = (
|
message = (
|
||||||
|
|
|
@ -4,8 +4,10 @@ from datetime import datetime, timezone
|
||||||
from enum import IntEnum, auto
|
from enum import IntEnum, auto
|
||||||
from multiprocessing.context import BaseContext
|
from multiprocessing.context import BaseContext
|
||||||
from signal import SIGINT
|
from signal import SIGINT
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
@ -16,6 +18,8 @@ def get_now():
|
||||||
|
|
||||||
class ProcessState(IntEnum):
|
class ProcessState(IntEnum):
|
||||||
IDLE = auto()
|
IDLE = auto()
|
||||||
|
RESTARTING = auto()
|
||||||
|
STARTING = auto()
|
||||||
STARTED = auto()
|
STARTED = auto()
|
||||||
ACKED = auto()
|
ACKED = auto()
|
||||||
JOINED = auto()
|
JOINED = auto()
|
||||||
|
@ -25,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
|
||||||
|
@ -54,8 +67,9 @@ class WorkerProcess:
|
||||||
f"{Colors.SANIC}%s{Colors.END}",
|
f"{Colors.SANIC}%s{Colors.END}",
|
||||||
self.name,
|
self.name,
|
||||||
)
|
)
|
||||||
|
self.set_state(ProcessState.STARTING)
|
||||||
|
self._current_process.start()
|
||||||
self.set_state(ProcessState.STARTED)
|
self.set_state(ProcessState.STARTED)
|
||||||
self._process.start()
|
|
||||||
if not self.worker_state[self.name].get("starts"):
|
if not self.worker_state[self.name].get("starts"):
|
||||||
self.worker_state[self.name] = {
|
self.worker_state[self.name] = {
|
||||||
**self.worker_state[self.name],
|
**self.worker_state[self.name],
|
||||||
|
@ -67,7 +81,7 @@ class WorkerProcess:
|
||||||
|
|
||||||
def join(self):
|
def join(self):
|
||||||
self.set_state(ProcessState.JOINED)
|
self.set_state(ProcessState.JOINED)
|
||||||
self._process.join()
|
self._current_process.join()
|
||||||
|
|
||||||
def terminate(self):
|
def terminate(self):
|
||||||
if self.state is not ProcessState.TERMINATED:
|
if self.state is not ProcessState.TERMINATED:
|
||||||
|
@ -80,7 +94,6 @@ class WorkerProcess:
|
||||||
)
|
)
|
||||||
self.set_state(ProcessState.TERMINATED, force=True)
|
self.set_state(ProcessState.TERMINATED, force=True)
|
||||||
try:
|
try:
|
||||||
# self._process.terminate()
|
|
||||||
os.kill(self.pid, SIGINT)
|
os.kill(self.pid, SIGINT)
|
||||||
del self.worker_state[self.name]
|
del self.worker_state[self.name]
|
||||||
except (KeyError, AttributeError, ProcessLookupError):
|
except (KeyError, AttributeError, ProcessLookupError):
|
||||||
|
@ -93,8 +106,11 @@ class WorkerProcess:
|
||||||
self.name,
|
self.name,
|
||||||
self.pid,
|
self.pid,
|
||||||
)
|
)
|
||||||
self._process.terminate()
|
self.set_state(ProcessState.RESTARTING, force=True)
|
||||||
self.set_state(ProcessState.IDLE, force=True)
|
if self.restart_order is RestartOrder.SHUTDOWN_FIRST:
|
||||||
|
self._terminate_now()
|
||||||
|
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()}}
|
||||||
)
|
)
|
||||||
|
@ -104,6 +120,9 @@ class WorkerProcess:
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
raise RuntimeError("Restart failed")
|
raise RuntimeError("Restart failed")
|
||||||
|
|
||||||
|
if self.restart_order is RestartOrder.STARTUP_FIRST:
|
||||||
|
self._terminate_soon()
|
||||||
|
|
||||||
self.worker_state[self.name] = {
|
self.worker_state[self.name] = {
|
||||||
**self.worker_state[self.name],
|
**self.worker_state[self.name],
|
||||||
"pid": self.pid,
|
"pid": self.pid,
|
||||||
|
@ -111,16 +130,59 @@ class WorkerProcess:
|
||||||
"restart_at": get_now(),
|
"restart_at": get_now(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def _terminate_now(self):
|
||||||
|
logger.debug(
|
||||||
|
f"{Colors.BLUE}Begin restart termination: "
|
||||||
|
f"{Colors.BOLD}{Colors.SANIC}"
|
||||||
|
f"%s {Colors.BLUE}[%s]{Colors.END}",
|
||||||
|
self.name,
|
||||||
|
self._current_process.pid,
|
||||||
|
)
|
||||||
|
self._current_process.terminate()
|
||||||
|
|
||||||
|
def _terminate_soon(self):
|
||||||
|
logger.debug(
|
||||||
|
f"{Colors.BLUE}Begin restart termination: "
|
||||||
|
f"{Colors.BOLD}{Colors.SANIC}"
|
||||||
|
f"%s {Colors.BLUE}[%s]{Colors.END}",
|
||||||
|
self.name,
|
||||||
|
self._current_process.pid,
|
||||||
|
)
|
||||||
|
termination_thread = Thread(target=self._wait_to_terminate)
|
||||||
|
termination_thread.start()
|
||||||
|
|
||||||
|
def _wait_to_terminate(self):
|
||||||
|
logger.debug(
|
||||||
|
f"{Colors.BLUE}Waiting for process to be acked: "
|
||||||
|
f"{Colors.BOLD}{Colors.SANIC}"
|
||||||
|
f"%s {Colors.BLUE}[%s]{Colors.END}",
|
||||||
|
self.name,
|
||||||
|
self._old_process.pid,
|
||||||
|
)
|
||||||
|
# TODO: Add a timeout?
|
||||||
|
while self.state is not ProcessState.ACKED:
|
||||||
|
...
|
||||||
|
else:
|
||||||
|
logger.debug(
|
||||||
|
f"{Colors.BLUE}Process acked. Terminating: "
|
||||||
|
f"{Colors.BOLD}{Colors.SANIC}"
|
||||||
|
f"%s {Colors.BLUE}[%s]{Colors.END}",
|
||||||
|
self.name,
|
||||||
|
self._old_process.pid,
|
||||||
|
)
|
||||||
|
self._old_process.terminate()
|
||||||
|
delattr(self, "_old_process")
|
||||||
|
|
||||||
def is_alive(self):
|
def is_alive(self):
|
||||||
try:
|
try:
|
||||||
return self._process.is_alive()
|
return self._current_process.is_alive()
|
||||||
except AssertionError:
|
except AssertionError:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def spawn(self):
|
def spawn(self):
|
||||||
if self.state is not ProcessState.IDLE:
|
if self.state not in (ProcessState.IDLE, ProcessState.RESTARTING):
|
||||||
raise Exception("Cannot spawn a worker process until it is idle.")
|
raise Exception("Cannot spawn a worker process until it is idle.")
|
||||||
self._process = self.factory(
|
self._current_process = self.factory(
|
||||||
name=self.name,
|
name=self.name,
|
||||||
target=self.target,
|
target=self.target,
|
||||||
kwargs=self.kwargs,
|
kwargs=self.kwargs,
|
||||||
|
@ -129,10 +191,12 @@ class WorkerProcess:
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def pid(self):
|
def pid(self):
|
||||||
return self._process.pid
|
return self._current_process.pid
|
||||||
|
|
||||||
|
|
||||||
class Worker:
|
class Worker:
|
||||||
|
WORKER_PREFIX = "Sanic-"
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
ident: str,
|
ident: str,
|
||||||
|
@ -140,22 +204,25 @@ 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 = ident
|
self.ident = f"{self.WORKER_PREFIX}{ident}"
|
||||||
self.context = context
|
self.context = context
|
||||||
self.serve = serve
|
self.serve = serve
|
||||||
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:
|
||||||
process = WorkerProcess(
|
process = WorkerProcess(
|
||||||
factory=self.context.Process,
|
factory=self.context.Process,
|
||||||
name=f"Sanic-{self.ident}-{len(self.processes)}",
|
name=f"{self.ident}-{len(self.processes)}",
|
||||||
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
|
||||||
|
|
|
@ -349,11 +349,11 @@ def test_get_app_does_not_exist():
|
||||||
with pytest.raises(
|
with pytest.raises(
|
||||||
SanicException,
|
SanicException,
|
||||||
match="Sanic app name 'does-not-exist' not found.\n"
|
match="Sanic app name 'does-not-exist' not found.\n"
|
||||||
"App instantiation must occur outside "
|
"App instantiation must occur outside "
|
||||||
"if __name__ == '__main__' "
|
"if __name__ == '__main__' "
|
||||||
"block or by using an AppLoader.\nSee "
|
"block or by using an AppLoader.\nSee "
|
||||||
"https://sanic.dev/en/guide/deployment/app-loader.html"
|
"https://sanic.dev/en/guide/deployment/app-loader.html"
|
||||||
" for more details."
|
" for more details.",
|
||||||
):
|
):
|
||||||
Sanic.get_app("does-not-exist")
|
Sanic.get_app("does-not-exist")
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,7 @@ from pytest import MonkeyPatch
|
||||||
|
|
||||||
from sanic import Sanic
|
from sanic import Sanic
|
||||||
from sanic.config import DEFAULT_CONFIG, Config
|
from sanic.config import DEFAULT_CONFIG, Config
|
||||||
from sanic.constants import LocalCertCreator
|
from sanic.constants import LocalCertCreator, RestartOrder
|
||||||
from sanic.exceptions import PyFileError
|
from sanic.exceptions import PyFileError
|
||||||
|
|
||||||
|
|
||||||
|
@ -436,3 +436,19 @@ def test_convert_local_cert_creator(passed, expected):
|
||||||
app = Sanic("Test")
|
app = Sanic("Test")
|
||||||
assert app.config.LOCAL_CERT_CREATOR is expected
|
assert app.config.LOCAL_CERT_CREATOR is expected
|
||||||
del os.environ["SANIC_LOCAL_CERT_CREATOR"]
|
del os.environ["SANIC_LOCAL_CERT_CREATOR"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"passed,expected",
|
||||||
|
(
|
||||||
|
("shutdown_first", RestartOrder.SHUTDOWN_FIRST),
|
||||||
|
("startup_first", RestartOrder.STARTUP_FIRST),
|
||||||
|
("SHUTDOWN_FIRST", RestartOrder.SHUTDOWN_FIRST),
|
||||||
|
("STARTUP_FIRST", RestartOrder.STARTUP_FIRST),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
def test_convert_restart_order(passed, expected):
|
||||||
|
os.environ["SANIC_RESTART_ORDER"] = passed
|
||||||
|
app = Sanic("Test")
|
||||||
|
assert app.config.RESTART_ORDER is expected
|
||||||
|
del os.environ["SANIC_RESTART_ORDER"]
|
||||||
|
|
|
@ -10,8 +10,7 @@ import pytest
|
||||||
import sanic
|
import sanic
|
||||||
|
|
||||||
from sanic import Sanic
|
from sanic import Sanic
|
||||||
from sanic.log import Colors
|
from sanic.log import LOGGING_CONFIG_DEFAULTS, Colors, logger
|
||||||
from sanic.log import LOGGING_CONFIG_DEFAULTS, logger
|
|
||||||
from sanic.response import text
|
from sanic.response import text
|
||||||
|
|
||||||
|
|
||||||
|
@ -254,11 +253,11 @@ def test_verbosity(app, caplog, app_verbosity, log_verbosity, exists):
|
||||||
|
|
||||||
|
|
||||||
def test_colors_enum_format():
|
def test_colors_enum_format():
|
||||||
assert f'{Colors.END}' == Colors.END.value
|
assert f"{Colors.END}" == Colors.END.value
|
||||||
assert f'{Colors.BOLD}' == Colors.BOLD.value
|
assert f"{Colors.BOLD}" == Colors.BOLD.value
|
||||||
assert f'{Colors.BLUE}' == Colors.BLUE.value
|
assert f"{Colors.BLUE}" == Colors.BLUE.value
|
||||||
assert f'{Colors.GREEN}' == Colors.GREEN.value
|
assert f"{Colors.GREEN}" == Colors.GREEN.value
|
||||||
assert f'{Colors.PURPLE}' == Colors.PURPLE.value
|
assert f"{Colors.PURPLE}" == Colors.PURPLE.value
|
||||||
assert f'{Colors.RED}' == Colors.RED.value
|
assert f"{Colors.RED}" == Colors.RED.value
|
||||||
assert f'{Colors.SANIC}' == Colors.SANIC.value
|
assert f"{Colors.SANIC}" == Colors.SANIC.value
|
||||||
assert f'{Colors.YELLOW}' == Colors.YELLOW.value
|
assert f"{Colors.YELLOW}" == Colors.YELLOW.value
|
||||||
|
|
|
@ -1,13 +1,18 @@
|
||||||
|
import re
|
||||||
import signal
|
import signal
|
||||||
|
import threading
|
||||||
|
|
||||||
from asyncio import Event
|
from asyncio import Event
|
||||||
|
from logging import DEBUG
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from unittest.mock import Mock
|
from unittest.mock import Mock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from sanic.app import Sanic
|
from sanic.app import Sanic
|
||||||
|
from sanic.constants import RestartOrder
|
||||||
from sanic.worker.loader import AppLoader
|
from sanic.worker.loader import AppLoader
|
||||||
|
from sanic.worker.process import ProcessState, WorkerProcess
|
||||||
from sanic.worker.reloader import Reloader
|
from sanic.worker.reloader import Reloader
|
||||||
|
|
||||||
|
|
||||||
|
@ -67,6 +72,64 @@ def test_iter_files():
|
||||||
assert len_total_files == len_python_files + len_static_files
|
assert len_total_files == len_python_files + len_static_files
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"order,expected",
|
||||||
|
(
|
||||||
|
(
|
||||||
|
"shutdown_first",
|
||||||
|
[
|
||||||
|
"Restarting a process",
|
||||||
|
"Begin restart termination",
|
||||||
|
"Starting a process",
|
||||||
|
],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"startup_first",
|
||||||
|
[
|
||||||
|
"Restarting a process",
|
||||||
|
"Starting a process",
|
||||||
|
"Begin restart termination",
|
||||||
|
"Waiting for process to be acked",
|
||||||
|
"Process acked. Terminating",
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
def test_default_reload_shutdown_order(monkeypatch, caplog, order, expected):
|
||||||
|
|
||||||
|
current_process = Mock()
|
||||||
|
worker_process = WorkerProcess(
|
||||||
|
lambda **_: current_process,
|
||||||
|
"Test",
|
||||||
|
lambda **_: ...,
|
||||||
|
{},
|
||||||
|
{},
|
||||||
|
RestartOrder[order.upper()],
|
||||||
|
)
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
worker_process.set_state(ProcessState.ACKED)
|
||||||
|
self._target()
|
||||||
|
|
||||||
|
orig = threading.Thread.start
|
||||||
|
monkeypatch.setattr(threading.Thread, "start", start)
|
||||||
|
|
||||||
|
with caplog.at_level(DEBUG):
|
||||||
|
worker_process.restart()
|
||||||
|
|
||||||
|
ansi = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])")
|
||||||
|
|
||||||
|
def clean(msg: str):
|
||||||
|
msg, _ = ansi.sub("", msg).split(":", 1)
|
||||||
|
return msg
|
||||||
|
|
||||||
|
debug = [clean(record[2]) for record in caplog.record_tuples]
|
||||||
|
assert debug == expected
|
||||||
|
current_process.start.assert_called_once()
|
||||||
|
current_process.terminate.assert_called_once()
|
||||||
|
monkeypatch.setattr(threading.Thread, "start", orig)
|
||||||
|
|
||||||
|
|
||||||
def test_reloader_triggers_start_stop_listeners(
|
def test_reloader_triggers_start_stop_listeners(
|
||||||
app: Sanic, app_loader: AppLoader
|
app: Sanic, app_loader: AppLoader
|
||||||
):
|
):
|
||||||
|
|
Loading…
Reference in New Issue
Block a user