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

Sleep in Reloader loop to conserve CPU resources #2567

Closed
wants to merge 8 commits into from
1 change: 1 addition & 0 deletions sanic/cli/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ def _build_run_kwargs(self):

kwargs = {
"access_log": self.args.access_log,
"auto_reload_interval": self.args.auto_reload_interval,
"coffee": self.args.coffee,
"debug": self.args.debug,
"fast": self.args.fast,
Expand Down
8 changes: 8 additions & 0 deletions sanic/cli/arguments.py
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,14 @@ def attach(self):
"changes"
),
)
self.container.add_argument(
"-i",
"--interval",
"--auto-reload-interval",
dest="auto_reload_interval",
type=float,
help="Interval in seconds to check for file changes [default 1.0]",
)
self.container.add_argument(
"-R",
"--reload-dir",
Expand Down
2 changes: 2 additions & 0 deletions sanic/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"ACCESS_LOG": False,
"AUTO_EXTEND": True,
"AUTO_RELOAD": False,
"AUTO_RELOAD_INTERVAL": 1.0,
"EVENT_AUTOREGISTER": False,
"DEPRECATION_FILTER": "once",
"FORWARDED_FOR_HEADER": "X-Forwarded-For",
Expand Down Expand Up @@ -85,6 +86,7 @@ class Config(dict, metaclass=DescriptorMeta):
ACCESS_LOG: bool
AUTO_EXTEND: bool
AUTO_RELOAD: bool
AUTO_RELOAD_INTERVAL: Union[int, float]
EVENT_AUTOREGISTER: bool
DEPRECATION_FILTER: FilterWarningType
FORWARDED_FOR_HEADER: str
Expand Down
18 changes: 17 additions & 1 deletion sanic/mixins/startup.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ def run(
dev: bool = False,
debug: bool = False,
auto_reload: Optional[bool] = None,
auto_reload_interval: Optional[Union[int, float]] = None,
version: HTTPVersion = HTTP.VERSION_1,
ssl: Union[None, SSLContext, dict, str, list, tuple] = None,
sock: Optional[socket] = None,
Expand Down Expand Up @@ -176,6 +177,7 @@ def run(
dev=dev,
debug=debug,
auto_reload=auto_reload,
auto_reload_interval=auto_reload_interval,
version=version,
ssl=ssl,
sock=sock,
Expand Down Expand Up @@ -213,6 +215,7 @@ def prepare(
dev: bool = False,
debug: bool = False,
auto_reload: Optional[bool] = None,
auto_reload_interval: Optional[Union[int, float]] = None,
version: HTTPVersion = HTTP.VERSION_1,
ssl: Union[None, SSLContext, dict, str, list, tuple] = None,
sock: Optional[socket] = None,
Expand Down Expand Up @@ -305,6 +308,7 @@ def prepare(
for attribute, value in {
"ACCESS_LOG": access_log,
"AUTO_RELOAD": auto_reload,
"AUTO_RELOAD_INTERVAL": auto_reload_interval,
"MOTD": motd,
"NOISY_EXCEPTIONS": noisy_exceptions,
}.items():
Expand Down Expand Up @@ -608,6 +612,12 @@ def get_motd_data(
extra = {}
if self.config.AUTO_RELOAD:
reload_display = "enabled"

if self.config.AUTO_RELOAD_INTERVAL:
reload_display += " [{:.1f}s]".format(
self.config.AUTO_RELOAD_INTERVAL
)

if self.state.reload_dirs:
reload_display += ", ".join(
[
Expand All @@ -618,6 +628,7 @@ def get_motd_data(
),
]
)

display["auto-reload"] = reload_display

packages = []
Expand Down Expand Up @@ -817,7 +828,12 @@ def serve(
reload_dirs: Set[Path] = primary.state.reload_dirs.union(
*(app.state.reload_dirs for app in apps)
)
reloader = Reloader(monitor_pub, 1.0, reload_dirs, app_loader)
reloader = Reloader(
monitor_pub,
primary.config.AUTO_RELOAD_INTERVAL,
reload_dirs,
app_loader,
)
manager.manage("Reloader", reloader, {}, transient=False)

inspector = None
Expand Down
15 changes: 9 additions & 6 deletions sanic/worker/reloader.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
from pathlib import Path
from signal import SIGINT, SIGTERM
from signal import signal as signal_func
from typing import Dict, Set
from threading import Event
from typing import Dict, Optional, Set, Union

from sanic.server.events import trigger_events
from sanic.worker.loader import AppLoader
Expand All @@ -19,14 +20,14 @@ class Reloader:
def __init__(
self,
publisher: Connection,
interval: float,
interval: Union[int, float],
reload_dirs: Set[Path],
app_loader: AppLoader,
):
self._stop: Optional[Event] = None
eirikrye marked this conversation as resolved.
Show resolved Hide resolved
self._publisher = publisher
self.interval = interval
self.interval = float(interval)
self.reload_dirs = reload_dirs
self.run = True
self.app_loader = app_loader

def __call__(self) -> None:
Expand All @@ -40,10 +41,11 @@ def __call__(self) -> None:
before_trigger = app.listeners.get("before_reload_trigger")
after_trigger = app.listeners.get("after_reload_trigger")
loop = new_event_loop()
self._stop = Event()
if reloader_start:
trigger_events(reloader_start, loop, app)

while self.run:
while not self._stop.is_set():
changed = set()
for filename in self.files():
try:
Expand All @@ -62,12 +64,13 @@ def __call__(self) -> None:
self.reload(",".join(changed) if changed else "unknown")
if after_trigger:
trigger_events(after_trigger, loop, app)
self._stop.wait(self.interval)
else:
if reloader_stop:
trigger_events(reloader_stop, loop, app)

def stop(self, *_):
self.run = False
self._stop.set()

def reload(self, reloaded_files):
message = f"__ALL_PROCESSES__:{reloaded_files}"
Expand Down
1 change: 1 addition & 0 deletions tests/fake/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ async def app_info_dump(app: Sanic, _):
app_data = {
"access_log": app.config.ACCESS_LOG,
"auto_reload": app.auto_reload,
"auto_reload_interval": app.config.AUTO_RELOAD_INTERVAL,
"debug": app.debug,
"noisy_exceptions": app.config.NOISY_EXCEPTIONS,
}
Expand Down
12 changes: 12 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,18 @@ def test_auto_reload(cmd: str, caplog):
assert info["auto_reload"] is True


@pytest.mark.parametrize(
"cmd",
(("--auto-reload-interval=5.0",), ("--interval=5.0",), ("-i", "5.0")),
)
def test_auto_reload_interval(cmd: str, caplog):
command = ["fake.server.app", *cmd]
lines = capture(command, caplog)
info = read_app_info(lines)

assert info["auto_reload_interval"] == 5.0


@pytest.mark.parametrize(
"cmd,expected",
(
Expand Down
17 changes: 16 additions & 1 deletion tests/test_motd.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,5 +79,20 @@ def test_reload_dirs(app):
reload_dir="./", auto_reload=True, motd_display={"foo": "bar"}
)
mock.assert_called()
assert mock.call_args.args[2]["auto-reload"] == f"enabled, {os.getcwd()}"
assert (
mock.call_args.args[2]["auto-reload"]
== f"enabled [1.0s], {os.getcwd()}"
)
assert mock.call_args.args[3] == {"foo": "bar"}


def test_reload_interval(app):
app.config.LOGO = None
app.config.MOTD = True
app.config.AUTO_RELOAD = True
app.config.AUTO_RELOAD_INTERVAL = 5.0

with patch.object(MOTD, "output") as mock:
app.prepare()
mock.assert_called()
assert mock.call_args.args[2]["auto-reload"] == "enabled [5.0s]"