Golang 的 failpoints 实现。故障点用于以用户可控的方式在代码中添加可注入错误的位置。故障点是一段代码片段,仅当对应的故障点处于激活状态时才会执行。
failpoint-ctl)从源码构建 failpoint-ctl
git clone https://github.com/pingcap/failpoint.git
cd failpoint
make
ls bin/failpoint-ctl
向你的程序注入故障点,例如:
package main
import "github.com/pingcap/failpoint"
func main() {
failpoint.Inject("testPanic", func() {
panic("failpoint triggerd")
})
}
使用 failpoint-ctl enable 转换你的代码
使用 go build 构建
使用 GO_FAILPOINTS 环境变量启用故障点
GO_FAILPOINTS="main/testPanic=return(true)" ./your-program
注意:GO_FAILPOINTS 不适用于 InjectCall 类型的标记。
如果你使用 go run 运行测试,别忘了在命令中加入生成的 binding__failpoint_binding__.go,例如:
failpoint-toolexec)从源码构建 failpoint-toolexec
git clone https://github.com/pingcap/failpoint.git
cd failpoint
make
ls bin/failpoint-toolexec
向你的程序注入故障点,例如:
package main
import "github.com/pingcap/failpoint"
func main() {
failpoint.Inject("testPanic", func() {
panic("failpoint triggerd")
})
}
使用独立的构建缓存以避免与未使用 failpoint-toolexec 的缓存混淆,然后构建
GOCACHE=/tmp/failpoint-cache go build -toolexec path/to/failpoint-toolexec
使用 GO_FAILPOINTS 环境变量启用故障点
GO_FAILPOINTS="main/testPanic=return(true)" ./your-program
你也可以使用 go run 或 go test,例如:
GOCACHE=/tmp/failpoint-cache GO_FAILPOINTS="main/testPanic=return(true)" go run -toolexec path/to/failpoint-toolexec your-program.go
用有效的 Golang 代码定义故障点,而不是注释或其他任何形式
故障点没有任何额外开销
故障点例程应可读可写,并应通过编译器检查
由故障点定义生成的代码易于阅读
与注入代码保持相同的行号(更易于调试)
支持使用 context.Context 进行并行测试
故障点
故障点是一段代码片段,仅当对应的故障点处于激活状态时才会执行。
如果执行了 failpoint.Disable("failpoint-name-for-demo"),该闭包将永远不会执行。
var outerVar = "declare in outer scope"
failpoint.Inject("failpoint-name-for-demo", func(val failpoint.Value) {
fmt.Println("unit-test", val, outerVar)
})
标记函数
它只是一个空函数
易于编写/阅读
引入编译器检查,如果故障点代码无效,则无法在常规模式下编译
标记函数列表
func Inject(fpname string, fpblock func(val Value)) {}func InjectContext(fpname string, ctx context.Context, fpblock func(val Value)) {}func InjectCall(fpname string, args ...any) {}func Break(label ...string) {}func Goto(label string) {}func Continue(label ...string) {}你可以在调用点调用 failpoint.Inject 来注入故障点,其中 failpoint-name 用于触发故障点,failpoint-closure 会被展开为 IF 语句的函数体。
failpoint.Inject("failpoint-name", func(val failpoint.Value) {
failpoint.Return("unit-test", val)
})
转换后的代码如下所示:
if val, _err_ := failpoint.Eval(_curpkg_("failpoint-name")); _err_ == nil {
return "unit-test", val
}
failpoint.Value 是通过 failpoint.Enable("failpoint-name", "return(5)") 传入的值,该值可以被忽略。
failpoint.Inject("failpoint-name", func(_ failpoint.Value) {
fmt.Println("unit-test")
})
或者
failpoint.Inject("failpoint-name", func() {
fmt.Println("unit-test")
})
转换后的代码如下所示:
if _, _err_ := failpoint.Eval(_curpkg_("failpoint-name")); _err_ == nil {
fmt.Println("unit-test")
}
将故障点注入 IF 初始化语句或条件表达式
if a, b := func() {
failpoint.Inject("failpoint-name", func(val failpoint.Value) {
fmt.Println("unit-test", val)
})
}, func() int { return rand.Intn(200) }(); b > func() int {
failpoint.Inject("failpoint-name", func(val failpoint.Value) int {
return val.(int)
})
return rand.Intn(3000)
}() && b < func() int {
failpoint.Inject("failpoint-name-2", func(val failpoint.Value) {
return rand.Intn(val.(int))
})
return rand.Intn(6000)
}() {
a()
failpoint.Inject("failpoint-name-3", func(val failpoint.Value) {
fmt.Println("unit-test", val)
})
}
上述代码块将生成类似如下的代码:
if a, b := func() {
if val, _err_ := failpoint.Eval(_curpkg_("failpoint-name")); _err_ == nil {
fmt.Println("unit-test", val)
}
}, func() int { return rand.Intn(200) }(); b > func() int {
if val, _err_ := failpoint.Eval(_curpkg_("failpoint-name")); _err_ == nil {
return val.(int)
}
return rand.Intn(3000)
}() && b < func() int {
if val, ok := failpoint.Eval(_curpkg_("failpoint-name-2")); ok {
return rand.Intn(val.(int))
}
return rand.Intn(6000)
}() {
a()
if val, ok := failpoint.Eval(_curpkg_("failpoint-name-3")); ok {
fmt.Println("unit-test", val)
}
}
将故障点注入 SELECT 语句,以便在故障点激活时阻塞某个 CASE
func (s *StoreService) ExecuteStoreTask() {
select {
case <-func() chan *StoreTask {
failpoint.Inject("priority-fp", func(_ failpoint.Value) {
return make(chan *StoreTask)
})
return s.priorityHighCh
}():
fmt.Println("execute high priority task")
case <- s.priorityNormalCh:
fmt.Println("execute normal priority task")
case <- s.priorityLowCh:
fmt.Println("execute normal low task")
}
}
如上所示,_curpkg_ 会自动将原始故障点名称包装在 failpoint.Eval 调用中。
你可以将 _curpkg_ 视作一个宏,它会自动将当前包路径添加到故障点名称前面。例如:
package ddl // which parent package is `github.com/pingcap/tidb`
func demo() {
// _curpkg_("the-original-failpoint-name") will be expanded as `github.com/pingcap/tidb/ddl/the-original-failpoint-name`
if val, ok := failpoint.Eval(_curpkg_("the-original-failpoint-name")); ok {...}
}
你不需要在应用程序中关心 _curpkg_。它会在运行 failpoint-ctl enable 后自动生成,并在 failpoint-ctl disable 时被删除。
由于一个包中的所有故障点共享同一个命名空间,我们需要小心避免名称冲突。以下是一些推荐的命名规则,以改善这种情况。
保持名称在当前子包中唯一
为故障点使用自解释的名称
你可以通过环境变量启用故障点
GO_FAILPOINTS="github.com/pingcap/tidb/ddl/renameTableErr=return(100);github.com/pingcap/tidb/planner/core/illegalPushDown=return(true);github.com/pingcap/pd/server/schedulers/balanceLeaderFailed=return(true)"
failpoint.Eval 来判断故障点是否激活,并在故障点启用时执行故障点代码
GO_FAILPOINTS="main/testPanic=return(true)" go run your-program.go binding__failpoint_binding__.go
func Fallthrough() {}func Return(results ...interface{}) {}func Label(label string) {}支持的故障点环境变量
可以通过导出符合以下模式的环境变量来启用故障点,这与 freebsd failpoint SYSCTL VARIABLES 非常相似
注意:InjectCall 不能通过环境变量启用。
[<percent>%][<count>*]<type>[(args...)][-><more terms>]
参数指定要执行的操作;可以是以下之一:
此外,故障点闭包可以是一个接收 context.Context 的函数。你可以利用 context.Context 做一些自定义操作,例如在并行测试或其他场景中控制故障点是否激活。例如:
failpoint.InjectContext(ctx, "failpoint-name", func(val failpoint.Value) {
fmt.Println("unit-test", val)
})
转换后的代码如下所示:
if val, _err_ := failpoint.EvalContext(ctx, _curpkg_("failpoint-name")); _err_ == nil {
fmt.Println("unit-test", val)
}
你可以忽略 context.Context,这将生成与上面非 context 版本相同的代码。例如:
failpoint.InjectContext(nil, "failpoint-name", func(val failpoint.Value) {
fmt.Println("unit-test", val)
})
转换为:
if val, _err_ := failpoint.EvalContext(nil, _curpkg_("failpoint-name")); _err_ == nil {
fmt.Println("unit-test", val)
}
你可以使用 failpoint.InjectCall 注入一个函数调用,这种类型的标记只能通过 failpoint.EnableCall 启用,并且必须在与 InjectCall 调用点相同的进程中调用。使用这种标记,你可以避免故障点代码污染你的源代码。参见 示例。
你可以通过 failpoint.WithHook 控制故障点
func (s *dmlSuite) TestCRUDParallel() {
sctx := failpoint.WithHook(context.Backgroud(), func(ctx context.Context, fpname string) bool {
return ctx.Value(fpname) != nil // Determine by ctx key
})
insertFailpoints = map[string]struct{} {
"insert-record-fp": {},
"insert-index-fp": {},
"on-duplicate-fp": {},
}
ictx := failpoint.WithHook(context.Backgroud(), func(ctx context.Context, fpname string) bool {
_, found := insertFailpoints[fpname] // Only enables some failpoints.
return found
})
deleteFailpoints = map[string]struct{} {
"tikv-is-busy-fp": {},
"fetch-tso-timeout": {},
}
dctx := failpoint.WithHook(context.Backgroud(), func(ctx context.Context, fpname string) bool {
_, found := deleteFailpoints[fpname] // Only disables failpoints.
return !found
})
// other DML parallel test cases.
s.RunParallel(buildSelectTests(sctx))
s.RunParallel(buildInsertTests(ictx))
s.RunParallel(buildDeleteTests(dctx))
}
如果你在循环上下文中使用故障点,你可能会用到其他标记函数。
failpoint.Label("outer")
for i := 0; i < 100; i++ {
inner:
for j := 0; j < 1000; j++ {
switch rand.Intn(j) + i {
case j / 5:
failpoint.Break()
case j / 7:
failpoint.Continue("outer")
case j / 9:
failpoint.Fallthrough()
case j / 10:
failpoint.Goto("outer")
default:
failpoint.Inject("failpoint-name", func(val failpoint.Value) {
fmt.Println("unit-test", val.(int))
if val == j/11 {
failpoint.Break("inner")
} else {
failpoint.Goto("outer")
}
})
}
}
}
上述代码块将生成如下代码:
outer:
for i := 0; i < 100; i++ {
inner:
for j := 0; j < 1000; j++ {
switch rand.Intn(j) + i {
case j / 5:
break
case j / 7:
continue outer
case j / 9:
fallthrough
case j / 10:
goto outer
default:
if val, _err_ := failpoint.Eval(_curpkg_("failpoint-name")); _err_ == nil {
fmt.Println("unit-test", val.(int))
if val == j/11 {
break inner
} else {
goto outer
}
}
}
}
}
你可能会疑惑,为什么我们不直接使用 label、break、continue 和 fallthrough,而是使用故障点标记函数。
Golang 不允许存在未使用的符号,例如标识符或标签。如果某个标签只在故障点闭包中使用,那将是非法的。例如:
label1: // compiler error: unused label1
failpoint.Inject("failpoint-name", func(val failpoint.Value) {
if val.(int) == 1000 {
goto label1 // illegal to use goto here
}
fmt.Println("unit-test", val)
})
break 和 continue 只能在循环上下文中使用,如果直接在闭包中使用它们,在 Golang 代码中是非法的。
上述代码块将生成类似如下的代码:
func (s *StoreService) ExecuteStoreTask() {
select {
case <-func() chan *StoreTask {
if _, ok := failpoint.Eval(_curpkg_("priority-fp")); ok {
return make(chan *StoreTask)
})
return s.priorityHighCh
}():
fmt.Println("execute high priority task")
case <- s.priorityNormalCh:
fmt.Println("execute normal priority task")
case <- s.priorityLowCh:
fmt.Println("execute normal low task")
}
}
将故障点注入到动态扩展 SWITCH CASE 分支中
switch opType := operator.Type(); {
case opType == "balance-leader":
fmt.Println("create balance leader steps")
case opType == "balance-region":
fmt.Println("create balance region steps")
case opType == "scatter-region":
fmt.Println("create scatter region steps")
case func() bool {
failpoint.Inject("dynamic-op-type", func(val failpoint.Value) bool {
return strings.Contains(val.(string), opType)
})
return false
}():
fmt.Println("do something")
default:
panic("unsupported operator type")
}
上述代码块将生成类似如下的代码:
switch opType := operator.Type(); {
case opType == "balance-leader":
fmt.Println("create balance leader steps")
case opType == "balance-region":
fmt.Println("create balance region steps")
case opType == "scatter-region":
fmt.Println("create scatter region steps")
case func() bool {
if val, ok := failpoint.Eval(_curpkg_("dynamic-op-type")); ok {
return strings.Contains(val.(string), opType)
}
return false
}():
fmt.Println("do something")
default:
panic("unsupported operator type")
}
更复杂的故障点