如@jsbueno和其他人指出的,安装第二个信号处理程序是有问题的。下面是一个完整的模板程序,它可以在uvicorn/FastAPI生态系统中运行,并且可以通过调用URL(/shutdown或/restart)或发送信号(例如SIGINT)来关闭。你需要将其保存为"main.py"才能使其工作。
根据程序的关闭方式返回不同的退出代码(对于/shutdown或/restart分别为0或1)。我在一个包装程序中使用这些返回代码来重新启动服务器,如果这是被请求的操作。
from fastapi import FastAPI
import uvicorn
import time
import asyncio
import signal
import sys
import os
class RuntimeVals:
shutdown = False
restart = False
shutdown_complete = False
runtime_cfg = RuntimeVals()
app = FastAPI()
async def worker(n):
while not runtime_cfg.shutdown:
await asyncio.sleep(0.1)
if n == 1:
raise RuntimeError("This is a demo error in worker 1")
else:
print(f"Worker {n} shutdown cleanly")
async def mainloop():
loop = asyncio.get_running_loop()
done = []
pending = [loop.create_task(worker(1)), loop.create_task(worker(2))]
while len(pending) > 0:
done, pending = await asyncio.wait(pending)
for task in done:
e = task.exception()
if e is not None:
task.print_stack()
else:
result = task.result()
if runtime_cfg.restart:
print("RESTART")
else:
print("SHUTDOWN")
runtime_cfg.shutdown_complete = True
os.kill(os.getpid(), signal.SIGINT)
@app.get("/shutdown")
async def clean_shutdown():
runtime_cfg.shutdown = True
@app.get("/restart")
async def clean_restart():
runtime_cfg.restart = True
runtime_cfg.shutdown = True
@app.on_event("startup")
async def startup_event():
loop = asyncio.get_running_loop()
loop.create_task(mainloop())
@app.on_event("shutdown")
async def shutdown_event():
runtime_cfg.shutdown = True
while runtime_cfg.shutdown_complete is False:
print("waiting")
await asyncio.sleep(1)
if __name__ == "__main__":
uv_cfg = uvicorn.Config(
"main:app",
host="0.0.0.0",
port=8000,
log_level="debug",
timeout_graceful_shutdown=2,
)
server = uvicorn.Server(config=uv_cfg)
server.run()
import main
if main.runtime_cfg.restart:
sys.exit(1)
else:
sys.exit(0)
大部分的操作都发生在
mainloop
中,它是从
startup_event
中调用的。Mainloop创建子任务并监视它们,根据需要收集和打印异常。每个子任务在
runtime_cfg.shutdown
为True时应返回。
关闭程序有两种方式:通过信号或通过URL(/shutdown或/restart)。如果使用SIGTERM/SIGINT信号,
shutdown_event
将设置
runtime_cfg.shutdown = True
然后等待。每个工作进程退出;一旦所有工作进程都停止,mainloop设置
runtime_cfg.shutdown_complete = True
,这允许
shutdown_event
返回并使uvicorn退出。mainloop发送的第二个信号被忽略,因为uvicorn服务器已经在关闭过程中。
如果调用了URL,它将设置
runtime_cfg.shutdown = True
。每个工作进程退出,
mainloop
向程序发送SIGINT信号,这会导致ucivorn启动关闭过程。
一个非常棘手的问题是在程序结束时,当
server.run()
返回时。在主函数中设置的任何模块变量(例如
runtime_cfg.restart
),在服务器运行时被遮蔽。我相当确定这是因为uvicorn在启动时再次导入主函数(它在
uvicorn.Config
中使用
"main:app"
)。这种遮蔽使得从服务器环境向主函数传递信息变得非常混乱。
CTRL+C
停止它。 - Sir l33tnamerunning
被设置为False
且没有调用sys.exit()
,否则客户端将收到内部服务器错误
响应而返回null
。你问如何在关闭时停止循环。那么,使用该方法,当按下CTRL+C
时,你的应用程序是否实际上已关闭?我猜没有,因为控制台中的应用程序仍在运行。因此,你需要调用sys.exit()
,如果像之前描述的那样这样做,应用程序将被强制退出,而不会先完成已经运行的事件循环中的任务。 - Chris