diff --git a/docs/deployment.md b/docs/deployment.md index d69fcf88e..463d65f9b 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -135,6 +135,10 @@ Options: buffer of an incomplete event. --factory Treat APP as an application factory, i.e. a () -> callable. + --worker-healthcheck-timeout FLOAT + Timeout for healthcheck between supervisor + and worker in seconds (used only if workers + > 1). --help Show this message and exit. ``` diff --git a/docs/index.md b/docs/index.md index bb6fc321a..c093a908a 100644 --- a/docs/index.md +++ b/docs/index.md @@ -205,6 +205,10 @@ Options: buffer of an incomplete event. --factory Treat APP as an application factory, i.e. a () -> callable. + --worker-healthcheck-timeout FLOAT + Timeout for healthcheck between supervisor + and worker in seconds (used only if workers + > 1). --help Show this message and exit. ``` diff --git a/docs/settings.md b/docs/settings.md index a4439c3d0..4469ea8ad 100644 --- a/docs/settings.md +++ b/docs/settings.md @@ -74,6 +74,7 @@ Using Uvicorn with watchfiles will enable the following options (which are other * `--ws-ping-timeout ` - Set the WebSockets ping timeout, in seconds. Please note that this can be used only with the default `websockets` protocol. **Default:** *20.0* * `--lifespan ` - Set the Lifespan protocol implementation. **Options:** *'auto', 'on', 'off'.* **Default:** *'auto'*. * `--h11-max-incomplete-event-size ` - Set the maximum number of bytes to buffer of an incomplete event. Only available for `h11` HTTP protocol implementation. **Default:** *'16384'* (16 KB). +* `--worker_healthcheck_timeout ` - Timeout for healthcheck between supervisor and worker in seconds (used only if workers > 1). **Default:** *'5.0'* (5 s). ## Application Interface diff --git a/uvicorn/config.py b/uvicorn/config.py index 65dfe651e..ae3e04b76 100644 --- a/uvicorn/config.py +++ b/uvicorn/config.py @@ -223,6 +223,7 @@ def __init__( headers: list[tuple[str, str]] | None = None, factory: bool = False, h11_max_incomplete_event_size: int | None = None, + worker_healthcheck_timeout: float = 5.0, ): self.app = app self.host = host @@ -268,6 +269,7 @@ def __init__( self.encoded_headers: list[tuple[bytes, bytes]] = [] self.factory = factory self.h11_max_incomplete_event_size = h11_max_incomplete_event_size + self.worker_healthcheck_timeout = worker_healthcheck_timeout self.loaded = False self.configure_logging() diff --git a/uvicorn/main.py b/uvicorn/main.py index 96a10d538..291285cb8 100644 --- a/uvicorn/main.py +++ b/uvicorn/main.py @@ -360,6 +360,13 @@ def print_version(ctx: click.Context, param: click.Parameter, value: bool) -> No help="Treat APP as an application factory, i.e. a () -> callable.", show_default=True, ) +@click.option( + "--worker-healthcheck-timeout", + "worker_healthcheck_timeout", + type=float, + default=5.0, + help="Timeout for healthcheck between supervisor and worker in seconds (used only if workers > 1).", +) def main( app: str, host: str, @@ -408,6 +415,7 @@ def main( app_dir: str, h11_max_incomplete_event_size: int | None, factory: bool, + worker_healthcheck_timeout: float, ) -> None: run( app, @@ -457,6 +465,7 @@ def main( factory=factory, app_dir=app_dir, h11_max_incomplete_event_size=h11_max_incomplete_event_size, + worker_healthcheck_timeout=worker_healthcheck_timeout, ) @@ -509,6 +518,7 @@ def run( app_dir: str | None = None, factory: bool = False, h11_max_incomplete_event_size: int | None = None, + worker_healthcheck_timeout: float = 5.0, ) -> None: if app_dir is not None: sys.path.insert(0, app_dir) @@ -560,6 +570,7 @@ def run( use_colors=use_colors, factory=factory, h11_max_incomplete_event_size=h11_max_incomplete_event_size, + worker_healthcheck_timeout=worker_healthcheck_timeout, ) server = Server(config=config) diff --git a/uvicorn/supervisors/multiprocess.py b/uvicorn/supervisors/multiprocess.py index e198fe780..b0e77c3f1 100644 --- a/uvicorn/supervisors/multiprocess.py +++ b/uvicorn/supervisors/multiprocess.py @@ -164,16 +164,17 @@ def keep_subprocess_alive(self) -> None: return # parent process is exiting, no need to keep subprocess alive for idx, process in enumerate(self.processes): - if process.is_alive(): + if process.is_alive(self.config.worker_healthcheck_timeout): continue + logger.info(f"Child process [{process.pid}] is unresponsive") + process.kill() # process is hung, kill it process.join() if self.should_exit.is_set(): return # pragma: full coverage - logger.info(f"Child process [{process.pid}] died") process = Process(self.config, self.target, self.sockets) process.start() self.processes[idx] = process