-
-
Notifications
You must be signed in to change notification settings - Fork 709
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
[QUESTION] interplay with multiprocessing #548
Comments
For more context, Uvicorn shows the same behavior in
import os
import time
from concurrent.futures import ProcessPoolExecutor
from flask import Flask
app = Flask(__name__)
def simple_routine(sleep_for):
print(f"PID {os.getpid()} has sleep time: {sleep_for}")
time.sleep(sleep_for)
return "done"
@app.route("/test-endpoint", methods=['GET', 'POST'])
def test_endpoint():
print(f"main process: {os.getpid()}")
START_TIME = time.time()
STOP_TIME = START_TIME + 2
with ProcessPoolExecutor(max_workers=2) as pool:
futures = [
pool.submit(simple_routine, 1),
pool.submit(simple_routine, 10),
]
results = []
for fut in futures:
remains = max(STOP_TIME - time.time(), 0)
try:
results.append(fut.result(timeout=remains))
except:
results.append("not done")
# terminate the processes which are still running
for pid, proc in pool._processes.items():
print("terminating pid ", pid)
proc.terminate()
print("exiting at: ", int(time.time() - START_TIME))
return "everything is good"
import requests
for _ in range(20):
print(requests.get("http://localhost:5000/test-endpoint").text) The Flask debug server in contrast behaves as expected. |
the below could be improved and maybe doesn't deal with all potential issues but this might be a good start for you.
|
Thank you @euri10 for looking into it. I'm not really conversant with async python, so I couldn't really figure out how to put in a timeout for the child processes. Also, the routines relying on multiprocessing are generally a couple levels deep than the API method so I'm a bit hesitant to introducing asyncio. That said, I don't quite see how running the Processpool executor in an asyncio loop would help. The underlying problem imo is that stopping python processes once started is generally tricky, and somehow the parent process is getting terminated, when running either of the async servers? Would it be useful to reframe the question or maybe mark it as a bug for Uvicorn (it shuts down even when running in wsgi mode, which is undesirable)? |
I'm not sure (I may be totally off on this, so please apologize in advance if that's the case) that even your wsgi case should behave the way you think it should.
running uvicorn puts you in an event loop by design, so should you want to run some blocking code ( usually it's about using 'loop.run_in_executor' but afaik there's no way to timeout your processes one way to deal with that would be maybe to do something like below : an AsyncPoolExecutor that would wait up to other may have better ideas or find it's a bug :)
|
Thanks @euri10 . This snippet however still waits for the longest running process to actually finish before returning. When using the pool (ProcessPoolExecutor) from concurrent.futures module, I don't think there is a way around it besides:
# works well with flask, not with asgi
children = {pid: child for pid, child in pool._processes.items()}
pool.shutdown(wait=False)
for pid, proc in children.items():
proc.terminate()
# using the psutil library. Seems to work with both asgi and flask.
import psutil, signal
def kill_process(process_id):
"""kill the process specified by the given id"""
try:
process = psutil.Process(process_id)
process.send_signal(signal.SIGKILL)
except psutil.NoSuchProcess:
return
children = [child for child in pool._processes]
pool.shutdown(wait=False)
for child_id in children:
kill_process(child_id) I thought it would be a bug since the wsgi servers behave as expected. Would you suggest I close this issue? |
no I'd wait for someone better on this to give you maybe better insights. |
Sorry for getting back late on this. In summary,
This probably has more to do with how the system signal handling works rather than uvicorn. Please feel free to close this. |
@ananis25 What platforms is this working on for you? As an FYI:
Just rolling the 🎲 |
Thank you for the link to the FastAPI issue, that explains the underlying issue. Using the |
Stumbled upon this as well. I believe this is a Python bug, which I just reported: https://bugs.python.org/issue43064 The issue is that on linux:
My current workaround is to subclass class UvicornServerPatchedSignalHandlers(uvicorn.Server):
def install_signal_handlers(self) -> None:
for sig in (signal.SIGINT, signal.SIGTERM):
signal.signal(sig, self.handle_exit) FYI, using Apologies for necro-ing, but I thought this would be useful to others. |
Edit: This startup method has been deprecated. See the solution here instead: For FastAPI usage, I solved this by setting import multiprocessing
...
app = FastAPI()
@app.on_event("startup")
def startup_event() -> None:
multiprocessing.set_start_method("spawn") |
Hi, @selimb there is an explanation what is happening and why asyncio setups signal handler in a specific way -- it calls Any child process will inherit not only signal handlers' behavior but an opened socket. And as a result, when we are sending a signal to the child process, it will be written to the socket and the parent process will receive it too, even though this signal was sent not to him; Or if you will send it to the parent process, the child process will receive this signal too; How you can avoid this behavior -- at the very beginning of the child process you can execute the following code
PS. I've downloaded an example from https://bugs.python.org/issue43064, and added
|
@Mixser Fascinating! Many thanks for the detailed explanation. I was able to replicate your results as well. |
Since my previous solution (using from collections.abc import AsyncIterator
from contextlib import asynccontextmanager
import multiprocessing
@asynccontextmanager
async def lifespan(_: FastAPI) -> AsyncIterator[None]:
"""Startup/shutdown logic."""
multiprocessing.set_start_method("spawn")
yield
app = FastAPI(lifespan=lifespan) |
Describe the bug
I'm trying to run parallel tasks with a timeout per task (using multiprocessing) inside an API method. On trying to terminate the child processes post the time limit, the server process shuts down and disconnects.
To Reproduce
repro.py
python repro.py
.Expected behavior
We start 2 processes one of which exceeds the time limit after which we try terminate it. The server shouldn't shut down and continue serving requests. Interestingly, the server doesn't actually exit until the long running process is complete.
With Flask, the behavior of an identical app is as expected.
Environment
Additional context
This came up while trying to port a WSGI application to FastAPI - link. On suggestion of @dmontagu, I tried to reproduce it with starlette and just uvicorn and saw that the error persists.
Hypercorn shows similar behavior in that the application shuts down after serving the first request. So, the issue likely has something to do with how async servers manage processes? Could you please point to where I might look to solve this?
Thank you for looking.
The text was updated successfully, but these errors were encountered: