古いワーカーによる結果の上書きを防ぐ
目標
SQLiteで、所有権のリースと、増加するfencing tokenを実装します。
なぜ重要なのか
ワーカーAが仕事を受け取ったあと、止まりました。リースが期限切れになるとBが同じ仕事を受け取って完了しましたが、あとから復活したAも完了の結果を書きました。リースの時間を決めるだけでは、古い作業者の書き込みを防げません。保存の時点でも、所有者と世代番号を確認する必要があります。
ステップ
/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
-
/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を同時に呼び出し、入力順の戻り値のリストを返します。新しい仕事は、ちょうど1つだけが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)を、冪等に作成します。
最初に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で確認してください。