请求失败时也要关闭资源
目标
把启动、关闭和异常路径分开,并真正运行 FastAPI 的 lifespan。
为什么重要
测试通过了,但生产环境重启时连接却残留了下来。原因是使用 TestClient 时没有用 context manager,所以启动和关闭的代码没有被执行。只看一次正常响应的测试,无法知道应用在什么时候打开和关闭资源。这里不用外部连接,而是用一个记录事件的小资源来观察生命周期。
步骤
- 在
/root/work/fa-resource-lifecycle-lab/service.py中,new_resource() 是 {open:False, events:[]} 的新字典,各次调用之间不共享 events。
先做一次准备。已有的文件不会被覆盖。
mkdir -p /root/work/fa-resource-lifecycle-lab
test -e /root/work/fa-resource-lifecycle-lab/service.py || cp /opt/fixtures/ten_labs/fa-resource-lifecycle-lab/service.py /root/work/fa-resource-lifecycle-lab/service.py
cd /root/work/fa-resource-lifecycle-lab
-
在
/root/work/fa-resource-lifecycle-lab/service.py中,start(resource) 在已经打开时是 ValueError,否则把 open 改为 True,并往 events 中追加 'open'。 -
在
/root/work/fa-resource-lifecycle-lab/service.py中,stop(resource) 只有在打开时才把 open 改为 False,并往 events 中追加 'close'。如果已经关闭,就保持原样。 -
在
/root/work/fa-resource-lifecycle-lab/service.py中,read(resource) 在已关闭时是 RuntimeError,在打开时返回 {ready:True}。 -
在
/root/work/fa-resource-lifecycle-lab/service.py中,scope(resource) 是一个 contextmanager。进入时调用 start,在代码块内 yield resource,无论代码块成功还是失败,都用 stop 关闭。代码块中的异常要向外传播。 -
在
/root/work/fa-resource-lifecycle-lab/service.py中,lifespan_for(resource) 返回一个 asynccontextmanager 函数 lifespan(app)。在 scope(resource) 之内设置 app.state.resource,并 yield。 -
在
/root/work/fa-resource-lifecycle-lab/service.py中,create_app(resource) 使用 lifespan_for。GET /ready 返回对 app.state.resource 执行 read 的结果。context 结束时必须关闭资源。 -
在
/root/work/fa-resource-lifecycle-lab/service.py中,exercise(resource, fail=False) 在 with TestClient(create_app(resource)) 之内调用 GET /ready。fail=True 时在其中抛出 RuntimeError,否则返回响应 JSON。两种情形下资源都必须被关闭。
参考
- 不联网、不安装软件包,在现有的 lab-dev 环境中进行。
- 每一步都在 45 秒的评分预算内运行。不要添加真实的 sleep 或网络调用。
- 评分会重新导入提交的模块,并用独立的输入和临时 DB 进行检查。请实现契约,而不要把预期值当作常量返回。
- FastAPI 官方文档 · pytest 官方文档 · Python sqlite3
- 局限:教学用的资源字典是一个代替真实 DB 连接池的观测装置。在生产环境中,还要设计部分初始化失败、连接池的并发、取消处理以及关闭的超时时间。检查的不是写进报告里的事件字符串,而是学习者的代码在运行过程中所改变的对象状态。
让资源状态彼此独立
在 /root/work/fa-resource-lifecycle-lab/service.py 中,new_resource() 是 {open:False, events:[]} 的新字典,各次调用之间不共享 events。
先做一次准备。已有的文件不会被覆盖。
mkdir -p /root/work/fa-resource-lifecycle-lab
test -e /root/work/fa-resource-lifecycle-lab/service.py || cp /opt/fixtures/ten_labs/fa-resource-lifecycle-lab/service.py /root/work/fa-resource-lifecycle-lab/service.py
cd /root/work/fa-resource-lifecycle-lab
不要通过全局变量或默认参数共享可变的列表。
保存后用 bash /opt/lab/checks/fa-resource-lifecycle-lab/01-contract.sh 确认。
拒绝重复启动
在 /root/work/fa-resource-lifecycle-lab/service.py 中,start(resource) 在已经打开时是 ValueError,否则把 open 改为 True,并往 events 中追加 'open'。
拒绝启动两次而弄丢一个资源的行为。
保存后用 bash /opt/lab/checks/fa-resource-lifecycle-lab/02-contract.sh 确认。
让关闭具有幂等性
在 /root/work/fa-resource-lifecycle-lab/service.py 中,stop(resource) 只有在打开时才把 open 改为 False,并往 events 中追加 'close'。如果已经关闭,就保持原样。
即使多条清理路径重叠,也不应产生重复的 close 事件。
保存后用 bash /opt/lab/checks/fa-resource-lifecycle-lab/03-contract.sh 确认。
阻止使用已关闭的资源
在 /root/work/fa-resource-lifecycle-lab/service.py 中,read(resource) 在已关闭时是 RuntimeError,在打开时返回 {ready:True}。
就绪状态与对象是否存在是两回事。对象即使存在,也可能是关闭的。
保存后用 bash /opt/lab/checks/fa-resource-lifecycle-lab/04-contract.sh 确认。
在异常路径上放置 finally
在 /root/work/fa-resource-lifecycle-lab/service.py 中,scope(resource) 是一个 contextmanager。进入时调用 start,在代码块内 yield resource,无论代码块成功还是失败,都用 stop 关闭。代码块中的异常要向外传播。
如果只在 yield 之后写 close,那么发生异常时就执行不到那一行。
保存后用 bash /opt/lab/checks/fa-resource-lifecycle-lab/05-contract.sh 确认。
把应用生命周期与资源连接起来
在 /root/work/fa-resource-lifecycle-lab/service.py 中,lifespan_for(resource) 返回一个 asynccontextmanager 函数 lifespan(app)。在 scope(resource) 之内设置 app.state.resource,并 yield。
不是调用 lifespan 函数本身,而是把它传给 FastAPI 的构造函数。
保存后用 bash /opt/lab/checks/fa-resource-lifecycle-lab/06-contract.sh 确认。
用真实请求读取就绪状态
在 /root/work/fa-resource-lifecycle-lab/service.py 中,create_app(resource) 使用 lifespan_for。GET /ready 返回对 app.state.resource 执行 read 的结果。context 结束时必须关闭资源。
必须使用 with TestClient,才会把 lifespan 的启动和关闭都执行。
保存后用 bash /opt/lab/checks/fa-resource-lifecycle-lab/07-contract.sh 确认。
请求之后的失败也要清理
在 /root/work/fa-resource-lifecycle-lab/service.py 中,exercise(resource, fail=False) 在 with TestClient(create_app(resource)) 之内调用 GET /ready。fail=True 时在其中抛出 RuntimeError,否则返回响应 JSON。两种情形下资源都必须被关闭。
把正常路径和异常路径放进同一种清理结构,就能减少遗漏的关闭路径。
保存后用 bash /opt/lab/checks/fa-resource-lifecycle-lab/08-contract.sh 确认。