coveragepy icon indicating copy to clipboard operation
coveragepy copied to clipboard

Exception when running uvicorn in subprocess

Open jonathan-boudreau-work opened this issue 7 months ago • 1 comments

Describe the bug When trying to run uvicorn in a subprocess (using multiprocessing) an exception is thrown in that subprocess after you terminate it.

To Reproduce

versions python version: 3.12.3 uvicorn version: 0.34.2 fastapi version: 0.115.12 pytest version: 8.3.5 coverage: 7.8.0

.coveragerc

[run]
concurrency = multiprocessing
parallel = true
sigterm = true
debug = multiproc
debug_file = /tmp/coverage.log

sample app

from fastapi import FastAPI

app = FastAPI()

@app.get("/items")
async def get_items_list():
    return [
        { "id": index, "name": f"item {id}" }
        for index in range(10)
    ]

sample test file

import coverage
from multiprocessing import Process
from os import environ
from app.app import app
import time
import httpx
import uvicorn


def run_app() -> None:
    coverage.process_startup()
    print('the app is run', environ, app)
    uvicorn.run(app, host="127.0.0.1", port=9991)


def test_app() -> None:
    process = Process(target=run_app)
    print('starting')
    process.start()
    time.sleep(.1)
    print('terminating')
    with httpx.Client() as client:
        response = client.get('http://localhost:9991/items')
        print('response', response.text)
    process.terminate()

Command to run:

poetry run coverage run -m pytest --capture=no

Exception thrown:

Process Process-1:
Traceback (most recent call last):
  File "/usr/lib/python3.12/multiprocessing/process.py", line 314, in _bootstrap
    self.run()
  File "/usr/lib/python3.12/multiprocessing/process.py", line 108, in run
    self._target(*self._args, **self._kwargs)
  File "/home/aghost-7/workspace/sample-subprocess/test/test_app.py", line 13, in run_app
    uvicorn.run(app, host="127.0.0.1", port=9991)
  File "/home/aghost-7/.cache/pypoetry/virtualenvs/sample-subprocess-vy0N4M8T-py3.12/lib/python3.12/site-packages/uvicorn/main.py", line 580, in run
    server.run()
  File "/home/aghost-7/.cache/pypoetry/virtualenvs/sample-subprocess-vy0N4M8T-py3.12/lib/python3.12/site-packages/uvicorn/server.py", line 66, in run
    return asyncio.run(self.serve(sockets=sockets))
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.12/asyncio/runners.py", line 194, in run
    return runner.run(main)
           ^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.12/asyncio/runners.py", line 118, in run
    return self._loop.run_until_complete(task)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.12/asyncio/base_events.py", line 687, in run_until_complete
    return future.result()
           ^^^^^^^^^^^^^^^
  File "/home/aghost-7/.cache/pypoetry/virtualenvs/sample-subprocess-vy0N4M8T-py3.12/lib/python3.12/site-packages/uvicorn/server.py", line 69, in serve
    with self.capture_signals():
  File "/usr/lib/python3.12/contextlib.py", line 144, in __exit__
    next(self.gen)
  File "/home/aghost-7/.cache/pypoetry/virtualenvs/sample-subprocess-vy0N4M8T-py3.12/lib/python3.12/site-packages/uvicorn/server.py", line 330, in capture_signals
    signal.raise_signal(captured_signal)
  File "/home/aghost-7/.cache/pypoetry/virtualenvs/sample-subprocess-vy0N4M8T-py3.12/lib/python3.12/site-packages/coverage/control.py", line 722, in _on_sigterm
    os.kill(os.getpid(), signal.SIGTERM)                # pragma: not covered
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/aghost-7/.cache/pypoetry/virtualenvs/sample-subprocess-vy0N4M8T-py3.12/lib/python3.12/site-packages/coverage/control.py", line 718, in _on_sigterm
    self._atexit("sigterm")
  File "/home/aghost-7/.cache/pypoetry/virtualenvs/sample-subprocess-vy0N4M8T-py3.12/lib/python3.12/site-packages/coverage/control.py", line 712, in _atexit
    self.stop()
  File "/home/aghost-7/.cache/pypoetry/virtualenvs/sample-subprocess-vy0N4M8T-py3.12/lib/python3.12/site-packages/coverage/control.py", line 691, in stop
    self._collector.stop()
  File "/home/aghost-7/.cache/pypoetry/virtualenvs/sample-subprocess-vy0N4M8T-py3.12/lib/python3.12/site-packages/coverage/collector.py", line 348, in stop
    assert self._collectors
           ^^^^^^^^^^^^^^^^
AssertionError

Expected behavior I expect there to be no exception in the subprocess.

Additional context There might be a conflict with uvicorn and coverage since they both attach to the global signal handlers. You can see this in uvicorn here: https://github.com/encode/uvicorn/blob/master/uvicorn/server.py#L313-L330

jonathan-boudreau-work avatar May 02 '25 17:05 jonathan-boudreau-work

Also, if you're willing to handle this use case I'd be interested in fixing it with some minor guidance.

jonathan-boudreau-work avatar May 02 '25 17:05 jonathan-boudreau-work