Compare commits
	
		
			10 Commits
		
	
	
		
			remove-get
			...
			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 | ||||
|   | ||||
| @@ -353,7 +353,7 @@ def test_get_app_does_not_exist(): | ||||
|         "if __name__ == '__main__' " | ||||
|         "block or by using an AppLoader.\nSee " | ||||
|         "https://sanic.dev/en/guide/deployment/app-loader.html" | ||||
|             " for more details." | ||||
|         " 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 | ||||
| ): | ||||
|   | ||||
		Reference in New Issue
	
	Block a user