プロフィール DavidHLPL · RHINE LAB RSS ↗
資料 X-1132 / プロジェクト実践

database lease だけでは足りない:UltiCode の fence token で期限切れ singleton task を止める

技術ノート

公開 / DavidHLPL

証拠の状態:この記事は固定コミット時点の UltiCode FencedJobLeaseMapper、FencedJobLeaseService、OwnerReconciler、ReconciliationRunMapper、ADR、MySQL migration に基づく。source と boundary test は stale completion が拒否されることを示すが、すべての external side effect に fencing があることや singleton task の exactly-once を意味しない。

system が一つの reconciliation task だけを許すとする。instance A が lease を取得し scan を開始する。しばらくして A が停止し lease が expire する。instance B が新しい lease を取得し、新しい scan を完了する。その後 A が復帰して最後の step、「task が完了した」と自分の result を write する。

database update が lease_name や owner_token だけを確認するなら A も成功し得る。A はかつて正しい owner だったが、database が知っているのは「現在 lease がある」ことだけで、どの lease generation から write が来たかではない。B の新しい result が古い executor に上書きされる可能性がある。

これが通常の lease と fenced lease の違いである。lease は誰が開始できるかを決め、fence token は誰が終了する資格をまだ持つかを決める。

素朴な設計が失敗する理由

process 内 lock は一つの process だけを解決する

synchronized、ReentrantLock、singleton Bean は同じ JVM の重複実行を止められる。しかし service が restart し、二つ目の replica や独立した運用 script が現れると lock の scope は終わる。一つの process だけの offline task には向くが、database write を global fact として扱う service cluster には向かない。

owner + expiry だけでは古い owner の復帰を許す

典型的な lease table には lease_name、owner_token、leased_until がある。expired lease を takeover し、write 時に owner と time を確認すれば crash recovery に対応できそうだが、「これは何回目の取得か」を表せない。

time check も万能ではない。old worker が check 後に pause し、lease expiry と new owner takeover の後に write を続ける可能性がある。application local time と database time がずれることもある。

Redis distributed lock は自動的に fencing しない

Redis lock は取得を調整できる。しかし downstream database update が monotonically increasing token を持ち、確認しなければ、古い owner は lock expiry 後にも write を完了できる。lock の保存先を変えるだけで stale-writer protection は得られない。

UltiCode の中心機構:takeover ごとに lease generation を増やす

UltiCode は singleton task の lease を fenced_job_leases に保存し、有効な takeover ごとに fence_token を割り当てる。

lease_name = admin:reconciliation

first owner:  owner=A, fence=7, leased_until=T1
lease expires
new owner:   owner=B, fence=8, leased_until=T2

FencedJobLeaseMapper は lease が expired の場合だけ takeover し、token を増やす。acquire、renew、release、isHeld はすべて lease_name + owner_token + fence_token の一致を要求する。lease time の判定は database の CURRENT_TIMESTAMP(3) を使い、application process の local clock を唯一の事実源にしない。

これにより「current owner は誰か」と「この owner はどの lease generation か」の曖昧さを解消する。A が古い owner token を持っていても fence は 7 のままであり、B の takeover 後 database は fence 8 へ進むため、A の後続 update は条件で拒否できる。

本当の fence は completion write にある

token を取得するだけでは足りない。最も damage を起こしやすい old worker の final write まで token を運び、そこで check する必要がある。

OwnerReconciler は admin:reconciliation を取得し、owner token と fence token を reconciliation run に記録し、実行中に renew する。run 完了時、ReconciliationRunMapper は join 付き conditional update を使う。

UPDATE reconciliation_runs AS r
JOIN fenced_job_leases AS l
  ON l.lease_name = :leaseName
SET r.status = :status,
    r.finished_at = :finishedAt
WHERE r.run_id = :runId
  AND r.fence_token = :runFenceToken
  AND l.owner_token = :ownerToken
  AND l.fence_token = :fenceToken
  AND l.leased_until > CURRENT_TIMESTAMP(3)

これは実装から取り出した簡略例で、他の field は省略しているが semantic condition は残している。一つでも条件を満たさなければ affected rows は 0 になる。呼び出し側は 0 rows を「後で再度 write」と扱わず、lease loss、old token、競合者 takeover と解釈する。

次のように Java だけで書く場合とは違う。

if (lease.isHeld()) {
    run.setFinished(true);
}

Java check と database update の間には race がある。conditional update は check と write を一つの atomic database operation に置く。

なぜ database pessimistic lock を直接使わないのか

SELECT … FOR UPDATE は一つの transaction 内の row を保護できる。しかし reconciliation は長時間動く可能性があり、database transaction を開き続けられない。long transaction は lock を保持し、resource 消費を拡大し、複数の external step に向かない。

fenced lease は long task を短い transaction に分ける。acquire、renew、progress 記録、finish の各回で token により current authority を確認する。「全 task を一つの lock で包む」単純さは失うが、task を long transaction の外で動かし、final commit は database に判定させられる。

pessimistic lock が不要という意味ではない。短い local critical section では直接的である。network、file、長時間計算を含む task では lease + fencing の方が lifecycle に合うことが多い。

この設計が実際に解決すること

  • instance 間 coordination:一つの lease_name と database constraint で複数 instance が singleton task を競合する。
  • expiry takeover:old instance が renew しなければ、expiry 後に new instance が takeover できる。
  • old completion の無効化:new takeover が fence token を増やし、old executor は final-write condition を満たせない。
  • database time の一貫性:lease validity は local application time ではなく database clock を使う。
  • failure の可観測性:0 rows は stale/lost-lease の明確な signal であり、silent overwrite ではない。

既に呼び出した external API を rollback できるか、file が write 済みか、third party が token を確認するか、old worker が CPU を使い続けるか、business 全体が一度だけ動くかは解決しない。Fencing が主に保護するのは token を持つ write seam であり、token 条件のない side effect には idempotency key、compensation、独立した fencing protocol が必要である。

コスト、適用条件、失敗境界

singleton 実行が必要、task が lease TTL を越える可能性がある、instance が pause/restart/replace される、final state を同じ database に write-back できる、という条件なら適用しやすい。

cost も明確である。token を acquire point からすべての side-effecting write へ渡し、renew と TTL を monitor し、各 terminal update に正しい condition を書き、new owner takeover 後に old owner が戻る test を行う必要がある。すべてが短い一つの transaction で完了するなら row lock や unique constraint の方が簡単である。

最も見落としやすい境界は acquire だけを fence し、completion、publish、delete、success mark を fence しないことである。それでは番号付き lock を持つだけで、stale completion を止められない。

LLM/Agent scheduling への適用

long-running Agent task も model switch、cancel-and-resume、worker restart により複数 executor を持ち得る。plan version や task epoch を fence token とし、tool result、final summary、task state update に同じ token を要求できる。old plan は自然終了まで動いても、new plan を complete と mark する資格はない。

移植の中心は「Agent に distributed lock を付けるか」ではない。どの side effect が database 境界で current plan version を再検証すべきかを問うことである。memory 内の isCurrent だけに依存する tool は check と write の間に stale になり得る。

最小検証経路

FencedJobLeaseMapper の acquire/renew/release condition を読み、ReconciliationRunMapper の completion update を読む。最後に FencedJobLeaseIT と OwnerReconcilerTest を確認する。一つは takeover 後の old release failure、もう一つは old owner の completion write 拒否を検証する。

UltiCode 分析段階では、この fenced lease の MySQL 8 Testcontainers integration test 二件と reconciliation の targeted unit test を実際に実行し、通過した。これは選択した競合 scenario の検証であり、性能、network partition、すべての external side effect の証明ではない。

出典と関連文書

研究記録

  1. 元の文書: src/content/note/ja/ulticode-fenced-singleton-lease.md
  2. 出典リビジョン: f49953fb0d4bd52a39bf81167b0c4b7dd7d78a64
  3. 証拠の状態: 暫定

原文の出典 ↗