golang实现故障注入测试的插件库failpoint的使用

Golang实现故障注入测试的插件库failpoint的使用

failpoint是一个用于Golang的故障注入测试库,它允许你在代码中添加可控的错误注入点。以下是使用failpoint的完整指南和示例。

快速开始(使用failpoint-ctl)

  1. 构建failpoint-ctl工具
git clone https://github.com/pingcap/failpoint.git
cd failpoint
make
ls bin/failpoint-ctl
  1. 在程序中注入故障点
package main

import "github.com/pingcap/failpoint"

func main() {
    failpoint.Inject("testPanic", func() {
        panic("failpoint triggerd")
    })
}
  1. 使用failpoint-ctl enable转换代码

  2. 使用go build构建程序

  3. 通过环境变量启用故障点

GO_FAILPOINTS="main/testPanic=return(true)" ./your-program

注意:GO_FAILPOINTS不适用于InjectCall类型的标记。

  1. 如果使用go run运行测试,不要忘记添加生成的binding__failpoint_binding__.go
GO_FAILPOINTS="main/testPanic=return(true)" go run your-program.go binding__failpoint_binding__.go

快速开始(使用failpoint-toolexec)

  1. 构建failpoint-toolexec工具
git clone https://github.com/pingcap/failpoint.git
cd failpoint
make
ls bin/failpoint-toolexec
  1. 在程序中注入故障点
package main

import "github.com/pingcap/failpoint"

func main() {
    failpoint.Inject("testPanic", func() {
        panic("failpoint triggerd")
    })
}
  1. 使用单独的构建缓存并构建
GOCACHE=/tmp/failpoint-cache go build -toolexec path/to/failpoint-toolexec
  1. 通过环境变量启用故障点
GO_FAILPOINTS="main/testPanic=return(true)" ./your-program
  1. 也可以使用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的并行测试

关键概念

故障点

故障点是一段代码片段,只有在相应的故障点激活时才会执行。

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) {}
func Fallthrough() {}
func Return(results ...interface{}) {}
func Label(label string) {}

支持的故障点环境变量

故障点可以通过导出以下模式的环境变量启用:

[<percent>%][<count>*]<type>[(args...)][-><more terms>]

参数指定要采取的操作,可以是以下之一:

  • off: 不采取任何操作(不触发故障点代码)
  • return: 使用指定参数触发故障点
  • sleep: 休眠指定的毫秒数
  • panic: 引发panic
  • break: 执行gdb并进入调试器
  • print: 打印注入变量的故障点路径
  • pause: 暂停直到故障点被禁用

如何向程序中注入故障点

基本用法

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.Inject("failpoint-name", func(_ failpoint.Value) {
    fmt.Println("unit-test")
})

failpoint.Inject("failpoint-name", func() {
    fmt.Println("unit-test")
})

使用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)
}

控制故障点

func (s *dmlSuite) TestCRUDParallel() {
    sctx := failpoint.WithHook(context.Backgroud(), func(ctx context.Context, fpname string) bool {
        return ctx.Value(fpname) != nil // 通过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] // 只启用某些故障点
        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] // 只禁用某些故障点
        return !found
    })
    // 其他DML并行测试用例
    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")
                }
            })
        }
    }
}

故障点命名最佳实践

所有故障点在同一个包中共享相同的命名空间,因此需要小心避免名称冲突。以下是一些推荐的命名规则:

  1. 在当前子包中保持名称唯一
  2. 为故障点使用自解释的名称

可以通过环境变量启用故障点:

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)"

实现细节

  1. 定义一组标记函数
  2. 解析导入并修剪不导入故障点的源文件
  3. 遍历AST查找标记函数调用
  4. 标记函数调用将被重写为IF语句,该语句调用failpoint.Eval来确定故障点是否处于活动状态,并在故障点启用时执行故障点代码

示例Demo

以下是一个完整的故障点使用示例:

package main

import (
	"fmt"
	"github.com/pingcap/failpoint"
)

func main() {
	// 简单的故障点注入
	failpoint.Inject("simple-failpoint", func() {
		fmt.Println("Simple failpoint triggered")
	})

	// 带返回值的故障点
	failpoint.Inject("return-failpoint", func(val failpoint.Value) {
		fmt.Printf("Return failpoint triggered with value: %v\n", val)
	})

	// 在循环中使用故障点
	failpoint.Label("outer-loop")
	for i := 0; i < 5; i++ {
		failpoint.Inject("loop-failpoint", func(val failpoint.Value) {
			fmt.Printf("Loop failpoint triggered at iteration %d with value: %v\n", i, val)
			if i == 3 {
				failpoint.Break("outer-loop")
			}
		})
	}

	// 使用context控制故障点
	ctx := failpoint.WithHook(context.Background(), func(ctx context.Context, fpname string) bool {
		return fpname == "context-aware-failpoint"
	})
	failpoint.InjectContext(ctx, "context-aware-failpoint", func() {
		fmt.Println("Context-aware failpoint triggered")
	})

	fmt.Println("Program completed successfully")
}

要启用这些故障点,可以设置环境变量:

GO_FAILPOINTS="main/simple-failpoint=return;main/return-failpoint=return(42);main/loop-failpoint=50%return(loop-value)" ./your-program

这个示例展示了:

  1. 基本故障点注入
  2. 带返回值的故障点
  3. 循环中的故障点控制
  4. 使用context控制故障点激活

通过这种方式,你可以在测试中模拟各种错误条件,验证程序的健壮性和错误处理能力。


更多关于golang实现故障注入测试的插件库failpoint的使用的实战教程也可以访问 https://www.itying.com/category-94-b0.html

1 回复

更多关于golang实现故障注入测试的插件库failpoint的使用的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


Golang故障注入测试:failpoint库使用指南

failpoint是Golang中一个强大的故障注入测试库,允许开发者在代码中注入各种故障场景(如延迟、错误、panic等)来测试系统的容错能力。下面详细介绍其使用方法。

安装failpoint

go get github.com/pingcap/failpoint

基本使用

1. 定义failpoint

在代码中插入failpoint标记:

import "github.com/pingcap/failpoint"

func DoSomething() error {
    // 注入一个错误failpoint
    failpoint.Inject("failpoint-name", func() error {
        return errors.New("injected error")
    })
    
    // 正常业务逻辑
    return nil
}

2. 启用failpoint

在测试中启用failpoint:

func TestDoSomething(t *testing.T) {
    // 启用failpoint
    defer failpoint.Enable("failpoint-name", "return(true)")()
    
    err := DoSomething()
    if err == nil {
        t.Error("Expected error but got nil")
    }
}

常用注入类型

1. 返回错误

failpoint.Inject("db-error", func() error {
    return fmt.Errorf("mock db error")
})

2. 延迟执行

failpoint.Inject("slow-request", func() error {
    time.Sleep(5 * time.Second)
    return nil
})

3. 随机行为

failpoint.Inject("random-error", func() error {
    if rand.Intn(100) < 30 { // 30%概率返回错误
        return errors.New("random error")
    }
    return nil
})

4. Panic注入

failpoint.Inject("panic-memory", func() error {
    panic("out of memory")
})

高级用法

1. 条件触发

failpoint.Inject("error-when-x-gt-5", func(val int) error {
    if val > 5 {
        return errors.New("value too large")
    }
    return nil
})(x)

2. 动态修改行为

func TestDynamicFailpoint(t *testing.T) {
    // 通过环境变量控制failpoint
    os.Setenv("GO_FAILPOINTS", "failpoint-name=return(true)")
    defer os.Unsetenv("GO_FAILPOINTS")
    
    defer failpoint.Enable("failpoint-name", "return(true)")()
    
    // 测试逻辑...
}

3. 链式failpoint

failpoint.Inject("fp1", func() error {
    return failpoint.Inject("fp2", func() error {
        return errors.New("chained error")
    })
})

完整示例

package main

import (
    "errors"
    "testing"
    "time"

    "github.com/pingcap/failpoint"
)

func ProcessRequest() error {
    // 模拟数据库操作
    failpoint.Inject("db-timeout", func() error {
        time.Sleep(2 * time.Second)
        return errors.New("db timeout")
    })

    // 模拟业务逻辑
    failpoint.Inject("business-error", func() error {
        return errors.New("invalid parameter")
    })

    // 正常流程
    return nil
}

func TestProcessRequest(t *testing.T) {
    // 测试超时场景
    t.Run("db timeout", func(t *testing.T) {
        defer failpoint.Enable("db-timeout", "return(true)")()
        
        err := ProcessRequest()
        if err == nil || err.Error() != "db timeout" {
            t.Errorf("Expected db timeout error, got %v", err)
        }
    })

    // 测试业务错误场景
    t.Run("business error", func(t *testing.T) {
        defer failpoint.Enable("business-error", "return(true)")()
        
        err := ProcessRequest()
        if err == nil || err.Error() != "invalid parameter" {
            t.Errorf("Expected business error, got %v", err)
        }
    })

    // 测试正常流程
    t.Run("normal flow", func(t *testing.T) {
        err := ProcessRequest()
        if err != nil {
            t.Errorf("Expected nil error, got %v", err)
        }
    })
}

最佳实践

  1. 命名规范:为failpoint使用有意义的名称,如service-timeoutdb-error
  2. 清理机制:确保测试后禁用所有failpoint,避免影响其他测试
  3. 文档记录:为每个failpoint添加注释说明其用途和触发条件
  4. 谨慎生产:确保生产代码中不会意外启用failpoint
  5. 组合测试:结合多个failpoint测试复杂故障场景

通过failpoint,您可以系统性地验证应用程序在各种故障条件下的行为,提高系统的健壮性和可靠性。

回到顶部