使用 ETag 防止更新被覆盖
目标
当两个用户修改同一份草稿时,用 SQL 条件更新和 HTTP 响应来防止更新丢失。
为什么重要
一次正常请求的成功,并不能保证边界值和故障恢复。本实验先把每个函数的契约以小规模实现或测试,再把它们连接到真实运行中。评分不是只看代码是否存在或报告里的措辞,而是检查结果、异常和存储状态。请保留前面步骤的代码,继续进行下一步。
步骤
- 在
/root/work/fa-conditional-lab/service.py中,让 init_db(path) 在 SQLite 文件中创建 notes(id TEXT PRIMARY KEY, title TEXT NOT NULL, version INTEGER NOT NULL)。重复调用也不会清除已有数据。第一步的准备用下面的命令完成。
mkdir -p /root/work/fa-conditional-lab
cp /opt/fixtures/practice_depth/fa-conditional-lab/* /root/work/fa-conditional-lab/
cd /root/work/fa-conditional-lab
- 在
/root/work/fa-conditional-lab/service.py中,让 create_note(path, note_id, title) 以版本 1 INSERT 一条备忘并提交。相同的 id 以 sqlite3.IntegrityError 拒绝,并且不覆盖已有的标题。 - 在
/root/work/fa-conditional-lab/service.py中,让 read_note(path, note_id) 返回 {id, title, version} 字典或 None。不要把 id 用字符串拼接进 SQL,而要使用绑定。 - 在
/root/work/fa-conditional-lab/service.py中,让 etag(version) 把整数版本返回为包含双引号的字符串。例如:版本 3 对应的字符串是 '"v3"'。 - 在
/root/work/fa-conditional-lab/service.py中,让 parse_match(header) 只接受双引号内为 v 加一个正的十进制整数的标签,并返回版本。None、weak 标签、通配符、列表、v0 等都是 ValueError。例如:'"v12"' → 12。 - 在
/root/work/fa-conditional-lab/service.py中,让 update_note(path, note_id, title, version) 只有在 id 和 version 都一致时才更新标题,并把版本加 1。有一行被修改时返回 True,没有时返回 False。即使是并发请求,赢家也必须只有一个。 - 在
/root/work/fa-conditional-lab/service.py中,让 put_note(path, note_id, title, header) 对不存在的 id 返回 404,缺少条件返回 428,不支持的条件或版本不一致返回 412,成功返回 204。拒绝时不要修改 DB。 - 在
/root/work/fa-conditional-lab/service.py中,让 create_app(path) 提供 GET /notes/{note_id} 和 PUT /notes/{note_id}。GET 返回备忘 JSON 和 ETag,PUT 接收 {title: 1–100 个字符的字符串} 和 If-Match,并按上述结果码返回空响应。空标题为 422,GET 不存在的备忘为 404。
参考
- 软件包已安装在镜像中。不需要联网,也不需要 pip install。
- 准备用的复制只做一次。再次复制会把工作文件重置。
- 评分器会导入你的 service.py,用不同的 id 和临时存储来运行。不要把结果写成常量。
- 每一步的评分都在 45 秒内运行。不要制造无限循环或真实的等待。
创建带有版本的存储
在 /root/work/fa-conditional-lab/service.py 中,让 init_db(path) 在 SQLite 文件中创建 notes(id TEXT PRIMARY KEY, title TEXT NOT NULL, version INTEGER NOT NULL)。重复调用也不会清除已有数据。第一步的准备用下面的命令完成。
mkdir -p /root/work/fa-conditional-lab
cp /opt/fixtures/practice_depth/fa-conditional-lab/* /root/work/fa-conditional-lab/
cd /root/work/fa-conditional-lab
使用 CREATE TABLE IF NOT EXISTS,并用 with sqlite3.connect(path) 确定提交的边界。
评分可以用 bash /opt/lab/checks/fa-conditional-lab/01-contract.sh 直接复现。保存文件后请重新运行。
固定首次保存的版本
在 /root/work/fa-conditional-lab/service.py 中,让 create_note(path, note_id, title) 以版本 1 INSERT 一条备忘并提交。相同的 id 以 sqlite3.IntegrityError 拒绝,并且不覆盖已有的标题。
INSERT OR REPLACE 会把重复创建变成更新。这一步的契约是创建新备忘。
评分可以用 bash /opt/lab/checks/fa-conditional-lab/02-contract.sh 直接复现。保存文件后请重新运行。
区分不存在的备忘和存在的备忘
在 /root/work/fa-conditional-lab/service.py 中,让 read_note(path, note_id) 返回 {id, title, version} 字典或 None。不要把 id 用字符串拼接进 SQL,而要使用绑定。
先检查 fetchone 是否为 None。不要直接返回元组。
评分可以用 bash /opt/lab/checks/fa-conditional-lab/03-contract.sh 直接复现。保存文件后请重新运行。
生成强 ETag
在 /root/work/fa-conditional-lab/service.py 中,让 etag(version) 把整数版本返回为包含双引号的字符串。例如:版本 3 对应的字符串是 '"v3"'。
HTTP 标签中的双引号不是说明用的记号,而是实际的头部值的一部分。
评分可以用 bash /opt/lab/checks/fa-conditional-lab/04-contract.sh 直接复现。保存文件后请重新运行。
只接受支持的条件语法
在 /root/work/fa-conditional-lab/service.py 中,让 parse_match(header) 只接受双引号内为 v 加一个正的十进制整数的标签,并返回版本。None、weak 标签、通配符、列表、v0 等都是 ValueError。例如:'"v12"' → 12。
使用 re.fullmatch,标签后面多出来的字符串就不会被默许。本实验并不是实现完整的 HTTP 标签语法。
评分可以用 bash /opt/lab/checks/fa-conditional-lab/05-contract.sh 直接复现。保存文件后请重新运行。
把比较和写入合并成一条 SQL
在 /root/work/fa-conditional-lab/service.py 中,让 update_note(path, note_id, title, version) 只有在 id 和 version 都一致时才更新标题,并把版本加 1。有一行被修改时返回 True,没有时返回 False。即使是并发请求,赢家也必须只有一个。
查看 UPDATE ... WHERE id=? AND version=? 的 rowcount。先 SELECT 再无条件 UPDATE,无法阻止竞争。
评分可以用 bash /opt/lab/checks/fa-conditional-lab/06-contract.sh 直接复现。保存文件后请重新运行。
把失败归类为 HTTP 结果
在 /root/work/fa-conditional-lab/service.py 中,让 put_note(path, note_id, title, header) 对不存在的 id 返回 404,缺少条件返回 428,不支持的条件或版本不一致返回 412,成功返回 204。拒绝时不要修改 DB。
先判断资源是否存在,再解析条件。不要通过重新查询把失败变成成功。
评分可以用 bash /opt/lab/checks/fa-conditional-lab/07-contract.sh 直接复现。保存文件后请重新运行。
用 HTTP 重现保存过期页面
在 /root/work/fa-conditional-lab/service.py 中,让 create_app(path) 提供 GET /notes/{note_id} 和 PUT /notes/{note_id}。GET 返回备忘 JSON 和 ETag,PUT 接收 {title: 1–100 个字符的字符串} 和 If-Match,并按上述结果码返回空响应。空标题为 422,GET 不存在的备忘为 404。
用 Header 接收 If-Match,并用 TestClient 先 GET,再把同一个标签发送两次试试。只有第一次修改生效,新的 GET 必须是 v2。
评分可以用 bash /opt/lab/checks/fa-conditional-lab/08-contract.sh 直接复现。保存文件后请重新运行。