背景: 「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」) を契機に
プロトコルを形式モデル化した。
| 仕様 | topology | バグサーフェス |
|---|---|---|
_hardlink_dynamic | Parent1 ‖ Parent2 → DynChild1 (sibling parents) | 一方の親の bundle が他方の sub[] から子を pull する migration race |
_hardlink_self_collision | R → A → C と R → C (root が直接子の親も兼ねる) | bundle(R) が R→C と R→A→C の両経路を歩く。is_bundle_root Phase 4 の m_missing=false override と二重経路が干渉 |
_hardlink_4node | Root → A, Root → B, A → C, B → C (C を A/B が共有) | bundle(Root) Phase 4 reachability gating + outer DISTURBED retry; 本番 race の再現 |
_hardlink_external | GN1 → 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 で扱う。
BundleUnbundle_hardlink_self_collision.tla① 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 に対応)
// 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 内部だけで完結する。
| cfg | States | 結果 |
|---|---|---|
| 2-thread (修正前) | 222 | ❌ FAIL — SnapshotConsistency 違反 |
| 2-thread (Phase 3 skip-Null 修正後) | 114 | ✅ Pass + liveness |
| 3-thread (修正後) | 760 | ✅ Pass + liveness |
BundleUnbundle_hardlink_4node.tla (gold standard)| Step | 動作 |
|---|---|
| T1 | bundle(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)。
// 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
}
| cfg | States | 結果 |
|---|---|---|
| 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 完走できる。
_hardlink_external.tla)bundle(GN1) が GN1 を ~missing finalise する瞬間に
GN2.sub[P2] = Null が GN1 から reverseLookup できない →
SnapshotConsistency 違反。Phase 4 reachability gate が直接これを防ぐ。
| cfg | States | 結果 |
|---|---|---|
| 1-thread (gate あり) | 15 | ✅ Pass + liveness |
| 2-thread (gate あり) | 136 | ✅ Pass + liveness |
_hardlink_external_migration.tla)↑ 同じ topology だが、bundle が P2 を P1 の tree から GN2.sub[P2] に
atomic に migrate する。bundle pipeline の各 step を peer thread が
P1 上で race する状況を full モデル化。
| cfg | States | 結果 |
|---|---|---|
| 1-thread | 8 | ✅ Pass + liveness |
| 2-thread | ~hundreds | ✅ Pass + liveness |
| 3-thread | 1,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(...) を付ける。理由は次スライド。
_hardlink_4node / _hardlink_external_migration\* 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))
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 な限り必ず発火することを強制できる。
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 を挿入 |
❷ TxInsertHardlink | Tx で hard-link を登録 |
❸ NonTxReleaseAC | 非 Tx で A → C を release。C は bundledBy = A のまま A は C を参照しない (limbo) |
❹ TxReleaseHardlink | Tx で hard-link 解放 |
| dtor | destructor が A と C を limbo から finalize |
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 なし |
| Spec / Variant | States | 結果 |
|---|---|---|
Spec (per-action WF) + master | 308 | ✅ Pass + liveness |
Spec + fix | 308 | ✅ Pass + liveness |
WeakSpec (blanket WF) + master | 308 | ✅ Pass + liveness |
WeakSpec + fix | 308 | ✅ 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 に合致したビルドになる。
BundleUnbundle_hardlink_dynamic.tla最も 基本的な hard-link 構造: 兄弟の 2 親が同じ子を共有する。
一方の親の bundle が他方の親の sub[] から DynChild1 を pull すべきタイミングで
peer thread と race する migration race を網羅。
| cfg | States | 結果 |
|---|---|---|
| 1-thread (MaxCommits=1) | 7 | ✅ Pass + liveness |
| 2-thread (MaxCommits=1) | 62 | ✅ Pass + liveness |
| 2-thread (MaxCommits=2) | 703 | ✅ Pass |
2026-05-20 のユーザフィードバック以降、全 hardlink モデル統一方針: retryCount なし、CONSTRAINT なし、MOD wrap-around なし。 人為的な打ち切りは活性検査で false-negative を生じうるため。 全 spec が bounded variable domain だけで TLC が自然に終了する。
| Model | retry bound | bounding 機構 |
|---|---|---|
_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 |
| Invariant / Property | 意味 |
|---|---|
SnapshotConsistency | C++ 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 カバー) |
| Commit | 内容 |
|---|---|
87892b35 | 初回 reachability gate 導入 (Packet::allSubReachable) |
92b15f62 | checkConsistensy も対応; mid-bundle 短絡対応 |
404fa137 | 呼び出し順序の見直し (m_missing を先にクリア) |
b12e1895 | global root parameter 導入による order-of-operations 修正 |
1ffd8dce | allSubReachable / checkConsistensy に optional globalroot 引数; hard-link Case B 対応 |
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。
| テスト | 影響 |
|---|---|
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_external | 15 / 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+ 検証体系で初めて以下を達成:
kamestm/tests/VERIFICATION.md §5 — full results + per-cfg runskamestm/tests/tlaplus/doc/verification_log.md — run logkamestm/tests/tlaplus/BundleUnbundle_hardlink_*.tla — spec 本体 (6 ファイル)slides_layer2_LLfree.html — base spec の説明 (LL-free Gen 3)