PR

古いAIワーカーが結果を上書きする?PostgreSQLのfencing tokenをXServer VPSで実測

【PR】この記事にはA8.netのアフィリエイト広告を含みます。

先に結論:XServer VPS上のPostgreSQL 17.10で、worker-bが保存した新結果へ古いworker-aが20回遅延書き込みする状態を再現しました。tokenなしでは20/20件が古い結果に上書きされましたが、owner交代ごとに増えるfencing tokenを更新条件に入れると古い20回を全拒否し、新結果20/20件を維持できました。PostgreSQL内の結果保存については条件付きで解決です。

Task Leaseだけで古いAIワーカーを止められる?

Leaseだけでは不十分です。期限切れになったworkerは、自分がownershipを失ったと知らずに処理を続ける場合があります。新workerが正しい結果を書いた後、旧workerの遅い応答が届くと上書き事故になります。

悩みの起点にしたZenn複数AIの二重実行を防ぐTask Lease」は、期限付きLeaseと単調増加するfencing tokenを組み合わせ、古いownerの書き込みをresource側で拒否する設計を説明しています。

fencing tokenとは?

ownershipが交代するたびに増える世代番号です。worker-atoken 1を持った後、worker-bへ再割当したらDB上のtokenを2へ進めます。worker-aが遅れてtoken 1で書こうとしても、現在値2と一致しないため拒否できます。

PostgreSQL 17のUPDATE公式資料では、WHERE条件が真の行だけが更新され、RETURNINGは実際に更新した行を返します。更新0件はエラーではないため、アプリ側で「stale token」として明示的に扱います。

XServer VPSでどんな実験をした?

項目 実測条件
VPS XServer VPS、4 vCPU4GB RAM、150GB
OS Ubuntu 26.04
Docker 29.7.2
PostgreSQL 17.10-alpine
job case 20
container上限 0.5 CPU256MiB RAM、追加swapなし、PID 128
network none、公開portなし

本番DBへ障害を入れず、tokenなし・ありで別々の一時PostgreSQL containerを作りました。実験後はcontainerと匿名volumeを削除しています。

[PR]
VPSでいろいろ試すなら『XServer VPS』

古いworkerが書き戻す順番をどう再現した?

  1. worker-aが20件をclaimし、generation 1を取得。
  2. lease回収相当の処理でworker-bへ再割当し、generationを2へ更新。
  3. worker-bNEW-Bを20件へ保存。
  4. 遅れて戻ったworker-ageneration 1のままOLD-Aを20件へ保存しようとする。

同時実行のばらつきを持ち込まず、問題になる順序を両caseで同一にしました。worker-bの書き込みには両caseともgeneration 2ownerstatusの条件を付け、違いは旧worker-aの書き込み条件だけです。

tokenなしでは何が起きた?

worker-aの古い書き込み20件がすべて成功しました。worker-bNEW-Bは先に20/20件受理されましたが、その後の無条件更新で最終結果は20件すべてOLD-Aになりました。

さらに最終行はworker_id=worker-blease_generation=2なのに、completed_by=worker-aresult=OLD-Aという矛盾した状態です。statusSUCCEEDEDだから正しいとは判断できません。

fencing tokenありでは何が起きた?

古いworker-aの20回は全拒否されました。DB上はすでにgeneration 2ownerworker-bstatusSUCCEEDEDです。generation 1を含む条件に一致する行がなく、UPDATE ... RETURNINGは0件でした。

拒否した試行もwrite_eventsaccepted=falseで記録しました。最終結果は20件すべてNEW-B、上書き0件、重複accepted write 0件です。

tokenなし・ありの実測差は?

指標 tokenなし tokenあり
worker-bNEW-B受理 20/20 20/20
古いworker-aOLD-A受理 20/20 0/20
古いworker-aの拒否 0/20 20/20
最終NEW-B 0/20 20/20
最終OLD-A 20/20 0/20
新結果の上書き 20件 0件
重複accepted write 20件 0件
stale write時間 65.472ms 68.666ms

PostgreSQLの更新条件はどう書く?

UPDATE jobs
SET status='SUCCEEDED',
    result=:result,
    completed_by=:worker_id
WHERE id=:job_id
  AND lease_generation=:fencing_token
  AND worker_id=:worker_id
  AND status='IN_PROGRESS'
RETURNING id;

重要なのは、tokenを事前にSELECTしてから別のUPDATEを行わないことです。その間にownerが交代する余地があります。token確認と結果保存を同じstatementの条件に入れ、返却行0件を正常な拒否として処理します。

時計ずれに強い?

generation比較そのものはclient時刻を使いません。workerの時計が多少ずれても、DBに保存された1と2の一致だけで判定できます。

ただし今回はclient時計を意図的にずらしていません。Lease期限を決める処理ではworker送信時刻に依存せず、PostgreSQLclock_timestamp()などDB側の時計を基準にする必要があります。前回の3秒Lease実験では、期限切れ10件を3.095秒で回収できました。

CPURAM・ストレージへの影響は?

case cgroup peak RAM CPU
tokenなし 85.43MiB 2.086078秒
tokenあり 84.81MiB 2.060520秒

host MemAvailableは約3279.96MiBから3201.85MiB、root disk使用量は57,344 byte増えました。2つのPostgreSQL起動と比較を含む総時間は9.638688秒です。単発・20件の小規模実測なので、大量同時更新時の性能値ではありません。

本番サービスと後片付けは?

公開/health/readyは開始前、各case中、終了後の全4回でHTTP 200でした。database=okartifact_storage=okも維持し、本番CaddyFastAPIPostgreSQLの停止や再起動はありません。

一時container 2個と匿名volumeを削除しました。dangling volumeは前後0件で、公開portは追加していません。

fencing tokenだけで二重投稿や二重課金も防げる?

防げるとは限りません。今回守れたのは、同じPostgreSQL行への結果保存です。SNS、メール、決済APIなどDB外のresourcetokenを理解しなければ、送信前にtokenを確認しても、その直後にownerが交代する競合が残ります。

外部作用には、安定したidempotency keyeffect receipttransactional outboxtimeout時のreadbackを組み合わせます。相手APIがtokenを受け付けない場合は、DB側でeffect intentを一意に予約し、結果不明を勝手に再送しない設計が必要です。

AIスマホ調査システムへどう組み込む?

初心者が迷いそうなポイントは?

XServer VPSでこの悩みはどこまで解決した?

PostgreSQL内の結果保存については条件付きで解決です。tokenなしで20件すべて上書きされる危険を再現し、generation条件ありでは古い20件を全拒否できました。本番APIを止めず、4GB環境でも約85MiB peakの隔離DBで検証できています。

一方、実際の同時threadnetwork partition、同じtokenretry、外部APIの二重作用までは検証していません。次はidempotency keyreceiptを含むeffect実行を別実験にします。

よくある質問

Q. fencing tokenはUUIDでもよいですか?

A. 古い・新しいをresource側で比較する用途では、ownershipごとに単調増加する整数が分かりやすいです。UUIDだけでは順序を比較できません。

Q. UPDATEが0件なら失敗ですか?

A. SQL上はエラーではありません。今回のアプリ設計では、RETURNING 0件をstale workerとして拒否記録へ回しました。

Q. status=’IN_PROGRESS’も必要ですか?

A. tokenとownerに加え、許可した状態遷移だけを受理するために入れました。完了済み行への同token再書き込みも防ぎやすくなります。

Q. Lease期限とtokenは同じものですか?

A. 違います。期限は再割当可能になる時点、tokenは再割当前の古いworkerを見分ける世代番号です。

Q. これでexactly-onceになりますか?

A. PostgreSQL行への条件付き更新は守れますが、外部APIまで含むexactly-onceは保証しません。idempotency keyreceiptreadbackが必要です。

[PR]
XServer VPSの公式情報を確認する

実験日:2026年9月2日。数値は筆者が契約中のXServer VPS 4GB環境における単発の実測です。契約planPostgreSQL version、同時実行数、storage性能により結果は変わります。

コメント

タイトルとURLをコピーしました