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

幂等性 — 点两次也只扣一次款

阻止旧工作进程覆盖新结果

在 TT Lab 中继续学习

目标

在 SQLite 中实现所有权租约和单调递增的 fencing token。

为什么重要

worker A 领走任务后卡住了。租约过期之后,B 领走了同一个任务并完成了,而后来恢复过来的 A 也写入了完成结果。仅仅规定租约时长,并不能阻止过期 worker 的写入。在写入存储时,也必须确认所有者和代数(generation)。

步骤

  1. 在 /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
  1. 在 /root/work/idem-fencing-lab/service.py 中,enqueue(path, job_id) 把不存在的 id 以默认状态插入并返回 True;如果已存在,则不改变状态并返回 False。

  2. 在 /root/work/idem-fencing-lab/service.py 中,state(path, job_id) 把行以键为 id、owner、until、fence、result、done 的 dict 返回,不存在时返回 None。

  3. 在 /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。

  4. 在 /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。

  5. 在 /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,并保留原有结果。

  6. 在 /root/work/idem-fencing-lab/service.py 中,release(path, job_id, owner, fence) 对 owner 和 fence 相同且 done=0 的行,把 owner 改为 NULL、until 改为 0,并返回 True。fence 要保留。其余情况返回 False。

  7. 在 /root/work/idem-fencing-lab/service.py 中,claim_many(path, job_id, owners, now, ttl) 用 ThreadPoolExecutor 同时调用每个 owner 的 claim,并按输入顺序返回返回值列表。对于新任务,只有一个 owner 能拿到 fence,其余必须是 None。

参考

创建租约状态表

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