mcpField
Y
yield-injections

yield-injections

github.com/tursodatabase测试与质量25.1k41 次安装今天更新
代码仓库

关于 yield-injections

yield-injections 是用于在可恢复状态机中注入协作式让出和故障的测试基础设施,面向需要验证重入、清理和确定性并发行为的开发者。

主要功能

  1. 01在状态机和游标钩子注入让出
  2. 02在转换钩子注入故障
  3. 03在单元测试中使用固定注入器
  4. 04规划确定性模拟器调度
  5. 05保留钩子序号以确保可复现

适用场景

  1. 01测试让出后的状态机重入
  2. 02检查工作中止时的清理
  3. 03根据种子复现并发调度

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/tursodatabase/turso --skill yield-injections

安装后在 Claude Code 中输入/yield-injections确认已加载。

详细介绍

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 } 用于标识某个点族中的一个钩子。点族是实现了 YieldPointMarker 的 #[repr(u8)] 枚举;ordinal 是源代码顺序对应的 self as u8,point_count 则来自 EnumCount。

请勿重新排列现有的 yield 点变体。模拟器计划存储的是原始序号,而不是名称。

重新排列为何重要:ordinal 是 self as u8,因此源代码顺序是确定性调度的一部分。如果某个种子计划“在序号 2 处让出”,重新排列就可能在不显眼的情况下,让同一个种子从一个概念上的钩子转向另一个。这会让 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/table id)与族标签混合,避免模拟器计划在不同族之间发生冲突。不要使用墙上时钟时间、随机值、内存分配地址,或与逻辑操作无关的偶然计数器。

添加一个位置

添加 hook 之前,先证明重新进入是安全的。合成 yield 会返回 StepResult::Yield;稍后可以立即再次推进同一条语句/状态机。重新进入时,必须从显式状态恢复,而不是重复执行非幂等操作。

规则:

  • 在 yield 之前,将状态机变更为可恢复的状态。
  • 除非重复执行该操作无害或状态已明确防重,否则不要在 push、insert、计数器递增、获取锁或清理操作之前放置 hook。
  • 如果锁/保护对象在 yield 期间仍被持有,则同时测试恢复路径和在 yield 处丢弃的路径。
  • 对于放弃操作的测试,在 yield 处丢弃语句,并通过 Drop/abort 清理恢复不变量。
  • 添加新的 yield 点时,始终将其添加到枚举的最后一个位置,以保留现有序号。
  • 除非绝对必要,否则不要重新排列现有 YieldPointMarker 枚举中的变体。重新排列会改变现有序号的含义,并可能破坏 CI 种子和二分定位的可复现性。

检查清单:

  1. 向相应的 *YieldPoint 枚举追加一个变体。不要重新排列。
  2. 将宏放在确保恢复安全的状态转换之后;如果要明确测试锁的交错,则将其放在获取锁之前。
  3. 避免在借用 &mut self.state 时调用宏;宏需要 self.yield_context()。
  4. 使用 FixedYieldInjector 或 FixedFailureInjector 添加确定性测试。
  5. 如果测试放弃的操作,则在 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,并且每个已配置的位置总共只触发一次。如果两个并发实例命中同一位置,则由先到者触发。

FixedFailureInjector 的行为类似,但会将一个位置映射到一个 LimboError。

在复用同一连接时清除注入器:

conn.set_yield_injector(None);
conn.set_failure_injector(None);

常见测试目标

  • Commit LogRecordPrepared:让 commit 停留在 Preparing 状态,并发交错另一个 writer/checkpoint,或丢弃语句。
  • Commit BeforeCommittedTimestampWatermarkUpdate:测试乱序完成和水位线单调性。
  • Commit BeforeFinishCommittedTx:测试在已进入 committed 状态、但尚未完成最终清理时放弃操作。
  • Commit AfterRemoveTx 故障:验证 tx 映射、连接 tx 槽位、锁和独占 tx 原子变量均未被遗留。
  • Checkpoint BeforeAcquireLock:在 checkpoint 边界采样之前进行交错。
  • Checkpoint AfterDurableBoundaryAdvanced 故障:测试持久状态推进后的重试/恢复。
  • Cursor NextStart 或 SeekStart:测试游标重新进入和丢弃语句后的清理,尤其是 rowid 分配器锁。

放弃 commit 的测试依赖清理路径:CommitStateMachine::drop 会调用 cleanup_unfinished_commit,而 abort 侧的清理会通过 cleanup_abandoned_mvcc_commit 执行。

模拟器用法

并发模拟器负责随机且确定性的注入。请勿在模拟器代码中安装 FixedYieldInjector。

运行时:

  • 每个 fiber 为当前操作持有一个 Arc<SimulatorYieldInjector>。
  • 操作初始化时,会将其替换为 SimulatorYieldInjector::new(fiber_yield_seed(seed, fiber_idx))。
  • 每次 stmt.step() 都会经过 step_stmt_with_injected_yield,该函数会安装注入器、执行一次 step,然后通过 RAII 清除注入器。

规划:

  • 计划以 (instance_id, selection_key, point_count) 为键。
  • MAX_YIELDS = 20。
  • 对于每个键,模拟器会随机选择一个 0..=MAX_YIELDS 范围内的数量;注入零个 yield 也是有效的。
  • 它会用 0..point_count 范围内的随机序号填充相应数量的槽位。
  • 匹配的槽位只会消费一次;重复的序号可让同一个钩子触发多次。

模拟器的可复现性:

  • 要复现某个调度,必须使用相同的顶层种子和 fiber 数量。
  • 调整变体顺序会改变序号的含义。
  • 添加变体会改变 point_count,这可能会扰动相同种子下后续的调度。
  • 更改钩子位置或 fiber 分配,可能导致相同种子探索不同的调度。

注意事项

  • 如果 yield 点能够表达交错执行,请勿添加仅用于测试的临时 Completion::new_yield()。当语义需要时,运行时因锁竞争而产生的 yield 没问题。
  • 请勿在状态更新为可恢复状态之前放置钩子。
  • set_yield_injector(Some(...)) 会断言槽位为空;set_yield_injector(None) 会断言注入器已安装。
  • 除 SimulatorYieldInjector 测试外,请勿依赖模拟器 yield 的确切数量。

评论

支持 Markdown