TT Lab
开始
学习 学习路径 课程

FastAPI — 类型就是契约

请求失败时也要关闭资源

在 TT Lab 中继续学习

目标

把启动、关闭和异常路径分开,并真正运行 FastAPI 的 lifespan。

为什么重要

测试通过了,但生产环境重启时连接却残留了下来。原因是使用 TestClient 时没有用 context manager,所以启动和关闭的代码没有被执行。只看一次正常响应的测试,无法知道应用在什么时候打开和关闭资源。这里不用外部连接,而是用一个记录事件的小资源来观察生命周期。

步骤

  1. 在 /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
  1. 在 /root/work/fa-resource-lifecycle-lab/service.py 中,start(resource) 在已经打开时是 ValueError,否则把 open 改为 True,并往 events 中追加 'open'。

  2. 在 /root/work/fa-resource-lifecycle-lab/service.py 中,stop(resource) 只有在打开时才把 open 改为 False,并往 events 中追加 'close'。如果已经关闭,就保持原样。

  3. 在 /root/work/fa-resource-lifecycle-lab/service.py 中,read(resource) 在已关闭时是 RuntimeError,在打开时返回 {ready:True}。

  4. 在 /root/work/fa-resource-lifecycle-lab/service.py 中,scope(resource) 是一个 contextmanager。进入时调用 start,在代码块内 yield resource,无论代码块成功还是失败,都用 stop 关闭。代码块中的异常要向外传播。

  5. 在 /root/work/fa-resource-lifecycle-lab/service.py 中,lifespan_for(resource) 返回一个 asynccontextmanager 函数 lifespan(app)。在 scope(resource) 之内设置 app.state.resource,并 yield。

  6. 在 /root/work/fa-resource-lifecycle-lab/service.py 中,create_app(resource) 使用 lifespan_for。GET /ready 返回对 app.state.resource 执行 read 的结果。context 结束时必须关闭资源。

  7. 在 /root/work/fa-resource-lifecycle-lab/service.py 中,exercise(resource, fail=False) 在 with TestClient(create_app(resource)) 之内调用 GET /ready。fail=True 时在其中抛出 RuntimeError,否则返回响应 JSON。两种情形下资源都必须被关闭。

参考

让资源状态彼此独立

在 /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 确认。