阻止旧工作进程覆盖新结果
目标
在 SQLite 中实现所有权租约和单调递增的 fencing token。
为什么重要
worker A 领走任务后卡住了。租约过期之后,B 领走了同一个任务并完成了,而后来恢复过来的 A 也写入了完成结果。仅仅规定租约时长,并不能阻止过期 worker 的写入。在写入存储时,也必须确认所有者和代数(generation)。
步骤
- 在
/root/work/idem-fencing-lab/service.py中,init_db(path) 以幂等方式创建 jobs(id TEXT PRIMARY KEY, owner TEXT, until REAL NOT NULL DEFAULT 0, fence INTEGER NOT NULL DEFAULT 0, result TEXT, done INTEGER NOT NULL DEFAULT 0)。
先做一次准备。已有的文件不会被覆盖。
mkdir -p /root/work/idem-fencing-lab
test -e /root/work/idem-fencing-lab/service.py || cp /opt/fixtures/ten_labs/idem-fencing-lab/service.py /root/work/idem-fencing-lab/service.py
cd /root/work/idem-fencing-lab
-
在
/root/work/idem-fencing-lab/service.py中,enqueue(path, job_id) 把不存在的 id 以默认状态插入并返回 True;如果已存在,则不改变状态并返回 False。 -
在
/root/work/idem-fencing-lab/service.py中,state(path, job_id) 把行以键为 id、owner、until、fence、result、done 的 dict 返回,不存在时返回 None。 -
在
/root/work/idem-fencing-lab/service.py中,claim(path, job_id, owner, now, ttl) 在 ttl<=0 时抛出 ValueError。不存在的任务、已完成的任务、until>now 的任务,都返回 None。其余情况保存 owner 和 until=now+ttl,把 fence 加 1,并返回新的 fence。 -
在
/root/work/idem-fencing-lab/service.py中,renew(path, job_id, owner, fence, now, ttl) 在 ttl<=0 时抛出 ValueError。仅当 owner 和 fence 一致、done=0 且 until>now 时,才把 until 改为 now+ttl 并返回 True,否则返回 False。 -
在
/root/work/idem-fencing-lab/service.py中,complete(path, job_id, owner, fence, now, result) 仅在租约为当前有效(owner 和 fence 一致、done=0、until>now)时,才保存 result 并设置 done=1,返回 True。其余情况返回 False,并保留原有结果。 -
在
/root/work/idem-fencing-lab/service.py中,release(path, job_id, owner, fence) 对 owner 和 fence 相同且 done=0 的行,把 owner 改为 NULL、until 改为 0,并返回 True。fence 要保留。其余情况返回 False。 -
在
/root/work/idem-fencing-lab/service.py中,claim_many(path, job_id, owners, now, ttl) 用 ThreadPoolExecutor 同时调用每个 owner 的 claim,并按输入顺序返回返回值列表。对于新任务,只有一个 owner 能拿到 fence,其余必须是 None。
参考
- 无需联网和安装软件包,在现有的 lab-dev 环境中进行。
- 每个步骤都在 45 秒的评分预算内运行。不要加入真实的 sleep 或网络调用。
- 评分会重新导入提交的模块,并用独立的输入和临时 DB 检查。不要直接返回预期值的常量,而要实现契约。
- FastAPI 官方文档 · pytest 官方文档 · Python sqlite3
- 局限:时钟是由调用方给出的非递减数字,不对多台服务器之间的时钟同步建模。业务的外部副作用并不会被自动 fencing。存储之外的系统同样需要相同的令牌校验,或者单独的幂等处理。SQLite 使用的是真实的锁和事务,但它并不代表大规模分布式队列的吞吐量。
创建租约状态表
在 /root/work/idem-fencing-lab/service.py 中,init_db(path) 以幂等方式创建 jobs(id TEXT PRIMARY KEY, owner TEXT, until REAL NOT NULL DEFAULT 0, fence INTEGER NOT NULL DEFAULT 0, result TEXT, done INTEGER NOT NULL DEFAULT 0)。
先做一次准备。已有的文件不会被覆盖。
mkdir -p /root/work/idem-fencing-lab
test -e /root/work/idem-fencing-lab/service.py || cp /opt/fixtures/ten_labs/idem-fencing-lab/service.py /root/work/idem-fencing-lab/service.py
cd /root/work/idem-fencing-lab
如果 worker 重启时把代数重置,过期的令牌就会重新变得有效。
保存后用 bash /opt/lab/checks/idem-fencing-lab/01-contract.sh 确认。
只登记首次出现的任务
在 /root/work/idem-fencing-lab/service.py 中,enqueue(path, job_id) 把不存在的 id 以默认状态插入并返回 True;如果已存在,则不改变状态并返回 False。
为了让重复登记不会重置正在进行中的租约,要使用 INSERT OR IGNORE。
保存后用 bash /opt/lab/checks/idem-fencing-lab/02-contract.sh 确认。
读取当前状态
在 /root/work/idem-fencing-lab/service.py 中,state(path, job_id) 把行以键为 id、owner、until、fence、result、done 的 dict 返回,不存在时返回 None。
通过单独的 DB 连接读取存储的状态,以便与进程内的缓存区分开。
保存后用 bash /opt/lab/checks/idem-fencing-lab/03-contract.sh 确认。
以原子方式领走已过期的租约
在 /root/work/idem-fencing-lab/service.py 中,claim(path, job_id, owner, now, ttl) 在 ttl<=0 时抛出 ValueError。不存在的任务、已完成的任务、until>now 的任务,都返回 None。其余情况保存 owner 和 until=now+ttl,把 fence 加 1,并返回新的 fence。
读取和更新要在同一个 BEGIN IMMEDIATE 内完成。边界情况 now==until 时可以重新分配。
保存后用 bash /opt/lab/checks/idem-fencing-lab/04-contract.sh 确认。
只有当前代数才能续约
在 /root/work/idem-fencing-lab/service.py 中,renew(path, job_id, owner, fence, now, ttl) 在 ttl<=0 时抛出 ValueError。仅当 owner 和 fence 一致、done=0 且 until>now 时,才把 until 改为 now+ttl 并返回 True,否则返回 False。
如果用 renew 把已经过期的所有权救活,就会与新的 worker 冲突。
保存后用 bash /opt/lab/checks/idem-fencing-lab/05-contract.sh 确认。
拒绝过期的完成
在 /root/work/idem-fencing-lab/service.py 中,complete(path, job_id, owner, fence, now, result) 仅在租约为当前有效(owner 和 fence 一致、done=0、until>now)时,才保存 result 并设置 done=1,返回 True。其余情况返回 False,并保留原有结果。
完成的写入也必须做租约检查,才能挡住迟到的 worker。
保存后用 bash /opt/lab/checks/idem-fencing-lab/06-contract.sh 确认。
只有当前 worker 才能归还租约
在 /root/work/idem-fencing-lab/service.py 中,release(path, job_id, owner, fence) 对 owner 和 fence 相同且 done=0 的行,把 owner 改为 NULL、until 改为 0,并返回 True。fence 要保留。其余情况返回 False。
如果归还时连 fence 也一起重置,就会复用过去的令牌编号。
保存后用 bash /opt/lab/checks/idem-fencing-lab/07-contract.sh 确认。
并发 claim 时赢家只有一个
在 /root/work/idem-fencing-lab/service.py 中,claim_many(path, job_id, owners, now, ttl) 用 ThreadPoolExecutor 同时调用每个 owner 的 claim,并按输入顺序返回返回值列表。对于新任务,只有一个 owner 能拿到 fence,其余必须是 None。
不要先查询、再关闭连接、然后更新。锁必须同时保护这两个操作。
保存后用 bash /opt/lab/checks/idem-fencing-lab/08-contract.sh 确认。