关于 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 注入是仅用于测试的基础设施,用于在可恢复状态机中强制触发协作式让出或失败边界。该实现目前位于 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 种子和二分定位的可复现性。
检查清单:
- 向相应的
*YieldPoint枚举追加一个变体。不要重新排列。 - 将宏放在确保恢复安全的状态转换之后;如果要明确测试锁的交错,则将其放在获取锁之前。
- 避免在借用
&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,并且每个已配置的位置总共只触发一次。如果两个并发实例命中同一位置,则由先到者触发。
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 的确切数量。