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:
|
||||
from sanic import Sanic
|
||||
|
||||
try:
|
||||
from sanic_ext import Extend # type: ignore
|
||||
except ImportError:
|
||||
...
|
||||
|
||||
|
||||
def setup_ext(app: Sanic, *, fail: bool = False, **kwargs):
|
||||
if not app.config.AUTO_EXTEND:
|
||||
|
@ -33,7 +28,7 @@ def setup_ext(app: Sanic, *, fail: bool = False, **kwargs):
|
|||
return
|
||||
|
||||
if not getattr(app, "_ext", None):
|
||||
Ext: Extend = getattr(sanic_ext, "Extend")
|
||||
Ext = getattr(sanic_ext, "Extend")
|
||||
app._ext = Ext(app, **kwargs)
|
||||
|
||||
return app.ext
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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 = (
|
||||
|
|
16
sanic/log.py
16
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
|
||||
|
|
|
@ -41,6 +41,7 @@ from sanic.application.motd import MOTD
|
|||
from sanic.application.state import ApplicationServerInfo, Mode, ServerStage
|
||||
from sanic.base.meta import SanicMeta
|
||||
from sanic.compat import OS_IS_WINDOWS, is_atty
|
||||
from sanic.constants import RestartOrder
|
||||
from sanic.exceptions import ServerKilled
|
||||
from sanic.helpers import Default
|
||||
from sanic.http.constants import HTTP
|
||||
|
@ -814,6 +815,7 @@ class StartupMixin(metaclass=SanicMeta):
|
|||
cls._get_context(),
|
||||
(monitor_pub, monitor_sub),
|
||||
worker_state,
|
||||
cast(RestartOrder, primary.config.RESTART_ORDER),
|
||||
)
|
||||
if cls.should_auto_reload():
|
||||
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 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
|
||||
|
@ -18,6 +19,7 @@ else:
|
|||
|
||||
class WorkerManager:
|
||||
THRESHOLD = 300 # == 30 seconds
|
||||
MAIN_IDENT = "Sanic-Main"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
|
@ -27,6 +29,7 @@ class WorkerManager:
|
|||
context,
|
||||
monitor_pubsub,
|
||||
worker_state,
|
||||
restart_order: RestartOrder = RestartOrder.SHUTDOWN_FIRST,
|
||||
):
|
||||
self.num_server = number
|
||||
self.context = context
|
||||
|
@ -34,8 +37,9 @@ class WorkerManager:
|
|||
self.durable: List[Worker] = []
|
||||
self.monitor_publisher, self.monitor_subscriber = monitor_pubsub
|
||||
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.restart_order = restart_order
|
||||
|
||||
if number == 0:
|
||||
raise RuntimeError("Cannot serve with no workers")
|
||||
|
@ -54,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):
|
||||
|
@ -122,11 +133,18 @@ class WorkerManager:
|
|||
process_names=process_names,
|
||||
reloaded_files=reloaded_files,
|
||||
)
|
||||
self._sync_states()
|
||||
except InterruptedError:
|
||||
if not OS_IS_WINDOWS:
|
||||
raise
|
||||
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
|
||||
misses = 0
|
||||
message = (
|
||||
|
|
|
@ -4,8 +4,10 @@ from datetime import datetime, timezone
|
|||
from enum import IntEnum, auto
|
||||
from multiprocessing.context import BaseContext
|
||||
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
|
||||
|
||||
|
||||
|
@ -16,6 +18,8 @@ def get_now():
|
|||
|
||||
class ProcessState(IntEnum):
|
||||
IDLE = auto()
|
||||
RESTARTING = auto()
|
||||
STARTING = auto()
|
||||
STARTED = auto()
|
||||
ACKED = auto()
|
||||
JOINED = auto()
|
||||
|
@ -25,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
|
||||
|
@ -54,8 +67,9 @@ class WorkerProcess:
|
|||
f"{Colors.SANIC}%s{Colors.END}",
|
||||
self.name,
|
||||
)
|
||||
self.set_state(ProcessState.STARTING)
|
||||
self._current_process.start()
|
||||
self.set_state(ProcessState.STARTED)
|
||||
self._process.start()
|
||||
if not self.worker_state[self.name].get("starts"):
|
||||
self.worker_state[self.name] = {
|
||||
**self.worker_state[self.name],
|
||||
|
@ -67,7 +81,7 @@ class WorkerProcess:
|
|||
|
||||
def join(self):
|
||||
self.set_state(ProcessState.JOINED)
|
||||
self._process.join()
|
||||
self._current_process.join()
|
||||
|
||||
def terminate(self):
|
||||
if self.state is not ProcessState.TERMINATED:
|
||||
|
@ -80,7 +94,6 @@ class WorkerProcess:
|
|||
)
|
||||
self.set_state(ProcessState.TERMINATED, force=True)
|
||||
try:
|
||||
# self._process.terminate()
|
||||
os.kill(self.pid, SIGINT)
|
||||
del self.worker_state[self.name]
|
||||
except (KeyError, AttributeError, ProcessLookupError):
|
||||
|
@ -93,8 +106,11 @@ class WorkerProcess:
|
|||
self.name,
|
||||
self.pid,
|
||||
)
|
||||
self._process.terminate()
|
||||
self.set_state(ProcessState.IDLE, force=True)
|
||||
self.set_state(ProcessState.RESTARTING, force=True)
|
||||
if self.restart_order is RestartOrder.SHUTDOWN_FIRST:
|
||||
self._terminate_now()
|
||||
else:
|
||||
self._old_process = self._current_process
|
||||
self.kwargs.update(
|
||||
{"config": {k.upper(): v for k, v in kwargs.items()}}
|
||||
)
|
||||
|
@ -104,6 +120,9 @@ class WorkerProcess:
|
|||
except AttributeError:
|
||||
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],
|
||||
"pid": self.pid,
|
||||
|
@ -111,16 +130,59 @@ class WorkerProcess:
|
|||
"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):
|
||||
try:
|
||||
return self._process.is_alive()
|
||||
return self._current_process.is_alive()
|
||||
except AssertionError:
|
||||
return False
|
||||
|
||||
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.")
|
||||
self._process = self.factory(
|
||||
self._current_process = self.factory(
|
||||
name=self.name,
|
||||
target=self.target,
|
||||
kwargs=self.kwargs,
|
||||
|
@ -129,10 +191,12 @@ class WorkerProcess:
|
|||
|
||||
@property
|
||||
def pid(self):
|
||||
return self._process.pid
|
||||
return self._current_process.pid
|
||||
|
||||
|
||||
class Worker:
|
||||
WORKER_PREFIX = "Sanic-"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
ident: str,
|
||||
|
@ -140,22 +204,25 @@ class Worker:
|
|||
server_settings,
|
||||
context: BaseContext,
|
||||
worker_state: Dict[str, Any],
|
||||
restart_order: RestartOrder,
|
||||
):
|
||||
self.ident = ident
|
||||
self.ident = f"{self.WORKER_PREFIX}{ident}"
|
||||
self.context = context
|
||||
self.serve = serve
|
||||
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:
|
||||
process = WorkerProcess(
|
||||
factory=self.context.Process,
|
||||
name=f"Sanic-{self.ident}-{len(self.processes)}",
|
||||
name=f"{self.ident}-{len(self.processes)}",
|
||||
target=self.serve,
|
||||
kwargs={**self.server_settings},
|
||||
worker_state=self.worker_state,
|
||||
restart_order=self.restart_order,
|
||||
)
|
||||
self.processes.add(process)
|
||||
return process
|
||||
|
|
|
@ -349,11 +349,11 @@ def test_get_app_does_not_exist():
|
|||
with pytest.raises(
|
||||
SanicException,
|
||||
match="Sanic app name 'does-not-exist' not found.\n"
|
||||
"App instantiation must occur outside "
|
||||
"if __name__ == '__main__' "
|
||||
"block or by using an AppLoader.\nSee "
|
||||
"https://sanic.dev/en/guide/deployment/app-loader.html"
|
||||
" for more details."
|
||||
"App instantiation must occur outside "
|
||||
"if __name__ == '__main__' "
|
||||
"block or by using an AppLoader.\nSee "
|
||||
"https://sanic.dev/en/guide/deployment/app-loader.html"
|
||||
" for more details.",
|
||||
):
|
||||
Sanic.get_app("does-not-exist")
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@ from pytest import MonkeyPatch
|
|||
|
||||
from sanic import Sanic
|
||||
from sanic.config import DEFAULT_CONFIG, Config
|
||||
from sanic.constants import LocalCertCreator
|
||||
from sanic.constants import LocalCertCreator, RestartOrder
|
||||
from sanic.exceptions import PyFileError
|
||||
|
||||
|
||||
|
@ -436,3 +436,19 @@ def test_convert_local_cert_creator(passed, expected):
|
|||
app = Sanic("Test")
|
||||
assert app.config.LOCAL_CERT_CREATOR is expected
|
||||
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
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic.log import Colors
|
||||
from sanic.log import LOGGING_CONFIG_DEFAULTS, logger
|
||||
from sanic.log import LOGGING_CONFIG_DEFAULTS, Colors, logger
|
||||
from sanic.response import text
|
||||
|
||||
|
||||
|
@ -254,11 +253,11 @@ def test_verbosity(app, caplog, app_verbosity, log_verbosity, exists):
|
|||
|
||||
|
||||
def test_colors_enum_format():
|
||||
assert f'{Colors.END}' == Colors.END.value
|
||||
assert f'{Colors.BOLD}' == Colors.BOLD.value
|
||||
assert f'{Colors.BLUE}' == Colors.BLUE.value
|
||||
assert f'{Colors.GREEN}' == Colors.GREEN.value
|
||||
assert f'{Colors.PURPLE}' == Colors.PURPLE.value
|
||||
assert f'{Colors.RED}' == Colors.RED.value
|
||||
assert f'{Colors.SANIC}' == Colors.SANIC.value
|
||||
assert f'{Colors.YELLOW}' == Colors.YELLOW.value
|
||||
assert f"{Colors.END}" == Colors.END.value
|
||||
assert f"{Colors.BOLD}" == Colors.BOLD.value
|
||||
assert f"{Colors.BLUE}" == Colors.BLUE.value
|
||||
assert f"{Colors.GREEN}" == Colors.GREEN.value
|
||||
assert f"{Colors.PURPLE}" == Colors.PURPLE.value
|
||||
assert f"{Colors.RED}" == Colors.RED.value
|
||||
assert f"{Colors.SANIC}" == Colors.SANIC.value
|
||||
assert f"{Colors.YELLOW}" == Colors.YELLOW.value
|
||||
|
|
|
@ -1,13 +1,18 @@
|
|||
import re
|
||||
import signal
|
||||
import threading
|
||||
|
||||
from asyncio import Event
|
||||
from logging import DEBUG
|
||||
from pathlib import Path
|
||||
from unittest.mock import Mock
|
||||
|
||||
import pytest
|
||||
|
||||
from sanic.app import Sanic
|
||||
from sanic.constants import RestartOrder
|
||||
from sanic.worker.loader import AppLoader
|
||||
from sanic.worker.process import ProcessState, WorkerProcess
|
||||
from sanic.worker.reloader import Reloader
|
||||
|
||||
|
||||
|
@ -67,6 +72,64 @@ def test_iter_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(
|
||||
app: Sanic, app_loader: AppLoader
|
||||
):
|
||||
|
|
Loading…
Reference in New Issue
Block a user