yield-injections について
yield-injectionsは、再開可能なステートマシンに協調的な譲歩や失敗を注入するテスト専用の仕組みです。再入、クリーンアップ、決定的な並行動作を検証する開発者向けです。
主な機能
- 01ステートマシンとカーソルのフックで譲歩を注入
- 02遷移フックで失敗を注入
- 03単体テストで固定インジェクターを利用
- 04決定的なシミュレーター実行計画
- 05再現性のためフックの序数を維持
活用シーン
- 01譲歩後のステートマシン再入をテスト
- 02処理中断時のクリーンアップを確認
- 03シードから並行スケジュールを再現
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ tursodatabase/ turso --skill yield-injectionsインストール後、Claude Code で/yield-injectionsを入力して読み込まれたか確認します。
詳細
Yield インジェクションガイド
Yield インジェクションは、再開可能な状態マシンで協調的な yield や失敗の境界を強制するためのテスト専用インフラストラクチャです。実装は現在 core/mvcc/ 配下にありますが、この仕組みは MVCC のセマンティクスではありません。
主なファイル
core/mvcc/yield_points.rs:インジェクターのトレイトとマクロ。core/mvcc/yield_hooks.rs:YieldPointMarker、YieldContext、ProvidesYieldContext。core/connection.rs:接続ごとのインジェクタースロットとyield_instance_id_counter。core/mvcc/database/mod.rs:コミットポイントとコミットのクリーンアップ。core/mvcc/database/checkpoint_state_machine.rs:チェックポイントポイント。core/mvcc/cursor.rs:カーソルポイント。core/mvcc/database/tests.rs:固定テストインジェクターと回帰テストの例。testing/concurrent-simulator/yield_injection.rs:シミュレーター用インジェクター。
すべてのフックは cfg(any(test, injected_yields)) の内側にあります。injected_yields は test_helper または simulator を意味します。
基本モデル
YieldPoint { ordinal, point_count } は、1 つのポイントファミリー内のフックを識別します。ファミリーは YieldPointMarker を実装する #[repr(u8)] の enum です。ordinal はソースコード上の順序を表す self as u8 で、point_count は EnumCount から取得します。
既存の yield ポイントのバリアントの順序を変更しないでください。シミュレーターのプランには名前ではなく、生の序数が保存されます。
順序変更が重要な理由:ordinal は self as u8 であるため、ソースコード上の順序は決定論的なスケジュールの一部です。シードが「序数 2 で yield」と計画していた場合、順序変更によって同じシードが別の概念上のフックを対象にするよう、気付かないうちに変わることがあります。その結果、CI のシード、二分探索、シミュレーターのカバレッジを再現しにくくなります。バリアントの追加でも point_count が変わり、今後のプランに影響する可能性がありますが、既存の序数の意味は維持されます。
yield に対応する各ライブオブジェクトは、次のものを持ちます。
instance_id:同時に存在する状態マシンやカーソルを区別します。selection_key:決定論的なプランで使われる、安定した論理操作キーです。- アクティブな接続インジェクター:
yield_injector()とfailure_injector()。
フックマクロ
inject_transition_yield!(self, Point):TransitionResult<T>を返すStateTransition用です。inject_io_yield!(self, Point):IOResult<T>を返すカーソルやヘルパー関数用です。inject_transition_failure!(self, Point):StateTransitionからErr(LimboError)を返します。
現時点では inject_io_failure! はありません。失敗のインジェクションは TransitionResult を使う状態マシンでのみ機能します。
インジェクションされた yield は Completion::new_yield() を返します。これはすでに完了状態ですが、is_explicit_yield() が true であるため、VDBE はすぐに続行するのではなく、StepResult::Yield として表面化させます。
新しい yield 対応型
新しい状態マシンやカーソルのファミリーを追加する場合は、次のすべてを cfg(any(test, injected_yields)) の内側に追加します。
yield_instance_id: u64,
接続から初期化します。
yield_instance_id: connection.next_yield_instance_id(),
ProvidesYieldContext を実装します。
impl ProvidesYieldContext for MyStateMachine {
fn yield_context(&self) -> YieldContext {
YieldContext::new(
self.connection.yield_injector(),
self.connection.failure_injector(),
self.yield_instance_id,
my_yield_key(self.logical_operation_id),
)
}
}
ファミリー固有の *_yield_key(...) -> u64 ヘルパーを追加します。シミュレーターのプランがファミリー間で衝突しないよう、tx ID やテーブル ID などの安定した論理 ID とファミリータグを組み合わせます。壁時計時刻、乱数、メモリー割り当てアドレス、論理操作に関係しない偶発的なカウンターは使用しないでください。
ポイントの追加
フックを追加する前に、再入しても安全であることを確認してください。合成的な yield は StepResult::Yield を返します。同じ文/状態マシンが、その後すぐに再度ステップされる可能性があります。再入時には、非冪等な処理を繰り返すのではなく、明示的な状態から再開できる必要があります。
ルール:
- yield する前に、状態マシンを再開可能な状態へ変更してください。
- 繰り返しても問題がないか、状態によって明示的に制御されている場合を除き、
push、insert、カウンターのインクリメント、ロックの取得、クリーンアップ処理の前にフックを置かないでください。 - yield 中にロック/ガードを保持する場合は、再開する経路と yield 時にドロップする経路の両方をテストしてください。
- 放棄のテストでは、yield 時に文をドロップし、Drop/中断時のクリーンアップによって不変条件が復元されることを確認してください。
- 新しい yield ポイントを追加するときは、既存の序数を維持するため、必ず enum の最後に追加してください。
- 絶対に必要な場合を除き、既存の
YieldPointMarkerenum のバリアントを並べ替えないでください。並べ替えると既存の序数の意味が変わり、CI シードや二分探索による再現性が損なわれる可能性があります。
チェックリスト:
- 適切な
*YieldPointenum にバリアントを追加してください。並べ替えないでください。 - 再開が安全になる状態遷移の後にマクロを配置してください。ただし、ロック取得のインターリーブを明示的にテストする場合は、その取得前に配置します。
&mut self.stateを借用している間はマクロを呼び出さないでください。マクロにはself.yield_context()が必要です。FixedYieldInjectorまたはFixedFailureInjectorを使って、決定的なテストを追加してください。- 放棄された処理をテストする場合は、yield 時に文をドロップし、クリーンアップの不変条件を検証してください。
再入/状態マシンのルールについて詳しくは、async-io-model も参照してください。
固定ユニットテスト用インジェクター
対象を絞ったテストでは、固定インジェクターを使用してください。
use crate::mvcc::yield_hooks::YieldPointMarker;
conn.set_yield_injector(Some(FixedYieldInjector::new([
CommitYieldPoint::LogRecordPrepared.point(),
])));
FixedYieldInjector は HashSet<YieldPoint> を保持し、instance_id/selection_key を無視して、設定された各ポイントを合計で一度だけ消費します。同じポイントに 2 つのインスタンスが同時に到達した場合、先に到達したインスタンスがそのポイントを消費します。
FixedFailureInjector も同様に動作しますが、1 つのポイントを 1 つの LimboError に対応付けます。
同じ接続を再利用する場合は、インジェクターをクリアしてください。
conn.set_yield_injector(None);
conn.set_failure_injector(None);
主なテスト対象
- コミットの
LogRecordPrepared:コミットをPreparingのままにし、別の書き込み/チェックポイントをインターリーブするか、文をドロップします。 - コミットの
BeforeCommittedTimestampWatermarkUpdate:順不同の完了と単調増加するウォーターマークをテストします。 - コミットの
BeforeFinishCommittedTx:コミット済み状態になった後、最終クリーンアップの前に処理が放棄されるケースをテストします。 - コミットの
AfterRemoveTxの失敗:tx マップ、接続の tx スロット、ロック、排他 tx アトミック値が解放されずに残らないことを確認します。 - チェックポイントの
BeforeAcquireLock:チェックポイント境界のサンプリング前にインターリーブします。 - チェックポイントの
AfterDurableBoundaryAdvancedの失敗:永続状態が進んだ後の再試行/復旧をテストします。 - カーソルの
NextStartまたはSeekStart:カーソルの再入と、文のドロップ時のクリーンアップをテストします。特に rowid アロケーターのロックに注意してください。
コミット放棄テストでは、クリーンアップ経路を利用します。CommitStateMachine::drop は cleanup_unfinished_commit を呼び出し、中断側のクリーンアップは cleanup_abandoned_mvcc_commit を通じて実行されます。
シミュレーターの使用
並行シミュレーターが、ランダム化された決定論的な注入を管理します。シミュレーターのコードからFixedYieldInjectorをインストールしないでください。
ランタイム:
- 各ファイバーは、現在の操作用の
Arc<SimulatorYieldInjector>を所有します。 - 操作の初期化時に、
SimulatorYieldInjector::new(fiber_yield_seed(seed, fiber_idx))で置き換えます。 - すべての
stmt.step()はstep_stmt_with_injected_yieldを通ります。この関数はインジェクターをインストールしてから一度ステップし、その後RAIIを介してクリアします。
計画:
- 計画は
(instance_id, selection_key, point_count)をキーにします。 MAX_YIELDS = 20です。- シミュレーターはキーごとに、
0..=MAX_YIELDSからランダムな回数を選びます。注入されるyieldが0回でも有効です。 - その回数分のスロットを、
0..point_countのランダムな序数で埋めます。 - 一致したスロットは一度だけ消費されます。序数が重複すると、同じフックが複数回実行されます。
シミュレーターの再現性:
- スケジュールを再現するには、同じトップレベルのシードとファイバー数が必要です。
- バリアントの順序を変えると、序数の意味が変わります。
- バリアントを追加すると
point_countが変わり、同じシードでも以降のスケジュールに影響することがあります。 - フックの配置やファイバーの割り当てを変えると、同じシードでも異なるスケジュールが探索されることがあります。
注意点
- yieldポイントでインターリーブを表現できる場合、テスト専用のアドホックな
Completion::new_yield()を追加しないでください。セマンティクス上必要なランタイムのロック競合によるyieldは問題ありません。 - 状態が再開可能な状態に更新される前にフックを配置しないでください。
set_yield_injector(Some(...))はスロットが空であることをアサートし、set_yield_injector(None)はインストール済みであることをアサートします。SimulatorYieldInjectorのテスト以外では、シミュレーターの正確なyield回数に依存しないでください。