▲ 俯瞰

Hard-link 系 TLA+ 検証 — 概要

5 つの topology + 非アトミックテストパターン + per-action fairness

背景: 「hard-link」=「1 つの子ノードが 2 つの異なる親から参照される」topology。 従来 CLAUDE.md / README.md では「transactions may always fail and retry」 という活性 caveat として informal に言及されていた。本番環境からの低頻度 dyn_node abort 報告 (「30/30 abort on another env」) を契機に プロトコルを形式モデル化した。

5 つの topology + 非アトミック版

仕様topologyバグサーフェス
_hardlink_dynamicParent1 ‖ Parent2 → DynChild1 (sibling parents)一方の親の bundle が他方の sub[] から子を pull する migration race
_hardlink_self_collisionR → A → C と R → C (root が直接子の親も兼ねる)bundle(R) が R→C と R→A→C の両経路を歩く。is_bundle_root Phase 4 の m_missing=false override と二重経路が干渉
_hardlink_4nodeRoot → A, Root → B, A → C, B → C (C を A/B が共有)bundle(Root) Phase 4 reachability gating + outer DISTURBED retry; 本番 race の再現
_hardlink_externalGN1 → P2 (hardlink), P1 → P2 (P2 の packet は P1 の tree)bundle(GN1) が GN1 ~missing を finalise する瞬間に GN2.sub[P2]=Null が GN1 から unreachable → SnapshotConsistency 違反 (Phase 4 gate なしの場合)
_hardlink_external_migration↑同じ、ただし bundle が P2 を P1 の tree から GN2 へ migrate するbundle(GN1) が P2 を atomic に P1 の sub[] から抜いて GN2 の sub[] に挿入する間、ピアが P1 上で race
_hardlink_nonatomic非トランザクション p1->insert(p2) / release(p2) をトランザクション操作と交互に走らせるbundledBy=A 状態のまま A が C を参照しない「limbo」状態でのファイナライズ。master の chain walk vs b23fa954 self-promote 比較

本層の位置づけ: 通常 (non-hard-link) topology は slides_layer2_LLfree.html + slides_layer2_LLfree_3level.html / _dynamic.html で完結。hard-link は別個の verification layerとして、本スライド + 上記 6 spec で扱う。

Self-collision (R-A-C) — bug repro + fix

BundleUnbundle_hardlink_self_collision.tla

Topology

    R
    ├── A ── C    (R → A → C)
    └── C         (R → C は hard-link; C は R の直接子 A の子の両方)

バグサーフェス (本番 trace からの再現)

bundle(R) Phase 2 開始: R は missing=TRUE、子 sub-packet 収集中
② ピア thread の insert(R, C) (hard-link 登録) が R の wrapper を CAS で missing=FALSE に上書き
③ bundling thread の Phase 4 CAS が失敗、リトライへ
④ Phase 1 で C を stale bundledBy=A 経由 で再収集
R.sub[A].sub[C] = Null と書き込み → パケット消失
SnapshotConsistency 違反 (C++ Packet::checkConsistensy transaction_impl.h:870-871 に対応)

提案される修正 (TLA+ で確認)

// bundle() Phase 3 — pseudo-diff
for (child in subnodes) {
    if (local.subpackets[child] == nullptr) continue;  // ← NEW
    // existing CAS to BundledRef(this)
}

Hard-link 参照 (=「parent.sub[c] = Null は packet が他所にある印」) は Phase 3 で CAS-tag しない。修正後、bundle はパケットを消失させずに完了する。

却下された代替案: insert(R, C)~R.missing で gating (進行中 bundle が完了するまで待つ) する案 — lock-freedom が壊れ、3+ thread で deadlock リスク。 Phase 3 skip は bundle protocol 内部だけで完結する。

検証結果

cfgStates結果
2-thread (修正前)222❌ FAIL — SnapshotConsistency 違反
2-thread (Phase 3 skip-Null 修正後)114✅ Pass + liveness
3-thread (修正後)760✅ Pass + liveness

4-node — production race repro

BundleUnbundle_hardlink_4node.tla (gold standard)

Topology

    Root
    ├── A ──┐
    └── B ──┴── C  (C を A と B が共有)

race パターン

Step動作
T1bundle(Root) Phase 4 で m_missing=false finalise を試みる
T2.❶非アトミック release: B.sub[C] = Null に書く (ReleaseBCNoMigrate)
T2.❷MigrateCToA: C の親情報を A に移し、A.sub[C] を再確立

T2 が ❶ と ❷ の間で停止した瞬間、Root の Phase 4 が走ると C が どちらの親の sub[] にも届かない状態 (production race)。

修正 = C++ Phase 4 reachability gate

// transaction_impl.h Phase 4 (commits 87892b35, 92b15f62, 404fa137, b12e1895)
newpacket->m_missing = false;             // 先にクリア (mid-bundle 短絡を回避)
if (! newpacket->allSubReachable(...)) {
    newpacket->m_missing = true;          // 戻す
    return BundledStatus::DISTURBED;     // outer snapshot() retry
}

検証結果

cfgStates結果
2-thread (修正前)97❌ FAIL — SnapshotConsistency 違反
2-thread (Phase 4 gate + DISTURBED retry + WF(MigrateCToA))531✅ Pass + liveness

4-node の State-space bounding policy: retryCount や CONSTRAINT なし、MOD wrap なしで有限化。"in_release" pc state が 2-step release を 1 つの atomic-from-caller API call に束ねる (C++ release(B, C) の API 構造に対応)。これによって他の変数の有限ドメインだけで TLC が exhaustion 完走できる。

External / External migration — cross-tree races

External (_hardlink_external.tla)

    GN1 ── P2  (hardlink)
    P1  ── P2  (P2 の packet は P1 の tree 内に住む)

bundle(GN1) が GN1 を ~missing finalise する瞬間に GN2.sub[P2] = Null が GN1 から reverseLookup できない → SnapshotConsistency 違反。Phase 4 reachability gate が直接これを防ぐ。

cfgStates結果
1-thread (gate あり)15✅ Pass + liveness
2-thread (gate あり)136✅ Pass + liveness

External migration (_hardlink_external_migration.tla)

↑ 同じ topology だが、bundle が P2 を P1 の tree から GN2.sub[P2]atomic に migrate する。bundle pipeline の各 step を peer thread が P1 上で race する状況を full モデル化。

cfgStates結果
1-thread8✅ Pass + liveness
2-thread~hundreds✅ Pass + liveness
3-thread1,202✅ Pass + liveness (per-action WF on every progress step)

External migration が per-action fairness を必要とする理由: 本 spec はバンドル pipeline (BundlePhase1 → BundlePullP1 → BundleCASP2 → BundleUpdateGN1 → BundlePhase4 → BundlePhase5) の各 step に WF_vars(...) を付ける。理由は次スライド。

Per-action fairness — KAME corpus 初導入

_hardlink_4node / _hardlink_external_migration

典型的な spec 書き方

\* 4-node:
Spec == Init /\ [][Next]_vars
        /\ WF_vars(NextStep)
        /\ \A t \in Threads : WF_vars(MigrateCToA(t))

\* external_migration: あらゆる progress step に WF を付与
Spec == Init /\ [][Next]_vars
        /\ WF_vars(NextStep)
        /\ \A t \in Threads :
            /\ WF_vars(BundlePhase1(t))
            /\ WF_vars(BundlePullP1(t))
            /\ WF_vars(BundleCASP2(t))
            /\ WF_vars(BundleUpdateGN1(t))
            /\ WF_vars(BundlePhase4(t))
            /\ WF_vars(BundlePhase5(t))

blanket WF_vars(NextStep) が弱すぎる理由

本番忠実な retry セマンティクス (Phase 4 reachability 失敗 → bundle_phase1 に戻る、 C++ の BundledStatus::DISTURBED 返し → outer snapshot() retry loop) の下で、bundling thread が任意回 retry し続ける execution が許容される。 そのリトライは peer の MigrateCToA が発火しない限り終わらない。

disjunction 上の WF は「enabled なアクションのどれか」が永遠に無視されない ことしか保証しない。BundlePhase1/2/4 ばかり無限に発火する trace でも条件を満たすが、 これは production の fairness 仮定に反する。per-action WF なら MigrateCToA が継続的に enabled な限り必ず発火することを強制できる。

"older-wins" がこの fairness を吸収しないのはなぜか

LL-free negotiate同じ linkage 上での CAS contentionに対する arbitration。 本 race は3 つの異なる linkage (T1: Root, T2.❶: B, T2.❷: A) にまたがるため、 T1/T2 は同じ linkage を CAS しない → older-wins は arbitrate するものがない。 T2 の進行は OS-level スレッドスケジューリングに依存する。 TLA+ では WF_vars(MigrateCToA) がこの OS 保証を抽象化する。

他の hardlink spec はなぜ per-action WF 不要か: _hardlink_2level_LLfree, _3level_LLfree, _dynamic 系, atomic_shared_ptr.tla, その他 hardlink spec の race surface は 1 本の linkage chain 上にあり、LL-free priority gating が retry 数を 構造的に bounded にする。WF_vars(NextStep) で十分。

非アトミックテストパターン

BundleUnbundle_hardlink_nonatomic.tla

動機

リモートブランチ claude/refactor-negotiate-scoped-f7de2 commit b23fa954 が 復元したテストパターン — 非 Tx p1->insert(p2) / release(p2) を Tx 操作と交互に走らせる — が master で本物の活性ギャップを露呈しているのか、 それともスケジューラアーティファクトか、を判定する。

モデル化

Step動作
NonTxInsertAC非 Tx で A → C の hard-link を挿入
TxInsertHardlinkTx で hard-link を登録
NonTxReleaseAC非 Tx で A → C を release。C は bundledBy = A のまま A は C を参照しない (limbo)
TxReleaseHardlinkTx で hard-link 解放
dtordestructor が A と C を limbo から finalize

2 つの finalize variants (CONSTANT UseFixVariant)

variant意味
FALSE (master)bundle fall-through; bundledBy chain を walk → Root に scope が必要 (peer Tx と重競合)
TRUE (b23fa954)C の linkage に直接 self-promote CAS — chain walk なし、Root contention なし

検証結果 (全 4 構成)

Spec / VariantStates結果
Spec (per-action WF) + master308✅ Pass + liveness
Spec + fix308✅ Pass + liveness
WeakSpec (blanket WF) + master308✅ Pass + liveness
WeakSpec + fix308✅ Pass + liveness

結論: このモデル化抽象度の下では両 path とも理論的に活性。 b23fa954 self-promote は CAS 数の最適化であって、master fall-through が 原理的に livelocked というわけではない。リモートブランチで報告された ~10% 残留ハングは real-OS スケジューラのアーティファクト (real schedulers do not strictly meet WF in finite time) として整合的。

最適化は KAME_STM_OPTIONAL_OPTIMIZATION (kame/transaction_definitions.h でデフォルト 1) でゲート。 -DKAME_STM_OPTIONAL_OPTIMIZATION=0 で無効化すると、 strictly TLA+-modelled master path に合致したビルドになる。

Dynamic (sibling parents) + 他の topology との関係

BundleUnbundle_hardlink_dynamic.tla

Topology

    Parent1   Parent2
       \   /
     DynChild1  (両方の親を持つ)

カバー範囲

最も 基本的な hard-link 構造: 兄弟の 2 親が同じ子を共有する。 一方の親の bundle が他方の親の sub[] から DynChild1 を pull すべきタイミングで peer thread と race する migration race を網羅。

検証結果

cfgStates結果
1-thread (MaxCommits=1)7✅ Pass + liveness
2-thread (MaxCommits=1)62✅ Pass + liveness
2-thread (MaxCommits=2)703✅ Pass

State-space bounding policy (全 hardlink 共通)

2026-05-20 のユーザフィードバック以降、全 hardlink モデル統一方針: retryCount なし、CONSTRAINT なし、MOD wrap-around なし。 人為的な打ち切りは活性検査で false-negative を生じうるため。 全 spec が bounded variable domain だけで TLC が自然に終了する。

Modelretry boundbounding 機構
_hardlink_4node削除 (gold standard)"in_release" pc state で 2-step release を atomic-from-caller に束ねる
_hardlink_self_collision削除変数ドメインが有限
_hardlink_external削除変数ドメインが有限
_hardlink_external_migration削除per-action WF on every progress step
_hardlink_dynamic初出から無し変数ドメイン有限 + MaxCommits=1

共通の不変条件 + Phase 4 gate と他モデルへの影響

検証対象の不変条件 / property

Invariant / Property意味
SnapshotConsistencyC++ Packet::checkConsistensy に対応。Null sub-slot は reverseLookup で hard-link 経路を通じて到達可能でなければならない
HardlinkExclusive子の packet を sub[] に持つ親は同時に 1 つだけ (no duplicate homing)
BundleRefConsistency子の bundledBy 親は priority を持ち、packet か InsertedRef のいずれかを持つ
NoPriorityLoss / NoMissingHole / MissingPropagation構造的健全性
EventuallyAllDone (PROPERTY)全 thread が eventually 完了 (2026-05-20 以降、全 hardlink cfg で safety+liveness uniform カバー)

Phase 4 reachability gate の C++ 側コミット履歴

Commit内容
87892b35初回 reachability gate 導入 (Packet::allSubReachable)
92b15f62checkConsistensy も対応; mid-bundle 短絡対応
404fa137呼び出し順序の見直し (m_missing を先にクリア)
b12e1895global root parameter 導入による order-of-operations 修正
1ffd8dceallSubReachable / checkConsistensy に optional globalroot 引数; hard-link Case B 対応

non-hard-link モデルへの影響: なし

2-level / 3-level LL-free (static + dynamic) の TLA+ モデルには更新不要。 non-hard-link topology では子の親は常に 1 つ。bundle(P) Phase 4 で Null sub-slot が出現したら reverseLookup は必ず成功する (packet が P のサブツリー外に住むことが構造的に不可能)。
既存の SnapshotConsistency 不変条件 (2level_LLfree_dynamic.tla:1093-1096, 3level_LLfree_dynamic.tla:1831-1835) が gate の precondition そのものを表現しており、 413M state の dynamic release superfine live 実行で違反 0 回。 C++ 側の else { return DISTURBED } 分岐は non-hard-link で provably unreachable。

C++ 側 implementation の影響

テスト影響
dyn_node_test / 3level_mixed_test / payload_integrity_*各 child packet は親 1 つの sub[] に住む。Null sub-slot が steady state で発生しない → fix は no-op

まとめ + 参考資料

仕様State数結果
_hardlink_dynamic (sibling parents, superfine)7 / 62 / 703✅ Pass + liveness
_hardlink_self_collision (R-A-C, after fix)114 / 760✅ Pass + liveness
_hardlink_4node (R+A+B+shared C, Phase 4 gate + DISTURBED retry)531✅ Pass + liveness
_hardlink_external15 / 136✅ Pass + liveness
_hardlink_external_migration (3-thread, per-action WF)1,202✅ Pass + liveness
_hardlink_nonatomic (master/fix × Spec/WeakSpec)308 (各 4)✅ Pass + liveness

意義

本層は KAME の TLA+ 検証体系で初めて以下を達成:

参考資料