TT Lab
はじめる
学ぶ 学習パス コース

冪等性 — 二度押しても決済は一度だけ

古いワーカーによる結果の上書きを防ぐ

TT Labで続きを見る

目標

SQLiteで、所有権のリースと、増加するfencing tokenを実装します。

なぜ重要なのか

ワーカーAが仕事を受け取ったあと、止まりました。リースが期限切れになるとBが同じ仕事を受け取って完了しましたが、あとから復活したAも完了の結果を書きました。リースの時間を決めるだけでは、古い作業者の書き込みを防げません。保存の時点でも、所有者と世代番号を確認する必要があります。

ステップ

  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)を、冪等に作成します。

最初に1回だけ準備してください。既存のファイルは上書きしません。

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を同時に呼び出し、入力順の戻り値のリストを返します。新しい仕事は、ちょうど1つだけが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)を、冪等に作成します。

最初に1回だけ準備してください。既存のファイルは上書きしません。

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

ワーカーの再起動時に世代番号を初期化すると、古いトークンが再び有効になります。

保存したあと、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を返します。

読み取りと更新は、1つの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で復活させると、新しいワーカーと衝突します。

保存したあと、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で、既存の結果は保持します。

完了の書き込みにもリースの検査があって初めて、遅れて戻ってきたワーカーを防げます。

保存したあと、bash /opt/lab/checks/idem-fencing-lab/06-contract.shで確認してください。

現在のワーカーだけがリースを返却する

/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で勝者は1つだけ

/root/work/idem-fencing-lab/service.pyで、claim_many(path, job_id, owners, now, ttl)は、ThreadPoolExecutorで各ownerのclaimを同時に呼び出し、入力順の戻り値のリストを返します。新しい仕事は、ちょうど1つだけがfenceを得て、残りはNoneである必要があります。

事前に参照して接続を閉じてから更新しないでください。ロックが2つの操作を一緒に保護する必要があります。

保存したあと、bash /opt/lab/checks/idem-fencing-lab/08-contract.shで確認してください。