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

feat: better reload options #362

Merged
merged 12 commits into from
Aug 22, 2024
21 changes: 20 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ A Rust HTTP server for Python applications.
The main reasons behind Granian design are:

- Have a single, correct HTTP implementation, supporting versions 1, 2 (and eventually 3)
- Provide a single package for several platforms
- Provide a single package for several platforms
- Avoid the usual Gunicorn + uvicorn + http-tools dependency composition on unix systems
- Provide stable [performance](https://github.com/emmett-framework/granian/blob/master/benchmarks/README.md) when compared to existing alternatives

Expand Down Expand Up @@ -192,6 +192,25 @@ Options:
changes (requires granian[reload] extra)
[env var: GRANIAN_RELOAD; default:
(disabled)]
--reload-paths TEXT WatchFile paths to watch for changes
(requires granian[reload] extra) [env var:
GRANIAN_RELOAD_PATHS; default:
/path/to/cwd]
--reload-ignore-paths TEXT WatchFile paths to ignore changes for
(requires granian[reload] extra) [env var:
GRANIAN_RELOAD_IGNORE_PATHS]
--reload-ignore-dirs TEXT WatchFile directories to ignore changes for
(requires granian[reload] extra). Replaces
the default list of directories to ignore in
watchfiles.filters.DefaultFilter. [env var:
GRANIAN_RELOAD_IGNORE_DIRS]
--reload-ignore-entity-patterns TEXT
WatchFile entity patterns to ignore changes
for (requires granian[reload] extra).
Replaces the default list of patterns to
ignore in watchfiles.filters.DefaultFilter.
[env var:
GRANIAN_RELOAD_IGNORE_ENTITY_PATTERNS]
--process-name TEXT Set a custom name for processes (requires
granian[pname] extra) [env var:
GRANIAN_PROCESS_NAME]
Expand Down
2 changes: 2 additions & 0 deletions granian/_imports.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,7 @@

try:
import watchfiles
from watchfiles import BaseFilter
gi0baro marked this conversation as resolved.
Show resolved Hide resolved
except ImportError:
watchfiles = None
BaseFilter = None
42 changes: 41 additions & 1 deletion granian/cli.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import json
import pathlib
from enum import Enum
from typing import Any, Callable, Optional, Type, TypeVar, Union
from typing import Any, Callable, List, Optional, Type, TypeVar, Union

import click

Expand Down Expand Up @@ -194,6 +194,38 @@ def option(*param_decls: str, cls: Optional[Type[click.Option]] = None, **attrs:
default=False,
help="Enable auto reload on application's files changes (requires granian[reload] extra)",
)
@option(
'--reload-paths',
type=click.Path(exists=True, file_okay=True, dir_okay=True, readable=True, path_type=pathlib.Path),
default=(pathlib.Path.cwd(),),
help='Paths to watch for changes',
multiple=True,
)
@option(
'--reload-ignore-dirs',
default=(),
iamkhav marked this conversation as resolved.
Show resolved Hide resolved
help=(
'Names of directories to ignore (i.e. should not trigger reload). '
'Extends the default list of directories to ignore in watchfiles.filters.DefaultFilter.'
),
multiple=True,
)
@option(
'--reload-ignore-entity-patterns',
iamkhav marked this conversation as resolved.
Show resolved Hide resolved
default=(),
help=(
'Regex patterns to ignore changes for. '
'Extends the default list of patterns to ignore in watchfiles.filters.DefaultFilter.'
),
multiple=True,
)
@option(
'--reload-ignore-paths',
type=click.Path(exists=False, path_type=pathlib.Path),
default=(),
help='Absolute paths to ignore changes for',
multiple=True,
)
@option(
'--process-name',
help='Set a custom name for processes (requires granian[pname] extra)',
Expand Down Expand Up @@ -242,6 +274,10 @@ def cli(
respawn_failed_workers: bool,
respawn_interval: float,
reload: bool,
reload_paths: List[pathlib.Path],
gi0baro marked this conversation as resolved.
Show resolved Hide resolved
gi0baro marked this conversation as resolved.
Show resolved Hide resolved
reload_ignore_dirs: Optional[List[str]],
reload_ignore_entity_patterns: Optional[List[str]],
reload_ignore_paths: Optional[List[pathlib.Path]],
process_name: Optional[str],
pid_file: Optional[pathlib.Path],
) -> None:
Expand Down Expand Up @@ -294,6 +330,10 @@ def cli(
respawn_failed_workers=respawn_failed_workers,
respawn_interval=respawn_interval,
reload=reload,
reload_paths=reload_paths,
reload_ignore_paths=reload_ignore_paths,
reload_ignore_dirs=reload_ignore_dirs,
reload_ignore_entity_patterns=reload_ignore_entity_patterns,
process_name=process_name,
pid_file=pid_file,
)
Expand Down
36 changes: 32 additions & 4 deletions granian/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import errno
import multiprocessing
import os
import pathlib
iamkhav marked this conversation as resolved.
Show resolved Hide resolved
import signal
import socket
import ssl
Expand All @@ -12,11 +13,11 @@
import time
from functools import partial
from pathlib import Path
from typing import Any, Callable, Dict, List, Optional, Tuple
from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple

from ._futures import future_watcher_wrapper
from ._granian import ASGIWorker, RSGIWorker, WSGIWorker
from ._imports import setproctitle, watchfiles
from ._imports import BaseFilter, setproctitle, watchfiles
from ._internal import load_target
from .asgi import LifespanProtocol, _callback_wrapper as _asgi_call_wrap
from .constants import HTTPModes, Interfaces, Loops, ThreadModes
Expand Down Expand Up @@ -95,6 +96,11 @@ def __init__(
respawn_failed_workers: bool = False,
respawn_interval: float = 3.5,
reload: bool = False,
reload_paths: Sequence[pathlib.Path] = (pathlib.Path.cwd(),),
reload_ignore_dirs: Optional[Sequence[str]] = (),
reload_ignore_entity_patterns: Optional[Sequence[str]] = (),
reload_ignore_paths: Optional[Sequence[pathlib.Path]] = (),
iamkhav marked this conversation as resolved.
Show resolved Hide resolved
reload_filter: Optional[BaseFilter] = None,
iamkhav marked this conversation as resolved.
Show resolved Hide resolved
process_name: Optional[str] = None,
pid_file: Optional[Path] = None,
):
Expand Down Expand Up @@ -129,6 +135,11 @@ def __init__(
self.respawn_failed_workers = respawn_failed_workers
self.reload_on_changes = reload
self.respawn_interval = respawn_interval
self.reload_paths = reload_paths
self.reload_ignore_paths = reload_ignore_paths
self.reload_ignore_dirs = reload_ignore_dirs
self.reload_ignore_entity_patterns = reload_ignore_entity_patterns
self.reload_filter = reload_filter
self.process_name = process_name
self.pid_file = pid_file

Expand Down Expand Up @@ -577,11 +588,28 @@ def _serve_with_reloader(self, spawn_target, target_loader):
logger.error('Using --reload requires the granian[reload] extra')
sys.exit(1)

reload_path = Path.cwd()
# Use given or default filter rules
reload_filter = self.reload_filter or watchfiles.filters.DefaultFilter
# Extend `reload_filter` with explicit args
ignore_dirs = (*reload_filter.ignore_dirs, *self.reload_ignore_dirs)
ignore_entity_patterns = (
*reload_filter.ignore_entity_patterns,
*self.reload_ignore_entity_patterns,
)
ignore_paths = (*reload_filter.ignore_paths, *self.reload_ignore_paths)
# Construct new filter
reload_filter = watchfiles.filters.DefaultFilter(
ignore_dirs=ignore_dirs, ignore_entity_patterns=ignore_entity_patterns, ignore_paths=ignore_paths
)
gi0baro marked this conversation as resolved.
Show resolved Hide resolved

sock = self.startup(spawn_target, target_loader)

try:
for _ in watchfiles.watch(reload_path, stop_event=self.main_loop_interrupt):
for _ in watchfiles.watch(
*self.reload_paths,
watch_filter=reload_filter,
stop_event=self.main_loop_interrupt,
):
logger.info('Changes detected, reloading workers..')
self._stop_workers()
self._spawn_workers(sock, spawn_target, target_loader)
Expand Down
Loading