Skip to content

test(supervisors): fix occasional assertion failures and hangs #2431

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

Open
wants to merge 30 commits into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
b1bf81c
test(supervisors): ensure cleanup in test_multiprocess by using 'fina…
vvanglro Aug 14, 2024
b4d8b09
test(supervisor): add timeout to multiprocess test subprocess
vvanglro Aug 14, 2024
a65c61a
try to solve
vvanglro Aug 14, 2024
d832a5a
try to solve
vvanglro Aug 14, 2024
cd7dcc9
Merge branch 'master' into fix/test_hang
vvanglro Aug 15, 2024
27c6968
feat: add retry for unstable tests
vvanglro Aug 15, 2024
f0d496e
test(supervisor): remove retry, ensure process close.
vvanglro Aug 15, 2024
239a3cf
Merge branch 'master' into fix/test_hang
vvanglro Aug 20, 2024
ffe43cb
test case
vvanglro Aug 20, 2024
41583fd
Merge branch 'master' into fix/test_hang
vvanglro Oct 9, 2024
5b97b39
Merge branch 'master' into fix/test_hang
vvanglro Oct 10, 2024
d737e28
Merge branch 'master' into fix/test_hang
vvanglro Oct 17, 2024
b8f2769
Merge branch 'master' into fix/test_hang
Kludex Nov 20, 2024
0b1b372
test(supervisors): improve test_multiprocess reliability
vvanglro Nov 21, 2024
bd08e0e
test(supervisors): improve test_multiprocess reliability
vvanglro Dec 16, 2024
0956647
Merge branch 'master' into fix/test_hang
vvanglro Dec 16, 2024
f95cd0f
test(supervisors): improve test_multiprocess reliability
vvanglro Dec 16, 2024
49c40db
Merge branch 'master' into fix/test_hang
vvanglro Jan 6, 2025
7c48f04
Release gil to let the supervisor thread switch execution
vvanglro Jan 6, 2025
418dc84
format
vvanglro Jan 6, 2025
b8a0686
Merge branch 'master' into fix/test_hang
vvanglro Jan 14, 2025
a711e2e
Merge branch 'master' into fix/test_hang
Kludex Jul 2, 2025
8a53143
Merge branch 'master' into fix/test_hang
vvanglro Jul 2, 2025
13c6970
test(supervisors): improve test_multiprocess reliability
vvanglro Jul 3, 2025
8ff5788
Merge branch 'master' into fix/test_hang
vvanglro Jul 3, 2025
d5bee19
Merge branch 'master' into fix/test_hang
vvanglro Jul 6, 2025
a447a5a
test(supervisors): implement a generic retry mechanism for tests
vvanglro Jul 16, 2025
68a59ed
test: add pragma nocover to retry decorator
vvanglro Jul 16, 2025
66fef01
lint
vvanglro Jul 16, 2025
e470d85
test: add pragma nocover to async_wrapper return statement
vvanglro Jul 16, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 15 additions & 7 deletions tests/supervisors/test_multiprocess.py
Original file line number Diff line number Diff line change
@@ -10,6 +10,7 @@

import pytest

from tests.utils import with_retry
from uvicorn import Config
from uvicorn._types import ASGIReceiveCallable, ASGISendCallable, Scope
from uvicorn.supervisors import Multiprocess
@@ -76,22 +77,29 @@ def test_multiprocess_run() -> None:


@new_console_in_windows
@with_retry(3)
def test_multiprocess_health_check() -> None:
"""
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)
time.sleep(0) # release gil.
time.sleep(1) # ensure server is up.
process = supervisor.processes[0]
assert process.is_alive()
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()
process.join()
try:
assert not process.is_alive(0.5)
time.sleep(0) # release gil.
time.sleep(1) # ensure process restart complete.
for p in supervisor.processes:
assert p.is_alive()
finally:
supervisor.signal_queue.append(signal.SIGINT)
supervisor.join_all()


@new_console_in_windows
36 changes: 36 additions & 0 deletions tests/utils.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
from __future__ import annotations

import asyncio
import functools
import inspect
import os
import signal
import sys
from collections.abc import AsyncIterator
from contextlib import asynccontextmanager, contextmanager
from pathlib import Path
from socket import socket
from typing import Any, Callable

from uvicorn import Config, Server

@@ -53,3 +56,36 @@ def get_asyncio_default_loop_per_os() -> type[asyncio.AbstractEventLoop]:
return asyncio.ProactorEventLoop # type: ignore # pragma: nocover
else:
return asyncio.SelectorEventLoop # pragma: nocover


def with_retry(retry_count: int = 1) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
if inspect.iscoroutinefunction(func):

@functools.wraps(func) # pragma: nocover
async def async_wrapper(*args, **kwargs):
for attempt in range(retry_count):
try:
return await func(*args, **kwargs)
except Exception as e:
if attempt == retry_count - 1:
raise e
return None

return async_wrapper # pragma: nocover

else:
# Maintain the original calling method of the test case, e.g. test_multiprocess_health_check.
@functools.wraps(func) # pragma: nocover
def sync_wrapper(*args, **kwargs):
for attempt in range(retry_count):
try:
return func(*args, **kwargs)
except Exception as e:
if attempt == retry_count - 1:
raise e
return None

return sync_wrapper

return decorator