Skip to content

Commit f7040cc

Browse files
authored
Implement restart ordering (#2632)
1 parent 518152d commit f7040cc

19 files changed

Lines changed: 374 additions & 70 deletions

sanic/app.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1534,6 +1534,7 @@ async def _startup(self):
15341534

15351535
self.state.is_started = True
15361536

1537+
def ack(self):
15371538
if hasattr(self, "multiplexer"):
15381539
self.multiplexer.ack()
15391540

sanic/cli/base.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ def add_usage(self, usage, actions, groups, prefix=None):
2020
if not usage:
2121
usage = SUPPRESS
2222
# Add one linebreak, but not two
23-
self.add_text("\x1b[1A'")
23+
self.add_text("\x1b[1A")
2424
super().add_usage(usage, actions, groups, prefix)
2525

2626

sanic/cli/inspector.py

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,26 @@
55

66

77
def _add_shared(parser: ArgumentParser) -> None:
8-
parser.add_argument("--host", "-H", default="localhost")
9-
parser.add_argument("--port", "-p", default=6457, type=int)
10-
parser.add_argument("--secure", "-s", action="store_true")
11-
parser.add_argument("--api-key", "-k")
8+
parser.add_argument(
9+
"--host",
10+
"-H",
11+
default="localhost",
12+
help="Inspector host address [default 127.0.0.1]",
13+
)
14+
parser.add_argument(
15+
"--port",
16+
"-p",
17+
default=6457,
18+
type=int,
19+
help="Inspector port [default 6457]",
20+
)
21+
parser.add_argument(
22+
"--secure",
23+
"-s",
24+
action="store_true",
25+
help="Whether to access the Inspector via TLS encryption",
26+
)
27+
parser.add_argument("--api-key", "-k", help="Inspector authentication key")
1228
parser.add_argument(
1329
"--raw",
1430
action="store_true",
@@ -32,17 +48,25 @@ def make_inspector_parser(parser: ArgumentParser) -> None:
3248
dest="action",
3349
description=(
3450
"Run one of the below subcommands. If you have created a custom "
35-
"Inspector instance, then you can run custom commands.\nSee ___ "
51+
"Inspector instance, then you can run custom commands. See ___ "
3652
"for more details."
3753
),
3854
title="Required\n========\n Subcommands",
3955
parser_class=InspectorSubParser,
4056
)
41-
subparsers.add_parser(
57+
reloader = subparsers.add_parser(
4258
"reload",
4359
help="Trigger a reload of the server workers",
4460
formatter_class=SanicHelpFormatter,
4561
)
62+
reloader.add_argument(
63+
"--zero-downtime",
64+
action="store_true",
65+
help=(
66+
"Whether to wait for the new process to be online before "
67+
"terminating the old"
68+
),
69+
)
4670
subparsers.add_parser(
4771
"shutdown",
4872
help="Shutdown the application and all processes",
@@ -53,7 +77,11 @@ def make_inspector_parser(parser: ArgumentParser) -> None:
5377
help="Scale the number of workers",
5478
formatter_class=SanicHelpFormatter,
5579
)
56-
scale.add_argument("replicas", type=int)
80+
scale.add_argument(
81+
"replicas",
82+
type=int,
83+
help="Number of workers requested",
84+
)
5785

5886
custom = subparsers.add_parser(
5987
"<custom>",

sanic/compat.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import sys
55

66
from contextlib import contextmanager
7+
from enum import Enum
78
from typing import Awaitable, Union
89

910
from multidict import CIMultiDict # type: ignore
@@ -30,6 +31,30 @@
3031
except ImportError:
3132
pass
3233

34+
# Python 3.11 changed the way Enum formatting works for mixed-in types.
35+
if sys.version_info < (3, 11, 0):
36+
37+
class StrEnum(str, Enum):
38+
pass
39+
40+
else:
41+
from enum import StrEnum # type: ignore # noqa
42+
43+
44+
class UpperStrEnum(StrEnum):
45+
def _generate_next_value_(name, start, count, last_values):
46+
return name.upper()
47+
48+
def __eq__(self, value: object) -> bool:
49+
value = str(value).upper()
50+
return super().__eq__(value)
51+
52+
def __hash__(self) -> int:
53+
return hash(self.value)
54+
55+
def __str__(self) -> str:
56+
return self.value
57+
3358

3459
@contextmanager
3560
def use_context(method: StartMethod):

sanic/constants.py

Lines changed: 4 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,9 @@
1-
from enum import Enum, auto
1+
from enum import auto
22

3+
from sanic.compat import UpperStrEnum
34

4-
class HTTPMethod(str, Enum):
5-
def _generate_next_value_(name, start, count, last_values):
6-
return name.upper()
75

8-
def __eq__(self, value: object) -> bool:
9-
value = str(value).upper()
10-
return super().__eq__(value)
11-
12-
def __hash__(self) -> int:
13-
return hash(self.value)
14-
15-
def __str__(self) -> str:
16-
return self.value
6+
class HTTPMethod(UpperStrEnum):
177

188
GET = auto()
199
POST = auto()
@@ -24,9 +14,7 @@ def __str__(self) -> str:
2414
DELETE = auto()
2515

2616

27-
class LocalCertCreator(str, Enum):
28-
def _generate_next_value_(name, start, count, last_values):
29-
return name.upper()
17+
class LocalCertCreator(UpperStrEnum):
3018

3119
AUTO = auto()
3220
TRUSTME = auto()

sanic/mixins/signals.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ def signal(
2020
event: Union[str, Enum],
2121
*,
2222
apply: bool = True,
23-
condition: Dict[str, Any] = None,
23+
condition: Optional[Dict[str, Any]] = None,
2424
exclusive: bool = True,
2525
) -> Callable[[SignalHandler], SignalHandler]:
2626
"""
@@ -64,7 +64,7 @@ def add_signal(
6464
self,
6565
handler: Optional[Callable[..., Any]],
6666
event: str,
67-
condition: Dict[str, Any] = None,
67+
condition: Optional[Dict[str, Any]] = None,
6868
exclusive: bool = True,
6969
):
7070
if not handler:

sanic/mixins/startup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -851,7 +851,7 @@ def serve(
851851
primary.config.INSPECTOR_TLS_KEY,
852852
primary.config.INSPECTOR_TLS_CERT,
853853
)
854-
manager.manage("Inspector", inspector, {}, transient=True)
854+
manager.manage("Inspector", inspector, {}, transient=False)
855855

856856
primary._inspector = inspector
857857
primary._manager = manager

sanic/server/runners.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,7 @@ def _serve_http_1(
229229

230230
loop.run_until_complete(app._startup())
231231
loop.run_until_complete(app._server_event("init", "before"))
232+
app.ack()
232233

233234
try:
234235
http_server = loop.run_until_complete(server_coroutine)
@@ -306,6 +307,7 @@ def _serve_http_3(
306307
server = AsyncioServer(app, loop, coro, [])
307308
loop.run_until_complete(server.startup())
308309
loop.run_until_complete(server.before_start())
310+
app.ack()
309311
loop.run_until_complete(server)
310312
_setup_system_signals(app, run_multiple, register_sys_signals, loop)
311313
loop.run_until_complete(server.after_start())

sanic/worker/constants.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
from enum import IntEnum, auto
2+
3+
from sanic.compat import UpperStrEnum
4+
5+
6+
class RestartOrder(UpperStrEnum):
7+
SHUTDOWN_FIRST = auto()
8+
STARTUP_FIRST = auto()
9+
10+
11+
class ProcessState(IntEnum):
12+
IDLE = auto()
13+
RESTARTING = auto()
14+
STARTING = auto()
15+
STARTED = auto()
16+
ACKED = auto()
17+
JOINED = auto()
18+
TERMINATED = auto()

sanic/worker/inspector.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,8 +101,10 @@ def _make_safe(obj: Dict[str, Any]) -> Dict[str, Any]:
101101
obj[key] = value.isoformat()
102102
return obj
103103

104-
def reload(self) -> None:
104+
def reload(self, zero_downtime: bool = False) -> None:
105105
message = "__ALL_PROCESSES__:"
106+
if zero_downtime:
107+
message += ":STARTUP_FIRST"
106108
self._publisher.send(message)
107109

108110
def scale(self, replicas) -> str:

0 commit comments

Comments
 (0)