Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Wait for new process to ACK before termination of old on restart #2623

Closed
wants to merge 10 commits into from
26 changes: 26 additions & 0 deletions sanic/compat.py
Expand Up @@ -3,6 +3,7 @@
import signal
import sys

from enum import Enum
from typing import Awaitable

from multidict import CIMultiDict # type: ignore
Expand All @@ -19,6 +20,31 @@
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

Expand Down
8 changes: 7 additions & 1 deletion sanic/config.py
Expand Up @@ -8,7 +8,7 @@
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
Expand Down Expand Up @@ -63,6 +63,7 @@
"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,
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -194,6 +196,10 @@ def _post_set(self, attr, value) -> None:
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()

Expand Down
26 changes: 10 additions & 16 deletions sanic/constants.py
@@ -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()
Expand All @@ -24,15 +14,19 @@ def __str__(self) -> str:
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 = (
Expand Down
16 changes: 2 additions & 14 deletions 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
Expand Down
1 change: 1 addition & 0 deletions sanic/mixins/startup.py
Expand Up @@ -814,6 +814,7 @@ def serve(
cls._get_context(),
(monitor_pub, monitor_sub),
worker_state,
primary.config.RESTART_ORDER,
)
if cls.should_auto_reload():
reload_dirs: Set[Path] = primary.state.reload_dirs.union(
Expand Down
22 changes: 20 additions & 2 deletions sanic/worker/manager.py
Expand Up @@ -5,6 +5,7 @@
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
Expand All @@ -18,6 +19,7 @@

class WorkerManager:
THRESHOLD = 300 # == 30 seconds
MAIN_IDENT = "Sanic-Main"

def __init__(
self,
Expand All @@ -27,15 +29,17 @@ def __init__(
context,
monitor_pubsub,
worker_state,
restart_order: RestartOrder,
):
self.num_server = number
self.context = context
self.transient: List[Worker] = []
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")
Expand All @@ -54,7 +58,14 @@ def __init__(
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):
Expand Down Expand Up @@ -122,11 +133,18 @@ def monitor(self):
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]["state"]
if process.state.name != state:
process.set_state(ProcessState[state], True)

def wait_for_ack(self): # no cov
misses = 0
message = (
Expand Down