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

New multiprocess manager #2183

Open
wants to merge 58 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 47 commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
bb4efdd
New multiprocess manager
abersheeran Dec 15, 2023
fde0284
lint it
abersheeran Dec 15, 2023
efd22a3
Fixed test
abersheeran Dec 15, 2023
c602782
Fixed `Process`.`__init__`
abersheeran Dec 15, 2023
c41e48d
Fix signal handling in Multiprocess class
abersheeran Dec 15, 2023
cf71b61
Update coverage fail_under value
abersheeran Dec 15, 2023
3928b3d
Remove redundant log message
abersheeran Dec 15, 2023
31c300b
Update coverage fail_under value
abersheeran Dec 15, 2023
fb13e27
Update coverage fail_under value
abersheeran Dec 15, 2023
993e695
Update fail_under value in coverage report
abersheeran Dec 15, 2023
29b3ad6
Remove unused threading event
abersheeran Dec 15, 2023
637a372
Merge branch 'master' of https://github.com/encode/uvicorn into multi…
abersheeran Mar 4, 2024
f767d9c
lint
abersheeran Mar 4, 2024
2a7e193
more tests
abersheeran Mar 5, 2024
cc7a2e1
More tests and fix bug
abersheeran Mar 5, 2024
636080a
lint
abersheeran Mar 5, 2024
939ed2b
Add pytest.mark.skipif for SIGHUP test on Windows
abersheeran Mar 5, 2024
e9760f4
delete unused code
abersheeran Mar 5, 2024
a207664
More tests
abersheeran Mar 5, 2024
b8947b0
Try fix tests in Windows
abersheeran Mar 5, 2024
8cbd3c2
make linter feels great
abersheeran Mar 5, 2024
edae1a9
delete pytest-xdist
abersheeran Mar 5, 2024
c724cfa
Try fix test in windows
abersheeran Mar 5, 2024
3511f48
Try make mypy happy
abersheeran Mar 5, 2024
298906a
Skip tests in windows
abersheeran Mar 5, 2024
f1f4d64
lint
abersheeran Mar 5, 2024
fe2fb08
Try test basic run in Windows
abersheeran Mar 5, 2024
fb46e20
Try fix error in Windows
abersheeran Mar 5, 2024
197fa3d
lint
abersheeran Mar 5, 2024
33f5692
Skip tests in window
abersheeran Mar 5, 2024
d3c4484
Try test in window
abersheeran Mar 5, 2024
d3f090a
lint
abersheeran Mar 5, 2024
4bae355
Add import statement and set current working directory in test_multip…
abersheeran Mar 5, 2024
b910b3f
lint
abersheeran Mar 5, 2024
6d2c6b6
giveup
abersheeran Mar 5, 2024
7ee21af
Refactor signal handling in Multiprocess class
abersheeran Mar 6, 2024
9397e8e
Merge branch 'master' into multiprocess-manager
abersheeran Apr 2, 2024
8efcad3
Tests in windows
abersheeran Apr 3, 2024
7c683ca
lint
abersheeran Apr 3, 2024
4e3ac6e
lint
abersheeran Apr 3, 2024
404b864
ignore mypy check in linux
abersheeran Apr 3, 2024
d3a3aab
Add __init__.py
abersheeran Apr 3, 2024
736510a
fix warning
abersheeran Apr 3, 2024
42c7187
coverage ignore
abersheeran Apr 3, 2024
6fce985
Update coverage
abersheeran Apr 4, 2024
6e916f3
coverage
abersheeran Apr 4, 2024
b8ee655
Add documents
abersheeran Apr 5, 2024
66395d0
Update docs/deployment.md
abersheeran Apr 14, 2024
a19f737
Update uvicorn/supervisors/multiprocess.py
abersheeran Apr 14, 2024
658a4fa
Update uvicorn/supervisors/multiprocess.py
abersheeran Apr 14, 2024
43e0190
Update uvicorn/supervisors/multiprocess.py
abersheeran Apr 14, 2024
cc25743
Update uvicorn/supervisors/multiprocess.py
abersheeran Apr 14, 2024
5f23d2f
Update uvicorn/supervisors/multiprocess.py
abersheeran Apr 14, 2024
a5d19cb
Update uvicorn/supervisors/multiprocess.py
abersheeran Apr 14, 2024
1b3ddc7
Update uvicorn/supervisors/multiprocess.py
abersheeran Apr 14, 2024
1e8db13
Update uvicorn/supervisors/multiprocess.py
abersheeran Apr 14, 2024
a4209cf
Do not output the PID information repeatedly.
abersheeran Apr 14, 2024
dbc34c8
Fix occasional abnormal exits.
abersheeran Apr 14, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
18 changes: 17 additions & 1 deletion docs/deployment.md
Expand Up @@ -178,7 +178,23 @@ Running Uvicorn using a process manager ensures that you can run multiple proces

A process manager will handle the socket setup, start-up multiple server processes, monitor process aliveness, and listen for signals to provide for processes restarts, shutdowns, or dialing up and down the number of running processes.

Uvicorn provides a lightweight way to run multiple worker processes, for example `--workers 4`, but does not provide any process monitoring.
### Built-in

Uvicorn includes a `--workers` option that allows you to run multiple worker processes.

```bash
$ uvicorn main:app --workers 4
```

Unlike gunicorn, uvicorn does not use pre-fork, but uses [`spwan`](https://docs.python.org/zh-cn/3/library/multiprocessing.html#contexts-and-start-methods), which allows uvicorn's multi-process manager to still work well on Windows.
abersheeran marked this conversation as resolved.
Show resolved Hide resolved

The default process manager monitors the status of child processes and automatically restarts child processes that die unexpectedly. Not only that, it will also monitor the status of the child process through the pipeline. When the child process is accidentally stuck, the corresponding child process will be killed through an unstoppable system signal or interface.

You can also manage child processes by sending specific signals to the main process. (Not supported on Windows.)

- `SIGHUP`: Work processeses are graceful restarted one after another. If you update the code, the new worker process will use the new code.
- `SIGTTIN`: Increase the number of worker processes by one.
- `SIGTTOU`: Decrease the number of worker processes by one.

### Gunicorn

Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Expand Up @@ -101,7 +101,7 @@ omit = [

[tool.coverage.report]
precision = 2
fail_under = 98.35
fail_under = 98.13
Copy link
Sponsor Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't want to decrease this, can we add pragmas where needed?

show_missing = true
skip_covered = true
exclude_lines = [
Expand All @@ -113,7 +113,7 @@ exclude_lines = [
]

[tool.coverage.coverage_conditional_plugin.omit]
"sys_platform == 'win32'" = ["uvicorn/loops/uvloop.py"]
"sys_platform == 'win32'" = ["uvicorn/loops/uvloop.py", "uvicorn/supervisors/multiprocess.py"]
"sys_platform != 'win32'" = ["uvicorn/loops/asyncio.py"]

[tool.coverage.coverage_conditional_plugin.rules]
Expand Down
Empty file added tests/supervisors/__init__.py
Empty file.
150 changes: 146 additions & 4 deletions tests/supervisors/test_multiprocess.py
@@ -1,22 +1,67 @@
from __future__ import annotations

import functools
import os
import signal
import socket
import threading
import time
from typing import Any, Callable

import pytest

from uvicorn import Config
from uvicorn._types import ASGIReceiveCallable, ASGISendCallable, Scope
from uvicorn.supervisors import Multiprocess
from uvicorn.supervisors.multiprocess import Process


def new_console_in_windows(test_function: Callable[[], Any]) -> Callable[[], Any]:
if os.name != "nt":
return test_function

@functools.wraps(test_function)
def new_function():
import subprocess
import sys

module = test_function.__module__
name = test_function.__name__

subprocess.check_call(
[
sys.executable,
"-c",
f"from {module} import {name}; {name}.__wrapped__()",
],
creationflags=subprocess.CREATE_NO_WINDOW, # type: ignore[attr-defined]
)

return new_function


async def app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable) -> None:
pass # pragma: no cover


def run(sockets: list[socket.socket] | None) -> None:
pass # pragma: no cover
while True:
time.sleep(1)


def test_process_ping_pong() -> None:
process = Process(Config(app=app), target=lambda x: None, sockets=[])
threading.Thread(target=process.always_pong, daemon=True).start()
assert process.ping()


def test_process_ping_pong_timeout() -> None:
process = Process(Config(app=app), target=lambda x: None, sockets=[])
assert not process.ping(0.1)


def test_multiprocess_run() -> None:
@new_console_in_windows
def test_multiprocess_run() -> None: # pragma: py-win32
"""
A basic sanity check.

Expand All @@ -25,5 +70,102 @@ def test_multiprocess_run() -> None:
"""
config = Config(app=app, workers=2)
supervisor = Multiprocess(config, target=run, sockets=[])
supervisor.signal_handler(sig=signal.SIGINT, frame=None)
supervisor.run()
threading.Thread(target=supervisor.run, daemon=True).start()
supervisor.signal_queue.append(signal.SIGINT)
supervisor.join_all()


@new_console_in_windows
def test_multiprocess_health_check() -> None: # pragma: py-win32
"""
Ensure that the health check works as expected.
"""
config = Config(app=app, workers=2)
supervisor = Multiprocess(config, target=run, sockets=[])
threading.Thread(target=supervisor.run, daemon=True).start()
time.sleep(1)
process = supervisor.processes[0]
process.kill()
assert not process.is_alive()
time.sleep(1)
for p in supervisor.processes:
assert p.is_alive()
supervisor.signal_queue.append(signal.SIGINT)
supervisor.join_all()


@new_console_in_windows
def test_multiprocess_sigterm() -> None: # pragma: py-win32
"""
Ensure that the SIGTERM signal is handled as expected.
"""
config = Config(app=app, workers=2)
supervisor = Multiprocess(config, target=run, sockets=[])
threading.Thread(target=supervisor.run, daemon=True).start()
time.sleep(1)
supervisor.signal_queue.append(signal.SIGTERM)
supervisor.join_all()


@pytest.mark.skipif(not hasattr(signal, "SIGBREAK"), reason="platform unsupports SIGBREAK")
@new_console_in_windows
def test_multiprocess_sigbreak() -> None: # pragma: py-win32
"""
Ensure that the SIGBREAK signal is handled as expected.
"""
config = Config(app=app, workers=2)
supervisor = Multiprocess(config, target=run, sockets=[])
threading.Thread(target=supervisor.run, daemon=True).start()
time.sleep(1)
supervisor.signal_queue.append(getattr(signal, "SIGBREAK"))
supervisor.join_all()


@pytest.mark.skipif(not hasattr(signal, "SIGHUP"), reason="platform unsupports SIGHUP")
def test_multiprocess_sighup() -> None: # pragma: py-win32
"""
Ensure that the SIGHUP signal is handled as expected.
"""
config = Config(app=app, workers=2)
supervisor = Multiprocess(config, target=run, sockets=[])
threading.Thread(target=supervisor.run, daemon=True).start()
time.sleep(1)
pids = [p.pid for p in supervisor.processes]
supervisor.signal_queue.append(signal.SIGHUP)
time.sleep(1)
assert pids != [p.pid for p in supervisor.processes]
supervisor.signal_queue.append(signal.SIGINT)
supervisor.join_all()


@pytest.mark.skipif(not hasattr(signal, "SIGTTIN"), reason="platform unsupports SIGTTIN")
def test_multiprocess_sigttin() -> None: # pragma: py-win32
"""
Ensure that the SIGTTIN signal is handled as expected.
"""
config = Config(app=app, workers=2)
supervisor = Multiprocess(config, target=run, sockets=[])
threading.Thread(target=supervisor.run, daemon=True).start()
supervisor.signal_queue.append(signal.SIGTTIN)
time.sleep(1)
assert len(supervisor.processes) == 3
supervisor.signal_queue.append(signal.SIGINT)
supervisor.join_all()


@pytest.mark.skipif(not hasattr(signal, "SIGTTOU"), reason="platform unsupports SIGTTOU")
def test_multiprocess_sigttou() -> None: # pragma: py-win32
"""
Ensure that the SIGTTOU signal is handled as expected.
"""
config = Config(app=app, workers=2)
supervisor = Multiprocess(config, target=run, sockets=[])
threading.Thread(target=supervisor.run, daemon=True).start()
supervisor.signal_queue.append(signal.SIGTTOU)
time.sleep(1)
assert len(supervisor.processes) == 1
supervisor.signal_queue.append(signal.SIGTTOU)
time.sleep(1)
assert len(supervisor.processes) == 1
supervisor.signal_queue.append(signal.SIGINT)
supervisor.join_all()