Compare commits
	
		
			10 Commits
		
	
	
		
			tlspath
			...
			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 | ||||||
| ): | ): | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user