
環境変数を介して実行時制御可能なフェイルポイントを追加し、パニック、スリープ、リターン、条件付きエラーパスをトリガーできるGo向けフォールトインジェクションライブラリ。
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 ステートメントにフェイルポイントを注入して、フェイルポイントがアクティブな場合に1つの CASE をブロックする
上記のように、_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 を呼び出す IF ステートメントで書き換えられ、フェイルポイントが有効な場合にフェイルポイントコードを実行します
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>]
引数は実行するアクションを指定します。以下から1つを選択できます:
また、フェイルポイントのクロージャは 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 を無視することもでき、その場合は上記の非コンテキスト版と同じコードが生成されます。例:
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 の呼び出しサイトと同じプロセス内で呼び出す必要があります。このマーカーを使用すると、フェイルポイントコードがソースコードを汚染するのを防げます。examples を参照してください。
フェイルポイントは 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 {
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")
}
}
上記のコードブロックは次のようなコードを生成します:
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")
}
より複雑なフェイルポイント