Reload services on debounced file edits and replacement saves
This commit is contained in:
parent
15e4ed81a2
commit
dc24c4d4fe
3 changed files with 77 additions and 9 deletions
|
|
@ -11,7 +11,6 @@ import uvicorn
|
||||||
from uvicorn._subprocess import get_subprocess
|
from uvicorn._subprocess import get_subprocess
|
||||||
from uvicorn.config import LOGGING_CONFIG, Config
|
from uvicorn.config import LOGGING_CONFIG, Config
|
||||||
from uvicorn.supervisors import BaseReload, Multiprocess
|
from uvicorn.supervisors import BaseReload, Multiprocess
|
||||||
from watchdog.events import FileSystemEvent, PatternMatchingEventHandler
|
|
||||||
from watchdog.observers import Observer
|
from watchdog.observers import Observer
|
||||||
|
|
||||||
from great_ai.constants import SERVER_NAME
|
from great_ai.constants import SERVER_NAME
|
||||||
|
|
@ -20,6 +19,7 @@ from great_ai.deploy import GreatAI
|
||||||
from great_ai.errors import ArgumentValidationError, MissingArgumentError
|
from great_ai.errors import ArgumentValidationError, MissingArgumentError
|
||||||
from great_ai.utilities import get_logger
|
from great_ai.utilities import get_logger
|
||||||
|
|
||||||
|
from .deploy.reload_event_handler import ReloadEventHandler
|
||||||
from .parse_arguments import parse_arguments
|
from .parse_arguments import parse_arguments
|
||||||
|
|
||||||
logger = get_logger(SERVER_NAME)
|
logger = get_logger(SERVER_NAME)
|
||||||
|
|
@ -83,18 +83,12 @@ def serve() -> None:
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
|
|
||||||
class EventHandler(PatternMatchingEventHandler):
|
class EventHandler(ReloadEventHandler):
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
super().__init__(
|
super().__init__(self.restart)
|
||||||
patterns=["*.py", "*.ipynb"], ignore_patterns=["__*.py"]
|
|
||||||
)
|
|
||||||
self.server: Optional[GreatAIReload] = None
|
self.server: Optional[GreatAIReload] = None
|
||||||
self.restart()
|
self.restart()
|
||||||
|
|
||||||
def on_closed(self, event: FileSystemEvent) -> None:
|
|
||||||
logger.warning(f"File {event.src_path} has triggered a restart")
|
|
||||||
self.restart()
|
|
||||||
|
|
||||||
def restart(self) -> None:
|
def restart(self) -> None:
|
||||||
file_name = get_script_name(args.file_name)
|
file_name = get_script_name(args.file_name)
|
||||||
app = find_app(file_name)
|
app = find_app(file_name)
|
||||||
|
|
@ -124,6 +118,7 @@ def serve() -> None:
|
||||||
Event().wait()
|
Event().wait()
|
||||||
finally:
|
finally:
|
||||||
observer.stop()
|
observer.stop()
|
||||||
|
restart_handler.close()
|
||||||
restart_handler.stop_server()
|
restart_handler.stop_server()
|
||||||
if args.file_name.endswith(".ipynb"):
|
if args.file_name.endswith(".ipynb"):
|
||||||
try:
|
try:
|
||||||
|
|
|
||||||
46
great_ai/deploy/reload_event_handler.py
Normal file
46
great_ai/deploy/reload_event_handler.py
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
from threading import RLock, Timer
|
||||||
|
from typing import Callable, Optional
|
||||||
|
|
||||||
|
from watchdog.events import FileSystemEvent, PatternMatchingEventHandler
|
||||||
|
|
||||||
|
|
||||||
|
class ReloadEventHandler(PatternMatchingEventHandler):
|
||||||
|
"""Debounce edit and replacement events emitted by supported OS observers."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, restart: Callable[[], None], debounce_seconds: float = 0.3
|
||||||
|
) -> None:
|
||||||
|
super().__init__(patterns=["*.py", "*.ipynb"], ignore_patterns=["__*.py"])
|
||||||
|
self._restart = restart
|
||||||
|
self._debounce_seconds = debounce_seconds
|
||||||
|
self._lock = RLock()
|
||||||
|
self._timer: Optional[Timer] = None
|
||||||
|
self._closed = False
|
||||||
|
|
||||||
|
def on_any_event(self, event: FileSystemEvent) -> None:
|
||||||
|
if event.is_directory or event.event_type not in {
|
||||||
|
"created",
|
||||||
|
"modified",
|
||||||
|
"moved",
|
||||||
|
"closed",
|
||||||
|
}:
|
||||||
|
return
|
||||||
|
with self._lock:
|
||||||
|
if self._closed:
|
||||||
|
return
|
||||||
|
if self._timer is not None:
|
||||||
|
self._timer.cancel()
|
||||||
|
self._timer = Timer(self._debounce_seconds, self._run_restart)
|
||||||
|
self._timer.daemon = True
|
||||||
|
self._timer.start()
|
||||||
|
|
||||||
|
def _run_restart(self) -> None:
|
||||||
|
with self._lock:
|
||||||
|
if not self._closed:
|
||||||
|
self._restart()
|
||||||
|
|
||||||
|
def close(self) -> None:
|
||||||
|
with self._lock:
|
||||||
|
self._closed = True
|
||||||
|
if self._timer is not None:
|
||||||
|
self._timer.cancel()
|
||||||
27
tests/test_cli_regressions.py
Normal file
27
tests/test_cli_regressions.py
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
from threading import Event
|
||||||
|
|
||||||
|
from watchdog.events import FileClosedEvent, FileModifiedEvent, FileMovedEvent
|
||||||
|
|
||||||
|
from great_ai.deploy.reload_event_handler import ReloadEventHandler
|
||||||
|
|
||||||
|
|
||||||
|
def test_reload_debounces_edit_and_replacement_events() -> None:
|
||||||
|
completed = Event()
|
||||||
|
calls = []
|
||||||
|
|
||||||
|
def restart() -> None:
|
||||||
|
calls.append("restart")
|
||||||
|
completed.set()
|
||||||
|
|
||||||
|
handler = ReloadEventHandler(restart, debounce_seconds=0.02)
|
||||||
|
try:
|
||||||
|
handler.dispatch(FileModifiedEvent("service.py"))
|
||||||
|
handler.dispatch(FileClosedEvent("service.py"))
|
||||||
|
handler.dispatch(FileMovedEvent("temporary", "service.py"))
|
||||||
|
assert completed.wait(timeout=2)
|
||||||
|
assert calls == ["restart"]
|
||||||
|
handler.dispatch(FileModifiedEvent("__service__.py"))
|
||||||
|
finally:
|
||||||
|
handler.close()
|
||||||
|
handler.dispatch(FileModifiedEvent("service.py"))
|
||||||
|
assert calls == ["restart"]
|
||||||
Loading…
Reference in a new issue